From 317d3970a5f09a00aab971e63cff579c8c91ea8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20D=C3=B6ll?= Date: Tue, 21 Jul 2020 17:41:33 +0200 Subject: [PATCH] (fix) using archived and active users for sync closes #1 --- CHANGELOG.md | 4 ++ cmd/root.go | 6 ++- internal/aws/client.go | 15 +++---- internal/aws/client_test.go | 79 ------------------------------------- internal/google/client.go | 34 ++++++++++------ internal/sync.go | 69 ++++++++++++++++---------------- 6 files changed, 71 insertions(+), 136 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..aff86c11 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +## 1.0.0-rc.1 + +- #1 Fix: Pagination does not work +- #3 Refactor: New features for Serverless Repo and Google best practices diff --git a/cmd/root.go b/cmd/root.go index 715c937e..f657d590 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,6 +15,7 @@ package cmd import ( + "context" "fmt" "os" @@ -47,7 +48,10 @@ var rootCmd = &cobra.Command{ Apps (G-Suite) users to AWS Single Sign-on (AWS SSO) Complete documentation is available at https://github.com/awslabs/ssosync`, RunE: func(cmd *cobra.Command, args []string) error { - err := internal.DoSync(cfg) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + err := internal.DoSync(ctx, cfg) if err != nil { return err } diff --git a/internal/aws/client.go b/internal/aws/client.go index fe41dfb9..505a4211 100644 --- a/internal/aws/client.go +++ b/internal/aws/client.go @@ -42,7 +42,6 @@ const ( // IClient represents an interface of methods used // to communicate with AWS SSO type IClient interface { - GetUsers() (*map[string]User, error) GetGroups() (*map[string]Group, error) IsUserInGroup(*User, *Group) (bool, error) FindUserByEmail(string) (*User, error) @@ -364,10 +363,10 @@ func (c *Client) RemoveUserFromGroup(u *User, g *Group) error { } // FindUserByEmail will find the user by the email address specified -func (c *Client) FindUserByEmail(email string) (user *User, err error) { +func (c *Client) FindUserByEmail(email string) (*User, error) { startURL, err := url.Parse(c.endpointURL.String()) if err != nil { - return + return nil, err } filter := fmt.Sprintf("userName eq \"%s\"", email) @@ -380,23 +379,21 @@ func (c *Client) FindUserByEmail(email string) (user *User, err error) { resp, err := c.sendRequest(http.MethodGet, startURL.String()) if err != nil { - return + return nil, err } var r UserFilterResults err = json.Unmarshal(resp, &r) if err != nil { - return + return nil, err } if r.TotalResults != 1 { err = fmt.Errorf("%s not found in AWS SSO", email) - return + return nil, err } - user = &r.Resources[0] - - return + return &r.Resources[0], nil } // CreateUser will create the user specified diff --git a/internal/aws/client_test.go b/internal/aws/client_test.go index 8114330f..17fe098d 100644 --- a/internal/aws/client_test.go +++ b/internal/aws/client_test.go @@ -82,85 +82,6 @@ func TestNewClient(t *testing.T) { assert.Nil(t, c) } -func TestClient_GetUsers(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - x := mock.NewMockIHttpClient(ctrl) - - c, err := NewClient(x, &Config{ - Endpoint: "https://scim.example.com/", - Token: "bearerToken", - }) - assert.NoError(t, err) - - r := ` -{ - "totalResults": 1, - "itemsPerPage": 1, - "startIndex": 1, - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:ListResponse" - ], - "Resources": [ - { - "id": "93671c1e63-33bc8a92-2fb0-487b-93ef-7a618aef932a", - "meta": { - "resourceType": "User", - "created": "2020-04-16T14:22:56Z", - "lastModified": "2020-04-16T14:22:56Z" - }, - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User" - ], - "userName": "lpackham@foo.org.uk", - "name": { - "familyName": "Packham", - "givenName": "Lee" - }, - "displayName": "Lee Packham", - "active": true, - "emails": [ - { - "value": "lpackham@foo.org.uk", - "type": "work", - "primary": true - } - ], - "addresses": [ - { - "type": "work" - } - ] - } - ] -} -` - - calledURL, _ := url.Parse("https://scim.example.com/Users?count=10&startIndex=1") - - req := httpReqMatcher{httpReq: &http.Request{ - URL: calledURL, - Method: http.MethodGet, - }} - - // We only have enough users for one page, so we should only - // see one call. - x.EXPECT().Do(&req).MaxTimes(1).Return(&http.Response{ - Status: "OK", - StatusCode: 200, - Body: nopCloser{bytes.NewBufferString(r)}, - }, nil) - - users, err := c.GetUsers() - - assert.NoError(t, err) - - // Check there's only user - assert.Equal(t, len(*users), 1) - assert.Contains(t, *users, "lpackham@foo.org.uk") -} - func TestClient_GetGroups(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/internal/google/client.go b/internal/google/client.go index 0589af7a..82aac3b9 100644 --- a/internal/google/client.go +++ b/internal/google/client.go @@ -25,6 +25,7 @@ import ( // Client is the Interface for the Client type Client interface { GetUsers() ([]*admin.User, error) + GetDeletedUsers() ([]*admin.User, error) GetGroups() ([]*admin.Group, error) GetGroupMembers(*admin.Group) ([]*admin.Member, error) } @@ -35,9 +36,7 @@ type client struct { } // NewClient creates a new client for Google's Admin API -func NewClient(adminEmail string, serviceAccountKey []byte) (Client, error) { - ctx := context.Background() - +func NewClient(ctx context.Context, adminEmail string, serviceAccountKey []byte) (Client, error) { config, err := google.JWTConfigFromJSON(serviceAccountKey, admin.AdminDirectoryGroupReadonlyScope, admin.AdminDirectoryGroupMemberReadonlyScope, admin.AdminDirectoryUserReadonlyScope) @@ -61,10 +60,21 @@ func NewClient(adminEmail string, serviceAccountKey []byte) (Client, error) { }, nil } +// GetDeletedUsers will get the deleted users from the Google's Admin API. +func (c *client) GetDeletedUsers() ([]*admin.User, error) { + u := make([]*admin.User, 0) + err := c.service.Users.List().Customer("my_customer").ShowDeleted("true").Pages(c.ctx, func(users *admin.Users) error { + u = append(u, users.Users...) + return nil + }) + + return u, err +} + // GetUsers will get the users from Google's Admin API -func (c *client) GetUsers() (u []*admin.User, err error) { - u = make([]*admin.User, 0) - err = c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error { +func (c *client) GetUsers() ([]*admin.User, error) { + u := make([]*admin.User, 0) + err := c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error { u = append(u, users.Users...) return nil }) @@ -73,9 +83,9 @@ func (c *client) GetUsers() (u []*admin.User, err error) { } // GetGroups will get the groups from Google's Admin API -func (c *client) GetGroups() (g []*admin.Group, err error) { - g = make([]*admin.Group, 0) - err = c.service.Groups.List().Customer("my_customer").Pages(context.TODO(), func(groups *admin.Groups) error { +func (c *client) GetGroups() ([]*admin.Group, error) { + g := make([]*admin.Group, 0) + err := c.service.Groups.List().Customer("my_customer").Pages(context.TODO(), func(groups *admin.Groups) error { g = append(g, groups.Groups...) return nil }) @@ -84,9 +94,9 @@ func (c *client) GetGroups() (g []*admin.Group, err error) { } // GetGroupMembers will get the members of the group specified -func (c *client) GetGroupMembers(g *admin.Group) (m []*admin.Member, err error) { - m = make([]*admin.Member, 0) - err = c.service.Members.List(g.Id).Pages(context.TODO(), func(members *admin.Members) error { +func (c *client) GetGroupMembers(g *admin.Group) ([]*admin.Member, error) { + m := make([]*admin.Member, 0) + err := c.service.Members.List(g.Id).Pages(context.TODO(), func(members *admin.Members) error { m = append(m, members.Members...) return nil }) diff --git a/internal/sync.go b/internal/sync.go index 55d5bd32..095edcdd 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -15,6 +15,7 @@ package internal import ( + "context" "io/ioutil" "net/http" @@ -52,54 +53,52 @@ func New(a aws.IClient, g google.Client) ISyncGSuite { // SyncUsers will Sync Google Users to AWS SSO SCIM func (s *SyncGSuite) SyncUsers() error { - log.Info("Start user sync") - log.Info("Get AWS Users") - - awsUsers, err := s.aws.GetUsers() + log.Debug("get deleted users") + deletedUsers, err := s.google.GetDeletedUsers() if err != nil { return err } - log.Debug("Get Google Users") + for _, u := range deletedUsers { + uu, _ := s.aws.FindUserByEmail(u.PrimaryEmail) + if uu == nil { + continue + } + + log.WithFields(log.Fields{ + "email": u.PrimaryEmail, + }).Info("deleting google user") + + if err := s.aws.DeleteUser(uu); err != nil { + return err + } + } + + log.Debug("get active google users") googleUsers, err := s.google.GetUsers() if err != nil { return err } for _, u := range googleUsers { - log := log.WithFields(log.Fields{ + ll := log.WithFields(log.Fields{ "email": u.PrimaryEmail, }) - log.Debug("Check user") - - if awsUser, ok := (*awsUsers)[u.PrimaryEmail]; ok { - log.Debug("Found user") - s.users[awsUser.Username] = &awsUser - } else { - log.Info("Create user in AWS") - newUser, err := s.aws.CreateUser(aws.NewUser( - u.Name.GivenName, - u.Name.FamilyName, - u.PrimaryEmail, - )) - if err != nil { - return err - } - - s.users[newUser.Username] = newUser + ll.Debug("finding user") + uu, _ := s.aws.FindUserByEmail(u.PrimaryEmail) + if uu != nil { + continue } - } - - log.Info("Clean up AWS Users") - for _, u := range *awsUsers { - if _, ok := s.users[u.Username]; !ok { - log.WithField("email", u.Username).Info("Delete User in AWS") - err := s.aws.DeleteUser(&u) - if err != nil { - return err - } + ll.Info("creating user") + _, err := s.aws.CreateUser(aws.NewUser( + u.Name.GivenName, + u.Name.FamilyName, + u.PrimaryEmail, + )) + if err != nil { + return err } } @@ -207,7 +206,7 @@ func (s *SyncGSuite) SyncGroups() error { // DoSync will create a logger and run the sync with the paths // given to do the sync. -func DoSync(cfg *config.Config) error { +func DoSync(ctx context.Context, cfg *config.Config) error { log.Info("Creating the Google and AWS Clients needed") creds := []byte(cfg.GoogleCredentials) @@ -220,7 +219,7 @@ func DoSync(cfg *config.Config) error { creds = b } - googleClient, err := google.NewClient(cfg.GoogleAdmin, creds) + googleClient, err := google.NewClient(ctx, cfg.GoogleAdmin, creds) if err != nil { return err }