Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cicd-mcp-server/cloudbuild/cloudbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ func setPermissionsForCloudBuildSA(ctx context.Context, projectID, serviceAccoun
gcbSARoles := []string{
"roles/artifactregistry.writer",
"roles/cloudbuild.builds.editor",
"roles/cloudbuild.workerpools.use",
"roles/developerconnect.tokenAccessor",
"roles/logging.logWriter",
"roles/run.developer",
Expand Down Expand Up @@ -174,14 +173,21 @@ func setPermissionsForCloudBuildSA(ctx context.Context, projectID, serviceAccoun
}
}

// 4. Role for the Cloud Build Service Agent on the Cloud Run SA (Default Compute SA)
// 4. Grant Cloud Build Service Agent permissions to impersonate the Cloud Run SA (Default Compute SA)
defaultComputeSA := fmt.Sprintf("serviceAccount:%d-compute@developer.gserviceaccount.com", projectNumber)
resource := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, strings.TrimPrefix(defaultComputeSA, "serviceAccount:"))
_, err = iamClient.AddIAMRoleBinding(ctx, resource, "roles/iam.serviceAccountUser", gcbP4sa)
if err != nil {
return "", fmt.Errorf("unable to add iam.serviceAccountUser role to Cloud Build P4SA %s on SA %s err: %w", gcbP4sa, defaultComputeSA, err)
}

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

return resolvedSA, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The function currently returns resolvedSA, which includes the serviceAccount: prefix. However, the Cloud Build API's serviceAccount field for triggers expects either the service account email address or its full resource path, not the IAM member string format. Providing the prefix will likely cause the subsequent CreateBuildTrigger call to fail with an invalid argument error.

Suggested change
return resolvedSA, nil
return strings.TrimPrefix(resolvedSA, "serviceAccount:"), nil

}

Expand Down
28 changes: 27 additions & 1 deletion cicd-mcp-server/cloudbuild/cloudbuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
gcbSARoles := []string{
"roles/artifactregistry.writer",
"roles/cloudbuild.builds.editor",
"roles/cloudbuild.workerpools.use",
"roles/developerconnect.tokenAccessor",
"roles/logging.logWriter",
"roles/run.developer",
Expand All @@ -57,6 +56,7 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
}
defaultComputeSA := fmt.Sprintf("%d-compute@developer.gserviceaccount.com", projectNumber)
saResource := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, defaultComputeSA)
userSAResource := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, "test-sa@example.com")

t.Run("with service account", func(t *testing.T) {
for _, r := range gcbSARoles {
Expand All @@ -68,6 +68,7 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, fmt.Sprintf("projects/%s", projectID), r, gcbP4sa).Return(nil, nil)
}
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, saResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, userSAResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)

resolvedSA, err := setPermissionsForCloudBuildSA(ctx, projectID, serviceAccount, mockRMClient, mockIAMClient)
assert.NoError(t, err)
Expand All @@ -84,6 +85,7 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, fmt.Sprintf("projects/%s", projectID), r, gcbP4sa).Return(nil, nil)
}
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, saResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, userSAResource, "roles/iam.serviceAccountUser", gcbP4sa).Return(nil, nil)

resolvedSA, err := setPermissionsForCloudBuildSA(ctx, projectID, serviceAccountWOPrefix, mockRMClient, mockIAMClient)
assert.NoError(t, err)
Expand Down Expand Up @@ -118,3 +120,27 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
assert.Error(t, err)
})
}

func TestIsValidServiceAccount(t *testing.T) {
tests := []struct {
name string
sa string
want bool
}{
{"valid sa", "serviceAccount:test-sa@project.iam.gserviceaccount.com", true},
{"valid sa with dashes", "serviceAccount:my-sa-123@my-project-456.iam.gserviceaccount.com", true},
{"missing prefix", "test-sa@project.iam.gserviceaccount.com", false},
{"wrong prefix", "user:test-sa@project.iam.gserviceaccount.com", false},
{"missing domain", "serviceAccount:test-sa@project", false},
{"wrong domain", "serviceAccount:test-sa@project.com", false},
Comment on lines +130 to +135
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validation logic for service accounts (and these corresponding tests) appears to be too restrictive. Google Cloud project IDs can contain dots and colons (e.g., for domain-scoped projects like example.com:my-project). The current regex [a-z0-9-]+ used in the implementation will reject valid service accounts from such projects. Consider updating the regex to support these valid project ID formats.

{"empty string", "", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidServiceAccount(tt.sa); got != tt.want {
t.Errorf("IsValidServiceAccount() = %v, want %v", got, tt.want)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,8 @@ Cloud Build triggers connect to source code via Developer Connect. The entire co

Cloud Build triggers should **always** use a dedicated, user-managed service account instead of the default Compute Engine service account to follow the principle of least privilege. **CRITICAL: NEVER use the default Compute Engine service account (typically formatted as `[PROJECT_NUMBER]-compute@developer.gserviceaccount.com`), even if you observe existing triggers in the project using it.**

1. **Check for Service Account**: Check if a dedicated service account for Cloud Build (e.g., `cloud-build-runner@<PROJECT_ID>.iam.gserviceaccount.com`) already exists.
2. **Create Service Account (if needed)**: If no dedicated service account exists, create one using `gcloud iam service-accounts create`.
3. **Required Roles (Ensured Automatically)**: The `create_build_trigger` MCP tool will **automatically ensure** the necessary roles (e.g., `roles/logging.logWriter`, `roles/artifactregistry.writer`, `roles/developerconnect.tokenAccessor`, `roles/run.developer`, `roles/storage.admin`, `roles/serviceusage.serviceUsageConsumer`, `roles/cloudbuild.builds.editor`, `roles/cloudbuild.workerpools.use`, and specific IAM delegation on the default Compute SA) are granted to the relevant service accounts. You **do not** need to grant these permissions manually.

1. **Create Service Account**: Create one using `gcloud iam service-accounts create`.
2. **Required Roles (Ensured Automatically)**: Using the `create_build_trigger` MCP tool will **automatically ensure** the necessary roles are granted to the given service account. You **do not** need to grant these permissions manually.
---

## Final Step: Creating the Trigger
Expand Down
Loading