@@ -35,10 +35,11 @@ type RoleBindingList struct {
3535
3636// Client is an interface for interacting with the IAM API.
3737type IAMClient interface {
38- CreateServiceAccount (ctx context.Context , projectID , displayName , accountID string ) (* iamv1.ServiceAccount , error )
39- AddIAMRoleBinding (ctx context.Context , resourceID , role , member string ) (* cloudresourcemanagerv1.Policy , error )
40- ListServiceAccounts (ctx context.Context , projectID string ) (* ServiceAccountList , error )
41- GetIAMRoleBinding (ctx context.Context , projectID , serviceAccountEmail string ) (* RoleBindingList , error )
38+ CreateServiceAccount (ctx context.Context , projectID , displayName , accountID string ) (* iamv1.ServiceAccount , error )
39+ AddIAMRoleBinding (ctx context.Context , resourceID , role , member string ) (* cloudresourcemanagerv1.Policy , error ) // Project-level
40+ AddServiceAccountRoleBinding (ctx context.Context , resourceID , role , member string ) (* iamv1.Policy , error ) // SA-level
41+ ListServiceAccounts (ctx context.Context , projectID string ) (* ServiceAccountList , error )
42+ GetIAMRoleBinding (ctx context.Context , projectID , serviceAccountEmail string ) (* RoleBindingList , error )
4243}
4344
4445// clientImpl is a client for interacting with the IAM API.
@@ -148,3 +149,46 @@ func (c *IAMClientImpl) GetIAMRoleBinding(ctx context.Context, projectID, servic
148149
149150 return & RoleBindingList {Items : roles }, nil
150151}
152+
153+ // AddServiceAccountRoleBinding adds a member to a role on a specific SA and returns the updated Policy.
154+ func (c * IAMClientImpl ) AddServiceAccountRoleBinding (ctx context.Context , resourceID , role , member string ) (* iamv1.Policy , error ) {
155+ // 1. Get the current policy (includes the ETag)
156+ policy , err := c .iamService .Projects .ServiceAccounts .GetIamPolicy (resourceID ).Context (ctx ).Do ()
157+ if err != nil {
158+ return nil , fmt .Errorf ("failed to get iam policy: %w" , err )
159+ }
160+
161+ // 2. Modify the policy
162+ updated := false
163+ for _ , binding := range policy .Bindings {
164+ if binding .Role == role {
165+ for _ , m := range binding .Members {
166+ if m == member {
167+ return policy , nil // Already exists, exit early to save an API call
168+ }
169+ }
170+ binding .Members = append (binding .Members , member )
171+ updated = true
172+ break
173+ }
174+ }
175+
176+ if ! updated {
177+ policy .Bindings = append (policy .Bindings , & iamv1.Binding {
178+ Role : role ,
179+ Members : []string {member },
180+ })
181+ }
182+
183+ // 3. Set the policy (The ETag in 'policy' ensures we don't overwrite concurrent changes)
184+ request := & iamv1.SetIamPolicyRequest {
185+ Policy : policy ,
186+ }
187+
188+ updatedPolicy , err := c .iamService .Projects .ServiceAccounts .SetIamPolicy (resourceID , request ).Context (ctx ).Do ()
189+ if err != nil {
190+ return nil , fmt .Errorf ("failed to set iam policy: %w" , err )
191+ }
192+
193+ return updatedPolicy , nil
194+ }
0 commit comments