From 72a397e40cce07950a2707155022873af8d67b49 Mon Sep 17 00:00:00 2001 From: Haveiss Date: Fri, 31 Mar 2023 15:58:48 +0700 Subject: [PATCH 1/3] refactor: draft poc --- domain/resource.go | 4 + plugins/providers/newpoc/client.go | 217 ++++++++++++++ plugins/providers/newpoc/client_test.go | 168 +++++++++++ plugins/providers/newpoc/config.go | 117 ++++++++ plugins/providers/newpoc/config_test.go | 367 ++++++++++++++++++++++++ plugins/providers/newpoc/errors.go | 15 + plugins/providers/newpoc/plugin.go | 116 ++++++++ plugins/providers/newpoc/resource.go | 6 + 8 files changed, 1010 insertions(+) create mode 100644 plugins/providers/newpoc/client.go create mode 100644 plugins/providers/newpoc/client_test.go create mode 100644 plugins/providers/newpoc/config.go create mode 100644 plugins/providers/newpoc/config_test.go create mode 100644 plugins/providers/newpoc/errors.go create mode 100644 plugins/providers/newpoc/plugin.go create mode 100644 plugins/providers/newpoc/resource.go diff --git a/domain/resource.go b/domain/resource.go index 85074a8f4..947d16a6b 100644 --- a/domain/resource.go +++ b/domain/resource.go @@ -19,6 +19,10 @@ type Resource struct { Children []*Resource `json:"children,omitempty" yaml:"children,omitempty"` } +func (r *Resource) GetType() string { + return r.Type +} + func (r *Resource) GetFlattened() []*Resource { resources := []*Resource{r} for _, child := range r.Children { diff --git a/plugins/providers/newpoc/client.go b/plugins/providers/newpoc/client.go new file mode 100644 index 000000000..ac135b8a3 --- /dev/null +++ b/plugins/providers/newpoc/client.go @@ -0,0 +1,217 @@ +package newpoc + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/goto/guardian/domain" + "github.com/mitchellh/mapstructure" + "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/iam/v1" +) + +const ( + AccountTypeUser = "user" + AccountTypeServiceAccount = "serviceAccount" + AccountTypeGroup = "group" + + ResourceNameOrganizationPrefix = "organizations/" + ResourceNameProjectPrefix = "projects/" +) + +// Client implements BasicProviderClient +type Client struct { + providerConfig *domain.ProviderConfig + cloudResourceManagerService *cloudresourcemanager.Service + iamService *iam.Service +} + +type ClientDependencies struct { + ProviderConfig *domain.ProviderConfig + CloudResourceManagerService *cloudresourcemanager.Service + IamService *iam.Service +} + +func NewClient(deps *ClientDependencies) (*Client, error) { + if deps == nil { + return nil, errors.New("dependencies can't be nil") + } + + if deps.ProviderConfig == nil { + return nil, errors.New("provider config can't be nil") + } + + if deps.CloudResourceManagerService == nil { + return nil, errors.New("cloud resource manager service can't be nil") + } + + if deps.IamService == nil { + return nil, errors.New("iam service can't be nil") + } + + c := &Client{ + providerConfig: deps.ProviderConfig, + cloudResourceManagerService: deps.CloudResourceManagerService, + iamService: deps.IamService, + } + + return c, nil +} + +func (c *Client) GetAllowedAccountTypes(ctx context.Context) []string { + return []string{ + AccountTypeUser, + AccountTypeServiceAccount, + AccountTypeGroup, + } +} + +func (c *Client) ListResources(ctx context.Context) ([]IResource, error) { + var creds credentials + if err := mapstructure.Decode(c.providerConfig.Credentials, &creds); err != nil { + return nil, err + } + + var t string + if strings.HasPrefix(creds.ResourceName, "project") { + t = ResourceTypeProject + } else if strings.HasPrefix(creds.ResourceName, "organization") { + t = ResourceTypeOrganization + } + + return []IResource{ + &domain.Resource{ + ProviderType: c.providerConfig.Type, + ProviderURN: c.providerConfig.URN, + Type: t, + URN: creds.ResourceName, + Name: fmt.Sprintf("%s - GCP IAM", creds.ResourceName), + }, + }, nil +} + +func (c *Client) GrantAccess(ctx context.Context, r IResource, accountID string, permissions []string) error { + var creds credentials + if err := mapstructure.Decode(c.providerConfig.Credentials, &creds); err != nil { + return err + } + + if r.GetType() == ResourceTypeProject || r.GetType() == ResourceTypeOrganization { + for _, permission := range permissions { + policy, err := c.getIamPolicy(ctx, creds.ResourceName) + if err != nil { + return err + } + + member := accountID + roleExists := false + for _, b := range policy.Bindings { + if b.Role == permission { + roleExists = true + if containsString(b.Members, member) { + // Permission already exists + continue + } + b.Members = append(b.Members, member) + } + } + if !roleExists { + policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ + Role: permission, + Members: []string{member}, + }) + } + + _, err = c.setIamPolicy(ctx, creds.ResourceName, policy) + if err != nil { + return err + } + } + } + return ErrInvalidResourceType +} + +func (c *Client) RevokeAccess(ctx context.Context, r IResource, accountID string, permissions []string) error { + var creds credentials + if err := mapstructure.Decode(c.providerConfig.Credentials, &creds); err != nil { + return err + } + + if r.GetType() == ResourceTypeProject || r.GetType() == ResourceTypeOrganization { + for _, permission := range permissions { + policy, err := c.getIamPolicy(ctx, creds.ResourceName) + if err != nil { + return err + } + + member := accountID + + for _, b := range policy.Bindings { + if b.Role == permission { + removeIndex := -1 + for i, m := range b.Members { + if m == member { + removeIndex = i + } + } + if removeIndex == -1 { + // permission doesn't exist + continue + } + b.Members = append(b.Members[:removeIndex], b.Members[removeIndex+1:]...) + } + } + + c.setIamPolicy(ctx, creds.ResourceName, policy) + return err + } + + return nil + } + + return ErrInvalidResourceType +} + +func (c *Client) getIamPolicy(ctx context.Context, resourceName string) (*cloudresourcemanager.Policy, error) { + if strings.HasPrefix(resourceName, ResourceNameProjectPrefix) { + projectID := strings.Replace(resourceName, ResourceNameProjectPrefix, "", 1) + return c.cloudResourceManagerService.Projects. + GetIamPolicy(projectID, &cloudresourcemanager.GetIamPolicyRequest{}). + Context(ctx).Do() + } else if strings.HasPrefix(resourceName, ResourceNameOrganizationPrefix) { + orgID := strings.Replace(resourceName, ResourceNameOrganizationPrefix, "", 1) + return c.cloudResourceManagerService.Organizations. + GetIamPolicy(orgID, &cloudresourcemanager.GetIamPolicyRequest{}). + Context(ctx).Do() + } + return nil, ErrInvalidResourceName +} + +func (c *Client) setIamPolicy(ctx context.Context, resourceName string, policy *cloudresourcemanager.Policy) (*cloudresourcemanager.Policy, error) { + setIamPolicyRequest := &cloudresourcemanager.SetIamPolicyRequest{ + Policy: policy, + } + if strings.HasPrefix(resourceName, ResourceNameProjectPrefix) { + projectID := strings.Replace(resourceName, ResourceNameProjectPrefix, "", 1) + return c.cloudResourceManagerService.Projects. + SetIamPolicy(projectID, setIamPolicyRequest). + Context(ctx).Do() + } else if strings.HasPrefix(resourceName, ResourceNameOrganizationPrefix) { + orgID := strings.Replace(resourceName, ResourceNameOrganizationPrefix, "", 1) + return c.cloudResourceManagerService.Organizations. + SetIamPolicy(orgID, setIamPolicyRequest). + Context(ctx).Do() + } + return nil, ErrInvalidResourceName +} + +func containsString(arr []string, v string) bool { + for _, item := range arr { + if item == v { + return true + } + } + return false +} diff --git a/plugins/providers/newpoc/client_test.go b/plugins/providers/newpoc/client_test.go new file mode 100644 index 000000000..fca38dcdd --- /dev/null +++ b/plugins/providers/newpoc/client_test.go @@ -0,0 +1,168 @@ +package newpoc_test + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/goto/guardian/domain" + "github.com/goto/guardian/plugins/providers/newpoc" + "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/iam/v1" +) + +func TestClient_GetAllowedAccountTypes(t *testing.T) { + type fields struct { + providerConfig *domain.ProviderConfig + cloudResourceManagerService *cloudresourcemanager.Service + iamService *iam.Service + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want []string + }{ + { + name: "should return allowed account types", + fields: fields{ + providerConfig: &domain.ProviderConfig{}, + cloudResourceManagerService: &cloudresourcemanager.Service{}, + iamService: &iam.Service{}, + }, + args: args{ + ctx: context.Background(), + }, + want: []string{ + newpoc.AccountTypeUser, + newpoc.AccountTypeServiceAccount, + newpoc.AccountTypeGroup, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := newpoc.NewClient( + &newpoc.ClientDependencies{ + ProviderConfig: tt.fields.providerConfig, + CloudResourceManagerService: tt.fields.cloudResourceManagerService, + IamService: tt.fields.iamService, + }, + ) + if err != nil { + t.Errorf("NewClient() error = %v", err) + return + } + + if got := c.GetAllowedAccountTypes(tt.args.ctx); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Client.GetAllowedAccountTypes() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClient_ListResources(t *testing.T) { + type fields struct { + providerConfig *domain.ProviderConfig + cloudResourceManagerService *cloudresourcemanager.Service + iamService *iam.Service + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want []newpoc.IResource + wantErr bool + }{ + { + name: "should return list of resources with type project", + fields: fields{ + providerConfig: &domain.ProviderConfig{ + Type: "newpoc", + URN: "newpoc", + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + }, + cloudResourceManagerService: &cloudresourcemanager.Service{}, + iamService: &iam.Service{}, + }, + args: args{ + ctx: context.Background(), + }, + want: []newpoc.IResource{ + &domain.Resource{ + ProviderType: "newpoc", + ProviderURN: "newpoc", + Type: newpoc.ResourceTypeProject, + URN: "projects/test", + Name: fmt.Sprintf("%s - GCP IAM", "projects/test"), + }, + }, + }, + { + name: "should return list of resources with type organization", + fields: fields{ + providerConfig: &domain.ProviderConfig{ + Type: "newpoc", + URN: "newpoc", + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "organizations/test", + }, + }, + cloudResourceManagerService: &cloudresourcemanager.Service{}, + iamService: &iam.Service{}, + }, + args: args{ + ctx: context.Background(), + }, + want: []newpoc.IResource{ + &domain.Resource{ + ProviderType: "newpoc", + ProviderURN: "newpoc", + Type: newpoc.ResourceTypeOrganization, + URN: "organizations/test", + Name: fmt.Sprintf("%s - GCP IAM", "organizations/test"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := newpoc.NewClient( + &newpoc.ClientDependencies{ + ProviderConfig: tt.fields.providerConfig, + CloudResourceManagerService: tt.fields.cloudResourceManagerService, + IamService: tt.fields.iamService, + }, + ) + if err != nil { + t.Errorf("NewClient() error = %v", err) + return + } + + got, err := c.ListResources(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("Client.ListResources() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != len(tt.want) { + t.Errorf("Client.ListResources() has %v elements, want %v elements", len(got), len(tt.want)) + } + for i := range got { + if !reflect.DeepEqual(got[i], tt.want[i]) { + t.Errorf("Client.ListResources()[%v] = %v, want %v", i, got[i], tt.want[i]) + } + } + }) + } +} diff --git a/plugins/providers/newpoc/config.go b/plugins/providers/newpoc/config.go new file mode 100644 index 000000000..301dfcd04 --- /dev/null +++ b/plugins/providers/newpoc/config.go @@ -0,0 +1,117 @@ +package newpoc + +import ( + "context" + "errors" + "fmt" + + "github.com/go-playground/validator/v10" + "github.com/goto/guardian/domain" + "github.com/mitchellh/mapstructure" +) + +var ( + ErrShouldHaveOneResource = errors.New("gcloud_iam should have one resource") + ErrInvalidCredentials = errors.New("invalid credentials type") + ErrRolesShouldNotBeEmpty = errors.New("gcloud_iam provider should not have empty roles") + ErrProviderShouldNotBeNil = errors.New("provider should not be nil") + + resourceTypeValidation = fmt.Sprintf("oneof=%s %s", ResourceTypeProject, ResourceTypeOrganization) +) + +type credentials struct { + ServiceAccountKey string `mapstructure:"service_account_key" json:"service_account_key" validate:"required"` + ResourceName string `mapstructure:"resource_name" json:"resource_name" validate:"startswith=projects/|startswith=organizations/"` +} + +func (c *credentials) Decode(v interface{}) error { + return mapstructure.Decode(v, c) +} + +func (c *credentials) Validate(validator *validator.Validate) error { + if err := validator.Struct(c); err != nil { + return err + } + return nil +} + +// ConfigManager implements IConfigManager interface +type ConfigManager struct { + validator *validator.Validate + crypto domain.Crypto +} + +// NewConfigManager returns a new ConfigManager +func NewConfigManager(validator *validator.Validate, crypto domain.Crypto) *ConfigManager { + return &ConfigManager{ + validator: validator, + crypto: crypto, + } +} + +func (m ConfigManager) Validate(ctx context.Context, p *domain.Provider) error { + if p == nil { + return ErrProviderShouldNotBeNil + } + + // validate credentials + creds := new(credentials) + if err := creds.Decode(p.Config.Credentials); err != nil { + return fmt.Errorf("decoding credentials: %w", err) + } + if err := creds.Validate(m.validator); err != nil { + return fmt.Errorf("validating credentials: %w", err) + } + + // validate resource config + if len(p.Config.Resources) != 1 { + return ErrShouldHaveOneResource + } + rc := p.Config.Resources[0] + if err := m.validator.Var(rc.Type, resourceTypeValidation); err != nil { + return fmt.Errorf("validating resource type %q: %w", rc.Type, err) + } + if len(rc.Roles) == 0 { + return ErrRolesShouldNotBeEmpty + } + + return nil +} + +func (m ConfigManager) Encrypt(ctx context.Context, p *domain.Provider) error { + credentials := new(credentials) + if err := credentials.Decode(p.Config.Credentials); err != nil { + return ErrInvalidCredentials + } + + // TODO: check if creds value is the decrypted one + + encryptedSA, err := m.crypto.Encrypt(credentials.ServiceAccountKey) + if err != nil { + return err + } + + credentials.ServiceAccountKey = encryptedSA + p.Config.Credentials = credentials + + return nil +} + +func (m ConfigManager) Decrypt(ctx context.Context, p *domain.Provider) error { + credentials := new(credentials) + if err := credentials.Decode(p.Config.Credentials); err != nil { + return ErrInvalidCredentials + } + + // TODO: check if creds value is the encrypted one + + decryptedSA, err := m.crypto.Decrypt(credentials.ServiceAccountKey) + if err != nil { + return err + } + + credentials.ServiceAccountKey = decryptedSA + p.Config.Credentials = credentials + + return nil +} diff --git a/plugins/providers/newpoc/config_test.go b/plugins/providers/newpoc/config_test.go new file mode 100644 index 000000000..9972ec503 --- /dev/null +++ b/plugins/providers/newpoc/config_test.go @@ -0,0 +1,367 @@ +package newpoc_test + +import ( + "context" + "errors" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/goto/guardian/domain" + "github.com/goto/guardian/plugins/providers/gcs/mocks" + "github.com/goto/guardian/plugins/providers/newpoc" + "github.com/stretchr/testify/mock" +) + +func TestConfigManager_Validate(t *testing.T) { + type fields struct { + validator *validator.Validate + crypto domain.Crypto + } + type args struct { + ctx context.Context + p *domain.Provider + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "should return error if provider is nil", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: nil, + }, + wantErr: true, + }, + { + name: "should return error decoding credentials", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: "invalid", + }, + }, + }, + wantErr: true, + }, + { + name: "should return error if credentials are invalid", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "", + "resource_name": "", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "should return error if resource length is not 1", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "should return error if resource name is invalid", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + Resources: []*domain.ResourceConfig{ + { + Type: "invalid", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "should return error if roles is empty", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + Resources: []*domain.ResourceConfig{ + { + Type: "project", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "should return nil if config is valid", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + Resources: []*domain.ResourceConfig{ + { + Type: "project", + Roles: []*domain.Role{ + { + Name: "roles/owner", + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := newpoc.NewConfigManager( + tt.fields.validator, + tt.fields.crypto, + ) + if err := m.Validate(tt.args.ctx, tt.args.p); (err != nil) != tt.wantErr { + t.Errorf("ConfigManager.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestConfigManager_Encrypt(t *testing.T) { + type fields struct { + validator *validator.Validate + crypto domain.Crypto + } + type args struct { + ctx context.Context + p *domain.Provider + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "should return error decoding credentials", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: "invalid", + }, + }, + }, + wantErr: true, + }, + { + name: "should return error if encrypt fails", + fields: fields{ + validator: validator.New(), + crypto: func() domain.Crypto { + c := new(mocks.Crypto) + c.On("Encrypt", mock.Anything, mock.Anything).Return("", errors.New("error")) + return c + }(), + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "should return nil if encrypt succeeds", + fields: fields{ + validator: validator.New(), + crypto: func() domain.Crypto { + c := new(mocks.Crypto) + c.On("Encrypt", mock.Anything, mock.Anything).Return("encrypted", nil) + return c + }(), + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := newpoc.NewConfigManager( + tt.fields.validator, + tt.fields.crypto, + ) + if err := m.Encrypt(tt.args.ctx, tt.args.p); (err != nil) != tt.wantErr { + t.Errorf("ConfigManager.Encrypt() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestConfigManager_Decrypt(t *testing.T) { + type fields struct { + validator *validator.Validate + crypto domain.Crypto + } + type args struct { + ctx context.Context + p *domain.Provider + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "should return error decoding credentials", + fields: fields{ + validator: validator.New(), + crypto: nil, + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: "invalid", + }, + }, + }, + wantErr: true, + }, + { + name: "should return error if decrypt fails", + fields: fields{ + validator: validator.New(), + crypto: func() domain.Crypto { + c := new(mocks.Crypto) + c.On("Decrypt", mock.Anything, mock.Anything).Return("", errors.New("error")) + return c + }(), + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "should return nil if decrypt succeeds", + fields: fields{ + validator: validator.New(), + crypto: func() domain.Crypto { + c := new(mocks.Crypto) + c.On("Decrypt", mock.Anything, mock.Anything).Return("encrypted", nil) + return c + }(), + }, + args: args{ + ctx: context.Background(), + p: &domain.Provider{ + Config: &domain.ProviderConfig{ + Credentials: map[string]interface{}{ + "service_account_key": "service_account_key", + "resource_name": "projects/test", + }, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := newpoc.NewConfigManager( + tt.fields.validator, + tt.fields.crypto, + ) + if err := m.Decrypt(tt.args.ctx, tt.args.p); (err != nil) != tt.wantErr { + t.Errorf("ConfigManager.Decrypt() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/plugins/providers/newpoc/errors.go b/plugins/providers/newpoc/errors.go new file mode 100644 index 000000000..b4af85e69 --- /dev/null +++ b/plugins/providers/newpoc/errors.go @@ -0,0 +1,15 @@ +package newpoc + +import "errors" + +var ( + ErrUnableToEncryptNilCredentials = errors.New("unable to encrypt nil credentials") + ErrUnableToDecryptNilCredentials = errors.New("unable to decrypt nil credentials") + ErrInvalidPermissionConfig = errors.New("invalid permission config type") + ErrPermissionAlreadyExists = errors.New("permission already exists") + ErrPermissionNotFound = errors.New("permission not found") + ErrInvalidResourceType = errors.New("invalid resource type") + ErrInvalidRole = errors.New("invalid role") + ErrInvalidResourceName = errors.New("invalid resource name: resource name should be projects/{{project-id}} or organizations/{{org-id}}") + ErrInvalidProjectRole = errors.New("provided role is not supported for project in gcloud") +) diff --git a/plugins/providers/newpoc/plugin.go b/plugins/providers/newpoc/plugin.go new file mode 100644 index 000000000..49125d557 --- /dev/null +++ b/plugins/providers/newpoc/plugin.go @@ -0,0 +1,116 @@ +package newpoc + +import ( + "context" + + "github.com/goto/guardian/domain" +) + +// domain/provider.go +// +// type ProviderConfigEncryptor interface { +// Encrypt(context.Context, *Provider) error +// } +// +// type Provider struct{} +// func (p *Provider) Encrypt(ctx context.Context, e ProviderConfigEncryptor) error { +// return e.Encrypt(ctx, p) +// } + +// plugin will export two main structs. 1. ConfigManager, 2. Client + +// TODO: all of these interfaces should be defined in core/provider/service.go only + +// ConfigManager mostly will be used for CRUD of provider config +type IConfigManager interface { + Validate(context.Context, *domain.Provider) error + Encrypt(context.Context, *domain.Provider) error + Decrypt(context.Context, *domain.Provider) error +} + +// BasicProviderClient depends on a valid provider config +type BasicProviderClient interface { + // GetType() string // Will be part of providerService or anything that initiate the provider client + GetAllowedAccountTypes(context.Context) []string + ListResources(context.Context) ([]IResource, error) + GrantAccess(ctx context.Context, r IResource, accountID string, permissions []string) error + RevokeAccess(ctx context.Context, r IResource, accountID string, permissions []string) error +} + +type IResource interface { + GetType() string + // GetURN() string + // GetDisplayName() string +} + +type RoleManager interface { + // for api: GET /providers/:id/resources/:type/roles + ListRoles(context.Context) ([]IRole, error) +} + +type IRole interface { + GetID() string + GetDisplayName() string + GetDescription() string + GetPermissions() []string +} + +type PermissionManager interface { + ListPermissions(ctx context.Context, resourceType, role string) ([]IPermission, error) +} + +type IPermission interface { + GetID() string +} + +type ActivityExtractor interface { + ListActivities(ctx context.Context) ([]IActivity, error) +} + +type IActivity interface { + GetID() string + // TODO: complete methods of activity interface +} + +// type Dataset struct {} +// func (d Dataset) GetType() string { return "dataset"} + +// type Table struct {} +// func (t Table) GetType() string { return "table"} + +// type IBigQueryResource interface { +// Dataset | Table +// } + +// type BigQueryResource[T IBigQueryResource] struct { + +// } + +// in provider service struct: +// cached bigqueryClient for provider A (credentials A) +// cached bigqueryClient for provider B (credentials B) +// cached gcsCLient for provider C (credentials C) + +// provider config: +// resource type config: +// roles config: +// - id: my-custom-role-1 +// permissions: roleA, roleB +// - id: my-custom-role-2 +// permissions: roleB, roleC + +// gcp +// role: roles/viewer, roles/bigquery.dataViewer, etc. +// permissions: projects.list, projects.get, datasets.list, etc. + +// pv.PermissionManager +// grafana +// metabase +// shield +// tableau + +// provider.PermissionManager +// bigquery +// gcloudiam +// gcs +// noop diff --git a/plugins/providers/newpoc/resource.go b/plugins/providers/newpoc/resource.go new file mode 100644 index 000000000..be580bb1a --- /dev/null +++ b/plugins/providers/newpoc/resource.go @@ -0,0 +1,6 @@ +package newpoc + +const ( + ResourceTypeProject = "project" + ResourceTypeOrganization = "organization" +) From 344e071c207f84d40064710b7f18afa37d3e6abe Mon Sep 17 00:00:00 2001 From: Rahmat Hidayat Date: Wed, 12 Apr 2023 14:03:29 +0700 Subject: [PATCH 2/3] chore: update poc --- pkg/option/option.go | 15 ++ plugins/providers/newpoc/client.go | 248 ++++++++++++++------------- plugins/providers/newpoc/config.go | 121 ++++++++----- plugins/providers/newpoc/errors.go | 1 + plugins/providers/newpoc/plugin.go | 42 ++--- plugins/providers/newpoc/resource.go | 23 +++ plugins/providers/newpoc/utils.go | 23 +++ 7 files changed, 286 insertions(+), 187 deletions(-) create mode 100644 pkg/option/option.go create mode 100644 plugins/providers/newpoc/utils.go diff --git a/pkg/option/option.go b/pkg/option/option.go new file mode 100644 index 000000000..dcbb0f864 --- /dev/null +++ b/pkg/option/option.go @@ -0,0 +1,15 @@ +package option + +import "github.com/go-playground/validator/v10" + +type options struct { + validator *validator.Validate +} + +type Option func(*options) + +func WithValidator(validator *validator.Validate) Option { + return func(opts *options) { + opts.validator = validator + } +} diff --git a/plugins/providers/newpoc/client.go b/plugins/providers/newpoc/client.go index ac135b8a3..dfc361034 100644 --- a/plugins/providers/newpoc/client.go +++ b/plugins/providers/newpoc/client.go @@ -6,10 +6,11 @@ import ( "fmt" "strings" + "github.com/go-playground/validator/v10" "github.com/goto/guardian/domain" - "github.com/mitchellh/mapstructure" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/iam/v1" + "google.golang.org/api/option" ) const ( @@ -23,38 +24,39 @@ const ( // Client implements BasicProviderClient type Client struct { - providerConfig *domain.ProviderConfig - cloudResourceManagerService *cloudresourcemanager.Service + config *Config iamService *iam.Service + cloudResourceManagerService *cloudresourcemanager.Service } -type ClientDependencies struct { - ProviderConfig *domain.ProviderConfig - CloudResourceManagerService *cloudresourcemanager.Service - IamService *iam.Service -} - -func NewClient(deps *ClientDependencies) (*Client, error) { - if deps == nil { - return nil, errors.New("dependencies can't be nil") +func NewClient(cfg *Config, opts ...option.ClientOption) (*Client, error) { + if cfg == nil { + return nil, errors.New("config is nil") } - if deps.ProviderConfig == nil { - return nil, errors.New("provider config can't be nil") + validator := validator.New() // TODO: use option to override validator + if err := cfg.Validate(context.TODO(), validator); err != nil { + return nil, err } - if deps.CloudResourceManagerService == nil { - return nil, errors.New("cloud resource manager service can't be nil") + ctx := context.Background() + options := []option.ClientOption{ + option.WithCredentialsJSON([]byte(cfg.credentials.ServiceAccountKey)), } - - if deps.IamService == nil { - return nil, errors.New("iam service can't be nil") + options = append(options, opts...) + iamService, err := iam.NewService(ctx, options...) + if err != nil { + return nil, err + } + cloudResourceManagerService, err := cloudresourcemanager.NewService(ctx, options...) + if err != nil { + return nil, err } c := &Client{ - providerConfig: deps.ProviderConfig, - cloudResourceManagerService: deps.CloudResourceManagerService, - iamService: deps.IamService, + config: cfg, + iamService: iamService, + cloudResourceManagerService: cloudResourceManagerService, } return c, nil @@ -69,149 +71,157 @@ func (c *Client) GetAllowedAccountTypes(ctx context.Context) []string { } func (c *Client) ListResources(ctx context.Context) ([]IResource, error) { - var creds credentials - if err := mapstructure.Decode(c.providerConfig.Credentials, &creds); err != nil { + resourceType, resourceID, err := getResourceIdentifier(c.config.credentials.ResourceName) + if err != nil { return nil, err } - var t string - if strings.HasPrefix(creds.ResourceName, "project") { - t = ResourceTypeProject - } else if strings.HasPrefix(creds.ResourceName, "organization") { - t = ResourceTypeOrganization - } - return []IResource{ - &domain.Resource{ - ProviderType: c.providerConfig.Type, - ProviderURN: c.providerConfig.URN, - Type: t, - URN: creds.ResourceName, - Name: fmt.Sprintf("%s - GCP IAM", creds.ResourceName), + resource{ + Type: resourceType, + ID: resourceID, }, }, nil } -func (c *Client) GrantAccess(ctx context.Context, r IResource, accountID string, permissions []string) error { - var creds credentials - if err := mapstructure.Decode(c.providerConfig.Credentials, &creds); err != nil { - return err - } - - if r.GetType() == ResourceTypeProject || r.GetType() == ResourceTypeOrganization { - for _, permission := range permissions { - policy, err := c.getIamPolicy(ctx, creds.ResourceName) - if err != nil { - return err - } +func (c *Client) GrantAccess(ctx context.Context, g domain.Grant) error { + for _, permission := range g.Permissions { + policy, err := c.getIamPolicy(ctx) + if err != nil { + return err + } - member := accountID - roleExists := false - for _, b := range policy.Bindings { - if b.Role == permission { - roleExists = true - if containsString(b.Members, member) { - // Permission already exists - continue - } - b.Members = append(b.Members, member) + member := fmt.Sprintf("%s:%s", g.AccountType, g.AccountID) + roleExists := false + for _, b := range policy.Bindings { + if b.Role == permission { + roleExists = true + if containsString(b.Members, member) { + // Permission already exists + continue } + b.Members = append(b.Members, member) } - if !roleExists { - policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ - Role: permission, - Members: []string{member}, - }) - } + } + if !roleExists { + policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ + Role: permission, + Members: []string{member}, + }) + } - _, err = c.setIamPolicy(ctx, creds.ResourceName, policy) - if err != nil { - return err - } + if _, err = c.setIamPolicy(ctx, policy); err != nil { + return err } } - return ErrInvalidResourceType + return nil + } -func (c *Client) RevokeAccess(ctx context.Context, r IResource, accountID string, permissions []string) error { - var creds credentials - if err := mapstructure.Decode(c.providerConfig.Credentials, &creds); err != nil { - return err +func (c *Client) RevokeAccess(ctx context.Context, g domain.Grant) error { + if g.Resource.Type != ResourceTypeProject && g.Resource.Type != ResourceTypeOrganization { + return ErrInvalidResourceType } - if r.GetType() == ResourceTypeProject || r.GetType() == ResourceTypeOrganization { - for _, permission := range permissions { - policy, err := c.getIamPolicy(ctx, creds.ResourceName) - if err != nil { - return err - } - - member := accountID + for _, permission := range g.Permissions { + policy, err := c.getIamPolicy(ctx) + if err != nil { + return err + } - for _, b := range policy.Bindings { - if b.Role == permission { - removeIndex := -1 - for i, m := range b.Members { - if m == member { - removeIndex = i - } - } - if removeIndex == -1 { - // permission doesn't exist - continue + member := fmt.Sprintf("%s:%s", g.AccountType, g.AccountID) + for _, b := range policy.Bindings { + if b.Role == permission { + removeIndex := -1 + for i, m := range b.Members { + if m == member { + removeIndex = i } - b.Members = append(b.Members[:removeIndex], b.Members[removeIndex+1:]...) } + if removeIndex == -1 { + // permission doesn't exist + continue + } + b.Members = append(b.Members[:removeIndex], b.Members[removeIndex+1:]...) } + } - c.setIamPolicy(ctx, creds.ResourceName, policy) + if _, err := c.setIamPolicy(ctx, policy); err != nil { return err } + } + return nil +} - return nil +func (c *Client) ListAccess(ctx context.Context, resources []*domain.Resource) (domain.MapResourceAccess, error) { + policy, err := c.getIamPolicy(ctx) + if err != nil { + return nil, fmt.Errorf("getting IAM policy: %w", err) + } + + access := make(domain.MapResourceAccess) + for _, resource := range resources { + for _, binding := range policy.Bindings { + for _, member := range binding.Members { + account := strings.Split(member, ":") + ae := domain.AccessEntry{ + AccountType: account[0], + AccountID: account[1], + Permission: binding.Role, + } + access[resource.URN] = append(access[resource.URN], ae) + } + } } - return ErrInvalidResourceType + return access, nil } -func (c *Client) getIamPolicy(ctx context.Context, resourceName string) (*cloudresourcemanager.Policy, error) { - if strings.HasPrefix(resourceName, ResourceNameProjectPrefix) { - projectID := strings.Replace(resourceName, ResourceNameProjectPrefix, "", 1) +func (c *Client) getIamPolicy(ctx context.Context) (*cloudresourcemanager.Policy, error) { + switch c.config.resourceType { + case ResourceTypeProject: return c.cloudResourceManagerService.Projects. - GetIamPolicy(projectID, &cloudresourcemanager.GetIamPolicyRequest{}). + GetIamPolicy(c.config.resourceID, &cloudresourcemanager.GetIamPolicyRequest{}). Context(ctx).Do() - } else if strings.HasPrefix(resourceName, ResourceNameOrganizationPrefix) { - orgID := strings.Replace(resourceName, ResourceNameOrganizationPrefix, "", 1) + case ResourceTypeOrganization: return c.cloudResourceManagerService.Organizations. - GetIamPolicy(orgID, &cloudresourcemanager.GetIamPolicyRequest{}). + GetIamPolicy(c.config.resourceID, &cloudresourcemanager.GetIamPolicyRequest{}). Context(ctx).Do() + default: + return nil, ErrInvalidResourceName } - return nil, ErrInvalidResourceName } -func (c *Client) setIamPolicy(ctx context.Context, resourceName string, policy *cloudresourcemanager.Policy) (*cloudresourcemanager.Policy, error) { +func (c *Client) setIamPolicy(ctx context.Context, policy *cloudresourcemanager.Policy) (*cloudresourcemanager.Policy, error) { setIamPolicyRequest := &cloudresourcemanager.SetIamPolicyRequest{ Policy: policy, } - if strings.HasPrefix(resourceName, ResourceNameProjectPrefix) { - projectID := strings.Replace(resourceName, ResourceNameProjectPrefix, "", 1) + switch c.config.resourceType { + case ResourceTypeProject: return c.cloudResourceManagerService.Projects. - SetIamPolicy(projectID, setIamPolicyRequest). + SetIamPolicy(c.config.resourceID, setIamPolicyRequest). Context(ctx).Do() - } else if strings.HasPrefix(resourceName, ResourceNameOrganizationPrefix) { - orgID := strings.Replace(resourceName, ResourceNameOrganizationPrefix, "", 1) + case ResourceTypeOrganization: return c.cloudResourceManagerService.Organizations. - SetIamPolicy(orgID, setIamPolicyRequest). + SetIamPolicy(c.config.resourceID, setIamPolicyRequest). Context(ctx).Do() + default: + return nil, ErrInvalidResourceName } - return nil, ErrInvalidResourceName } -func containsString(arr []string, v string) bool { - for _, item := range arr { - if item == v { - return true - } +func (c *Client) listGrantableRoles(ctx context.Context) ([]string, error) { + req := &iam.QueryGrantableRolesRequest{ + FullResourceName: fmt.Sprintf("//cloudresourcemanager.googleapis.com/%s", c.config.credentials.ResourceName), + } + res, err := c.iamService.Roles.QueryGrantableRoles(req).Context(ctx).Do() + if err != nil { + return nil, err + } + + roles := make([]string, len(res.Roles)) + for i, r := range res.Roles { + roles[i] = r.Name } - return false + return roles, nil } diff --git a/plugins/providers/newpoc/config.go b/plugins/providers/newpoc/config.go index 301dfcd04..386638382 100644 --- a/plugins/providers/newpoc/config.go +++ b/plugins/providers/newpoc/config.go @@ -10,6 +10,10 @@ import ( "github.com/mitchellh/mapstructure" ) +const ( + ProviderType = "gcloud_iam" +) + var ( ErrShouldHaveOneResource = errors.New("gcloud_iam should have one resource") ErrInvalidCredentials = errors.New("invalid credentials type") @@ -25,6 +29,10 @@ type credentials struct { } func (c *credentials) Decode(v interface{}) error { + if decodedCreds, ok := v.(*credentials); ok { + *c = *decodedCreds + return nil + } return mapstructure.Decode(v, c) } @@ -35,83 +43,118 @@ func (c *credentials) Validate(validator *validator.Validate) error { return nil } -// ConfigManager implements IConfigManager interface -type ConfigManager struct { - validator *validator.Validate - crypto domain.Crypto +type Config struct { + pc *domain.ProviderConfig + credentials *credentials + resourceType string + resourceID string } -// NewConfigManager returns a new ConfigManager -func NewConfigManager(validator *validator.Validate, crypto domain.Crypto) *ConfigManager { - return &ConfigManager{ - validator: validator, - crypto: crypto, +func NewConfig(pc *domain.ProviderConfig) (*Config, error) { + if pc.Type != ProviderType { + return nil, fmt.Errorf("%w: expected provider type: %q", ErrInvalidProviderType, ProviderType) } + + creds := new(credentials) + if err := creds.Decode(pc.Credentials); err != nil { + return nil, fmt.Errorf("decoding credentials: %w", err) + } + pc.Credentials = creds + + resourceType, resourceID, err := getResourceIdentifier(creds.ResourceName) + if err != nil { + return nil, err + } + + return &Config{ + pc: pc, + credentials: creds, + resourceType: resourceType, + resourceID: resourceID, + }, nil } -func (m ConfigManager) Validate(ctx context.Context, p *domain.Provider) error { - if p == nil { +func (c *Config) GetProviderConfig() *domain.ProviderConfig { + return c.pc +} + +func (c *Config) Validate(ctx context.Context, validator *validator.Validate) error { + if c.pc == nil { return ErrProviderShouldNotBeNil } // validate credentials - creds := new(credentials) - if err := creds.Decode(p.Config.Credentials); err != nil { - return fmt.Errorf("decoding credentials: %w", err) - } - if err := creds.Validate(m.validator); err != nil { + if err := c.credentials.Validate(validator); err != nil { return fmt.Errorf("validating credentials: %w", err) } // validate resource config - if len(p.Config.Resources) != 1 { + if len(c.pc.Resources) != 1 { return ErrShouldHaveOneResource } - rc := p.Config.Resources[0] - if err := m.validator.Var(rc.Type, resourceTypeValidation); err != nil { + rc := c.pc.Resources[0] + if err := validator.Var(rc.Type, resourceTypeValidation); err != nil { return fmt.Errorf("validating resource type %q: %w", rc.Type, err) } if len(rc.Roles) == 0 { return ErrRolesShouldNotBeEmpty } + // validate permissions (gcloud roles) + tmpClient, err := NewClient(c) // TODO: client should be overrideable with an existing client instance through option param + if err != nil { + return fmt.Errorf("initializing client: %w", err) + } + grantableRoles, err := tmpClient.listGrantableRoles(ctx) + if err != nil { + return fmt.Errorf("listing grantable roles: %w", err) + } + grantableRolesMap := make(map[string]bool) + for _, r := range grantableRoles { + grantableRolesMap[r] = true + } + for _, role := range rc.Roles { + for _, permission := range role.Permissions { + permissionString, ok := permission.(string) + if !ok { + return fmt.Errorf("invalid permission type for %q: %T", permission, permission) + } + if !grantableRolesMap[permissionString] { + return fmt.Errorf("permission %q is not grantable to %q", permissionString, c.credentials.ResourceName) + } + } + } + return nil } -func (m ConfigManager) Encrypt(ctx context.Context, p *domain.Provider) error { - credentials := new(credentials) - if err := credentials.Decode(p.Config.Credentials); err != nil { - return ErrInvalidCredentials +// Encrypt encrypts the service account key in ProviderConfig.Credentials +func (c *Config) Encrypt(encryptor domain.Encryptor) error { + credentialsString, ok := c.pc.Credentials.(map[string]interface{})["service_account_key"].(string) + if !ok { + return fmt.Errorf("invalid credentials type: %T", c.pc.Credentials) } - // TODO: check if creds value is the decrypted one - - encryptedSA, err := m.crypto.Encrypt(credentials.ServiceAccountKey) + encryptedSA, err := encryptor.Encrypt(credentialsString) if err != nil { return err } - credentials.ServiceAccountKey = encryptedSA - p.Config.Credentials = credentials - + c.pc.Credentials.(map[string]interface{})["service_account_key"] = encryptedSA return nil } -func (m ConfigManager) Decrypt(ctx context.Context, p *domain.Provider) error { - credentials := new(credentials) - if err := credentials.Decode(p.Config.Credentials); err != nil { - return ErrInvalidCredentials +func (c *Config) Decrypt(decryptor domain.Decryptor) error { + credentialsString, ok := c.pc.Credentials.(map[string]interface{})["service_account_key"].(string) + if !ok { + return fmt.Errorf("invalid credentials type: %T", c.pc.Credentials) } - // TODO: check if creds value is the encrypted one - - decryptedSA, err := m.crypto.Decrypt(credentials.ServiceAccountKey) + decryptedSA, err := decryptor.Decrypt(credentialsString) if err != nil { return err } - credentials.ServiceAccountKey = decryptedSA - p.Config.Credentials = credentials - + c.pc.Credentials.(map[string]interface{})["service_account_key"] = decryptedSA return nil } diff --git a/plugins/providers/newpoc/errors.go b/plugins/providers/newpoc/errors.go index b4af85e69..312326445 100644 --- a/plugins/providers/newpoc/errors.go +++ b/plugins/providers/newpoc/errors.go @@ -12,4 +12,5 @@ var ( ErrInvalidRole = errors.New("invalid role") ErrInvalidResourceName = errors.New("invalid resource name: resource name should be projects/{{project-id}} or organizations/{{org-id}}") ErrInvalidProjectRole = errors.New("provided role is not supported for project in gcloud") + ErrInvalidProviderType = errors.New("invalid provider type") ) diff --git a/plugins/providers/newpoc/plugin.go b/plugins/providers/newpoc/plugin.go index 49125d557..3debf7885 100644 --- a/plugins/providers/newpoc/plugin.go +++ b/plugins/providers/newpoc/plugin.go @@ -3,6 +3,7 @@ package newpoc import ( "context" + "github.com/go-playground/validator/v10" "github.com/goto/guardian/domain" ) @@ -21,50 +22,33 @@ import ( // TODO: all of these interfaces should be defined in core/provider/service.go only -// ConfigManager mostly will be used for CRUD of provider config -type IConfigManager interface { - Validate(context.Context, *domain.Provider) error - Encrypt(context.Context, *domain.Provider) error - Decrypt(context.Context, *domain.Provider) error +type ProviderConfigurator interface { + Validate(*validator.Validate) error + Encrypt(domain.Encryptor) error + Decrypt(domain.Decryptor) error } // BasicProviderClient depends on a valid provider config type BasicProviderClient interface { - // GetType() string // Will be part of providerService or anything that initiate the provider client GetAllowedAccountTypes(context.Context) []string ListResources(context.Context) ([]IResource, error) - GrantAccess(ctx context.Context, r IResource, accountID string, permissions []string) error - RevokeAccess(ctx context.Context, r IResource, accountID string, permissions []string) error + GrantAccess(context.Context, domain.Grant) error + RevokeAccess(context.Context, domain.Grant) error } type IResource interface { GetType() string - // GetURN() string - // GetDisplayName() string -} - -type RoleManager interface { - // for api: GET /providers/:id/resources/:type/roles - ListRoles(context.Context) ([]IRole, error) -} - -type IRole interface { - GetID() string + GetURN() string GetDisplayName() string - GetDescription() string - GetPermissions() []string -} - -type PermissionManager interface { - ListPermissions(ctx context.Context, resourceType, role string) ([]IPermission, error) + GetMetadata() map[string]interface{} } -type IPermission interface { - GetID() string +type AccessImporter interface { + ListAccess(context.Context, domain.ProviderConfig) (domain.MapResourceAccess, error) } -type ActivityExtractor interface { - ListActivities(ctx context.Context) ([]IActivity, error) +type ActivityImporter interface { + ListActivities(context.Context) ([]IActivity, error) } type IActivity interface { diff --git a/plugins/providers/newpoc/resource.go b/plugins/providers/newpoc/resource.go index be580bb1a..8744a20c4 100644 --- a/plugins/providers/newpoc/resource.go +++ b/plugins/providers/newpoc/resource.go @@ -1,6 +1,29 @@ package newpoc +import "fmt" + const ( ResourceTypeProject = "project" ResourceTypeOrganization = "organization" ) + +type resource struct { + Type string + ID string +} + +func (r resource) GetType() string { + return r.Type +} + +func (r resource) GetURN() string { + return fmt.Sprintf("%s/%s", r.Type, r.ID) +} + +func (r resource) GetDisplayName() string { + return fmt.Sprintf("%s - GCP IAM", r.GetURN()) +} + +func (r resource) GetMetadata() map[string]interface{} { + return nil +} diff --git a/plugins/providers/newpoc/utils.go b/plugins/providers/newpoc/utils.go new file mode 100644 index 000000000..d871aec85 --- /dev/null +++ b/plugins/providers/newpoc/utils.go @@ -0,0 +1,23 @@ +package newpoc + +import ( + "fmt" + "strings" +) + +func containsString(arr []string, v string) bool { + for _, item := range arr { + if item == v { + return true + } + } + return false +} + +func getResourceIdentifier(urn string) (rType, id string, err error) { + resourceName := strings.Split(urn, "/") + if len(resourceName) != 2 { + return "", "", fmt.Errorf("invalid resource name: %s", urn) + } + return resourceName[0], resourceName[1], nil +} From 35b25e98ea22f599d6bbdee65383faee247ba86b Mon Sep 17 00:00:00 2001 From: Rahmat Hidayat Date: Fri, 14 Apr 2023 15:50:52 +0700 Subject: [PATCH 3/3] chore: update poc --- core/provider/client_factory.go | 61 ++++++++++ core/provider/common.go | 2 + core/provider/plugin_adapter.go | 168 +++++++++++++++++++++++++++ core/provider/service.go | 35 ++++++ domain/provider.go | 8 ++ domain/resource.go | 7 ++ internal/server/services.go | 1 + plugins/providers/newpoc/client.go | 28 ++--- plugins/providers/newpoc/config.go | 16 ++- plugins/providers/newpoc/resource.go | 4 +- plugins/providers/newpoc/utils.go | 9 +- 11 files changed, 316 insertions(+), 23 deletions(-) create mode 100644 core/provider/client_factory.go create mode 100644 core/provider/plugin_adapter.go diff --git a/core/provider/client_factory.go b/core/provider/client_factory.go new file mode 100644 index 000000000..2996587e0 --- /dev/null +++ b/core/provider/client_factory.go @@ -0,0 +1,61 @@ +package provider + +import ( + "fmt" + + "github.com/goto/guardian/domain" + "github.com/goto/guardian/plugins/providers/newpoc" +) + +// TODO: move this to guardian/plugins/providers +type pluginFactory struct { + clients map[string]clientV2 + configs map[string]providerConfig +} + +func (f *pluginFactory) getConfig(pc *domain.ProviderConfig) (providerConfig, error) { + if f.configs == nil { + f.configs = make(map[string]providerConfig) + } + + key := pc.URN + if config, ok := f.configs[key]; ok { + return config, nil + } + + switch pc.Type { + case newpoc.ProviderType: + config, err := newpoc.NewConfig(pc) + if err != nil { + return nil, err + } + f.configs[key] = config + return config, nil + default: + return nil, fmt.Errorf("unknown provider type: %q", pc.Type) + } +} + +func (f *pluginFactory) getClient(cfg providerConfig) (clientV2, error) { + if f.clients == nil { + f.clients = make(map[string]clientV2) + } + + key := cfg.GetProviderConfig().URN + if client, ok := f.clients[key]; ok { + return client, nil + } + + providerType := cfg.GetProviderConfig().Type + switch providerType { + case newpoc.ProviderType: + client, err := newpoc.NewClient(cfg.(*newpoc.Config)) + if err != nil { + return nil, err + } + f.clients[key] = client + return client, nil + default: + return nil, fmt.Errorf("unknown provider type: %q", providerType) + } +} diff --git a/core/provider/common.go b/core/provider/common.go index cd81a2f7b..710b69a16 100644 --- a/core/provider/common.go +++ b/core/provider/common.go @@ -1,5 +1,7 @@ package provider +// TODO: remove this file + import ( "context" "fmt" diff --git a/core/provider/plugin_adapter.go b/core/provider/plugin_adapter.go new file mode 100644 index 000000000..5c0769526 --- /dev/null +++ b/core/provider/plugin_adapter.go @@ -0,0 +1,168 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/go-playground/validator/v10" + "github.com/goto/guardian/domain" + "github.com/goto/guardian/plugins/providers/newpoc" +) + +type pluginAdapter struct { + providerType string + allowedAccountTypes []string + factory *pluginFactory + + validator *validator.Validate + crypto domain.Crypto +} + +func (a *pluginAdapter) GetType() string { + return a.providerType +} + +func (a *pluginAdapter) CreateConfig(pc *domain.ProviderConfig) error { + config, err := a.factory.getConfig(pc) + if err != nil { + return fmt.Errorf("initializing config for %q: %w", pc.Type, err) + } + + if err := config.Validate(context.TODO(), a.validator); err != nil { + return fmt.Errorf("invalid config: %w", err) + } + + if encryptableConfig, ok := config.(encryptable); ok { + if err := encryptableConfig.Encrypt(a.crypto); err != nil { + return fmt.Errorf("encrypting config: %w", err) + } + } + + return nil +} + +func (a *pluginAdapter) GetResources(pc *domain.ProviderConfig) ([]*domain.Resource, error) { + config, err := a.factory.getConfig(pc) + if err != nil { + return nil, fmt.Errorf("initializing config for %q: %w", pc.URN, err) + } + client, err := a.factory.getClient(config) + if err != nil { + return nil, fmt.Errorf("initializing client for %q: %w", pc.URN, err) + } + + resourceables, err := client.ListResources(context.TODO()) + if err != nil { + return nil, fmt.Errorf("listing resources for %q: %w", pc.URN, err) + } + + resources := make([]*domain.Resource, 0, len(resourceables)) + for _, resourceable := range resourceables { + r := &domain.Resource{ + ProviderType: pc.Type, + ProviderURN: pc.URN, + Type: resourceable.GetType(), + URN: resourceable.GetURN(), + Name: resourceable.GetDisplayName(), + } + if md := resourceable.GetMetadata(); md != nil { + r.Details = map[string]interface{}{ + "__metadata": resourceable.GetMetadata(), + } + } + resources = append(resources, r) + } + + return resources, nil +} + +func (a *pluginAdapter) GrantAccess(pc *domain.ProviderConfig, grant domain.Grant) error { + config, err := a.factory.getConfig(pc) + if err != nil { + return fmt.Errorf("initializing config for %q: %w", pc.URN, err) + } + client, err := a.factory.getClient(config) + if err != nil { + return fmt.Errorf("initializing client for %q: %w", pc.URN, err) + } + + return client.GrantAccess(context.TODO(), grant) +} + +func (a *pluginAdapter) RevokeAccess(pc *domain.ProviderConfig, grant domain.Grant) error { + config, err := a.factory.getConfig(pc) + if err != nil { + return fmt.Errorf("initializing config for %q: %w", pc.URN, err) + } + client, err := a.factory.getClient(config) + if err != nil { + return fmt.Errorf("initializing client for %q: %w", pc.URN, err) + } + + return client.RevokeAccess(context.TODO(), grant) +} + +func (a *pluginAdapter) GetRoles(pc *domain.ProviderConfig, resourceType string) ([]*domain.Role, error) { + for _, r := range pc.Resources { + if r.Type == resourceType { + return r.Roles, nil + } + } + + return nil, ErrInvalidResourceType +} + +func (a *pluginAdapter) GetAccountTypes() []string { + return a.allowedAccountTypes +} + +func (a *pluginAdapter) ListAccess(ctx context.Context, pc domain.ProviderConfig, resources []*domain.Resource) (domain.MapResourceAccess, error) { + config, err := a.factory.getConfig(&pc) + if err != nil { + return nil, fmt.Errorf("initializing config for %q: %w", pc.URN, err) + } + client, err := a.factory.getClient(config) + if err != nil { + return nil, fmt.Errorf("initializing client for %q: %w", pc.URN, err) + } + + if accessImporter, ok := client.(AccessImporter); ok { + return accessImporter.ListAccess(ctx, pc, resources) + } + + return nil, fmt.Errorf("ListAccess %w", ErrUnimplementedMethod) +} + +func (a *pluginAdapter) GetPermissions(pc *domain.ProviderConfig, resourceType, role string) ([]interface{}, error) { + for _, rc := range pc.Resources { + if rc.Type != resourceType { + continue + } + for _, r := range rc.Roles { + if r.ID == role { + if r.Permissions == nil { + return make([]interface{}, 0), nil + } + return r.Permissions, nil + } + } + return nil, ErrInvalidRole + } + return nil, ErrInvalidResourceType +} + +func getNewPlugins(pluginFactory *pluginFactory, validator *validator.Validate, crypto domain.Crypto) map[string]Client { + return map[string]Client{ + newpoc.ProviderType: &pluginAdapter{ + providerType: newpoc.ProviderType, + allowedAccountTypes: []string{ + newpoc.AccountTypeUser, + newpoc.AccountTypeGroup, + newpoc.AccountTypeServiceAccount, + }, + factory: pluginFactory, + validator: validator, + crypto: crypto, + }, + } +} diff --git a/core/provider/service.go b/core/provider/service.go index 95ef16c87..4fd2eac95 100644 --- a/core/provider/service.go +++ b/core/provider/service.go @@ -26,6 +26,12 @@ const ( ReservedDetailsKeyPolicyQuestions = "__policy_questions" ) +var ( + migratedPluginTypes = map[string]bool{ + "newpoc": true, + } +) + //go:generate mockery --name=repository --exported --with-expecter type repository interface { Create(context.Context, *domain.Provider) error @@ -37,6 +43,29 @@ type repository interface { Delete(ctx context.Context, id string) error } +type providerConfig interface { + GetProviderConfig() *domain.ProviderConfig + Validate(ctx context.Context, validator *validator.Validate) error +} + +type encryptable interface { + Encrypt(encryptor domain.Encryptor) error +} + +type decryptable interface { + Decrypt(decryptor domain.Decryptor) error +} + +type clientV2 interface { + ListResources(context.Context) ([]domain.Resourceable, error) + GrantAccess(context.Context, domain.Grant) error + RevokeAccess(context.Context, domain.Grant) error +} + +type AccessImporter interface { + ListAccess(context.Context, domain.ProviderConfig, []*domain.Resource) (domain.MapResourceAccess, error) +} + //go:generate mockery --name=Client --exported --with-expecter type Client interface { providers.PermissionManager @@ -79,14 +108,20 @@ type ServiceDeps struct { Validator *validator.Validate Logger log.Logger AuditLogger auditLogger + Crypto domain.Crypto } // NewService returns service struct func NewService(deps ServiceDeps) *Service { + pluginFactory := &pluginFactory{} + mapProviderClients := make(map[string]Client) for _, c := range deps.Clients { mapProviderClients[c.GetType()] = c } + for providerType, adaptedPlugin := range getNewPlugins(pluginFactory, deps.Validator, deps.Crypto) { + mapProviderClients[providerType] = adaptedPlugin + } return &Service{ deps.Repository, diff --git a/domain/provider.go b/domain/provider.go index 1890800e4..3d789b76c 100644 --- a/domain/provider.go +++ b/domain/provider.go @@ -1,9 +1,12 @@ package domain import ( + "context" "fmt" "sort" "time" + + "github.com/go-playground/validator/v10" ) const ( @@ -105,3 +108,8 @@ type ProviderType struct { Name string `json:"name" yaml:"name"` ResourceTypes []string `json:"resource_types" yaml:"resource_types"` } + +type ProviderConfigurable interface { + GetProvider() *Provider + Validate(context.Context, *validator.Validate) error +} diff --git a/domain/resource.go b/domain/resource.go index 947d16a6b..08e47fd99 100644 --- a/domain/resource.go +++ b/domain/resource.go @@ -2,6 +2,13 @@ package domain import "time" +type Resourceable interface { + GetType() string + GetURN() string + GetDisplayName() string + GetMetadata() map[string]interface{} +} + // Resource struct type Resource struct { ID string `json:"id" yaml:"id"` diff --git a/internal/server/services.go b/internal/server/services.go index 2ee12be3b..fe612dfd9 100644 --- a/internal/server/services.go +++ b/internal/server/services.go @@ -128,6 +128,7 @@ func InitServices(deps ServiceDeps) (*Services, error) { Validator: deps.Validator, Logger: deps.Logger, AuditLogger: auditLogger, + Crypto: deps.Crypto, }) activityService := activity.NewService(activity.ServiceDeps{ Repository: activityRepository, diff --git a/plugins/providers/newpoc/client.go b/plugins/providers/newpoc/client.go index dfc361034..500220ca1 100644 --- a/plugins/providers/newpoc/client.go +++ b/plugins/providers/newpoc/client.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/go-playground/validator/v10" "github.com/goto/guardian/domain" "google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/iam/v1" @@ -34,10 +33,10 @@ func NewClient(cfg *Config, opts ...option.ClientOption) (*Client, error) { return nil, errors.New("config is nil") } - validator := validator.New() // TODO: use option to override validator - if err := cfg.Validate(context.TODO(), validator); err != nil { - return nil, err - } + // validator := validator.New() // TODO: use option to override validator + // if err := cfg.Validate(context.TODO(), validator); err != nil { + // return nil, err + // } ctx := context.Background() options := []option.ClientOption{ @@ -70,16 +69,11 @@ func (c *Client) GetAllowedAccountTypes(ctx context.Context) []string { } } -func (c *Client) ListResources(ctx context.Context) ([]IResource, error) { - resourceType, resourceID, err := getResourceIdentifier(c.config.credentials.ResourceName) - if err != nil { - return nil, err - } - - return []IResource{ - resource{ - Type: resourceType, - ID: resourceID, +func (c *Client) ListResources(ctx context.Context) ([]domain.Resourceable, error) { + return []domain.Resourceable{ + &resource{ + Type: c.config.resourceType, + URN: c.config.credentials.ResourceName, }, }, nil } @@ -188,7 +182,7 @@ func (c *Client) getIamPolicy(ctx context.Context) (*cloudresourcemanager.Policy GetIamPolicy(c.config.resourceID, &cloudresourcemanager.GetIamPolicyRequest{}). Context(ctx).Do() default: - return nil, ErrInvalidResourceName + return nil, fmt.Errorf("%w: %q", ErrInvalidResourceType, c.config.resourceType) } } @@ -206,7 +200,7 @@ func (c *Client) setIamPolicy(ctx context.Context, policy *cloudresourcemanager. SetIamPolicy(c.config.resourceID, setIamPolicyRequest). Context(ctx).Do() default: - return nil, ErrInvalidResourceName + return nil, fmt.Errorf("%w: %q", ErrInvalidResourceType, c.config.resourceType) } } diff --git a/plugins/providers/newpoc/config.go b/plugins/providers/newpoc/config.go index 386638382..2d0963ddf 100644 --- a/plugins/providers/newpoc/config.go +++ b/plugins/providers/newpoc/config.go @@ -2,6 +2,7 @@ package newpoc import ( "context" + "encoding/base64" "errors" "fmt" @@ -11,7 +12,7 @@ import ( ) const ( - ProviderType = "gcloud_iam" + ProviderType = "newpoc" ) var ( @@ -33,7 +34,17 @@ func (c *credentials) Decode(v interface{}) error { *c = *decodedCreds return nil } - return mapstructure.Decode(v, c) + + if err := mapstructure.Decode(v, c); err != nil { + return err + } + + // attempt to decode service account key in case of it is base64 encoded + if decoded, err := base64.StdEncoding.DecodeString(c.ServiceAccountKey); err == nil { + c.ServiceAccountKey = string(decoded) + } + + return nil } func (c *credentials) Validate(validator *validator.Validate) error { @@ -59,7 +70,6 @@ func NewConfig(pc *domain.ProviderConfig) (*Config, error) { if err := creds.Decode(pc.Credentials); err != nil { return nil, fmt.Errorf("decoding credentials: %w", err) } - pc.Credentials = creds resourceType, resourceID, err := getResourceIdentifier(creds.ResourceName) if err != nil { diff --git a/plugins/providers/newpoc/resource.go b/plugins/providers/newpoc/resource.go index 8744a20c4..95c186dd1 100644 --- a/plugins/providers/newpoc/resource.go +++ b/plugins/providers/newpoc/resource.go @@ -9,7 +9,7 @@ const ( type resource struct { Type string - ID string + URN string } func (r resource) GetType() string { @@ -17,7 +17,7 @@ func (r resource) GetType() string { } func (r resource) GetURN() string { - return fmt.Sprintf("%s/%s", r.Type, r.ID) + return r.URN } func (r resource) GetDisplayName() string { diff --git a/plugins/providers/newpoc/utils.go b/plugins/providers/newpoc/utils.go index d871aec85..0405856b5 100644 --- a/plugins/providers/newpoc/utils.go +++ b/plugins/providers/newpoc/utils.go @@ -19,5 +19,12 @@ func getResourceIdentifier(urn string) (rType, id string, err error) { if len(resourceName) != 2 { return "", "", fmt.Errorf("invalid resource name: %s", urn) } - return resourceName[0], resourceName[1], nil + resourceType := resourceName[0] + if resourceType == "projects" { + resourceType = ResourceTypeProject + } else if resourceType == "organizations" { + resourceType = ResourceTypeOrganization + } + + return resourceType, resourceName[1], nil }