diff --git a/cmd/schema.go b/cmd/schema.go index e2b34f20c..0029c9032 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -18,7 +18,7 @@ var ( schemaCmd = &cobra.Command{ Use: "schema", Short: "write the schema into SpiceDB", - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cobra.Command, _ []string) { writeSchema(cmd.Context(), dryRun, globalCfg) }, } @@ -43,7 +43,7 @@ func init() { } } -func writeSchema(ctx context.Context, dryRun bool, cfg *config.AppConfig) { +func writeSchema(_ context.Context, dryRun bool, cfg *config.AppConfig) { var ( err error policy iapl.Policy diff --git a/docs/rbac.md b/docs/rbac.md index ded0a9f24..924c13516 100644 --- a/docs/rbac.md +++ b/docs/rbac.md @@ -36,12 +36,10 @@ In the IAPL, a new `rbac` directive is introduced to define the RBAC configurati property | yaml | type | description -|-|-|- RoleResource |`rbac.roleresource`| string | name of the resource type that represents a role. -RoleRelationshipSubject |`rbac.rolerelationshipsubject`| string | name of the relationship that connects a role to a subject. -RoleOwners |`rbac.roleowners`| []string | names of the resource types that can own a role. +RoleSubjectTypes |`rbac.rolesubjecttypes`| string | a list of subject types that the relationships in a role resource will contain. +RoleOwners |`rbac.roleowners`| []string | the list of resource types that can own a role. These resources should be (but not limited to) organizational resources like tenant, organization, project, group, etc When a role is owned by an entity, say a group, that means this role will be available to perform role-bindings for resources that are owned by this group and its subgroups. The RoleOwners relationship is particularly useful to limit access to custom roles. RoleBindingResource |`rbac.rolebindingresource`| string | name of the resource type that represents a role binding. RoleBindingSubjects |`rbac.rolebindingsubjects`| []string | names of the resource types that can be subjects in a role binding. -RolebindingPermissionsPrefix |`rbac.rolebindingpermissionsprefix`| string | generates the permissions sets to manage role bindings, -GrantRelationship |`rbac.grantrelationship`| string | name of the relationship that connects a role binding to a resource. e.g. rolebinding_create, rolebinding_list, rolebinding_delete, etc. For example, consider the following spicedb schema: diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index dbd0d878b..bb2144625 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -41,23 +41,16 @@ type Union struct { } // Action represents an action that can be taken in an authorization policy. -// AdditionalPermissions can be defined to allow additional permissions to be -// created in addition to the permission with the same name as the action. type Action struct { Name string } // ActionBinding represents a binding of an action to a resource type or union. -// RenamePermission is the name of the permission that should be used instead of -// the action name when checking for permission to perform the action, this -// allows the permission to be renamed to avoid conflicts with relationships -// with the same name. type ActionBinding struct { - ActionName string - TypeName string - RenamePermission string - Conditions []Condition - ConditionSets []types.ConditionSet + ActionName string + TypeName string + Conditions []Condition + ConditionSets []types.ConditionSet } // Condition represents a necessary condition for performing an action. @@ -71,9 +64,10 @@ type Condition struct { type ConditionRoleBinding struct{} // ConditionRoleBindingV2 represents a condition where a role binding is necessary to perform an action. -// This is the new version of the condition, and it is used to support the new role binding resource type. +// Using this condition type in the policy will instruct the policy engine to +// create all the necessary relationships in the schema to support RBAC V2 type ConditionRoleBindingV2 struct { - InheritGrants []string + InheritGrantsFrom []string } // ConditionRelationshipAction represents a condition where another action must be allowed on a resource @@ -99,10 +93,6 @@ type policy struct { rb map[string]map[string]struct{} bn []ActionBinding p PolicyDocument - - permissions []string - rolebindingActions []string - roleOwners map[string]struct{} } // NewPolicy creates a policy from the given policy document. @@ -117,39 +107,27 @@ func NewPolicy(p PolicyDocument) Policy { un[t.Name] = t } - // permissions for performing role-bindings (i.e., create role-binding, get - // role-binding, delete role-binding) - rbActions := []string{"create", "list", "get", "update", "delete"} - rolebindingActions := make([]string, len(rbActions)) ac := map[string]Action{} for _, a := range p.Actions { ac[a.Name] = a } - if p.RBAC != nil { - for i, rba := range rbActions { - actionName := fmt.Sprintf("%s_%s", p.RBAC.RoleBindingPermissionsPrefix, rba) - rolebindingActions[i] = actionName - ac[actionName] = Action{Name: actionName} - } - - ac[AvailableRoleRelation] = Action{Name: AvailableRoleRelation} - } - out := policy{ - rt: rt, - un: un, - ac: ac, - p: p, - permissions: []string{}, - rolebindingActions: rolebindingActions, - roleOwners: map[string]struct{}{}, + rt: rt, + un: un, + ac: ac, + p: p, } if p.RBAC != nil { - out.expandRole() - out.expandRoleBinding() + for _, rba := range p.RBAC.RoleBindingActions() { + out.ac[rba.Name] = rba + } + + out.createV2RoleResourceType() + out.createRoleBindingResourceType() + out.expandRBACV2Relationships() } out.expandActionBindings() @@ -207,14 +185,17 @@ func (v *policy) validateResourceTypes() error { return false } - for _, resourceType := range v.p.ResourceTypes { + for _, resourceType := range v.rt { for _, rel := range resourceType.Relationships { + // two kinds of relationships target types to be validated + // 1. simple target type names for _, name := range rel.TargetTypeNames { if _, ok := v.rt[name]; !ok { return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, name, ErrorUnknownType) } } + // 2. target types with optional subject relation or subject identifier for _, tt := range rel.TargetTypes { if _, ok := v.rt[tt.Name]; !ok { return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, tt.Name, ErrorUnknownType) @@ -306,6 +287,10 @@ func (v *policy) validateActionBindings() error { return nil } +// validateRoles validates V2 role resource types to ensure that: +// 1. role resource type has a valid owner relationship +// 2. role-owner resource types have a valid member-role relationship points +// to the role resource func (v *policy) validateRoles() error { if v.p.RBAC == nil { return nil @@ -316,39 +301,42 @@ func (v *policy) validateRoles() error { // check if role owner exists if !ok { - return fmt.Errorf("%w: role owner %s does not exists", ErrorUnknownType, roleOwnerName) + return fmt.Errorf("%w: role owner %s does not exist", ErrorUnknownType, roleOwnerName) } - relationshipExists := false - targetOk := false - // role owner must have `member_role` relationship to `role` resource - rel_loop: - for _, rel := range owner.Relationships { - if rel.Relation != RoleOwnerMemberRoleRelation { - continue - } + ensureMemberRole := func() bool { + relationshipExists := false + targetOk := false - relationshipExists = true + for _, rel := range owner.Relationships { + if rel.Relation != RoleOwnerMemberRoleRelation { + continue + } - for i := 0; i < len(rel.TargetTypes) && !targetOk; i++ { - if rel.TargetTypes[i].Name == v.p.RBAC.RoleResource { - targetOk = true + relationshipExists = true + + for i := 0; i < len(rel.TargetTypes) && !targetOk; i++ { + if rel.TargetTypes[i].Name == v.p.RBAC.RoleResource { + targetOk = true + } } - } - for i := 0; i < len(rel.TargetTypeNames) && !targetOk; i++ { - if rel.TargetTypeNames[i] == v.p.RBAC.RoleResource { - targetOk = true + for i := 0; i < len(rel.TargetTypeNames) && !targetOk; i++ { + if rel.TargetTypeNames[i] == v.p.RBAC.RoleResource { + targetOk = true + } } - } - if !targetOk { - break rel_loop + if !targetOk { + return false + } } + + return relationshipExists && targetOk } - if !relationshipExists || !targetOk { + if ok := ensureMemberRole(); !ok { return fmt.Errorf( "%w: role owner %s must have %s relation to %s", ErrorMissingRelationship, roleOwnerName, RoleOwnerMemberRoleRelation, @@ -399,21 +387,16 @@ func (v *policy) expandActionBindings() { } } -// expandRole creates a list of all permissions, and a resource containing -// a list of relationship to all permissions. -func (v *policy) expandRole() { - // 1. create a list of all permissions - perms := make(map[string]struct{}) - - for _, action := range v.ac { - perms[action.Name] = struct{}{} - } - - for perm := range perms { - v.permissions = append(v.permissions, perm) +// createV2RoleResourceType creates a v2 role resource type contains a list of relationships +// representing all the actions, as well as relationships and permissions for +// the management of the roles themselves. +func (v *policy) createV2RoleResourceType() { + role, ok := v.rt[v.p.RBAC.RoleResource] + if !ok { + panic("v2 role is specified but role resource type is not defined") } - // 2. create a relationship for role owners + // 1. create a relationship for role owners roleOwners := Relationship{ Relation: RoleOwnerRelation, TargetTypes: make([]types.TargetType, len(v.p.RBAC.RoleOwners)), @@ -423,47 +406,41 @@ func (v *policy) expandRole() { roleOwners.TargetTypes[i] = types.TargetType{Name: owner} } - // 3. create a list of relationships for all permissions - permsRel := make([]Relationship, len(perms)) + // 2. create a list of relationships for all permissions + permsRel := make([]Relationship, 0, len(v.ac)) - for i, perm := range v.permissions { - targettypes := make([]types.TargetType, len(v.p.RBAC.RoleRelationshipSubjects)) + for _, action := range v.ac { + targettypes := make([]types.TargetType, len(v.p.RBAC.RoleSubjectTypes)) - for j, subject := range v.p.RBAC.RoleRelationshipSubjects { + for j, subject := range v.p.RBAC.RoleSubjectTypes { targettypes[j] = types.TargetType{Name: subject, SubjectIdentifier: "*"} } - permsRel[i] = Relationship{ - Relation: perm + "_rel", - TargetTypes: targettypes, - } + permsRel = append(permsRel, + Relationship{ + Relation: action.Name + PermissionRelationSuffix, + TargetTypes: targettypes, + }, + ) } - // 4. create a role containing all the relationships shown above - var role ResourceType - + // 3. create a role resource type containing all the relationships shown above permsRel = append(permsRel, roleOwners) - if _, ok := v.rt[v.p.RBAC.RoleResource]; ok { - role = v.rt[v.p.RBAC.RoleResource] - role.Relationships = permsRel - } else { - role = ResourceType{ - Name: v.p.RBAC.RoleResource, - Relationships: permsRel, - } - } - + role.Relationships = permsRel v.rt[role.Name] = role +} - // 5. create a list of role owners - for _, owner := range v.p.RBAC.RoleOwners { - v.roleOwners[owner] = struct{}{} +// createRoleBindingResourceType creates a role-binding resource type contains +// a list of all the actions. +// The role-binding resources will be used to create a 3-way relationship +// between a resource, a subject and a role +func (v *policy) createRoleBindingResourceType() { + rolebinding, ok := v.rt[v.p.RBAC.RoleBindingResource] + if !ok { + panic("v2 role-binding is specified but role-binding resource type is not defined") } -} -func (v *policy) expandRoleBinding() { - // 1. create relationship to role role := Relationship{ Relation: RolebindingRoleRelation, TargetTypes: []types.TargetType{ @@ -479,8 +456,7 @@ func (v *policy) expandRoleBinding() { // 3. create a list of action-bindings representing permissions for all the // actions - actionbindings := make([]ActionBinding, len(v.ac)) - i := 0 + actionbindings := make([]ActionBinding, 0, len(v.ac)) for actionName := range v.ac { ab := ActionBinding{ @@ -492,7 +468,7 @@ func (v *policy) expandRoleBinding() { { RelationshipAction: &types.ConditionRelationshipAction{ Relation: RolebindingRoleRelation, - ActionName: actionName + "_rel", + ActionName: actionName + PermissionRelationSuffix, }, }, }, @@ -505,61 +481,31 @@ func (v *policy) expandRoleBinding() { }, } - actionbindings[i] = ab - i++ + actionbindings = append(actionbindings, ab) } v.bn = append(v.bn, actionbindings...) // 4. create role-binding resource type - var rolebinding ResourceType - - if _, ok := v.rt[v.p.RBAC.RoleBindingResource]; ok { - rolebinding = v.rt[v.p.RBAC.RoleBindingResource] - rolebinding.Relationships = []Relationship{role, subjects} - } else { - rolebinding = ResourceType{ - Name: v.p.RBAC.RoleBindingResource, - Relationships: []Relationship{role, subjects}, - } - } - + rolebinding.Relationships = []Relationship{role, subjects} v.rt[v.p.RBAC.RoleBindingResource] = rolebinding } -func (v *policy) expandResourceTypes() { +// expandRBACV2Relationships adds RBAC V2 relationships to all the resource +// types that has `ResourceRoleBindingV2` defined. +// Relationships like member_roles, available_roles, are created to support +// role inheritance, e.g., an org should be able to use roles defined by its +// parents +func (v *policy) expandRBACV2Relationships() { for name, resourceType := range v.rt { - for i, rel := range resourceType.Relationships { - var typeNames []string - - targettypes := rel.TargetTypes - - for _, typeName := range rel.TargetTypeNames { - if u, ok := v.un[typeName]; ok { - if len(u.ResourceTypes) > 0 { - targettypes = append(targettypes, u.ResourceTypes...) - } else { - typeNames = append(typeNames, u.ResourceTypeNames...) - } - } else { - typeNames = append(typeNames, typeName) - } - } - - for _, tn := range typeNames { - targettypes = append(targettypes, types.TargetType{Name: tn}) - } - - resourceType.Relationships[i].TargetTypeNames = typeNames - resourceType.Relationships[i].TargetTypes = targettypes - } - - // role-binding V2 configs - availRoleConditions := []Condition{} + // not all roles are available for all resources, available roles are + // the roles that a resource owners (if it is a role-owner) or inherited + // from their owner or parent + availableRoles := []Condition{} // if resource type is a role-owner, add the role-relationship to the // resource - if _, ok := v.roleOwners[name]; ok { + if _, ok := v.RBAC().RoleOwnersSet()[name]; ok { memberRoleRelation := Relationship{ Relation: RoleOwnerMemberRoleRelation, TargetTypes: []types.TargetType{ @@ -570,7 +516,7 @@ func (v *policy) expandResourceTypes() { resourceType.Relationships = append(resourceType.Relationships, memberRoleRelation) // i.e. avail_role = member_role - availRoleConditions = append(availRoleConditions, Condition{ + availableRoles = append(availableRoles, Condition{ RelationshipAction: &ConditionRelationshipAction{ Relation: RoleOwnerMemberRoleRelation, }, @@ -580,7 +526,7 @@ func (v *policy) expandResourceTypes() { // i.e. avail_role = from[0]->avail_role + from[1]->avail_role ... if resourceType.RoleBindingV2 != nil { for _, from := range resourceType.RoleBindingV2.AvailableRolesFrom { - availRoleConditions = append(availRoleConditions, Condition{ + availableRoles = append(availableRoles, Condition{ RelationshipAction: &ConditionRelationshipAction{ Relation: from, ActionName: AvailableRoleRelation, @@ -590,11 +536,11 @@ func (v *policy) expandResourceTypes() { } // create available role permission - if len(availRoleConditions) > 0 { + if len(availableRoles) > 0 { action := ActionBinding{ ActionName: AvailableRoleRelation, TypeName: resourceType.Name, - Conditions: availRoleConditions, + Conditions: availableRoles, } v.bn = append(v.bn, action) @@ -604,6 +550,40 @@ func (v *policy) expandResourceTypes() { } } +func (v *policy) expandResourceTypes() { + for name, resourceType := range v.rt { + for i, rel := range resourceType.Relationships { + var typeNames []string + + targettypes := rel.TargetTypes + + for _, typeName := range rel.TargetTypeNames { + if u, ok := v.un[typeName]; ok { + // two kinds of relationships target types to be expanded + if len(u.ResourceTypes) > 0 { + // 1. target types with optional subject relation or subject identifier + targettypes = append(targettypes, u.ResourceTypes...) + } else { + // 2. simple target type names + typeNames = append(typeNames, u.ResourceTypeNames...) + } + } else { + typeNames = append(typeNames, typeName) + } + } + + for _, tn := range typeNames { + targettypes = append(targettypes, types.TargetType{Name: tn}) + } + + resourceType.Relationships[i].TargetTypeNames = typeNames + resourceType.Relationships[i].TargetTypes = targettypes + } + + v.rt[name] = resourceType + } +} + func (v *policy) Validate() error { if err := v.validateUnions(); err != nil { return fmt.Errorf("unions: %w", err) @@ -649,10 +629,6 @@ func (v *policy) Schema() []types.ResourceType { for _, b := range v.bn { actionName := b.ActionName - if b.RenamePermission != "" { - actionName = b.RenamePermission - } - action := types.Action{ Name: actionName, } @@ -665,58 +641,24 @@ func (v *policy) Schema() []types.ResourceType { conditions = []types.Condition{ { RelationshipAction: &types.ConditionRelationshipAction{ - Relation: actionName + "_rel", + Relation: actionName + PermissionRelationSuffix, }, RoleBinding: &types.ConditionRoleBinding{}, }, } actionRel := types.ResourceTypeRelationship{ - Relation: actionName + "_rel", + Relation: actionName + PermissionRelationSuffix, Types: []types.TargetType{{Name: RolebindingRoleRelation, SubjectRelation: RolebindingSubjectRelation}}, } typeMap[b.TypeName].Relationships = append(typeMap[b.TypeName].Relationships, actionRel) case c.RoleBindingV2 != nil: - mkConditions := func(actionName string) []types.Condition { - conds := []types.Condition{ - { - RelationshipAction: &types.ConditionRelationshipAction{ - Relation: v.RBAC().GrantRelationship, - ActionName: actionName, - }, - RoleBindingV2: &types.ConditionRoleBindingV2{}, - }, - } - - for _, inheritGrant := range c.RoleBindingV2.InheritGrants { - conds = append(conds, types.Condition{ - RelationshipAction: &types.ConditionRelationshipAction{ - Relation: inheritGrant, - ActionName: actionName, - }, - }) - } - - return conds - } - - conditions = mkConditions(actionName) + conditions = v.RBAC().CreateRoleBindingConditionsForAction(actionName, c.RoleBindingV2.InheritGrantsFrom...) // add role-binding v2 conditions to the resource, if not exists if _, ok := rbv2Actions[b.TypeName]; !ok { - rolebindingActions := []types.Action{} - - for _, rba := range v.rolebindingActions { - // e.g. rolebinding_create - rbAction := types.Action{Name: rba} - // e.g. grant->rolebinding_create + parent->rolebinding_create - rbAction.Conditions = mkConditions(rba) - - rolebindingActions = append(rolebindingActions, rbAction) - } - - rbv2Actions[b.TypeName] = rolebindingActions + rbv2Actions[b.TypeName] = v.RBAC().CreateRoleBindingActionsForResource(c.RoleBindingV2.InheritGrantsFrom...) } default: conditions = []types.Condition{ diff --git a/internal/iapl/policy_test.go b/internal/iapl/policy_test.go index d180dbc98..21061e69a 100644 --- a/internal/iapl/policy_test.go +++ b/internal/iapl/policy_test.go @@ -7,10 +7,11 @@ import ( "github.com/stretchr/testify/require" "go.infratographer.com/permissions-api/internal/testingx" + "go.infratographer.com/permissions-api/internal/types" ) func TestPolicy(t *testing.T) { - rbac := DefaultRBAC() + rbac := defaultRBAC() cases := []testingx.TestCase[PolicyDocument, Policy]{ { @@ -380,6 +381,14 @@ func TestPolicy(t *testing.T) { { Name: "foo", }, + { + Name: "rolev2", + IDPrefix: "permrv2", + }, + { + Name: "role_binding", + IDPrefix: "permrbn", + }, }, }, CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { @@ -391,6 +400,16 @@ func TestPolicy(t *testing.T) { Name: "RoleOwnerMissing", Input: PolicyDocument{ RBAC: &rbac, + ResourceTypes: []ResourceType{ + { + Name: "rolev2", + IDPrefix: "permrv2", + }, + { + Name: "role_binding", + IDPrefix: "permrbn", + }, + }, }, CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { // unknown resource type: role owner tenant does not exists @@ -400,11 +419,29 @@ func TestPolicy(t *testing.T) { { Name: "RBAC_OK", Input: PolicyDocument{ - RBAC: &rbac, + RBAC: &RBAC{ + RoleResource: "rolev2", + RoleSubjectTypes: []string{"user"}, + RoleOwners: []string{"tenant"}, + RoleBindingResource: "role_binding", + RoleBindingSubjects: []types.TargetType{{Name: "user"}}, + }, ResourceTypes: []ResourceType{ { Name: "tenant", }, + { + Name: "rolev2", + IDPrefix: "permrv2", + }, + { + Name: "role_binding", + IDPrefix: "permrbn", + }, + { + Name: "user", + IDPrefix: "idntusr", + }, }, }, CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { @@ -426,3 +463,13 @@ func TestPolicy(t *testing.T) { testingx.RunTests(context.Background(), t, cases, testFn) } + +func defaultRBAC() RBAC { + return RBAC{ + RoleResource: "rolev2", + RoleSubjectTypes: []string{"user", "client"}, + RoleOwners: []string{"tenant"}, + RoleBindingResource: "role_binding", + RoleBindingSubjects: []types.TargetType{{Name: "user"}, {Name: "client"}, {Name: "group", SubjectRelation: "member"}}, + } +} diff --git a/internal/iapl/rbac.go b/internal/iapl/rbac.go index a301008cd..c988fc8e7 100644 --- a/internal/iapl/rbac.go +++ b/internal/iapl/rbac.go @@ -19,25 +19,44 @@ const ( RolebindingSubjectRelation = "subject" // RoleOwnerParentRelation is the name of the relationship that connects a role's owner to its parent. RoleOwnerParentRelation = "parent" + // PermissionRelationSuffix is the suffix append to the name of the relationship + // representing a permission in a role + PermissionRelationSuffix = "_rel" + // GrantRelationship is the name of the relationship that connects a role binding to a resource. + GrantRelationship = "grant" +) + +// RoleBindingAction is the list of actions that can be performed on a role-binding resource +type RoleBindingAction string + +const ( + // RoleBindingActionCreate is the action name to create a role binding + RoleBindingActionCreate RoleBindingAction = "rolebinding_create" + // RoleBindingActionUpdate is the action name to update a role binding + RoleBindingActionUpdate RoleBindingAction = "rolebinding_update" + // RoleBindingActionDelete is the action name to delete a role binding + RoleBindingActionDelete RoleBindingAction = "rolebinding_delete" + // RoleBindingActionGet is the action name to get a role binding + RoleBindingActionGet RoleBindingAction = "rolebinding_get" + // RoleBindingActionList is the action name to list role bindings + RoleBindingActionList RoleBindingAction = "rolebinding_list" ) // ResourceRoleBindingV2 describes the relationships that will be created // for a resource to support role-binding V2 type ResourceRoleBindingV2 struct { // AvailableRolesFrom is the list of resource types that can provide roles to this resource + // Note that not all roles are available to all resources. This relationship is used to + // determine which roles are available to a resource. + // Before creating a role binding for a resource, one should check whether or + // not the role is available for the resource. + // + // Also see the RoleOwners field in the RBAC struct AvailableRolesFrom []string } /* RBAC represents a role-based access control policy. - - RoleResource is the name of the resource type that represents a role. - - RoleRelationshipSubject is the name of the relationship that connects a role to a subject. - - RoleOwners is the names of the resource types that can own a role. - - RoleBindingResource is the name of the resource type that represents a role binding. - - RoleBindingSubjects is the names of the resource types that can be subjects in a role binding. - - RolebindingPermissionsPrefix generates the permissions sets to manage role bindings, - - GrantRelationship is the name of the relationship that connects a role binding to a resource. - e.g. rolebinding_create, rolebinding_list, rolebinding_delete For example, consider the following spicedb schema: ```zed @@ -83,60 +102,113 @@ in IAPL policy terms: - the RoleBindingResource would be "role_binding", - the RoleRelationshipSubject would be `[user, client]`. - the RoleBindingSubjects would be `[{name: user}, {name: group, subjectrelation: member}]`. -- the RolebindingPermissionsPrefix would be "rolebinding" -- the GrantRelationship would be "grant" */ type RBAC struct { - RoleResource string - RoleRelationshipSubjects []string - RoleOwners []string - RoleBindingResource string - RoleBindingSubjects []types.TargetType - RoleBindingPermissionsPrefix string - GrantRelationship string + // RoleResource is the name of the resource type that represents a role. + RoleResource string + // RoleBindingResource is the name of the resource type that represents a role binding. + RoleBindingResource string + // RoleSubjectTypes is a list of subject types that the relationships in a + // role resource will contain, see the example above. + RoleSubjectTypes []string + // RoleOwners is the list of resource types that can own a role. + // These resources should be (but not limited to) organizational resources + // like tenant, organization, project, group, etc + // When a role is owned by an entity, say a group, that means this role + // will be available to perform role-bindings for resources that are owned + // by this group and its subgroups. + // The RoleOwners relationship is particularly useful to limit access to + // custom roles. + RoleOwners []string + // RoleBindingSubjects is the names of the resource types that can be subjects in a role binding. + // e.g. rolebinding_create, rolebinding_list, rolebinding_delete + RoleBindingSubjects []types.TargetType + + roleownersset map[string]struct{} } -// UnmarshalYAML is a custom YAML unmarshaller for the RBAC policy, with -// default values set for the fields. -func (r *RBAC) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rbac RBAC +// CreateRoleBindingConditionsForAction creates the conditions that is used for role binding v2, +// for a given action name. e.g. for a doc_read action, it will create the following conditions: +// doc_read = grant->doc_read + from[0]->doc_read + ... from[n]->doc_read +func (r *RBAC) CreateRoleBindingConditionsForAction(actionName string, inheritFrom ...string) []types.Condition { + conds := make([]types.Condition, 0, len(inheritFrom)+1) + + conds = append(conds, types.Condition{ + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: GrantRelationship, + ActionName: actionName, + }, + RoleBindingV2: &types.ConditionRoleBindingV2{}, + }) + + for _, from := range inheritFrom { + conds = append(conds, types.Condition{ + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: from, + ActionName: actionName, + }, + }) + } - rbacYAML := rbac(DefaultRBAC()) + return conds +} - if err := unmarshal(&rbacYAML); err != nil { - return err +// CreateRoleBindingActionsForResource should be used when an RBAC V2 condition +// is created for an action, the resource that the action is belong to must +// support role binding V2. This function creates the list of actions that can be performed +// on a role binding resource. +// e.g. If action `read_doc` is created with RBAC V2 condition, then the resource, +// in this example `doc`, must also support actions like `rolebinding_create`. +func (r *RBAC) CreateRoleBindingActionsForResource(inheritFrom ...string) []types.Action { + actionsStr := []RoleBindingAction{ + RoleBindingActionCreate, + RoleBindingActionUpdate, + RoleBindingActionDelete, + RoleBindingActionGet, + RoleBindingActionList, } - *r = RBAC(rbacYAML) + actions := make([]types.Action, 0, len(actionsStr)) + + for _, action := range actionsStr { + conditions := r.CreateRoleBindingConditionsForAction(string(action), inheritFrom...) + actions = append(actions, types.Action{Name: string(action), Conditions: conditions}) + } - return nil + return actions } -// DefaultRBAC returns the default values for the RBAC policy. -// the default values are: -// rbac: -// -// roleresource: rolev2 -// rolerelationshipsubjects: -// - user -// - client -// roleowners: -// - tenant -// rolebindingresource: role_binding -// rolebindingsubjects: -// - name: user -// - name: client -// - name: group -// subjectrelation: member -// rolebindingpermissionsprefix: rolebinding -func DefaultRBAC() RBAC { - return RBAC{ - RoleResource: "rolev2", - RoleRelationshipSubjects: []string{"user", "client"}, - RoleOwners: []string{"tenant"}, - RoleBindingResource: "role_binding", - RoleBindingSubjects: []types.TargetType{{Name: "user"}, {Name: "client"}, {Name: "group", SubjectRelation: "member"}}, - RoleBindingPermissionsPrefix: "rolebinding", - GrantRelationship: "grant", +// RoleBindingActions returns the list of actions that can be performed on a role resource +// plus the AvailableRoleRelation action that is used to decide whether or not +// a role is available for a resource +func (r *RBAC) RoleBindingActions() []Action { + actionsStr := []RoleBindingAction{ + RoleBindingActionCreate, + RoleBindingActionUpdate, + RoleBindingActionDelete, + RoleBindingActionGet, + RoleBindingActionList, } + + actions := make([]Action, 0, len(actionsStr)+1) + + for _, action := range actionsStr { + actions = append(actions, Action{Name: string(action)}) + } + + actions = append(actions, Action{Name: AvailableRoleRelation}) + + return actions +} + +// RoleOwnersSet returns the set of role owners for easy role owner lookups +func (r *RBAC) RoleOwnersSet() map[string]struct{} { + if r.roleownersset == nil { + r.roleownersset = make(map[string]struct{}, len(r.RoleOwners)) + for _, owner := range r.RoleOwners { + r.roleownersset[owner] = struct{}{} + } + } + + return r.roleownersset } diff --git a/policy.example.yaml b/policy.example.yaml index 682f1bd1c..2cf56876b 100644 --- a/policy.example.yaml +++ b/policy.example.yaml @@ -1,6 +1,6 @@ rbac: roleresource: rolev2 - rolerelationshipsubjects: + rolesubjecttypes: - user - client roleowners: @@ -157,7 +157,7 @@ actionbindings: conditions: - &rbv2parent rolebindingv2: - inheritgrants: + inheritgrantsfrom: - parent - rolebinding: {} - actionname: role_create @@ -212,7 +212,7 @@ actionbindings: - rolebinding: {} - &rbv2owner rolebindingv2: - inheritgrants: + inheritgrantsfrom: - owner - actionname: loadbalancer_update typename: loadbalancer