Skip to content

Commit

Permalink
Merge pull request #27 from nais/teamrole
Browse files Browse the repository at this point in the history
Add custom team role
  • Loading branch information
jhrv authored Feb 20, 2025
2 parents 99060b4 + 56111cd commit d5d4a02
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 28 deletions.
13 changes: 9 additions & 4 deletions internal/reconcilers/google/gcp/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,17 @@ func (r *googleGcpReconciler) Reconcile(ctx context.Context, client *apiclient.A
return fmt.Errorf("create CNRM service account for project %q for team %q in environment %q: %w", teamProject.ProjectId, naisTeam.Slug, env.EnvironmentName, err)
}

role, err := r.createCNRMRole(ctx, teamProject.ProjectId)
cnrmRole, err := r.createCNRMRole(ctx, teamProject.ProjectId)
if err != nil {
return fmt.Errorf("create CNRM role for project %q for team %q in environment %q: %w", teamProject.ProjectId, naisTeam.Slug, env.EnvironmentName, err)
}
cnrmRoleName := role.Name

if err := r.setProjectPermissions(ctx, teamProject, naisTeam, cluster.ProjectID, cnrmServiceAccount, cnrmRoleName); err != nil {
teamRole, err := r.createTeamRole(ctx, teamProject.ProjectId)
if err != nil {
return fmt.Errorf("create team role for project %q for team %q in environment %q: %w", teamProject.ProjectId, naisTeam.Slug, env.EnvironmentName, err)
}

if err := r.setProjectPermissions(ctx, teamProject, naisTeam, cluster.ProjectID, cnrmServiceAccount, cnrmRole.Name, teamRole.Name); err != nil {
return fmt.Errorf("set group permissions to project %q for team %q in environment %q: %w", teamProject.ProjectId, naisTeam.Slug, env.EnvironmentName, err)
}

Expand Down Expand Up @@ -414,7 +418,7 @@ func (r *googleGcpReconciler) getOrCreateProject(ctx context.Context, projectID

// setProjectPermissions Make sure that the project has the necessary permissions, and don't remove permissions we don't
// control
func (r *googleGcpReconciler) setProjectPermissions(ctx context.Context, teamProject *cloudresourcemanager.Project, naisTeam *protoapi.Team, clusterProjectID string, cnrmServiceAccount *iam.ServiceAccount, cnrmRoleName string) error {
func (r *googleGcpReconciler) setProjectPermissions(ctx context.Context, teamProject *cloudresourcemanager.Project, naisTeam *protoapi.Team, clusterProjectID string, cnrmServiceAccount *iam.ServiceAccount, cnrmRoleName, teamRoleName string) error {
member := "serviceAccount:" + clusterProjectID + ".svc.id.goog[cnrm-system/cnrm-controller-manager-" + naisTeam.Slug + "]"
_, err := r.gcpServices.IamProjectsServiceAccountsService.SetIamPolicy(cnrmServiceAccount.Name, &iam.SetIamPolicyRequest{
Policy: &iam.Policy{
Expand All @@ -438,6 +442,7 @@ func (r *googleGcpReconciler) setProjectPermissions(ctx context.Context, teamPro
newBindings, updated := CalculateRoleBindings(policy.Bindings, map[string][]string{
"roles/owner": {"group:" + *naisTeam.GoogleGroupEmail},
cnrmRoleName: {"serviceAccount:" + cnrmServiceAccount.Email},
teamRoleName: {"group:" + *naisTeam.GoogleGroupEmail},
})

if !updated {
Expand Down
42 changes: 38 additions & 4 deletions internal/reconcilers/google/gcp/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ func TestReconcile(t *testing.T) {
},
}
expectedTeamProjectID := "slug-prod-ea99"
expectedRoleId := "CustomCNRMRole"
expectedCnrmRoleName := "projects/slug-prod-ea99/roles/" + expectedRoleId
expectedCNRMRoleId := "CustomCNRMRole"
expectedTeamRoleId := "CustomTeamRole"
expectedCnrmRoleName := "projects/slug-prod-ea99/roles/" + expectedCNRMRoleId
expectedTeamRoleName := "projects/slug-prod-ea99/roles/" + expectedTeamRoleId
flags := config.FeatureFlags{
AttachSharedVpc: true,
}
Expand Down Expand Up @@ -292,8 +294,8 @@ func TestReconcile(t *testing.T) {
payload := iam.CreateRoleRequest{}
_ = json.NewDecoder(r.Body).Decode(&payload)

if payload.RoleId != expectedRoleId {
t.Errorf("expected role id %q, got %q", expectedRoleId, payload.RoleId)
if payload.RoleId != expectedCNRMRoleId {
t.Errorf("expected role id %q, got %q", expectedCNRMRoleId, payload.RoleId)
}

if expected := 35; payload.Role.IncludedPermissions != nil && len(payload.Role.IncludedPermissions) != expected {
Expand All @@ -306,6 +308,37 @@ func TestReconcile(t *testing.T) {
_, _ = w.Write(resp)
},

// get existing custom team role
func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected HTTP GET, got: %q", r.Method)
}
w.WriteHeader(404)
},

// create custom team role
func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected HTTP POST, got: %q", r.Method)
}

payload := iam.CreateRoleRequest{}
_ = json.NewDecoder(r.Body).Decode(&payload)

if payload.RoleId != expectedTeamRoleId {
t.Errorf("expected role id %q, got %q", expectedTeamRoleId, payload.RoleId)
}

if expected := 32; payload.Role.IncludedPermissions != nil && len(payload.Role.IncludedPermissions) != expected {
t.Errorf("expected %d permissions, got %d", expected, len(payload.Role.IncludedPermissions))
}

payload.Role.Name = expectedTeamRoleName

resp, _ := payload.Role.MarshalJSON()
_, _ = w.Write(resp)
},

// set workload identity for service account
func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
Expand Down Expand Up @@ -347,6 +380,7 @@ func TestReconcile(t *testing.T) {
expectedBindings := map[string]string{
payload.Policy.Bindings[0].Role: payload.Policy.Bindings[0].Members[0],
payload.Policy.Bindings[1].Role: payload.Policy.Bindings[1].Members[0],
payload.Policy.Bindings[2].Role: payload.Policy.Bindings[2].Members[0],
}

if expectedBindings["roles/owner"] != "group:[email protected]" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,74 @@ import (
"google.golang.org/api/iam/v1"
)

const CNRMRoleId = "CustomCNRMRole"

func (r *googleGcpReconciler) createCNRMRole(ctx context.Context, projectId string) (*iam.Role, error) {
func (r *googleGcpReconciler) createOrUpdateRole(ctx context.Context, projectId, roleId string, role *iam.Role) (*iam.Role, error) {
parent := fmt.Sprintf("projects/%s", projectId)
name := fmt.Sprintf("projects/%s/roles/%s", projectId, CNRMRoleId)
name := fmt.Sprintf("projects/%s/roles/%s", projectId, roleId)
existingRole, _ := r.gcpServices.ProjectsRolesService.Get(name).Context(ctx).Do()

if existingRole == nil {
req := &iam.CreateRoleRequest{
Role: role,
RoleId: roleId,
}
return r.gcpServices.ProjectsRolesService.Create(parent, req).Context(ctx).Do()
}

slices.Sort(existingRole.IncludedPermissions)
slices.Sort(role.IncludedPermissions)

if !slices.Equal(existingRole.IncludedPermissions, role.IncludedPermissions) {
return r.gcpServices.ProjectsRolesService.Patch(name, role).Context(ctx).Do()
}

return existingRole, nil
}

func (r *googleGcpReconciler) createTeamRole(ctx context.Context, projectId string) (*iam.Role, error) {
role := &iam.Role{
Title: "NAIS Custom Team Role",
Description: "Custom role for members of a Nais team",
Stage: "GA",
IncludedPermissions: []string{
"cloudsql.databases.delete",
"cloudsql.databases.get",
"cloudsql.databases.list",
"cloudsql.databases.update",
"cloudsql.instances.delete",
"cloudsql.instances.get",
"cloudsql.instances.list",
"cloudsql.instances.update",
"cloudsql.users.create",
"cloudsql.users.delete",
"cloudsql.users.list",
"cloudsql.users.update",
"resourcemanager.projects.get",
"resourcemanager.projects.list",
"resourcemanager.projects.getIamPolicy",
"storage.buckets.get",
"storage.buckets.getIamPolicy",
"storage.buckets.list",
"storage.buckets.update",
"storage.buckets.delete",
"bigquery.datasets.get",
"bigquery.datasets.getIamPolicy",
"bigquery.models.getData",
"bigquery.models.getMetadata",
"bigquery.models.list",
"bigquery.routines.get",
"bigquery.routines.list",
"bigquery.tables.get",
"bigquery.tables.getData",
"bigquery.tables.getIamPolicy",
"bigquery.tables.list",
"bigquery.tables.replicateData",
},
}

return r.createOrUpdateRole(ctx, projectId, "CustomTeamRole", role)
}

func (r *googleGcpReconciler) createCNRMRole(ctx context.Context, projectId string) (*iam.Role, error) {
role := &iam.Role{
Title: "NAIS Custom CNRM Role",
Description: "Custom role for namespaced CNRM users to allow creation of GCP resources",
Expand Down Expand Up @@ -58,20 +119,5 @@ func (r *googleGcpReconciler) createCNRMRole(ctx context.Context, projectId stri
},
}

if existingRole == nil {
req := &iam.CreateRoleRequest{
Role: role,
RoleId: CNRMRoleId,
}
return r.gcpServices.ProjectsRolesService.Create(parent, req).Context(ctx).Do()
}

slices.Sort(existingRole.IncludedPermissions)
slices.Sort(role.IncludedPermissions)

if !slices.Equal(existingRole.IncludedPermissions, role.IncludedPermissions) {
return r.gcpServices.ProjectsRolesService.Patch(name, role).Context(ctx).Do()
}

return existingRole, nil
return r.createOrUpdateRole(ctx, projectId, "CustomCNRMRole", role)
}

0 comments on commit d5d4a02

Please sign in to comment.