From 80ce978f2596d8760fc1a43049276790fe409b37 Mon Sep 17 00:00:00 2001 From: Rahmat Hidayat Date: Tue, 3 Dec 2024 15:21:28 +0700 Subject: [PATCH] feat: support grant dependencies --- core/appeal/mocks/providerService.go | 55 ++++++++++ core/appeal/service.go | 40 ++++++- core/provider/service.go | 47 ++++++++ domain/provider.go | 7 ++ plugins/providers/maxcompute/config.go | 2 + plugins/providers/maxcompute/provider.go | 133 ++++++++++++++++++----- 6 files changed, 255 insertions(+), 29 deletions(-) diff --git a/core/appeal/mocks/providerService.go b/core/appeal/mocks/providerService.go index 877675400..2ec2c6dd8 100644 --- a/core/appeal/mocks/providerService.go +++ b/core/appeal/mocks/providerService.go @@ -76,6 +76,61 @@ func (_c *ProviderService_Find_Call) RunAndReturn(run func(context.Context) ([]* return _c } +// GetDependencyGrants provides a mock function with given fields: _a0, _a1 +func (_m *ProviderService) GetDependencyGrants(_a0 context.Context, _a1 domain.Grant) ([]*domain.Grant, error) { + ret := _m.Called(_a0, _a1) + + var r0 []*domain.Grant + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, domain.Grant) ([]*domain.Grant, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, domain.Grant) []*domain.Grant); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*domain.Grant) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, domain.Grant) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProviderService_GetDependencyGrants_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDependencyGrants' +type ProviderService_GetDependencyGrants_Call struct { + *mock.Call +} + +// GetDependencyGrants is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 domain.Grant +func (_e *ProviderService_Expecter) GetDependencyGrants(_a0 interface{}, _a1 interface{}) *ProviderService_GetDependencyGrants_Call { + return &ProviderService_GetDependencyGrants_Call{Call: _e.mock.On("GetDependencyGrants", _a0, _a1)} +} + +func (_c *ProviderService_GetDependencyGrants_Call) Run(run func(_a0 context.Context, _a1 domain.Grant)) *ProviderService_GetDependencyGrants_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(domain.Grant)) + }) + return _c +} + +func (_c *ProviderService_GetDependencyGrants_Call) Return(_a0 []*domain.Grant, _a1 error) *ProviderService_GetDependencyGrants_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ProviderService_GetDependencyGrants_Call) RunAndReturn(run func(context.Context, domain.Grant) ([]*domain.Grant, error)) *ProviderService_GetDependencyGrants_Call { + _c.Call.Return(run) + return _c +} + // GetPermissions provides a mock function with given fields: _a0, _a1, _a2, _a3 func (_m *ProviderService) GetPermissions(_a0 context.Context, _a1 *domain.ProviderConfig, _a2 string, _a3 string) ([]interface{}, error) { ret := _m.Called(_a0, _a1, _a2, _a3) diff --git a/core/appeal/service.go b/core/appeal/service.go index 07d347b5a..641e654a7 100644 --- a/core/appeal/service.go +++ b/core/appeal/service.go @@ -83,6 +83,7 @@ type providerService interface { ValidateAppeal(context.Context, *domain.Appeal, *domain.Provider, *domain.Policy) error GetPermissions(context.Context, *domain.ProviderConfig, string, string) ([]interface{}, error) IsExclusiveRoleAssignment(context.Context, string, string) bool + GetDependencyGrants(context.Context, domain.Grant) ([]*domain.Grant, error) } //go:generate mockery --name=resourceService --exported --with-expecter @@ -1489,15 +1490,46 @@ func (s *Service) GrantAccessToProvider(ctx context.Context, a *domain.Appeal, o } } - g := a.Grant appealCopy := *a appealCopy.Grant = nil - g.Appeal = &appealCopy - if err := s.providerService.GrantAccess(ctx, *a.Grant); err != nil { + grantWithAppeal := *a.Grant + grantWithAppeal.Appeal = &appealCopy + + // grant access dependencies (if any) + dependencyGrants, err := s.providerService.GetDependencyGrants(ctx, grantWithAppeal) + if err != nil { + return fmt.Errorf("getting grant dependencies: %w", err) + } + for _, dg := range dependencyGrants { + activeGrants, err := s.grantService.List(ctx, domain.ListGrantsFilter{ + Statuses: []string{string(domain.GrantStatusActive)}, + AccountIDs: []string{dg.AccountID}, + AccountTypes: []string{dg.AccountType}, + ResourceIDs: []string{dg.Resource.ID}, + Permissions: dg.Permissions, + Size: 1, + }) + if err != nil { + return fmt.Errorf("failed to get existing active grant dependency: %w", err) + } + + if len(activeGrants) > 0 { + break + } + + dg.Appeal = &appealCopy + if err := s.providerService.GrantAccess(ctx, *dg); err != nil { + return fmt.Errorf("failed to grant an access dependency: %w", err) + } + // TODO: store grant to db + } + + // grant main access + if err := s.providerService.GrantAccess(ctx, grantWithAppeal); err != nil { return fmt.Errorf("granting access: %w", err) } - g.Appeal = nil + grantWithAppeal.Appeal = nil return nil } diff --git a/core/provider/service.go b/core/provider/service.go index 241cf7bf7..58448cea3 100644 --- a/core/provider/service.go +++ b/core/provider/service.go @@ -56,6 +56,10 @@ type assignmentTyper interface { IsExclusiveRoleAssignment(context.Context) bool } +type grantDependenciesResolver interface { + GetDependencyGrants(context.Context, domain.Provider, domain.Grant) ([]*domain.Grant, error) +} + //go:generate mockery --name=resourceService --exported --with-expecter type resourceService interface { Find(context.Context, domain.ListResourcesFilter) ([]*domain.Resource, error) @@ -569,6 +573,49 @@ func (s *Service) IsExclusiveRoleAssignment(ctx context.Context, providerType, r return false } +func (s *Service) GetDependencyGrants(ctx context.Context, g domain.Grant) ([]*domain.Grant, error) { + client := s.getClient(g.Resource.ProviderType) + if client == nil { + return nil, ErrInvalidProviderType + } + + c, ok := client.(grantDependenciesResolver) + if !ok { + return nil, nil + } + + p, err := s.getProviderConfig(ctx, g.Resource.ProviderType, g.Resource.ProviderURN) + if err != nil { + return nil, err + } + + dependencies, err := c.GetDependencyGrants(ctx, *p, g) + if err != nil { + return nil, err + } + + for _, d := range dependencies { + resources, err := s.resourceService.Find(ctx, domain.ListResourcesFilter{ + ProviderType: d.Resource.ProviderType, + ProviderURN: d.Resource.ProviderURN, + ResourceType: d.Resource.Type, + ResourceURN: d.Resource.URN, + Size: 1, + }) + if err != nil { + return nil, fmt.Errorf("unable to resolve resource %q for grant dependency: %w", d.Resource.URN, err) + } + if len(resources) == 0 { + return nil, fmt.Errorf("unable to resolve resource %q for grant dependency: not found", d.Resource.URN) + } + + d.ResourceID = resources[0].ID + d.Resource = resources[0] + } + + return dependencies, nil +} + func (s *Service) fetchNewResources(ctx context.Context, p *domain.Provider) ([]*domain.Resource, int, error) { c := s.getClient(p.Type) if c == nil { diff --git a/domain/provider.go b/domain/provider.go index e5eb5b82d..4ac795a85 100644 --- a/domain/provider.go +++ b/domain/provider.go @@ -84,6 +84,13 @@ func (pc ProviderConfig) GetResourceTypes() (resourceTypes []string) { return } +func (pc ProviderConfig) GetParameterKeys() (keys []string) { + for _, param := range pc.Parameters { + keys = append(keys, param.Key) + } + return +} + func (pc ProviderConfig) GetFilterForResourceType(resourceType string) string { for _, resource := range pc.Resources { if resource.Type == resourceType { diff --git a/plugins/providers/maxcompute/config.go b/plugins/providers/maxcompute/config.go index c1d5f3f40..1d89dfceb 100644 --- a/plugins/providers/maxcompute/config.go +++ b/plugins/providers/maxcompute/config.go @@ -17,6 +17,8 @@ const ( resourceTypeTable = "table" parameterRAMRoleKey = "ram_role" + + projectPermissionMember = "member" ) var ( diff --git a/plugins/providers/maxcompute/provider.go b/plugins/providers/maxcompute/provider.go index 440bf6f81..c47bad88e 100644 --- a/plugins/providers/maxcompute/provider.go +++ b/plugins/providers/maxcompute/provider.go @@ -1,7 +1,9 @@ package maxcompute import ( + "errors" "fmt" + "net/http" "slices" "strings" "sync" @@ -12,6 +14,8 @@ import ( sts "github.com/alibabacloud-go/sts-20150401/client" "github.com/aliyun/aliyun-odps-go-sdk/odps" "github.com/aliyun/aliyun-odps-go-sdk/odps/account" + "github.com/aliyun/aliyun-odps-go-sdk/odps/restclient" + "github.com/aliyun/aliyun-odps-go-sdk/odps/security" pv "github.com/goto/guardian/core/provider" "github.com/goto/guardian/domain" "github.com/goto/guardian/pkg/log" @@ -148,7 +152,14 @@ func (p *provider) GetResources(ctx context.Context, pc *domain.ProviderConfig) } func (p *provider) GrantAccess(ctx context.Context, pc *domain.ProviderConfig, g domain.Grant) error { - ramRole, _ := getParametersFromGrant[string](g, parameterRAMRoleKey) + var ramRole string + if slices.Contains(pc.GetParameterKeys(), parameterRAMRoleKey) { + r, _, err := getParametersFromGrant[string](g, parameterRAMRoleKey) + if err != nil { + return fmt.Errorf("failed to get %q parameter value from grant: %w", parameterRAMRoleKey, err) + } + ramRole = r + } client, err := p.getOdpsClient(pc, ramRole) if err != nil { return err @@ -162,7 +173,7 @@ func (p *provider) GrantAccess(ctx context.Context, pc *domain.ProviderConfig, g addAsProjectMember := false var permissions []string for _, p := range g.Permissions { - if p == "member" { + if p == projectPermissionMember { addAsProjectMember = true continue } @@ -171,26 +182,28 @@ func (p *provider) GrantAccess(ctx context.Context, pc *domain.ProviderConfig, g if addAsProjectMember { query := fmt.Sprintf("ADD USER %s", g.AccountID) - job, err := securityManager.Run(query, true, "") + job, err := execQuery(securityManager, query) if err != nil { return fmt.Errorf("failed to add %q as member in %q: %v", g.AccountID, project, err) } - - if _, err := job.WaitForSuccess(); err != nil { - return fmt.Errorf("failed to add %q as member in %q: %v", g.AccountID, project, err) + if job != nil { + if _, err := job.WaitForSuccess(); err != nil { + return fmt.Errorf("failed to add %q as member in %q: %v", g.AccountID, project, err) + } } } if len(permissions) > 0 { mcRoles := strings.Join(permissions, ", ") query := fmt.Sprintf("GRANT %s TO %s", mcRoles, g.AccountID) - job, err := securityManager.Run(query, true, "") + job, err := execQuery(securityManager, query) if err != nil { return fmt.Errorf("failed to grant %q to %q for %q: %v", mcRoles, project, g.AccountID, err) } - - if _, err := job.WaitForSuccess(); err != nil { - return fmt.Errorf("failed to grant %q to %q for %q: %v", mcRoles, project, g.AccountID, err) + if job != nil { + if _, err := job.WaitForSuccess(); err != nil { + return fmt.Errorf("failed to grant %q to %q for %q: %v", mcRoles, project, g.AccountID, err) + } } } case resourceTypeTable: @@ -198,7 +211,7 @@ func (p *provider) GrantAccess(ctx context.Context, pc *domain.ProviderConfig, g securityManager := client.Project(project).SecurityManager() actions := strings.Join(g.Permissions, ", ") - query := fmt.Sprintf("GRANT %s ON TABLE %s TO USER %s", actions, g.Resource.URN, g.AccountID) + query := fmt.Sprintf("GRANT %s ON TABLE %s TO USER %s", actions, g.Resource.Name, g.AccountID) job, err := securityManager.Run(query, true, "") if err != nil { return fmt.Errorf("failed to grant %q to %q for %q: %v", actions, g.Resource.URN, g.AccountID, err) @@ -215,7 +228,14 @@ func (p *provider) GrantAccess(ctx context.Context, pc *domain.ProviderConfig, g } func (p *provider) RevokeAccess(ctx context.Context, pc *domain.ProviderConfig, g domain.Grant) error { - ramRole, _ := getParametersFromGrant[string](g, parameterRAMRoleKey) + var ramRole string + if slices.Contains(pc.GetParameterKeys(), parameterRAMRoleKey) { + r, _, err := getParametersFromGrant[string](g, parameterRAMRoleKey) + if err != nil { + return fmt.Errorf("failed to get %q parameter value from grant: %w", parameterRAMRoleKey, err) + } + ramRole = r + } client, err := p.getOdpsClient(pc, ramRole) if err != nil { return err @@ -229,7 +249,7 @@ func (p *provider) RevokeAccess(ctx context.Context, pc *domain.ProviderConfig, revokeFromProjectMember := false var permissions []string for _, p := range g.Permissions { - if p == "member" { + if p == projectPermissionMember { revokeFromProjectMember = true continue } @@ -248,23 +268,24 @@ func (p *provider) RevokeAccess(ctx context.Context, pc *domain.ProviderConfig, } } - mcRoles := strings.Join(permissions, ", ") - query := fmt.Sprintf("REVOKE %s FROM %s", mcRoles, g.AccountID) - job, err := securityManager.Run(query, true, "") - if err != nil { - return fmt.Errorf("failed to revoke %q from %q for %q: %v", mcRoles, project, g.AccountID, err) - } + if len(permissions) > 0 { + mcRoles := strings.Join(permissions, ", ") + query := fmt.Sprintf("REVOKE %s FROM %s", mcRoles, g.AccountID) + job, err := securityManager.Run(query, true, "") + if err != nil { + return fmt.Errorf("failed to revoke %q from %q for %q: %v", mcRoles, project, g.AccountID, err) + } - if _, err := job.WaitForSuccess(); err != nil { - return fmt.Errorf("failed to revoke %q from %q for %q: %v", mcRoles, project, g.AccountID, err) + if _, err := job.WaitForSuccess(); err != nil { + return fmt.Errorf("failed to revoke %q from %q for %q: %v", mcRoles, project, g.AccountID, err) + } } - case resourceTypeTable: project := strings.Split(g.Resource.URN, ".")[0] securityManager := client.Project(project).SecurityManager() actions := strings.Join(g.Permissions, ", ") - query := fmt.Sprintf("REVOKE %s ON TABLE %s FROM USER %s", actions, g.Resource.URN, g.AccountID) + query := fmt.Sprintf("REVOKE %s ON TABLE %s FROM USER %s", actions, g.Resource.Name, g.AccountID) job, err := securityManager.Run(query, true, "") if err != nil { return fmt.Errorf("failed to revoke %q from %q for %q: %v", actions, g.Resource.URN, g.AccountID, err) @@ -280,6 +301,45 @@ func (p *provider) RevokeAccess(ctx context.Context, pc *domain.ProviderConfig, return nil } +func (p *provider) GetDependencyGrants(ctx context.Context, pd domain.Provider, g domain.Grant) ([]*domain.Grant, error) { + if g.Resource.ProviderType != "maxcompute" { + return nil, fmt.Errorf("unsupported provider type: %q", g.Resource.ProviderType) + } + + var projectName string + switch g.Resource.Type { + case resourceTypeProject: + if !slices.Contains(g.Permissions, projectPermissionMember) { + projectName = g.Resource.URN + } + case resourceTypeTable: + projectName = strings.Split(g.Resource.URN, ".")[0] + default: + return nil, fmt.Errorf("invalid resource type: %q", g.Resource.Type) + } + + if projectName == "" { + return nil, nil + } + + projectMember := &domain.Grant{ + AccountID: g.AccountID, + AccountType: g.AccountType, + Role: projectPermissionMember, + Permissions: []string{projectPermissionMember}, + IsPermanent: true, + AppealID: g.AppealID, + Resource: &domain.Resource{ + ProviderType: g.Resource.ProviderType, + ProviderURN: g.Resource.ProviderURN, + Type: resourceTypeProject, + URN: projectName, + }, + } + + return []*domain.Grant{projectMember}, nil +} + func (p *provider) getCreds(pc *domain.ProviderConfig) (*credentials, error) { cfg := &config{pc} creds, err := cfg.getCredentials() @@ -392,8 +452,31 @@ func (p *provider) getOdpsClient(pc *domain.ProviderConfig, overrideRAMRole stri return client, nil } -func getParametersFromGrant[T any](g domain.Grant, key string) (T, bool) { +func getParametersFromGrant[T any](g domain.Grant, key string) (T, bool, error) { + var value T + if g.Appeal == nil { + return value, false, fmt.Errorf("appeal is missing in grant") + } appealParams, _ := g.Appeal.Details[domain.ReservedDetailsKeyProviderParameters].(map[string]any) + if appealParams == nil { + return value, false, nil + } + value, ok := appealParams[key].(T) - return value, ok + return value, ok, nil +} + +func execQuery(sm security.Manager, query string) (*security.AuthQueryInstance, error) { + instance, err := sm.Run(query, true, "") + if err != nil { + var restErr restclient.HttpError + if errors.As(err, &restErr) { + if restErr.StatusCode == http.StatusConflict && restErr.ErrorMessage.ErrorCode == "ObjectAlreadyExists" { + return nil, nil + } + } + return nil, err + } + + return instance, nil }