Skip to content

Commit da645fe

Browse files
alex-doudouharoonc
authored andcommitted
Enforce mandatory service account in create_build_trigger tool
1 parent 66df7ea commit da645fe

File tree

2 files changed

+13
-29
lines changed

2 files changed

+13
-29
lines changed

cicd-mcp-server/cloudbuild/cloudbuild.go

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type CreateTriggerArgs struct {
9191
Location string `json:"location" jsonschema:"The Google Cloud location for the trigger."`
9292
TriggerID string `json:"trigger_id" jsonschema:"The ID of the trigger."`
9393
RepoLink string `json:"repo_link" jsonschema:"The Developer Connect repository link, use dev connect setup repo to create a connect and repo link"`
94-
ServiceAccount string `json:"service_account,omitempty" jsonschema:"The service account to use for the build. E.g. serviceAccount:name@project-id.iam.gserviceaccount.com optional"`
94+
ServiceAccount string `json:"service_account" jsonschema:"The service account to use for the build. E.g. serviceAccount:name@project-id.iam.gserviceaccount.com. This MUST be a dedicated service account, not the default Compute Engine service account."`
9595
Branch string `json:"branch,omitempty" jsonschema:"Create builds on push to branch. Should be regex e.g. '^main$'"`
9696
Tag string `json:"tag,omitempty" jsonschema:"Create builds on new tag push. Should be regex e.g. '^nightly$'"`
9797
}
@@ -100,10 +100,13 @@ var createTriggerToolFunc func(ctx context.Context, req *mcp.CallToolRequest, ar
100100

101101
func addCreateTriggerTool(server *mcp.Server, cbClient cloudbuildclient.CloudBuildClient, iamClient iamclient.IAMClient, rmClient resourcemanagerclient.ResourcemanagerClient) {
102102
createTriggerToolFunc = func(ctx context.Context, req *mcp.CallToolRequest, args CreateTriggerArgs) (*mcp.CallToolResult, any, error) {
103-
if args.ServiceAccount != "" && !strings.HasPrefix(args.ServiceAccount, "serviceAccount:") {
103+
if args.ServiceAccount == "" {
104+
return &mcp.CallToolResult{}, nil, fmt.Errorf("service_account is required and must be a dedicated service account")
105+
}
106+
if !strings.HasPrefix(args.ServiceAccount, "serviceAccount:") {
104107
args.ServiceAccount = fmt.Sprintf("serviceAccount:%s", args.ServiceAccount)
105108
}
106-
if args.ServiceAccount != "" && !IsValidServiceAccount(args.ServiceAccount) {
109+
if !IsValidServiceAccount(args.ServiceAccount) {
107110
return &mcp.CallToolResult{}, nil, fmt.Errorf("service account needs to be of the form serviceAccount:name@project-id.iam.gserviceaccount.com")
108111
}
109112
resolvedSA, err := setPermissionsForCloudBuildSA(ctx, args.ProjectID, args.ServiceAccount, rmClient, iamClient)
@@ -119,18 +122,9 @@ func addCreateTriggerTool(server *mcp.Server, cbClient cloudbuildclient.CloudBui
119122
mcp.AddTool(server, &mcp.Tool{Name: "create_build_trigger", Description: "Creates a new Cloud Build trigger."}, createTriggerToolFunc)
120123
}
121124

122-
// setPermissionsForSA resolves the SA (if default) and grants it a role.
123-
// It creates and manages its own Resource Manager client.
125+
// setPermissionsForSA resolves the SA and grants it a role.
124126
func setPermissionsForCloudBuildSA(ctx context.Context, projectID, serviceAccount string, rmClient resourcemanagerclient.ResourcemanagerClient, iamClient iamclient.IAMClient) (string, error) {
125-
// Construct the Compute Engine default service account email
126127
resolvedSA := serviceAccount
127-
if resolvedSA == "" {
128-
projectNumber, err := rmClient.ToProjectNumber(ctx, projectID)
129-
if err != nil {
130-
return "", fmt.Errorf("unable to resolve project id to number: %w", err)
131-
}
132-
resolvedSA = fmt.Sprintf("serviceAccount:%d-compute@developer.gserviceaccount.com", projectNumber)
133-
}
134128

135129
// If the serviceAccount prefix is not there, add it.
136130
if !strings.HasPrefix(resolvedSA, "serviceAccount:") {

cicd-mcp-server/cloudbuild/cloudbuild_test.go

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,13 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
5454
assert.Equal(t, serviceAccount, resolvedSA)
5555
})
5656

57-
t.Run("without service account", func(t *testing.T) {
58-
projectNumber := int64(12345)
59-
expectedSA := fmt.Sprintf("serviceAccount:%d-compute@developer.gserviceaccount.com", projectNumber)
57+
t.Run("no service account provided fails", func(t *testing.T) {
58+
// Since fallback is removed, providing an empty SA should result in an invalid SA prefix
59+
// and fail during IAM role binding.
60+
mockIAMClient.EXPECT().AddIAMRoleBinding(gomock.Any(), gomock.Any(), gomock.Any(), "serviceAccount:").Return(nil, fmt.Errorf("invalid member"))
6061

61-
mockRMClient.EXPECT().ToProjectNumber(ctx, projectID).Return(projectNumber, nil)
62-
mockIAMClient.EXPECT().AddIAMRoleBinding(ctx, fmt.Sprintf("projects/%s", projectID), "roles/developerconnect.tokenAccessor", expectedSA).Return(nil, nil)
63-
64-
resolvedSA, err := setPermissionsForCloudBuildSA(ctx, projectID, "", mockRMClient, mockIAMClient)
65-
assert.NoError(t, err)
66-
assert.Equal(t, expectedSA, resolvedSA)
62+
_, err := setPermissionsForCloudBuildSA(ctx, projectID, "", mockRMClient, mockIAMClient)
63+
assert.Error(t, err)
6764
})
6865

6966
t.Run("iam error", func(t *testing.T) {
@@ -72,11 +69,4 @@ func TestSetPermissionsForCloudBuildSA(t *testing.T) {
7269
_, err := setPermissionsForCloudBuildSA(ctx, projectID, serviceAccount, mockRMClient, mockIAMClient)
7370
assert.Error(t, err)
7471
})
75-
76-
t.Run("resourcemanager error", func(t *testing.T) {
77-
mockRMClient.EXPECT().ToProjectNumber(gomock.Any(), gomock.Any()).Return(int64(0), fmt.Errorf("some error"))
78-
79-
_, err := setPermissionsForCloudBuildSA(ctx, projectID, "", mockRMClient, mockIAMClient)
80-
assert.Error(t, err)
81-
})
8272
}

0 commit comments

Comments
 (0)