Skip to content

Commit

Permalink
feat(gcloud_iam): manage service account access (#58)
Browse files Browse the repository at this point in the history
* feat: introduce new resource type "service_account" in gcloud_iam provider

* feat: grant and revoke access to service account

* test: add test cases for service account provider

* refactor: use switch case

* chore: user goreleaser v1.18.2

* chore: use goreleaser v1.8.3

* chore: fix release pipeline

* fix: fix fetching grantable roles next page token

* refactor: remove additional checking

* chore: use email as service account resource name

* test: add more unit tests for GetResources

* test: add more unit tests for Grant and Revoke Access
  • Loading branch information
rahmatrhd authored Aug 31, 2023
1 parent 1740dd4 commit 18895d4
Show file tree
Hide file tree
Showing 6 changed files with 862 additions and 264 deletions.
144 changes: 107 additions & 37 deletions plugins/providers/gcloudiam/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,55 @@ func newIamClient(credentialsJSON []byte, resourceName string) (*iamClient, erro
}, nil
}

func (c *iamClient) GetRoles() ([]*Role, error) {
var roles []*Role
func (c *iamClient) ListServiceAccounts(ctx context.Context) ([]*iam.ServiceAccount, error) {
res, err := c.iamService.Projects.ServiceAccounts.List(c.resourceName).Context(ctx).Do()
if err != nil {
return nil, err
}
return res.Accounts, nil
}

ctx := context.TODO()
req := c.iamService.Roles.List()
if err := req.Pages(ctx, func(page *iam.ListRolesResponse) error {
for _, role := range page.Roles {
roles = append(roles, c.fromIamRole(role))
func (c *iamClient) GetGrantableRoles(ctx context.Context, resourceType string) ([]*iam.Role, error) {
var fullResourceName string
switch resourceType {
case ResourceTypeOrganization:
orgID := strings.Replace(c.resourceName, ResourceNameOrganizationPrefix, "", 1)
fullResourceName = fmt.Sprintf("//cloudresourcemanager.googleapis.com/organizations/%s", orgID)

case ResourceTypeProject:
projectID := strings.Replace(c.resourceName, ResourceNameProjectPrefix, "", 1)
fullResourceName = fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectID)

case ResourceTypeServiceAccount:
projectID := strings.Replace(c.resourceName, ResourceNameProjectPrefix, "", 1)
res, err := c.iamService.Projects.ServiceAccounts.List(c.resourceName).PageSize(1).Context(ctx).Do()
if err != nil {
return nil, fmt.Errorf("getting a sample of service account: %w", err)
}
return nil
}); err != nil {
return nil, err
if res.Accounts == nil || len(res.Accounts) == 0 {
return nil, fmt.Errorf("no service accounts found in project %s", projectID)
}
fullResourceName = fmt.Sprintf("//iam.googleapis.com/%s", res.Accounts[0].Name)

default:
return nil, fmt.Errorf("unknown resource type %s", resourceType)
}

if strings.HasPrefix(c.resourceName, ResourceNameProjectPrefix) {
projectRolesReq := c.iamService.Projects.Roles.List(c.resourceName)
if err := projectRolesReq.Pages(ctx, func(page *iam.ListRolesResponse) error {
for _, role := range page.Roles {
roles = append(roles, c.fromIamRole(role))
}
return nil
}); err != nil {
return nil, err
roles := []*iam.Role{}
nextPageToken := ""
for {
req := &iam.QueryGrantableRolesRequest{
FullResourceName: fullResourceName,
PageToken: nextPageToken,
}
} else if strings.HasPrefix(c.resourceName, ResourceNameOrganizationPrefix) {
orgRolesReq := c.iamService.Organizations.Roles.List(c.resourceName)
if err := orgRolesReq.Pages(ctx, func(page *iam.ListRolesResponse) error {
for _, role := range page.Roles {
roles = append(roles, c.fromIamRole(role))
}
return nil
}); err != nil {
res, err := c.iamService.Roles.QueryGrantableRoles(req).Context(ctx).Do()
if err != nil {
return nil, err
}
} else {
return nil, ErrInvalidResourceName
roles = append(roles, res.Roles...)
if nextPageToken = res.NextPageToken; nextPageToken == "" {
break
}
}

return roles, nil
Expand Down Expand Up @@ -138,6 +151,71 @@ func (c *iamClient) RevokeAccess(accountType, accountID, role string) error {
return err
}

func (c *iamClient) GrantServiceAccountAccess(ctx context.Context, sa, accountType, accountID, role string) error {
policy, err := c.iamService.Projects.ServiceAccounts.
GetIamPolicy(sa).Context(ctx).Do()
if err != nil {
return fmt.Errorf("getting IAM policy of service account %q: %w", sa, err)
}

member := fmt.Sprintf("%s:%s", accountType, accountID)
roleExists := false
for _, b := range policy.Bindings {
if b.Role == role {
if containsString(b.Members, member) {
return ErrPermissionAlreadyExists
}
b.Members = append(b.Members, member)
}
}
if !roleExists {
policy.Bindings = append(policy.Bindings, &iam.Binding{
Role: role,
Members: []string{member},
})
}

if _, err := c.iamService.Projects.ServiceAccounts.
SetIamPolicy(sa, &iam.SetIamPolicyRequest{Policy: policy}).
Context(ctx).Do(); err != nil {
return fmt.Errorf("setting IAM policy of service account %q: %w", sa, err)
}

return nil
}

func (c *iamClient) RevokeServiceAccountAccess(ctx context.Context, sa, accountType, accountID, role string) error {
policy, err := c.iamService.Projects.ServiceAccounts.
GetIamPolicy(sa).Context(ctx).Do()
if err != nil {
return fmt.Errorf("getting IAM policy of service account %q: %w", sa, err)
}

member := fmt.Sprintf("%s:%s", accountType, accountID)
for _, b := range policy.Bindings {
if b.Role == role {
removeIndex := -1
for i, m := range b.Members {
if m == member {
removeIndex = i
}
}
if removeIndex == -1 {
return ErrPermissionNotFound
}
b.Members = append(b.Members[:removeIndex], b.Members[removeIndex+1:]...)
}
}

if _, err := c.iamService.Projects.ServiceAccounts.
SetIamPolicy(sa, &iam.SetIamPolicyRequest{Policy: policy}).
Context(ctx).Do(); err != nil {
return fmt.Errorf("setting IAM policy of service account %q: %w", sa, err)
}

return nil
}

func (c *iamClient) ListAccess(ctx context.Context, resources []*domain.Resource) (domain.MapResourceAccess, error) {
policy, err := c.getIamPolicy(ctx)
if err != nil {
Expand Down Expand Up @@ -195,14 +273,6 @@ func (c *iamClient) setIamPolicy(ctx context.Context, policy *cloudresourcemanag
return nil, ErrInvalidResourceName
}

func (c *iamClient) fromIamRole(r *iam.Role) *Role {
return &Role{
Name: r.Name,
Title: r.Title,
Description: r.Description,
}
}

func containsString(arr []string, v string) bool {
for _, item := range arr {
if item == v {
Expand Down
60 changes: 28 additions & 32 deletions plugins/providers/gcloudiam/config.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package gcloudiam

import (
"context"
"encoding/base64"
"errors"
"fmt"
"strings"

"github.com/go-playground/validator/v10"
"github.com/goto/guardian/domain"
"github.com/goto/guardian/utils"
"github.com/mitchellh/mapstructure"
)

Expand Down Expand Up @@ -102,12 +104,29 @@ func (c *Config) parseAndValidate() error {
c.ProviderConfig.Credentials = credentials
}

if len(c.ProviderConfig.Resources) != 1 {
return ErrShouldHaveOneResource
if c.ProviderConfig.Resources == nil || len(c.ProviderConfig.Resources) == 0 {
return errors.New("empty resource config")
}
r := c.ProviderConfig.Resources[0]
if err := c.validateResourceConfig(r); err != nil {
validationErrors = append(validationErrors, err)
uniqueResourceTypes := make(map[string]bool)
for _, rc := range c.ProviderConfig.Resources {
if _, ok := uniqueResourceTypes[rc.Type]; ok {
validationErrors = append(validationErrors, fmt.Errorf("duplicate resource type: %q", rc.Type))
}
uniqueResourceTypes[rc.Type] = true

allowedResourceTypes := []string{}
if strings.HasPrefix(credentials.ResourceName, ResourceNameOrganizationPrefix) {
allowedResourceTypes = []string{ResourceTypeOrganization}
} else if strings.HasPrefix(credentials.ResourceName, ResourceNameProjectPrefix) {
allowedResourceTypes = []string{ResourceTypeProject, ResourceTypeServiceAccount}
}
if !utils.ContainsString(allowedResourceTypes, rc.Type) {
validationErrors = append(validationErrors, fmt.Errorf("invalid resource type: %q", rc.Type))
}

if len(rc.Roles) == 0 {
validationErrors = append(validationErrors, ErrRolesShouldNotBeEmpty)
}
}

if len(validationErrors) > 0 {
Expand Down Expand Up @@ -142,37 +161,14 @@ func (c *Config) validateCredentials(value interface{}) (*Credentials, error) {
return &credentials, nil
}

func (c *Config) validateResourceConfig(resource *domain.ResourceConfig) error {
resourceTypeValidation := fmt.Sprintf("oneof=%s %s", ResourceTypeProject, ResourceTypeOrganization)
if err := c.validator.Var(resource.Type, resourceTypeValidation); err != nil {
return err
}

if len(resource.Roles) == 0 {
return ErrRolesShouldNotBeEmpty
}

return nil
}

func (c *Config) validatePermissions(resource *domain.ResourceConfig, client GcloudIamClient) error {
iamRoles, err := client.GetRoles()
iamRoles, err := client.GetGrantableRoles(context.TODO(), resource.Type)
if err != nil {
return err
}

var roles []*domain.Role
for _, r := range iamRoles {
roles = append(roles, &domain.Role{
ID: r.Name,
Name: r.Title,
Description: r.Description,
})
}

rolesMap := make(map[string]*domain.Role)
for _, role := range roles {
rolesMap[role.ID] = role
rolesMap := make(map[string]bool)
for _, role := range iamRoles {
rolesMap[role.Name] = true
}

for _, ro := range resource.Roles {
Expand Down
Loading

0 comments on commit 18895d4

Please sign in to comment.