From d248054d313be21ea04d28dc218678be3df3b5eb Mon Sep 17 00:00:00 2001 From: Guiheux Steven Date: Thu, 17 Oct 2024 16:58:07 +0200 Subject: [PATCH] feat(api): add region_project rbac model (#7176) --- engine/api/rbac/dao_rbac.go | 9 +++ engine/api/rbac/dao_rbac_region_project.go | 22 ++++++ .../api/rbac/dao_rbac_region_project_key.go | 55 +++++++++++++++ engine/api/rbac/gorp_model.go | 69 ++++++++++++++----- engine/api/rbac/loader.go | 64 ++++++++++++----- engine/api/v2_rbac.go | 39 +++++++++-- engine/api/v2_rbac_test.go | 18 ++++- engine/sql/api/304_v2_rbac_region_project.sql | 31 +++++++++ sdk/rbac.go | 21 +++--- sdk/rbac_region_project.go | 13 ++++ 10 files changed, 289 insertions(+), 52 deletions(-) create mode 100644 engine/api/rbac/dao_rbac_region_project.go create mode 100644 engine/api/rbac/dao_rbac_region_project_key.go create mode 100644 engine/sql/api/304_v2_rbac_region_project.sql create mode 100644 sdk/rbac_region_project.go diff --git a/engine/api/rbac/dao_rbac.go b/engine/api/rbac/dao_rbac.go index f373f6f6a1..fe657a837d 100644 --- a/engine/api/rbac/dao_rbac.go +++ b/engine/api/rbac/dao_rbac.go @@ -103,6 +103,15 @@ func Insert(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rb *sdk.RBAC) return err } } + for i := range rb.RegionProjects { + dbRbRegionProject := rbacRegionProject{ + RbacID: dbRb.ID, + RBACRegionProject: rb.RegionProjects[i], + } + if err := insertRBACRegionProject(ctx, db, &dbRbRegionProject); err != nil { + return err + } + } *rb = dbRb.RBAC return nil diff --git a/engine/api/rbac/dao_rbac_region_project.go b/engine/api/rbac/dao_rbac_region_project.go new file mode 100644 index 0000000000..409c1de806 --- /dev/null +++ b/engine/api/rbac/dao_rbac_region_project.go @@ -0,0 +1,22 @@ +package rbac + +import ( + "context" + + "github.com/ovh/cds/engine/api/database/gorpmapping" + "github.com/ovh/cds/engine/gorpmapper" +) + +func insertRBACRegionProject(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacRegionProject *rbacRegionProject) error { + if err := gorpmapping.InsertAndSign(ctx, db, rbacRegionProject); err != nil { + return err + } + + for _, projectKey := range rbacRegionProject.RBACProjectKeys { + if err := insertRBACRegionProjectKey(ctx, db, rbacRegionProject.ID, projectKey); err != nil { + return err + } + } + + return nil +} diff --git a/engine/api/rbac/dao_rbac_region_project_key.go b/engine/api/rbac/dao_rbac_region_project_key.go new file mode 100644 index 0000000000..ecdd2556be --- /dev/null +++ b/engine/api/rbac/dao_rbac_region_project_key.go @@ -0,0 +1,55 @@ +package rbac + +import ( + "context" + + "github.com/go-gorp/gorp" + "github.com/ovh/cds/engine/api/database/gorpmapping" + "github.com/ovh/cds/engine/gorpmapper" + "github.com/ovh/cds/sdk" + "github.com/rockbears/log" +) + +func getAllRBACRegionProjectKeys(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query) ([]rbacRegionProjectKey, error) { + var rbacRegionProjectIdentifier []rbacRegionProjectKey + if err := gorpmapping.GetAll(ctx, db, q, &rbacRegionProjectIdentifier); err != nil { + return nil, err + } + rbacProjectIdentifierFiltered := make([]rbacRegionProjectKey, 0, len(rbacRegionProjectIdentifier)) + for _, projectDatas := range rbacRegionProjectIdentifier { + isValid, err := gorpmapping.CheckSignature(projectDatas, projectDatas.Signature) + if err != nil { + return nil, sdk.WrapError(err, "error when checking signature for rbac_region_project_keys_project %d", projectDatas.ID) + } + if !isValid { + log.Error(ctx, "rbac.getAllRBACRegionProjectKeys> rbac_region_project_keys_project %d data corrupted", projectDatas.ID) + continue + } + rbacProjectIdentifierFiltered = append(rbacProjectIdentifierFiltered, projectDatas) + } + return rbacProjectIdentifierFiltered, nil +} + +func insertRBACRegionProjectKey(ctx context.Context, db gorpmapper.SqlExecutorWithTx, rbacParentID int64, projectKey string) error { + rpk := rbacRegionProjectKey{ + RbacRegionProjectID: rbacParentID, + ProjectKey: projectKey, + } + if err := gorpmapping.InsertAndSign(ctx, db, &rpk); err != nil { + return err + } + return nil +} + +func loadRBACRegionProjectKeys(ctx context.Context, db gorp.SqlExecutor, rbacRegionProject *rbacRegionProject) error { + q := gorpmapping.NewQuery("SELECT * FROM rbac_region_project_keys_project WHERE rbac_region_project_id = $1").Args(rbacRegionProject.ID) + rbacRegionProjectKeys, err := getAllRBACRegionProjectKeys(ctx, db, q) + if err != nil { + return err + } + rbacRegionProject.RBACRegionProject.RBACProjectKeys = make([]string, 0, len(rbacRegionProjectKeys)) + for _, projectDatas := range rbacRegionProjectKeys { + rbacRegionProject.RBACRegionProject.RBACProjectKeys = append(rbacRegionProject.RBACRegionProject.RBACProjectKeys, projectDatas.ProjectKey) + } + return nil +} diff --git a/engine/api/rbac/gorp_model.go b/engine/api/rbac/gorp_model.go index bce9bde671..673a8d3774 100644 --- a/engine/api/rbac/gorp_model.go +++ b/engine/api/rbac/gorp_model.go @@ -33,9 +33,9 @@ func (rg rbacGlobal) Canonical() gorpmapper.CanonicalForms { } type rbacGlobalUser struct { - ID int64 `db:"id"` - RbacGlobalID int64 `db:"rbac_global_id"` - RbacGlobalUserID string `db:"user_id"` + ID int64 `json:"-" db:"id"` + RbacGlobalID int64 `json:"-" db:"rbac_global_id"` + RbacGlobalUserID string `json:"-" db:"user_id"` gorpmapper.SignedEntity } @@ -48,8 +48,8 @@ func (rgu rbacGlobalUser) Canonical() gorpmapper.CanonicalForms { type rbacGlobalGroup struct { ID int64 `json:"-" db:"id"` - RbacGlobalID int64 `db:"rbac_global_id"` - RbacGlobalGroupID int64 `db:"group_id"` + RbacGlobalID int64 `json:"-" db:"rbac_global_id"` + RbacGlobalGroupID int64 `json:"-" db:"group_id"` gorpmapper.SignedEntity } @@ -130,6 +130,34 @@ func (rh rbacHatchery) Canonical() gorpmapper.CanonicalForms { } } +type rbacRegionProject struct { + ID int64 `json:"-" db:"id"` + RbacID string `json:"-" db:"rbac_id"` + sdk.RBACRegionProject + gorpmapper.SignedEntity +} + +func (rr rbacRegionProject) Canonical() gorpmapper.CanonicalForms { + _ = []interface{}{rr.ID, rr.RbacID, rr.RegionID, rr.Role} + return []gorpmapper.CanonicalForm{ + "{{.ID}}{{.RbacID}}{{.RegionID}}{{.Role}}", + } +} + +type rbacRegionProjectKey struct { + ID int64 `json:"-" db:"id"` + RbacRegionProjectID int64 `json:"-" db:"rbac_region_project_id"` + ProjectKey string `json:"-" db:"project_key"` + gorpmapper.SignedEntity +} + +func (rr rbacRegionProjectKey) Canonical() gorpmapper.CanonicalForms { + _ = []interface{}{rr.ID, rr.RbacRegionProjectID, rr.ProjectKey} + return []gorpmapper.CanonicalForm{ + "{{.ID}}{{.RbacRegionProjectID}}{{.ProjectKey}}", + } +} + type rbacRegion struct { sdk.RBACRegion gorpmapper.SignedEntity @@ -144,8 +172,8 @@ func (rr rbacRegion) Canonical() gorpmapper.CanonicalForms { type rbacRegionUser struct { ID int64 `json:"-" db:"id"` - RbacRegionID int64 `db:"rbac_region_id"` - RbacUserID string `db:"user_id"` + RbacRegionID int64 `json:"-" db:"rbac_region_id"` + RbacUserID string `json:"-" db:"user_id"` gorpmapper.SignedEntity } @@ -158,8 +186,8 @@ func (rru rbacRegionUser) Canonical() gorpmapper.CanonicalForms { type rbacRegionGroup struct { ID int64 `json:"-" db:"id"` - RbacRegionID int64 `db:"rbac_region_id"` - RbacGroupID int64 `db:"group_id"` + RbacRegionID int64 `json:"-" db:"rbac_region_id"` + RbacGroupID int64 `json:"-" db:"group_id"` gorpmapper.SignedEntity } @@ -172,8 +200,8 @@ func (rrg rbacRegionGroup) Canonical() gorpmapper.CanonicalForms { type rbacRegionOrganization struct { ID int64 `json:"-" db:"id"` - RbacRegionID int64 `db:"rbac_region_id"` - RbacOrganizationID string `db:"organization_id"` + RbacRegionID int64 `json:"-" db:"rbac_region_id"` + RbacOrganizationID string ` json:"-" db:"organization_id"` gorpmapper.SignedEntity } @@ -200,8 +228,8 @@ func (rp rbacVariableSet) Canonical() gorpmapper.CanonicalForms { type rbacVariableSetUser struct { ID int64 `json:"-" db:"id"` - RbacVariableSetID int64 `db:"rbac_variableset_id"` - RbacVariableSetUserID string `db:"user_id"` + RbacVariableSetID int64 `json:"-" db:"rbac_variableset_id"` + RbacVariableSetUserID string `json:"-" db:"user_id"` gorpmapper.SignedEntity } @@ -214,8 +242,8 @@ func (rgu rbacVariableSetUser) Canonical() gorpmapper.CanonicalForms { type rbacVariableSetGroup struct { ID int64 `json:"-" db:"id"` - RbacVariableSetID int64 `db:"rbac_variableset_id"` - RbacVariableSetGroupID int64 `db:"group_id"` + RbacVariableSetID int64 `json:"-" db:"rbac_variableset_id"` + RbacVariableSetGroupID int64 ` json:"-" db:"group_id"` gorpmapper.SignedEntity } @@ -242,8 +270,8 @@ func (rp rbacWorkflow) Canonical() gorpmapper.CanonicalForms { type rbacWorkflowUser struct { ID int64 `json:"-" db:"id"` - RbacWorkflowID int64 `db:"rbac_workflow_id"` - RbacWorkflowUserID string `db:"user_id"` + RbacWorkflowID int64 `json:"-" db:"rbac_workflow_id"` + RbacWorkflowUserID string ` json:"-" db:"user_id"` gorpmapper.SignedEntity } @@ -256,8 +284,8 @@ func (rgu rbacWorkflowUser) Canonical() gorpmapper.CanonicalForms { type rbacWorkflowGroup struct { ID int64 `json:"-" db:"id"` - RbacWorkflowID int64 `db:"rbac_workflow_id"` - RbacWorkflowGroupID int64 `db:"group_id"` + RbacWorkflowID int64 `json:"-" db:"rbac_workflow_id"` + RbacWorkflowGroupID int64 `json:"-" db:"group_id"` gorpmapper.SignedEntity } @@ -294,4 +322,7 @@ func init() { gorpmapping.Register(gorpmapping.New(rbacVariableSetUser{}, "rbac_variableset_users", true, "id")) gorpmapping.Register(gorpmapping.New(rbacVariableSetGroup{}, "rbac_variableset_groups", true, "id")) + gorpmapping.Register(gorpmapping.New(rbacRegionProject{}, "rbac_region_project", true, "id")) + gorpmapping.Register(gorpmapping.New(rbacRegionProjectKey{}, "rbac_region_project_keys_project", true, "id")) + } diff --git a/engine/api/rbac/loader.go b/engine/api/rbac/loader.go index 881ff9e3cd..709558faf4 100644 --- a/engine/api/rbac/loader.go +++ b/engine/api/rbac/loader.go @@ -15,23 +15,25 @@ type LoadOptionFunc func(context.Context, gorp.SqlExecutor, *rbac) error // LoadOptions provides all options on rbac loads functions var LoadOptions = struct { - Default LoadOptionFunc - LoadRBACGlobal LoadOptionFunc - LoadRBACProject LoadOptionFunc - LoadRBACHatchery LoadOptionFunc - LoadRBACRegion LoadOptionFunc - LoadRBACWorkflow LoadOptionFunc - LoadRBACVariableSet LoadOptionFunc - All LoadOptionFunc + Default LoadOptionFunc + LoadRBACGlobal LoadOptionFunc + LoadRBACProject LoadOptionFunc + LoadRBACHatchery LoadOptionFunc + LoadRBACRegion LoadOptionFunc + LoadRBACWorkflow LoadOptionFunc + LoadRBACVariableSet LoadOptionFunc + LoadRbacRegionProject LoadOptionFunc + All LoadOptionFunc }{ - Default: loadDefault, - LoadRBACGlobal: loadRBACGlobal, - LoadRBACProject: loadRBACProject, - LoadRBACHatchery: loadRBACHatchery, - LoadRBACRegion: loadRBACRegion, - LoadRBACWorkflow: loadRBACWorkflow, - LoadRBACVariableSet: loadRBACVariableSet, - All: loadAll, + Default: loadDefault, + LoadRBACGlobal: loadRBACGlobal, + LoadRBACProject: loadRBACProject, + LoadRBACHatchery: loadRBACHatchery, + LoadRBACRegion: loadRBACRegion, + LoadRBACWorkflow: loadRBACWorkflow, + LoadRBACVariableSet: loadRBACVariableSet, + LoadRbacRegionProject: loadRBACRegionProject, + All: loadAll, } func loadDefault(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error { @@ -63,6 +65,9 @@ func loadAll(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error { if err := loadRBACVariableSet(ctx, db, rbac); err != nil { return err } + if err := loadRBACRegionProject(ctx, db, rbac); err != nil { + return err + } return nil } @@ -187,6 +192,33 @@ func loadRBACGlobal(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error return nil } +func loadRBACRegionProject(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error { + query := "SELECT * FROM rbac_region_project WHERE rbac_id = $1" + var rbacRegionProjects []rbacRegionProject + if err := gorpmapping.GetAll(ctx, db, gorpmapping.NewQuery(query).Args(rbac.ID), &rbacRegionProjects); err != nil { + return err + } + rbac.RegionProjects = make([]sdk.RBACRegionProject, 0, len(rbacRegionProjects)) + for i := range rbacRegionProjects { + rbacRegionProject := &rbacRegionProjects[i] + isValid, err := gorpmapping.CheckSignature(rbacRegionProject, rbacRegionProject.Signature) + if err != nil { + return sdk.WrapError(err, "error when checking signature for rbac_region_project %d", rbacRegionProject.ID) + } + if !isValid { + log.Error(ctx, "loadRBACRegionProject> rbac_region_project %d data corrupted", rbacRegionProject.ID) + continue + } + if !rbacRegionProject.AllProjects { + if err := loadRBACRegionProjectKeys(ctx, db, rbacRegionProject); err != nil { + return err + } + } + rbac.RegionProjects = append(rbac.RegionProjects, rbacRegionProject.RBACRegionProject) + } + return nil +} + func loadRBACRegion(ctx context.Context, db gorp.SqlExecutor, rbac *rbac) error { query := "SELECT * FROM rbac_region WHERE rbac_id = $1" var rbacRegions []rbacRegion diff --git a/engine/api/v2_rbac.go b/engine/api/v2_rbac.go index 79457bc5b7..06d00ed1f2 100644 --- a/engine/api/v2_rbac.go +++ b/engine/api/v2_rbac.go @@ -67,12 +67,7 @@ func (api *API) getRBACHandler() ([]service.RbacChecker, service.Handler) { vars := mux.Vars(req) rbacIdentifier := vars["rbacIdentifier"] perm, err := api.getRBACByIdentifier(ctx, rbacIdentifier, - rbac.LoadOptions.LoadRBACGlobal, - rbac.LoadOptions.LoadRBACProject, - rbac.LoadOptions.LoadRBACWorkflow, - rbac.LoadOptions.LoadRBACHatchery, - rbac.LoadOptions.LoadRBACVariableSet, - rbac.LoadOptions.LoadRBACRegion) + rbac.LoadOptions.All) if err != nil { return err } @@ -214,6 +209,13 @@ func (rl *RBACLoader) FillRBACWithNames(ctx context.Context, r *sdk.RBAC) error return err } } + for rpID := range r.RegionProjects { + rp := &r.RegionProjects[rpID] + if err := rl.fillRBACRegionProjectWithNames(ctx, rp); err != nil { + return err + } + } + return nil } @@ -265,6 +267,12 @@ func (rl *RBACLoader) FillRBACWithIDs(ctx context.Context, r *sdk.RBAC) error { return err } } + for rpID := range r.RegionProjects { + rp := &r.RegionProjects[rpID] + if err := rl.fillRBACRegionProjectWithID(ctx, rp); err != nil { + return err + } + } return nil } @@ -298,6 +306,25 @@ func (rl *RBACLoader) fillRBACHatcheryWithID(ctx context.Context, rbacHatchery * return nil } +func (rl *RBACLoader) fillRBACRegionProjectWithNames(ctx context.Context, rbacRegionProject *sdk.RBACRegionProject) error { + rg, err := region.LoadRegionByID(ctx, rl.db, rbacRegionProject.RegionID) + if err != nil { + return err + } + rbacRegionProject.RegionName = rg.Name + return nil +} + +func (rl *RBACLoader) fillRBACRegionProjectWithID(ctx context.Context, rbacRegionProject *sdk.RBACRegionProject) error { + rg, err := region.LoadRegionByName(ctx, rl.db, rbacRegionProject.RegionName) + if err != nil { + return err + } + rbacRegionProject.RegionID = rg.ID + + return nil +} + func (rl *RBACLoader) fillRBACRegionWithNames(ctx context.Context, rbacRegion *sdk.RBACRegion) error { rg, err := region.LoadRegionByID(ctx, rl.db, rbacRegion.RegionID) if err != nil { diff --git a/engine/api/v2_rbac_test.go b/engine/api/v2_rbac_test.go index 30300b5e93..fac56d2797 100644 --- a/engine/api/v2_rbac_test.go +++ b/engine/api/v2_rbac_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ovh/cds/engine/api/rbac" + "github.com/ovh/cds/engine/api/region" "github.com/ovh/cds/engine/api/test" "github.com/ovh/cds/engine/api/test/assets" @@ -27,6 +28,14 @@ func Test_crudRbacHandler(t *testing.T) { u, pass := assets.InsertAdminUser(t, db) g := assets.InsertTestGroup(t, db, sdk.RandomString(10)) + reg := sdk.Region{ + Name: sdk.RandomString(10), + } + require.NoError(t, region.Insert(context.TODO(), db, ®)) + t.Cleanup(func() { + db.Exec("DELETE FROM region WHERE id = $1", reg.ID) + }) + vars := map[string]string{} uri := api.Router.GetRouteV2("POST", api.postImportRBACHandler, vars) test.NotEmpty(t, uri) @@ -52,7 +61,11 @@ variablesets: all_users: true all_variablesets: true project: %s -`, p.Key, u.Username, g.Name, u.Username, g.Name, p.Key, p.Key) +region_projects: + - role: execute + projects: [%s] + region: %s +`, p.Key, u.Username, g.Name, u.Username, g.Name, p.Key, p.Key, p.Key, reg.Name) // Here, we insert the vcs server as a CDS administrator req.Body = io.NopCloser(strings.NewReader(body)) @@ -92,6 +105,9 @@ variablesets: require.Equal(t, 1, len(rbacGET.VariableSets)) + require.Equal(t, 1, len(rbacGET.RegionProjects)) + require.Equal(t, reg.Name, rbacGET.RegionProjects[0].RegionName) + // Delete varsDelete := map[string]string{"rbacIdentifier": rbacGET.ID} uriDelete := api.Router.GetRouteV2("DELETE", api.deleteRBACHandler, varsDelete) diff --git a/engine/sql/api/304_v2_rbac_region_project.sql b/engine/sql/api/304_v2_rbac_region_project.sql new file mode 100644 index 0000000000..99d96fadcc --- /dev/null +++ b/engine/sql/api/304_v2_rbac_region_project.sql @@ -0,0 +1,31 @@ +-- +migrate Up +CREATE TABLE rbac_region_project +( + "id" BIGSERIAL PRIMARY KEY, + "rbac_id" uuid NOT NULL, + "all_projects" BOOLEAN NOT NULL DEFAULT FALSE, + "region_id" uuid NOT NULL, + "role" VARCHAR(255) NOT NULL, + sig BYTEA, + signer TEXT +); +SELECT create_foreign_key_idx_cascade('FK_rbac_region_project', 'rbac_region_project', 'rbac', 'rbac_id', 'id'); +SELECT create_foreign_key_idx_cascade('FK_rbac_region_project_region_id', 'rbac_region_project', 'region', 'region_id', 'id'); +SELECT create_index('rbac_region_project', 'idx_rbac_region_project_role', 'role'); + + +CREATE TABLE rbac_region_project_keys_project +( + "id" BIGSERIAL PRIMARY KEY, + "rbac_region_project_id" BIGINT, + "project_key" VARCHAR(255), + sig BYTEA, + signer TEXT +); +SELECT create_foreign_key_idx_cascade('FK_rbac_region_project_projects', 'rbac_region_project_keys_project', 'rbac_region_project', 'rbac_region_project_id', 'id'); +SELECT create_foreign_key_idx_cascade('FK_rbac_region_project_keys_project', 'rbac_region_project_keys_project', 'project', 'project_key', 'projectkey'); +SELECT create_unique_index('rbac_region_project_keys_project', 'idx_unq_rbac_region_project_keys', 'rbac_region_project_id,project_key'); + +-- +migrate Down +DROP TABLE rbac_region_project_keys_project; +DROP TABLE rbac_region_project; \ No newline at end of file diff --git a/sdk/rbac.go b/sdk/rbac.go index 45494c5332..7a32bf66ed 100644 --- a/sdk/rbac.go +++ b/sdk/rbac.go @@ -33,16 +33,17 @@ const ( ) type RBAC struct { - ID string `json:"id" db:"id"` - Name string `json:"name" db:"name" cli:"name"` - Created time.Time `json:"created" db:"created"` - LastModified time.Time `json:"last_modified" db:"last_modified" cli:"last_modified"` - Global []RBACGlobal `json:"global,omitempty" db:"-"` - Projects []RBACProject `json:"projects,omitempty" db:"-"` - Regions []RBACRegion `json:"regions,omitempty" db:"-"` - Hatcheries []RBACHatchery `json:"hatcheries,omitempty" db:"-"` - Workflows []RBACWorkflow `json:"workflows,omitempty" db:"-"` - VariableSets []RBACVariableSet `json:"variablesets,omitempty" db:"-"` + ID string `json:"id" db:"id"` + Name string `json:"name" db:"name" cli:"name"` + Created time.Time `json:"created" db:"created"` + LastModified time.Time `json:"last_modified" db:"last_modified" cli:"last_modified"` + Global []RBACGlobal `json:"global,omitempty" db:"-"` + Projects []RBACProject `json:"projects,omitempty" db:"-"` + Regions []RBACRegion `json:"regions,omitempty" db:"-"` + Hatcheries []RBACHatchery `json:"hatcheries,omitempty" db:"-"` + Workflows []RBACWorkflow `json:"workflows,omitempty" db:"-"` + VariableSets []RBACVariableSet `json:"variablesets,omitempty" db:"-"` + RegionProjects []RBACRegionProject `json:"region_projects,omitempty" db:"-"` } func (rbac *RBAC) IsEmpty() bool { diff --git a/sdk/rbac_region_project.go b/sdk/rbac_region_project.go new file mode 100644 index 0000000000..dd25644ef9 --- /dev/null +++ b/sdk/rbac_region_project.go @@ -0,0 +1,13 @@ +package sdk + +var ( + RegionProjectRoles = StringSlice{RegionRoleExecute} +) + +type RBACRegionProject struct { + Role string `json:"role" db:"role"` + RegionID string `json:"region_id" db:"region_id"` + AllProjects bool `json:"all_projects,omitempty" db:"all_projects"` + RBACProjectKeys []string `json:"projects,omitempty" db:"-"` + RegionName string `json:"region,omitempty" db:"-"` +}