From d334b2a2c069fb5a7f6ae5f19ce735461874bd6d Mon Sep 17 00:00:00 2001 From: Michael Lin Date: Thu, 26 Oct 2023 08:43:45 -0700 Subject: [PATCH] msp: support custom project-level iam config (#57905) --- .../internal/stack/cloudrun/BUILD.bazel | 3 ++ .../internal/stack/cloudrun/cloudrun.go | 41 +++++++++++++++- .../internal/stack/project/project.go | 5 +- .../managedservicesplatform.go | 6 +++ dev/managedservicesplatform/spec/BUILD.bazel | 9 ++++ dev/managedservicesplatform/spec/service.go | 48 ++++++++++++++++++- .../spec/service_test.go | 45 +++++++++++++++++ dev/sg/msp/sg_msp.go | 3 +- 8 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 dev/managedservicesplatform/spec/service_test.go diff --git a/dev/managedservicesplatform/internal/stack/cloudrun/BUILD.bazel b/dev/managedservicesplatform/internal/stack/cloudrun/BUILD.bazel index 0bc112e3209..bc9ea23dab9 100644 --- a/dev/managedservicesplatform/internal/stack/cloudrun/BUILD.bazel +++ b/dev/managedservicesplatform/internal/stack/cloudrun/BUILD.bazel @@ -29,11 +29,14 @@ go_library( "//lib/errors", "//lib/pointers", "@com_github_aws_constructs_go_constructs_v10//:constructs", + "@com_github_aws_jsii_runtime_go//:jsii-runtime-go", + "@com_github_grafana_regexp//:regexp", "@com_github_hashicorp_terraform_cdk_go_cdktf//:cdktf", "@com_github_sourcegraph_managed_services_platform_cdktf_gen_google//cloudrunv2service", "@com_github_sourcegraph_managed_services_platform_cdktf_gen_google//cloudrunv2serviceiammember", "@com_github_sourcegraph_managed_services_platform_cdktf_gen_google//computenetwork", "@com_github_sourcegraph_managed_services_platform_cdktf_gen_google//computesubnetwork", + "@com_github_sourcegraph_managed_services_platform_cdktf_gen_google//projectiamcustomrole", "@com_github_sourcegraph_managed_services_platform_cdktf_gen_google//vpcaccessconnector", "@org_golang_x_exp//maps", "@org_golang_x_exp//slices", diff --git a/dev/managedservicesplatform/internal/stack/cloudrun/cloudrun.go b/dev/managedservicesplatform/internal/stack/cloudrun/cloudrun.go index 327544e2a36..e5a69b9f406 100644 --- a/dev/managedservicesplatform/internal/stack/cloudrun/cloudrun.go +++ b/dev/managedservicesplatform/internal/stack/cloudrun/cloudrun.go @@ -4,9 +4,12 @@ import ( "fmt" "strings" + "github.com/aws/jsii-runtime-go" + "github.com/grafana/regexp" "github.com/hashicorp/terraform-cdk-go/cdktf" "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/cloudrunv2service" "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/cloudrunv2serviceiammember" + "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/projectiamcustomrole" "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/googlesecretsmanager" "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resource/bigquery" @@ -117,16 +120,46 @@ func NewStack(stacks *stack.Set, vars Variables) (*Output, error) { ByteLength: 8, }) + id := resourceid.New("cloudrun") + + var customRole projectiamcustomrole.ProjectIamCustomRole + if vars.Service.IAM != nil && len(vars.Service.IAM.Permissions) > 0 { + customRole = projectiamcustomrole.NewProjectIamCustomRole(stack, id.ResourceID("custom-role"), &projectiamcustomrole.ProjectIamCustomRoleConfig{ + RoleId: pointers.Ptr(fmt.Sprintf("%s_custom_role", id.DisplayName())), + Title: pointers.Ptr(fmt.Sprintf("%s custom role", id.DisplayName())), + Project: &vars.ProjectID, + Permissions: jsii.Strings(vars.Service.IAM.Permissions...), + }) + } + // Set up configuration for the core Cloud Run service cloudRun := &cloudRunServiceBuilder{ ServiceAccount: serviceaccount.New(stack, - resourceid.New("cloudrun"), + id, serviceaccount.Config{ ProjectID: vars.ProjectID, AccountID: fmt.Sprintf("%s-sa", vars.Service.ID), DisplayName: fmt.Sprintf("%s Service Account", pointers.Deref(vars.Service.Name, vars.Service.ID)), - Roles: serviceAccountRoles, + Roles: func() []serviceaccount.Role { + if vars.Service.IAM != nil && len(vars.Service.IAM.Roles) > 0 { + var rs []serviceaccount.Role + for _, r := range vars.Service.IAM.Roles { + rs = append(rs, serviceaccount.Role{ + ID: matchNonAlphaNumericRegex.ReplaceAllString(r, "_"), + Role: r, + }) + } + serviceAccountRoles = append(rs, serviceAccountRoles...) + } + if customRole != nil { + serviceAccountRoles = append(serviceAccountRoles, serviceaccount.Role{ + ID: "role_cloudrun_custom_role", + Role: *customRole.Name(), + }) + } + return serviceAccountRoles + }(), }), DiagnosticsSecret: diagnosticsSecret, @@ -451,3 +484,7 @@ func (c cloudRunServiceBuilder) Build(stack cdktf.TerraformStack, vars Variables Volumes: c.AdditionalVolumes, }}), nil } + +var ( + matchNonAlphaNumericRegex = regexp.MustCompile("[^a-zA-Z0-9]+") +) diff --git a/dev/managedservicesplatform/internal/stack/project/project.go b/dev/managedservicesplatform/internal/stack/project/project.go index 12d93159f6a..6b4c4195860 100644 --- a/dev/managedservicesplatform/internal/stack/project/project.go +++ b/dev/managedservicesplatform/internal/stack/project/project.go @@ -83,6 +83,9 @@ type Variables struct { // EnableAuditLogs ships GCP audit logs to security cluster. // TODO: Not yet implemented EnableAuditLogs bool + + // Services is a list of additional GCP services to enable. + Services []string } const StackName = "project" @@ -144,7 +147,7 @@ func NewStack(stacks *stack.Set, vars Variables) (*Output, error) { }), } - for _, service := range gcpServices { + for _, service := range append(gcpServices, vars.Services...) { projectservice.NewProjectService(stack, id.ResourceID("project-service-%s", strings.ReplaceAll(service, ".", "-")), &projectservice.ProjectServiceConfig{ diff --git a/dev/managedservicesplatform/managedservicesplatform.go b/dev/managedservicesplatform/managedservicesplatform.go index 7b787c4b1c9..4132c707100 100644 --- a/dev/managedservicesplatform/managedservicesplatform.go +++ b/dev/managedservicesplatform/managedservicesplatform.go @@ -84,6 +84,12 @@ func (r *Renderer) RenderEnvironment( "environment": env.ID, "msp": "true", }, + Services: func() []string { + if svc.IAM != nil && len(svc.IAM.Services) > 0 { + return svc.IAM.Services + } + return nil + }(), }) if err != nil { return nil, errors.Wrap(err, "failed to create project stack") diff --git a/dev/managedservicesplatform/spec/BUILD.bazel b/dev/managedservicesplatform/spec/BUILD.bazel index c707449ee2f..2cb335ea3ea 100644 --- a/dev/managedservicesplatform/spec/BUILD.bazel +++ b/dev/managedservicesplatform/spec/BUILD.bazel @@ -1,3 +1,4 @@ +load("//dev:go_defs.bzl", "go_test") load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( @@ -12,6 +13,14 @@ go_library( visibility = ["//visibility:public"], deps = [ "//lib/errors", + "@com_github_grafana_regexp//:regexp", "@io_k8s_sigs_yaml//:yaml", ], ) + +go_test( + name = "spec_test", + srcs = ["service_test.go"], + embed = [":spec"], + deps = ["@com_github_stretchr_testify//assert"], +) diff --git a/dev/managedservicesplatform/spec/service.go b/dev/managedservicesplatform/spec/service.go index 2c119e690fe..a2e529a0bae 100644 --- a/dev/managedservicesplatform/spec/service.go +++ b/dev/managedservicesplatform/spec/service.go @@ -1,6 +1,10 @@ package spec -import "github.com/sourcegraph/sourcegraph/lib/errors" +import ( + "github.com/grafana/regexp" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) type ServiceSpec struct { // ID is an all-lowercase, hyphen-delimited identifier for the service, @@ -25,6 +29,10 @@ type ServiceSpec struct { // ProjectIDSuffixLength can be configured to truncate the length of the // service's generated project IDs. ProjectIDSuffixLength *int `json:"projectIDSuffixLength,omitempty"` + + // IAM is an optional IAM configuration for the service account on the + // service's GCP project. + IAM *ServiceIAMSpec `json:"iam,omitempty"` } func (s ServiceSpec) Validate() []error { @@ -34,6 +42,10 @@ func (s ServiceSpec) Validate() []error { errs = append(errs, errors.New("projectIDSuffixLength must be >= 4")) } + if s.IAM != nil { + errs = append(errs, s.IAM.Validate()...) + } + // TODO: Add validation return errs } @@ -41,3 +53,37 @@ func (s ServiceSpec) Validate() []error { type Protocol string const ProtocolH2C Protocol = "h2c" + +type ServiceIAMSpec struct { + // Services is a list of GCP services to enable in the service's project. + Services []string `json:"services,omitempty"` + + // Roles is a list of IAM roles to grant to the service account. + Roles []string `json:"roles,omitempty"` + // Permissions is a list of IAM permissions to grant to the service account. + // + // MSP will create a custom role with these permissions and grant it to the + // service account. + Permissions []string `json:"permissions,omitempty"` +} + +func (s ServiceIAMSpec) Validate() []error { + var errs []error + + for _, role := range s.Roles { + if !validIAMRole(role) { + errs = append(errs, errors.Errorf("invalid IAM role %q, must be one of custom role or predefined role", role)) + } + } + + return errs +} + +func validIAMRole(role string) bool { + return matchCustomRole.MatchString(role) || matchPredefinedRole.MatchString(role) +} + +var ( + matchCustomRole = regexp.MustCompile(`^(projects|organizations)/[a-z0-9_-]+/roles/[a-zA-Z_\.]+$`) + matchPredefinedRole = regexp.MustCompile(`^roles/[a-zA-Z\.]+$`) +) diff --git a/dev/managedservicesplatform/spec/service_test.go b/dev/managedservicesplatform/spec/service_test.go new file mode 100644 index 00000000000..7dd528778e1 --- /dev/null +++ b/dev/managedservicesplatform/spec/service_test.go @@ -0,0 +1,45 @@ +package spec + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidIAMRole(t *testing.T) { + tests := []struct { + name string + role string + ok bool + }{ + { + name: "project custom role", + role: "projects/project-id/roles/custom_role", + ok: true, + }, + { + name: "organization custom role", + role: "organizations/org-id/roles/custom_role", + ok: true, + }, + { + name: "predefined role", + role: "roles/iam.getIamPolicy", + ok: true, + }, + { + name: "invalid role", + role: "invalid-role", + }, + { + name: "invalid role", + role: "projects/project-id/roles/a-b-c", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.ok, validIAMRole(tt.role)) + }) + } +} diff --git a/dev/sg/msp/sg_msp.go b/dev/sg/msp/sg_msp.go index 6a6c7973b25..53851aee642 100644 --- a/dev/sg/msp/sg_msp.go +++ b/dev/sg/msp/sg_msp.go @@ -107,7 +107,8 @@ func init() { Disabled: pointers.Ptr(true), }, Env: map[string]string{ - "SRC_LOG_LEVEL": "info", + "SRC_LOG_LEVEL": "info", + "SRC_LOG_FORMAT": "json_gcp", }, }, },