msp: support custom project-level iam config (#57905)

This commit is contained in:
Michael Lin 2023-10-26 08:43:45 -07:00 committed by GitHub
parent cc4f7ee9b6
commit d334b2a2c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 5 deletions

View File

@ -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",

View File

@ -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]+")
)

View File

@ -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{

View File

@ -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")

View File

@ -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"],
)

View File

@ -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\.]+$`)
)

View File

@ -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))
})
}
}

View File

@ -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",
},
},
},