@@ -41,10 +41,18 @@ func WithMaxUsersToProvision(num int64) EnterpriseRWOpt {
4141 }
4242}
4343
44+ // WithUserDeactivationSanityCheck sets the sanity check function for SCIM user deactivation.
45+ func WithUserDeactivationSanityCheck (f func (context.Context , * groupsync.User ) (bool , error )) EnterpriseRWOpt {
46+ return func (rw * EnterpriseUserWriter ) {
47+ rw .userDeactivationSanityCheck = f
48+ }
49+ }
50+
4451// EnterpriseUserWriter manages enterprise users via a direct GHES SCIM API client.
4552type EnterpriseUserWriter struct {
46- scimClient * SCIMClient
47- maxUsersToProvision int64
53+ scimClient * SCIMClient
54+ maxUsersToProvision int64
55+ userDeactivationSanityCheck func (context.Context , * groupsync.User ) (bool , error )
4856}
4957
5058// NewEnterpriseUserWriter creates a new EnterpriseUserWriter with default 1000
@@ -57,6 +65,9 @@ func NewEnterpriseUserWriter(httpClient *http.Client, enterpriseBaseURL string,
5765 w := & EnterpriseUserWriter {
5866 maxUsersToProvision : defaultMaxUsersToProvision ,
5967 scimClient : scimClient ,
68+ userDeactivationSanityCheck : func (context.Context , * groupsync.User ) (bool , error ) {
69+ return true , nil
70+ },
6071 }
6172 for _ , opt := range opts {
6273 opt (w )
@@ -73,8 +84,9 @@ func (w *EnterpriseUserWriter) SetMembers(ctx context.Context, _ string, members
7384 return fmt .Errorf ("failed to list users: %w" , err )
7485 }
7586 desiredUsersMap := make (map [string ]* SCIMUser )
87+ userMemberMap := make (map [string ]* groupsync.User )
7688 // Use a list to maintain the ordering of the desired users to avoid unit test flakiness.
77- desiredUsersName := []string {}
89+ var desiredUsersName []string
7890 for _ , m := range members {
7991 if ! m .IsUser () {
8092 logger .DebugContext (ctx , "skipping non-user member" , "member" , m .ID ())
@@ -86,6 +98,7 @@ func (w *EnterpriseUserWriter) SetMembers(ctx context.Context, _ string, members
8698 logger .DebugContext (ctx , "skipping non-SCIM user member" , "member" , m .ID ())
8799 continue
88100 }
101+ userMemberMap [scimUser .UserName ] = u
89102 desiredUsersMap [scimUser .UserName ] = scimUser
90103 desiredUsersName = append (desiredUsersName , scimUser .UserName )
91104 }
@@ -99,11 +112,19 @@ func (w *EnterpriseUserWriter) SetMembers(ctx context.Context, _ string, members
99112 }
100113 // Deactivate user who is not in desiredUsersMap and remove any role grants.
101114 if _ , ok := desiredUsersMap [username ]; ! ok {
102- logger .InfoContext (ctx , "deactivating user" , "user" , username )
115+ deactivate , err := w .userDeactivationSanityCheck (ctx , userMemberMap [username ])
116+ if err != nil {
117+ merr = errors .Join (merr , fmt .Errorf ("failed to check user ACL for deactivation user %q host %q: %w" , username , err , w .scimClient .baseURL .Host ))
118+ }
119+ if ! deactivate {
120+ logger .InfoContext (ctx , "skipping user deactivation due to sanity check failed" , "user" , username , "host" , w .scimClient .baseURL .Host )
121+ continue
122+ }
123+ logger .InfoContext (ctx , "deactivating user" , "user" , username , "host" , w .scimClient .baseURL .Host )
103124 scimUser .Active = github .Bool (false )
104125 scimUser .Roles = nil
105126 if _ , _ , err := w .scimClient .UpdateUser (ctx , * scimUser .ID , scimUser ); err != nil {
106- merr = errors .Join (merr , fmt .Errorf ("failed to deactivate %q: %w" , username , err ))
127+ merr = errors .Join (merr , fmt .Errorf ("failed to deactivate %q, host %q : %w" , w . scimClient . baseURL . Host , username , err ))
107128 }
108129 }
109130 }
@@ -113,7 +134,7 @@ func (w *EnterpriseUserWriter) SetMembers(ctx context.Context, _ string, members
113134 for _ , username := range desiredUsersName {
114135 count ++
115136 if count > w .maxUsersToProvision {
116- merr = errors .Join (merr , fmt .Errorf ("exceeded max users to provision: %d" , w .maxUsersToProvision ))
137+ merr = errors .Join (merr , fmt .Errorf ("exceeded max users to provision, host %q : %d" , w . scimClient . baseURL . Host , w .maxUsersToProvision ))
117138 break
118139 }
119140
@@ -122,9 +143,9 @@ func (w *EnterpriseUserWriter) SetMembers(ctx context.Context, _ string, members
122143 // Create the user if not found in currentUsersMap.
123144 currentUser , ok := currentUsersMap [username ]
124145 if ! ok {
125- logger .InfoContext (ctx , "creating user" , "user" , username )
146+ logger .InfoContext (ctx , "creating user" , "user" , username , "host" , w . scimClient . baseURL . Host )
126147 if _ , _ , err := w .scimClient .CreateUser (ctx , desiredUser ); err != nil {
127- merr = errors .Join (merr , fmt .Errorf ("failed to create %q: %w" , username , err ))
148+ merr = errors .Join (merr , fmt .Errorf ("failed to create %q host %q : %w" , username , w . scimClient . baseURL . Host , err ))
128149 }
129150 continue
130151 }
0 commit comments