Skip to content

Commit 137dbeb

Browse files
Fix IAM client to use seperate method for add IAM service account permissions (#131)
1 parent 07b8efb commit 137dbeb

4 files changed

Lines changed: 71 additions & 12 deletions

File tree

cicd-mcp-server/cloudbuild/cloudbuild.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func setPermissionsForCloudBuildSA(ctx context.Context, projectID, serviceAccoun
154154
}
155155

156156
// 2. Roles for the Developer Connect Service Agent (Project Level)
157-
dcP4sa := fmt.Sprintf("serviceAccount:service-%d@gcp-sa-developerconnect.iam.gserviceaccount.com", projectNumber)
157+
dcP4sa := fmt.Sprintf("serviceAccount:service-%d@gcp-sa-devconnect.iam.gserviceaccount.com", projectNumber)
158158
_, err = iamClient.AddIAMRoleBinding(ctx, fmt.Sprintf("projects/%s", projectID), "roles/secretmanager.admin", dcP4sa)
159159
if err != nil {
160160
return "", fmt.Errorf("unable to add secretmanager.admin role to Developer Connect P4SA %s: %w", dcP4sa, err)
@@ -176,14 +176,14 @@ func setPermissionsForCloudBuildSA(ctx context.Context, projectID, serviceAccoun
176176
// 4. Grant Cloud Build Service Agent permissions to impersonate the Cloud Run SA (Default Compute SA)
177177
defaultComputeSA := fmt.Sprintf("serviceAccount:%d-compute@developer.gserviceaccount.com", projectNumber)
178178
resource := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, strings.TrimPrefix(defaultComputeSA, "serviceAccount:"))
179-
_, err = iamClient.AddIAMRoleBinding(ctx, resource, "roles/iam.serviceAccountUser", gcbP4sa)
179+
_, err = iamClient.AddServiceAccountRoleBinding(ctx, resource, "roles/iam.serviceAccountUser", gcbP4sa)
180180
if err != nil {
181181
return "", fmt.Errorf("unable to add iam.serviceAccountUser role to Cloud Build P4SA %s on SA %s err: %w", gcbP4sa, defaultComputeSA, err)
182182
}
183183

184184
// 5. Grant Cloud Build Service Agent permissions to impersonate the SA passed in (SA that is going to run the build)
185185
r := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, strings.TrimPrefix(resolvedSA, "serviceAccount:"))
186-
_, err = iamClient.AddIAMRoleBinding(ctx, r, "roles/iam.serviceAccountUser", gcbP4sa)
186+
_, err = iamClient.AddServiceAccountRoleBinding(ctx, r, "roles/iam.serviceAccountUser", gcbP4sa)
187187
if err != nil {
188188
return "", fmt.Errorf("unable to add iam.serviceAccountUser role to Cloud Build P4SA %s on SA %s err: %w", gcbP4sa, r, err)
189189
}

cicd-mcp-server/cloudbuild/cloudbuild_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
4848
"roles/serviceusage.serviceUsageConsumer",
4949
"roles/storage.admin",
5050
}
51-
dcP4sa := fmt.Sprintf("serviceAccount:service-%d@gcp-sa-developerconnect.iam.gserviceaccount.com", projectNumber)
51+
dcP4sa := fmt.Sprintf("serviceAccount:service-%d@gcp-sa-devconnect.iam.gserviceaccount.com", projectNumber)
5252
gcbP4sa := fmt.Sprintf("serviceAccount:service-%d@gcp-sa-cloudbuild.iam.gserviceaccount.com", projectNumber)
5353
gcbP4saRoles := []string{
5454
"roles/cloudbuild.serviceAgent",
@@ -67,8 +67,8 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
6767
for _, r := range gcbP4saRoles {
6868
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, fmt.Sprintf("projects/%s", projectID), r, gcbP4sa).Return(nil, nil)
6969
}
70-
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, saResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
71-
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, userSAResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
70+
mockIAMClient.EXPECT().AddServiceAccountRoleBinding(ctx, saResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
71+
mockIAMClient.EXPECT().AddServiceAccountRoleBinding(ctx, userSAResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
7272

7373
resolvedSA, err := setPermissionsForCloudBuildSA(ctx, projectID, serviceAccount, mockRMClient, mockIAMClient)
7474
assert.NoError(t, err)
@@ -84,8 +84,8 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
8484
for _, r := range gcbP4saRoles {
8585
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, fmt.Sprintf("projects/%s", projectID), r, gcbP4sa).Return(nil, nil)
8686
}
87-
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, saResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
88-
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, userSAResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
87+
mockIAMClient.EXPECT().AddServiceAccountRoleBinding(ctx, saResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
88+
mockIAMClient.EXPECT().AddServiceAccountRoleBinding(ctx, userSAResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
8989

9090
resolvedSA, err := setPermissionsForCloudBuildSA(ctx, projectID, serviceAccountWOPrefix, mockRMClient, mockIAMClient)
9191
assert.NoError(t, err)

cicd-mcp-server/iam/client/iamclient.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ type RoleBindingList struct {
3535

3636
// Client is an interface for interacting with the IAM API.
3737
type 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+
}

cicd-mcp-server/iam/client/mocks/mock_iamclient.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)