@@ -4,16 +4,13 @@ import (
44 "bytes"
55 "context"
66 "encoding/json"
7+ "errors"
78 "fmt"
89 "io"
910 "log/slog"
1011 "net/http"
1112 "net/url"
1213 "path"
13- "reflect"
14- "strings"
15-
16- "github.com/pkg/errors"
1714)
1815
1916// AWS SSO SCIM API
@@ -32,40 +29,40 @@ const (
3229
3330var (
3431 // ErrURLEmpty is returned when the URL is empty.
35- ErrURLEmpty = errors .Errorf ("aws: url may not be empty" )
32+ ErrURLEmpty = errors .New ("aws: url may not be empty" )
3633
3734 // ErrCreateGroupRequestEmpty is returned when the create group request is empty.
38- ErrCreateGroupRequestEmpty = errors .Errorf ("aws: create group request may not be empty" )
35+ ErrCreateGroupRequestEmpty = errors .New ("aws: create group request may not be empty" )
3936
4037 // ErrCreateUserRequestEmpty is returned when the create user request is empty.
41- ErrCreateUserRequestEmpty = errors .Errorf ("aws: create user request may not be empty" )
38+ ErrCreateUserRequestEmpty = errors .New ("aws: create user request may not be empty" )
4239
4340 // ErrPatchGroupRequestEmpty is returned when the patch group request is empty.
44- ErrPatchGroupRequestEmpty = errors .Errorf ("aws: patch group request may not be empty" )
41+ ErrPatchGroupRequestEmpty = errors .New ("aws: patch group request may not be empty" )
4542
4643 // ErrGroupIDEmpty is returned when the group id is empty.
47- ErrGroupIDEmpty = errors .Errorf ("aws: group id may not be empty" )
44+ ErrGroupIDEmpty = errors .New ("aws: group id may not be empty" )
4845
4946 // ErrPatchUserRequestEmpty is returned when the patch user request is empty.
50- ErrPatchUserRequestEmpty = errors .Errorf ("aws: patch user request may not be empty" )
47+ ErrPatchUserRequestEmpty = errors .New ("aws: patch user request may not be empty" )
5148
5249 // ErrPutUserRequestEmpty is returned when the put user request is empty.
53- ErrPutUserRequestEmpty = errors .Errorf ("aws: put user request may not be empty" )
50+ ErrPutUserRequestEmpty = errors .New ("aws: put user request may not be empty" )
5451
5552 // ErrUserExternalIDEmpty is returned when the user externalId is empty.
56- ErrUserExternalIDEmpty = errors .Errorf ("aws: externalId may not be empty" )
53+ ErrUserExternalIDEmpty = errors .New ("aws: externalId may not be empty" )
5754
5855 // ErrGroupDisplayNameEmpty is returned when the userName is empty.
59- ErrGroupDisplayNameEmpty = errors .Errorf ("aws: displayName may not be empty" )
56+ ErrGroupDisplayNameEmpty = errors .New ("aws: displayName may not be empty" )
6057
6158 // ErrGroupExternalIDEmpty is returned when the userName is empty.
62- ErrGroupExternalIDEmpty = errors .Errorf ("aws: externalId may not be empty" )
59+ ErrGroupExternalIDEmpty = errors .New ("aws: externalId may not be empty" )
6360
6461 // ErrBearerTokenEmpty is returned when the bearer token is empty.
65- ErrBearerTokenEmpty = errors .Errorf ("aws: bearer token may not be empty" )
62+ ErrBearerTokenEmpty = errors .New ("aws: bearer token may not be empty" )
6663
6764 // ErrServiceProviderConfigEmpty is returned when the service provider config is empty.
68- ErrServiceProviderConfigEmpty = errors .Errorf ("aws: service provider config may not be empty" )
65+ ErrServiceProviderConfigEmpty = errors .New ("aws: service provider config may not be empty" )
6966)
7067
7168//go:generate go tool mockgen -package=mocks -destination=../../mocks/aws/scim_mocks.go -source=scim.go HTTPClient
@@ -144,14 +141,10 @@ func (s *SCIMService) newRequest(ctx context.Context, method string, u *url.URL,
144141// do sends an HTTP request and returns an HTTP response, following policy (e.g. redirects, cookies, auth) as configured on the client.
145142func (s * SCIMService ) do (ctx context.Context , req * http.Request ) (* http.Response , error ) {
146143 // Check if context is already cancelled
147- select {
148- case <- ctx .Done ():
149- return nil , ctx .Err ()
150- default :
144+ if err := ctx .Err (); err != nil {
145+ return nil , err
151146 }
152147
153- req = req .WithContext (ctx )
154-
155148 // Set bearer token
156149 req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , s .bearerToken ))
157150
@@ -269,10 +262,8 @@ func (s *SCIMService) CreateOrGetUser(ctx context.Context, cur *CreateUserReques
269262 defer resp .Body .Close ()
270263
271264 if e := s .checkHTTPResponse (resp ); e != nil {
272- httpErr := new (HTTPResponseError )
273-
274265 // http.StatusConflict is 409
275- if errors .As ( e , & httpErr ) && httpErr .StatusCode == http .StatusConflict {
266+ if httpErr , ok := errors.AsType [ * HTTPResponseError ]( e ); ok && httpErr .StatusCode == http .StatusConflict {
276267 slog .Warn (
277268 "aws CreateOrGetUser: user already exists with same name or externalId, trying to get the user information" ,
278269 "user" , cur .UserName ,
@@ -297,13 +288,13 @@ func (s *SCIMService) CreateOrGetUser(ctx context.Context, cur *CreateUserReques
297288 // when response.ID is empty, the user does not exists, so this is the case when the new user is a existing user
298289 // with a different email same externalId.
299290 if response .ID == "" {
300- slog .Warn ("aws CreateOrGetUser: group already exists, but with a different name, same id" ,
291+ slog .Warn ("aws CreateOrGetUser: user already exists, but with a different name, same id" ,
301292 "user" , cur .UserName ,
302293 "name" , cur .DisplayName ,
303294 )
304295
305296 // remove the ExternalID from the user request, and call itself again to create the new user
306- slog .Warn ("aws CreateOrGetUser: removing ExternalID from the group request, calling itself again to create the new group name " ,
297+ slog .Warn ("aws CreateOrGetUser: removing ExternalID from the user request, calling itself again to create the new user " ,
307298 "user" , cur .UserName ,
308299 "name" , cur .DisplayName ,
309300 )
@@ -312,77 +303,35 @@ func (s *SCIMService) CreateOrGetUser(ctx context.Context, cur *CreateUserReques
312303 return s .CreateOrGetUser (ctx , cur )
313304 }
314305
315- curesp := & CreateUserResponse {
316- ID : response .ID ,
317- ExternalID : response .ExternalID ,
318- Meta : response .Meta ,
319- Schemas : response .Schemas ,
320- UserName : response .UserName ,
321- DisplayName : response .DisplayName ,
322- Title : response .Title ,
323- UserType : response .UserType ,
324- PreferredLanguage : response .PreferredLanguage ,
325- Active : response .Active ,
326- Emails : response .Emails ,
327- Addresses : response .Addresses ,
328- PhoneNumbers : response .PhoneNumbers ,
329- Name : response .Name ,
330- SchemaEnterpriseUser : response .SchemaEnterpriseUser ,
331- }
306+ curesp := CreateUserResponse (* (* User )(response ))
332307
333308 // check if the user attributes are the same
334309 // maybe the user in the SCIM side was changed, so we need to update the user in the SCIM Side
335310 // according to the create user request
336- if ! reflect .DeepEqual (cur , response ) {
311+ existingUser := (* User )(response )
312+ requestedUser := (* User )(cur )
313+ if ! usersEqual (requestedUser , existingUser ) {
337314 slog .Warn ("aws CreateOrGetUser: user already exists, but attributes are different, updating the user" ,
338315 "user" , response .UserName ,
339316 "id" , response .ID ,
340317 "externalId" , response .ExternalID ,
341318 "active" , response .Active ,
342319 "displayName" , response .DisplayName ,
343- "email" , ( * User )( response ) .GetPrimaryEmailAddress (),
320+ "email" , existingUser .GetPrimaryEmailAddress (),
344321 )
345322
346- pur := & PutUserRequest {
347- ID : response .ID ,
348- ExternalID : cur .ExternalID ,
349- UserName : cur .UserName ,
350- DisplayName : cur .DisplayName ,
351- Title : cur .Title ,
352- UserType : cur .UserType ,
353- PreferredLanguage : cur .PreferredLanguage ,
354- Active : cur .Active ,
355- Emails : cur .Emails ,
356- Addresses : cur .Addresses ,
357- PhoneNumbers : cur .PhoneNumbers ,
358- Name : cur .Name ,
359- SchemaEnterpriseUser : cur .SchemaEnterpriseUser ,
360- }
323+ pur := PutUserRequest (* requestedUser )
324+ pur .ID = response .ID
361325
362- resp , err := s .PutUser (ctx , pur )
326+ resp , err := s .PutUser (ctx , & pur )
363327 if err != nil {
364328 return nil , fmt .Errorf ("aws CreateOrGetUser: error updating user: %w" , err )
365329 }
366330
367- // update the user information
368- curesp .ID = resp .ID
369- curesp .ExternalID = resp .ExternalID
370- curesp .Meta = resp .Meta
371- curesp .Schemas = resp .Schemas
372- curesp .UserName = resp .UserName
373- curesp .DisplayName = resp .DisplayName
374- curesp .Title = resp .Title
375- curesp .UserType = resp .UserType
376- curesp .PreferredLanguage = resp .PreferredLanguage
377- curesp .Active = resp .Active
378- curesp .Emails = resp .Emails
379- curesp .Addresses = resp .Addresses
380- curesp .PhoneNumbers = resp .PhoneNumbers
381- curesp .Name = resp .Name
382- curesp .SchemaEnterpriseUser = resp .SchemaEnterpriseUser
331+ curesp = CreateUserResponse (* (* User )(resp ))
383332 }
384333
385- return curesp , nil
334+ return & curesp , nil
386335 }
387336 return nil , e
388337 }
@@ -395,6 +344,38 @@ func (s *SCIMService) CreateOrGetUser(ctx context.Context, cur *CreateUserReques
395344 return & response , nil
396345}
397346
347+ // usersEqual compares two users by their sync-relevant attributes,
348+ // ignoring server-assigned fields (ID, Meta, Schemas).
349+ func usersEqual (a , b * User ) bool {
350+ if a .UserName != b .UserName || a .DisplayName != b .DisplayName || a .Active != b .Active {
351+ return false
352+ }
353+ if a .ExternalID != b .ExternalID || a .Title != b .Title || a .UserType != b .UserType {
354+ return false
355+ }
356+ if a .PreferredLanguage != b .PreferredLanguage || a .NickName != b .NickName {
357+ return false
358+ }
359+ if a .ProfileURL != b .ProfileURL || a .Locale != b .Locale || a .Timezone != b .Timezone {
360+ return false
361+ }
362+ if (a .Name == nil ) != (b .Name == nil ) {
363+ return false
364+ }
365+ if a .Name != nil && (a .Name .GivenName != b .Name .GivenName || a .Name .FamilyName != b .Name .FamilyName || a .Name .Formatted != b .Name .Formatted || a .Name .MiddleName != b .Name .MiddleName ) {
366+ return false
367+ }
368+ if len (a .Emails ) != len (b .Emails ) {
369+ return false
370+ }
371+ for i := range a .Emails {
372+ if a .Emails [i ] != b .Emails [i ] {
373+ return false
374+ }
375+ }
376+ return true
377+ }
378+
398379// DeleteUser deletes a user in the AWS SSO Using the API.
399380func (s * SCIMService ) DeleteUser (ctx context.Context , id string ) error {
400381 if id == "" {
@@ -420,16 +401,12 @@ func (s *SCIMService) DeleteUser(ctx context.Context, id string) error {
420401 defer resp .Body .Close ()
421402
422403 if e := s .checkHTTPResponse (resp ); e != nil {
423- httpErr := new (HTTPResponseError )
424-
425404 // http.StatusNotFound is 404
426405 // in this case, the user was already deleted manually, so we can ignore the error
427- if errors .As ( e , & httpErr ) && httpErr .StatusCode == http .StatusNotFound {
406+ if httpErr , ok := errors.AsType [ * HTTPResponseError ]( e ); ok && httpErr .StatusCode == http .StatusNotFound {
428407 slog .Warn ("aws DeleteUser: user id does not exist, maybe it was already deleted because the username changed" , "id" , id )
429-
430408 return nil
431409 }
432- // different error not handled yet
433410 return e
434411 }
435412 return nil
@@ -473,17 +450,12 @@ func (s *SCIMService) GetUserByUserName(ctx context.Context, userName string) (*
473450 return nil , fmt .Errorf ("aws GetUserByUserName: userName: %s, error decoding response body: %w" , userName , err )
474451 }
475452
476- var response GetUserResponse
477-
478453 if len (lur .Resources ) > 0 {
479- dataJSON := lur .Resources [0 ].String ()
480- data := strings .NewReader (dataJSON )
481- if err = json .NewDecoder (data ).Decode (& response ); err != nil {
482- return nil , fmt .Errorf ("aws GetUserByUserName: userName: %s, error decoding response body: %w" , userName , err )
483- }
454+ response := GetUserResponse (* lur .Resources [0 ])
455+ return & response , nil
484456 }
485457
486- return & response , nil
458+ return & GetUserResponse {} , nil
487459}
488460
489461// GetUser returns an user from the AWS SSO Using the API
@@ -672,18 +644,12 @@ func (s *SCIMService) GetGroupByDisplayName(ctx context.Context, displayName str
672644 return nil , fmt .Errorf ("aws GetGroupByDisplayName: displayName: %s, error decoding response body: %w" , displayName , err )
673645 }
674646
675- var response GetGroupResponse
676-
677647 if len (lgr .Resources ) > 0 {
678- dataJSON := lgr .Resources [0 ].String ()
679-
680- data := strings .NewReader (dataJSON )
681- if err = json .NewDecoder (data ).Decode (& response ); err != nil {
682- return nil , fmt .Errorf ("aws GetGroupByDisplayName: displayName: %s, error decoding response body: %w" , displayName , err )
683- }
648+ response := GetGroupResponse (* lgr .Resources [0 ])
649+ return & response , nil
684650 }
685651
686- return & response , nil
652+ return & GetGroupResponse {} , nil
687653}
688654
689655// ListGroups returns a list of groups from the AWS SSO Using the API
@@ -759,12 +725,7 @@ func (s *SCIMService) CreateGroup(ctx context.Context, cgr *CreateGroupRequest)
759725
760726 var response CreateGroupResponse
761727 if err := json .NewDecoder (resp .Body ).Decode (& response ); err != nil {
762- b , err := io .ReadAll (resp .Body )
763- if err != nil {
764- return nil , fmt .Errorf ("aws CreateGroup: error reading response body: %w" , err )
765- }
766-
767- return nil , fmt .Errorf ("aws CreateGroup: error decoding response body: %w, body: %s" , err , string (b ))
728+ return nil , fmt .Errorf ("aws CreateGroup: error decoding response body: %w" , err )
768729 }
769730
770731 return & response , nil
@@ -807,10 +768,8 @@ func (s *SCIMService) CreateOrGetGroup(ctx context.Context, cgr *CreateGroupRequ
807768 defer resp .Body .Close ()
808769
809770 if e := s .checkHTTPResponse (resp ); e != nil {
810- httpErr := new (HTTPResponseError )
811-
812771 // http.StatusConflict is 409
813- if errors .As ( e , & httpErr ) && httpErr .StatusCode == http .StatusConflict {
772+ if httpErr , ok := errors.AsType [ * HTTPResponseError ]( e ); ok && httpErr .StatusCode == http .StatusConflict {
814773 slog .Warn ("aws CreateOrGetGroup: groups already exists with same name or externalId, trying to get the group information" , "name" , cgr .DisplayName )
815774
816775 // This is because the group already exists, but exists with the same name
@@ -834,25 +793,16 @@ func (s *SCIMService) CreateOrGetGroup(ctx context.Context, cgr *CreateGroupRequ
834793 return s .CreateOrGetGroup (ctx , cgr )
835794 }
836795
837- return & CreateGroupResponse {
838- ID : response .ID ,
839- Meta : response .Meta ,
840- Schemas : response .Schemas ,
841- DisplayName : response .DisplayName ,
842- }, nil
796+ cgresp := CreateGroupResponse (* (* Group )(response ))
797+ return & cgresp , nil
843798 }
844799
845800 return nil , e
846801 }
847802
848803 var response CreateGroupResponse
849804 if err := json .NewDecoder (resp .Body ).Decode (& response ); err != nil {
850- b , err := io .ReadAll (resp .Body )
851- if err != nil {
852- return nil , fmt .Errorf ("aws CreateOrGetGroup: group: %s, error reading response body: %w" , cgr .DisplayName , err )
853- }
854-
855- return nil , fmt .Errorf ("aws CreateOrGetGroup: group: %s, error decoding response body: %w, body: %s" , cgr .DisplayName , err , string (b ))
805+ return nil , fmt .Errorf ("aws CreateOrGetGroup: group: %s, error decoding response body: %w" , cgr .DisplayName , err )
856806 }
857807
858808 return & response , nil
@@ -883,10 +833,8 @@ func (s *SCIMService) DeleteGroup(ctx context.Context, id string) error {
883833 defer resp .Body .Close ()
884834
885835 if e := s .checkHTTPResponse (resp ); e != nil {
886- httpErr := new (HTTPResponseError )
887-
888836 // http.StatusNotFound is 404
889- if errors .As ( e , & httpErr ) && httpErr .StatusCode == http .StatusNotFound {
837+ if httpErr , ok := errors.AsType [ * HTTPResponseError ]( e ); ok && httpErr .StatusCode == http .StatusNotFound {
890838 slog .Warn ("aws DeleteGroup: group id does not exists, maybe it was already deleted because the name changed" , "id" , id )
891839 return nil
892840 }
0 commit comments