diff --git a/.gitignore b/.gitignore index de52ae4d..e9b6c471 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,26 @@ # Emacs stuff *~ +# vscode stuff +.vscode/* +.vscode/settings.json +!.vscode/tasks.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + # .tools dir .tools/ # NATS dirs .devcontainer/nsc/ +resolver.conf + +# binary files +permissions-api +tmp diff --git a/cmd/schema_mermaid.go b/cmd/schema_mermaid.go index d20eecb8..46cb2b03 100644 --- a/cmd/schema_mermaid.go +++ b/cmd/schema_mermaid.go @@ -13,6 +13,13 @@ import ( var ( mermaidTemplate = `erDiagram +{{- if ne .RBAC nil}} + {{ .RBAC.RoleBindingResource }} }o--o{ {{ .RBAC.RoleResource }} : role + {{- range $subj := .RBAC.RoleBindingSubjects }} + {{ $.RBAC.RoleBindingResource }} }o--o{ {{ $subj.Name }} : subject + {{- end }} +{{- end }} + {{- range $resource := .ResourceTypes }} {{ $resource.Name }} { id_prefix {{ $resource.IDPrefix }} @@ -26,9 +33,11 @@ var ( {{- end }} } {{- range $rel := $resource.Relationships }} - {{- range $targetName := $rel.TargetTypeNames }} - {{ $resource.Name }} }o--o{ {{ $targetName }} : {{ $rel.Relation }} + + {{- range $target := $rel.TargetTypes }} + {{ $resource.Name }} }o--o{ {{ $target.Name -}} : {{ $rel.Relation -}} {{- end }} + {{- end }} {{- end }} {{- range $union := .Unions }} @@ -42,10 +51,12 @@ var ( {{- end }} {{- end }} } - {{- range $typ := $union.ResourceTypeNames }} - {{ $union.Name }} ||--|| {{ $typ }} : alias - {{- end }} -{{- end }}` + {{- range $typ := $union.ResourceTypes }} + {{ $union.Name }} ||--|| {{ $typ.Name -}} : alias + {{- end}} + +{{- end }} +` mermaidTmpl = template.Must(template.New("mermaid").Parse(mermaidTemplate)) ) @@ -55,6 +66,7 @@ type mermaidContext struct { Unions []iapl.Union Actions map[string][]string RelatedActions map[string]map[string][]string + RBAC *iapl.RBAC } func outputPolicyMermaid(filePaths []string, markdown bool) { @@ -104,6 +116,11 @@ func outputPolicyMermaid(filePaths []string, markdown bool) { Unions: policy.Unions, Actions: actions, RelatedActions: relatedActions, + RBAC: nil, + } + + if policy.RBAC != nil { + ctx.RBAC = policy.RBAC } var out bytes.Buffer diff --git a/docs/iapl.md b/docs/iapl.md index 84ff1ca8..e9d47be5 100644 --- a/docs/iapl.md +++ b/docs/iapl.md @@ -53,9 +53,9 @@ A `Relationship` describes a named relation between a resource of a given type a | Key | Type | Description | |-------------------|------------|--------------------------------------------------------------------------------------------------------| | `relation` | `string` | The name of the relationship. Must be all alphabetical. | -| `targetTypeNames` | `[]string` | The types of resources on the other side of the relationship. Must be defined resource type or unions. | +| `targetTypes` | `[]TargetTypes` | The types of resources on the other side of the relationship. Must be defined resource type or unions. | -Specifying a `targetTypeName` value of `[foo]` where `foo` is a union over types `bar` and `baz` is equivalent to specifying a value of `[bar, baz]`. +Specifying a `targetType` value of `[name: foo]` where `foo` is a union over types `bar` and `baz` is equivalent to specifying a value of `[bar, baz]`. #### `Action` @@ -115,8 +115,8 @@ resourceTypes: idPrefix: idntten relationships: - relation: parent - targetTypeNames: - - tenant + targettypes: + - name: tenant --- # Provided by enterprise-api resourceTypes: @@ -124,14 +124,14 @@ resourceTypes: idPrefix: entrprj relationships: - relation: parent - targetTypeNames: - - organization + targettypes: + - name: organization - name: organization idPrefix: entrorg relationships: - relation: parent - targetTypeNames: - - tenant + targettypes: + - name: tenant --- # Provided by load-balancer-api resourceTypes: @@ -139,8 +139,8 @@ resourceTypes: idPrefix: loadbal relationships: - relation: owner - targetTypeNames: - - resourceowner + targettypes: + - name: resourceowner actions: - name: loadbalancer_get - name: loadbalancer_create @@ -177,10 +177,10 @@ actionBindings: # Provided by resource-owner-config unions: - name: resourceowner - resourceTypeNames: - - tenant - - project - - organization + resourceTypes: + - name: tenant + - name: project + - name: organization ``` ### Policy validation algorithm @@ -198,10 +198,10 @@ BN = [] BNKeys = [] for bn in actionBindings: if bn.typeName in UN: - for typeName in UN[bn.typeName].targetTypeNames: + for type in UN[bn.typeName].targetTypes: BN += [ ActionBinding( - typeName: typeName, + typeName: type.Name, actionName: bn.actionName, conditions: bn.conditions, ), @@ -217,13 +217,13 @@ for bn in actionBindings: for rt in RT: rels = [] for rel in rt.relationships: - typeNames = [] - for typeName in rel.targetTypeNames: - if typeName in UN: - typeNames += UN[typeName].resourceTypeNames + types = [] + for type in rel.targetTypes: + if type in UN: + types += UN[type.Name].resourceTypes else: - typeNames += [typeName] - rel.typeNames = typeNames + types += [type] + rel.types = type rels += [rel] rt.relationships = rels @@ -235,12 +235,12 @@ for bn in BN: # validation phase for un in UN: - for name in un.resourceTypeNames: - assert name in UN + for type in un.resourceTypes: + assert type.name in UN for rt in RT: for rel in rt.relationships: - for tn in rel.targetTypeNames: + for tn in rel.targetTypes: assert tn in RT for bn in BN: @@ -256,7 +256,7 @@ for bn in BN: rel = find(rt.relationships, lambda x: c.relation == x.relation) assert rel - for tn in rel.targetTypeNames: + for tn in rel.targetTypes: assert bn.actionName in RB[tn] ``` diff --git a/docs/rbac.md b/docs/rbac.md new file mode 100644 index 00000000..4918a59d --- /dev/null +++ b/docs/rbac.md @@ -0,0 +1,415 @@ +# RBAC V2 + +## Role-Bindings + +The following diagram describes the most basic role-binding relationships: + +```mermaid +erDiagram + RoleBinding }o--|| Role : role + RoleBinding }o--o{ Subject : subject + Resource ||--o{ RoleBinding : grant + + RoleBinding { + permission read_document + permission write_document + permission etc + } + + Role { + relationship read_document_rel + relationship write_document_rel + relationship etc + } + + Resource { + permission read_document + permission write_document + permission etc + } +``` + +### RBAC IAPL + +In the IAPL, a new `rbac` directive is introduced to define the RBAC configurations. + +property | yaml | type | description +-|-|-|- +RoleResource |`rbac.roleresource`| string | name of the resource type that represents 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. + +For example, consider the following spicedb schema: + +```zed + + definition user {} + definition client {} + + definition group { + relation member: user | client + } + + definition organization { + relation parent: organization + relation member: user | client | organization#member + relation member_role: role | organization#member_role + relation grant: rolebinding + + permissions rolebinding_list: grant->rolebinding_list + permissions rolebinding_create: grant->rolebinding_create + permissions rolebinding_delete: grant->rolebinding_delete + } + + definition role { + relation owner: organization + relation view_organization: user:* | client:* + relation rolebinding_list_rel: user:* | client:* + relation rolebinding_create_rel: user:* | client:* + relation rolebinding_delete_rel: user:* | client:* + } + + definition role_binding { + relation role: role + relation subject: user | group#member + permission view_organization = subject & role->view_organization + permissions rolebinding_list: subject & role->rolebinding_list + permissions rolebinding_create: subject & role->rolebinding_create + permissions rolebinding_delete: subject & role->rolebinding_delete + } + +``` + +in IAPL policy terms: + +- the RoleResource would be "role" +- the RoleBindingResource would be "role_binding", +- the RoleRelationshipSubject would be `[user, client]`. +- the RoleBindingSubjects would be `[{name: user}, {name: group, subjectrelation: member}]`. + +### Roles + +A `Role` is a spicedb entity that contains a set of permissions as relationships, +in the form of: + + ```zed + role:[role_id]#[permission_a]_rel@subject:* + role:[role_id]#[permission_b]_rel@subject:* + ``` + +`subject:*` indicates any subjects [in possession](#bindings) of the role will be granted +those permissions. + +### Bindings + +A `RoleBinding` establishes a three-way relationship between a role, +a resource, and the subjects, in the form of: + + ```zed + role_binding:[rb_id]#role@role:[role_id] + role_binding:[rb_id]#subject@subject:[subject_id] + resource:[res_id]#grant@role_binding:[rb_id] + ``` + +### Permission Lookups + +Following is an example of looking up permission `read_doc` for subject `user_1` +on a resource. + +Relationships: + +1. create role `doc_viewer` + + ```zed + role:doc_viewer#read_doc_rel@subject:* + ``` + +2. create role-binding + + ```zed + role_binding:rb_1#role@role:doc_viwer + role_binding:rb_1#subject@subject:user_1 + resource:res_1#grant@role_binding:rb_1 + ``` + +Lookup: + +```mermaid +flowchart TD + res("`Resource: + permission: read_doc + `") + + grant{Grant} + rb("`RoleBinding: rb_1`") + + role_rel{Role} + role(doc_viwer) + perm>read_doc_rel] + + subj_rel{Subj} + subj(user_1) + + res-->grant + grant-->rb + + rb-->role_rel + role_rel-->role + role-->perm + perm-->permok{{ok ✅}} + + rb-->subj_rel + subj_rel-->subj + subj-->subjok{{ok ✅}} + + permok-->ok{{ok ✅}} + subjok-->ok +``` + +## Hierarchical Grants + +In a lot of permissions scenarios, permission relationships are not as clean-cut +as the example shown above. Most of the times, role-bindings involve binding a role to +subjects on a higher level (e.g., projects, tenant, etc.,) and the users expect +those permissions to be propagated all the resources that it owns. Moreover, +there are cases where, instead of a single user or client being the role-binding +subject, IAM users expect a role can be bind to a group of subjects. The IAPL +is modified to generate a SpiceDB schema to accommodate these more complex +use cases. + +### Ownerships + +To accommodate inheritance of the grant relationships, +a new property `RoleBindingV2` is added to the resource type definitions +and a new type of `Condition` is introduced to the IAPL: + +```diff + type ResourceType struct { + Name string + IDPrefix string ++ RoleBindingV2 *ResourceRoleBindingV2 + Relationships []Relationship + } + + type Condition struct { + RoleBinding *ConditionRoleBinding ++ RoleBindingV2 *ConditionRoleBindingV2 + RelationshipAction *ConditionRelationshipAction + } +``` + +A property of `InheritPermissionsFrom []string` is defined in `ResourceRoleBindingV2` +that allows the IAPL to generate a permission line in the SpiceDB schema that +allows grants and roles to be inherited from its owner or parent. + +When `RoleBindingV2` is defined in a given `Condition`, the IAPL will look for +the `resourcetype.RoleBindingV2.InheritPermissionsFrom` property in the resource +type that the condition's action belongs to. + +For example, consider the following `ActionBinding`: + +```yaml +# ... + +resourcetypes: + - name: doc + idprefix: doc + rolebindingv2: + inheritpermissionsfrom: + - owner + - name: tenant + idprefix: tenant + rolebindingv2: + inheritpermissionsfrom: + - parent + +actionbindings: + - actionname: read_doc + typename: doc + conditions: + rolebindingv2: {} + - actionname: read_doc + typename: tenant + conditions: + rolebindingv2: {} + +# ... +``` + +The IAPL will generate the following SpiceDB schema: + +```zed +definition doc { + relation owner: tenant + relation grant: role_binding + permissions read_doc: grant->read_doc + owner->read_doc +} + +definition tenant { + relation parent: tenant + relation grant: role_binding + permissions read_doc: grant->read_doc + parent->read_doc +} +``` + +#### Ownership Example + +Consider the following relationships, which is based on the one defined in +[*permissions lookups*](#permission-lookups) section: + +```diff + # create role + role:doc_viewer#read_doc_rel@subject:* + + # grant role doc_viwer to user_1 on tenant parent + role_binding:rb_1#role@role:doc_viwer + role_binding:rb_1#subject@subject:user_1 + tenant:parent#grant@role_binding:rb_1 + ++ # create child tenant ++ tenant:child#parent@tenant:parent ++ # create doc resource ++ doc:doc_1#owner@tenant:child +``` + +Lookup: + +```mermaid +flowchart TD + doc("`Doc: + permission: read_doc + `") + + tenant_child(Tenant: Child) + owner_rel{Owner} + tenant_parent(Tenant: Parent) + parent_rel{Parent} + + grant{Grant} + rb("`RoleBinding: rb_1`") + + role_rel{Role} + role(doc_viwer) + perm>read_doc_rel] + + subj_rel{Subj} + subj(user_1) + + doc-->owner_rel + owner_rel-->tenant_child + tenant_child-->parent_rel + parent_rel-->tenant_parent + + tenant_parent-->grant + grant-->rb + + rb-->role_rel + role_rel-->role + role-->perm + perm-->permok{{ok ✅}} + + rb-->subj_rel + subj_rel-->subj + subj-->subjok{{ok ✅}} + + permok-->ok{{ok ✅}} + subjok-->ok +``` + +### Memberships + +To allowing groups, or any other type of resources with memberships to be the +role-binding subject, all that is needed is to define the membership relationship +in the `rbac.rolebindingsubjects` directive. + +For example, consider the following `rbac` directive: + +```yaml +rbac: + rolebindingsubjects: + - name: user + - name: group + subjectrelation: member +``` + +this will generate the following SpiceDB schema: + +```zed +definition role_binding { + relation role: role + relation subject: user | group#member + permission view_doc = subject & role->view_doc +} +``` + +In this case, the `role_binding` entity can have a `subject` that is either a `user` +or a member of the `group` entity. + +Consider the following relationships, which is based on the one defined in +[*permissions lookups*](#permission-lookups) section: + +```diff + # create role + role:doc_viewer#read_doc_rel@subject:* + ++ # create group_1 ++ group:group_1#member@user:user_1 + ++ # instead of binding to user_1, bind to group_1 + role_binding:rb_1#role@role:doc_viwer +- role_binding:rb_1#subject@subject:user_1 ++ role_binding:rb_2#subject@subject:group_1#member + + tenant:parent#grant@role_binding:rb_1 +``` + +Lookup: + +```mermaid +flowchart TD + res("`Resource: + permission: read_doc + `") + + grant{Grant} + rb("`RoleBinding: rb_1`") + + role_rel{Role} + role(doc_viwer) + perm>read_doc_rel] + + subj_rel{Subj} + subj(group_1) + + member_rel{Member} + member(user_1) + + res-->grant + grant-->rb + + rb-->role_rel + role_rel-->role + role-->perm + perm-->permok{{ok ✅}} + + rb-->subj_rel + subj_rel-->subj + subj-->member_rel + member_rel-->member + member-->memberok{{ok ✅}} + + permok-->ok{{ok ✅}} + memberok-->ok +``` + +## Glossary + +- **Subject**: The entities that permissions can be granted to, such as users, clients, or group members +- **Role**: An entity that contains a set of permissions +- **RoleBinding**: An entity that creates a relationship between a role and some subjects, + meaning that these subjects are "in possession" of the permissions defined in the role +- **Grant**: The relationship between a role-binding and a resource, effectively creating a + three way relationship between a role, a resource, and the subjects +- **Inheritance**: The ability to propagate permissions and roles from a parent resource to its children diff --git a/internal/iapl/default.go b/internal/iapl/default.go index e9454d3f..f9069b50 100644 --- a/internal/iapl/default.go +++ b/internal/iapl/default.go @@ -1,5 +1,7 @@ package iapl +import "go.infratographer.com/permissions-api/internal/types" + // DefaultPolicyDocument returns the default policy document for permissions-api. func DefaultPolicyDocument() PolicyDocument { return PolicyDocument{ @@ -10,8 +12,8 @@ func DefaultPolicyDocument() PolicyDocument { Relationships: []Relationship{ { Relation: "subject", - TargetTypeNames: []string{ - "subject", + TargetTypes: []types.TargetType{ + {Name: "subject"}, }, }, }, @@ -30,8 +32,8 @@ func DefaultPolicyDocument() PolicyDocument { Relationships: []Relationship{ { Relation: "parent", - TargetTypeNames: []string{ - "tenant", + TargetTypes: []types.TargetType{ + {Name: "tenant"}, }, }, }, @@ -42,8 +44,8 @@ func DefaultPolicyDocument() PolicyDocument { Relationships: []Relationship{ { Relation: "owner", - TargetTypeNames: []string{ - "resourceowner", + TargetTypes: []types.TargetType{ + {Name: "resourceowner"}, }, }, }, @@ -52,15 +54,15 @@ func DefaultPolicyDocument() PolicyDocument { Unions: []Union{ { Name: "subject", - ResourceTypeNames: []string{ - "user", - "client", + ResourceTypes: []types.TargetType{ + {Name: "user"}, + {Name: "client"}, }, }, { Name: "resourceowner", - ResourceTypeNames: []string{ - "tenant", + ResourceTypes: []types.TargetType{ + {Name: "tenant"}, }, }, }, diff --git a/internal/iapl/errors.go b/internal/iapl/errors.go index 302392be..199b3a5d 100644 --- a/internal/iapl/errors.go +++ b/internal/iapl/errors.go @@ -15,4 +15,8 @@ var ( ErrorUnknownRelation = errors.New("unknown relation") // ErrorUnknownAction represents an error where an action is not defined. ErrorUnknownAction = errors.New("unknown action") + // ErrorMissingRelationship represents an error where a mandatory relationship is missing. + ErrorMissingRelationship = errors.New("missing relationship") + // ErrorDuplicateRBACDefinition represents an error where a duplicate RBAC definition was declared. + ErrorDuplicateRBACDefinition = errors.New("duplicated RBAC definition") ) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index 5cca323e..17dd27e2 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -17,25 +17,27 @@ type PolicyDocument struct { Unions []Union Actions []Action ActionBindings []ActionBinding + RBAC *RBAC } // ResourceType represents a resource type in the authorization policy. type ResourceType struct { Name string IDPrefix string + RoleBindingV2 *ResourceRoleBindingV2 Relationships []Relationship } // Relationship represents a named relation between two resources. type Relationship struct { - Relation string - TargetTypeNames []string + Relation string + TargetTypes []types.TargetType } // Union represents a named union of multiple concrete resource types. type Union struct { - Name string - ResourceTypeNames []string + Name string + ResourceTypes []types.TargetType } // Action represents an action that can be taken in an authorization policy. @@ -45,20 +47,27 @@ type Action struct { // ActionBinding represents a binding of an action to a resource type or union. type ActionBinding struct { - ActionName string - TypeName string - Conditions []Condition + ActionName string + TypeName string + Conditions []Condition + ConditionSets []types.ConditionSet } // Condition represents a necessary condition for performing an action. type Condition struct { RoleBinding *ConditionRoleBinding + RoleBindingV2 *ConditionRoleBindingV2 RelationshipAction *ConditionRelationshipAction } // ConditionRoleBinding represents a condition where a role binding is necessary to perform an action. type ConditionRoleBinding struct{} +// ConditionRoleBindingV2 represents a condition where a role binding is necessary to perform an action. +// 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{} + // ConditionRelationshipAction represents a condition where another action must be allowed on a resource // along a relation to perform an action. type ConditionRelationshipAction struct { @@ -70,6 +79,7 @@ type ConditionRelationshipAction struct { type Policy interface { Validate() error Schema() []types.ResourceType + RBAC() *RBAC } var _ Policy = &policy{} @@ -85,7 +95,7 @@ type policy struct { // NewPolicy creates a policy from the given policy document. func NewPolicy(p PolicyDocument) Policy { - rt := make(map[string]ResourceType, len(p.ResourceTypes)) + rt := make(map[string]ResourceType) for _, r := range p.ResourceTypes { rt[r.Name] = r } @@ -95,7 +105,8 @@ func NewPolicy(p PolicyDocument) Policy { un[t.Name] = t } - ac := make(map[string]Action, len(p.Actions)) + ac := map[string]Action{} + for _, a := range p.Actions { ac[a.Name] = a } @@ -107,6 +118,16 @@ func NewPolicy(p PolicyDocument) Policy { p: p, } + if p.RBAC != nil { + for _, rba := range p.RBAC.RoleBindingActions() { + out.ac[rba.Name] = rba + } + + out.createV2RoleResourceType() + out.createRoleBindingResourceType() + out.expandRBACV2Relationships() + } + out.expandActionBindings() out.expandResourceTypes() @@ -123,6 +144,10 @@ func (p PolicyDocument) MergeWithPolicyDocument(other PolicyDocument) PolicyDocu p.ActionBindings = append(p.ActionBindings, other.ActionBindings...) + if other.RBAC != nil { + p.RBAC = other.RBAC + } + return p } @@ -159,6 +184,10 @@ func NewPolicyFromFiles(filePaths []string) (Policy, error) { return nil, err } + if mergedPolicy.RBAC != nil && filePolicy.RBAC != nil { + return nil, ErrorDuplicateRBACDefinition + } + mergedPolicy = mergedPolicy.MergeWithPolicyDocument(filePolicy) } @@ -189,9 +218,9 @@ func (v *policy) validateUnions() error { return fmt.Errorf("%s: %w", union.Name, ErrorTypeExists) } - for _, rtName := range union.ResourceTypeNames { - if _, ok := v.rt[rtName]; !ok { - return fmt.Errorf("%s: resourceTypeNames: %s: %w", union.Name, rtName, ErrorUnknownType) + for _, rt := range union.ResourceTypes { + if _, ok := v.rt[rt.Name]; !ok { + return fmt.Errorf("%s: resourceTypes: %s: %w", union.Name, rt.Name, ErrorUnknownType) } } } @@ -200,11 +229,15 @@ func (v *policy) validateUnions() error { } func (v *policy) validateResourceTypes() error { - for _, resourceType := range v.p.ResourceTypes { + for _, resourceType := range v.rt { for _, rel := range resourceType.Relationships { - for _, name := range rel.TargetTypeNames { - if _, ok := v.rt[name]; !ok { - return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, name, ErrorUnknownType) + for _, tt := range rel.TargetTypes { + if _, ok := v.rt[tt.Name]; !ok { + return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, tt.Name, ErrorUnknownType) + } + + if tt.SubjectRelation != "" && !v.findRelationship(v.rt[tt.Name].Relationships, tt.SubjectRelation) { + return fmt.Errorf("%s: subject-relation: %s: %w", resourceType.Name, tt.SubjectRelation, ErrorUnknownRelation) } } } @@ -232,9 +265,24 @@ func (v *policy) validateConditionRelationshipAction(rt ResourceType, c Conditio return fmt.Errorf("%s: %w", c.Relation, ErrorUnknownRelation) } - for _, tn := range rel.TargetTypeNames { - if _, ok := v.rb[tn][c.ActionName]; !ok { - return fmt.Errorf("%s: %s: %s: %w", c.Relation, tn, c.ActionName, ErrorUnknownAction) + // if there's a relationship action defined with only the relation, + // e.g., + // ```yaml + // actionname: someaction + // typename: someresource + // conditions: + // - relationshipaction: + // relation: some_relation + // ``` + // the above logics ensure that `some_relation` exists, thus can safely + // return without error + if c.ActionName == "" { + return nil + } + + for _, tt := range rel.TargetTypes { + if _, ok := v.rb[tt.Name][c.ActionName]; !ok { + return fmt.Errorf("%s: %s: %s: %w", c.Relation, tt.Name, c.ActionName, ErrorUnknownAction) } } @@ -248,6 +296,10 @@ func (v *policy) validateConditions(rt ResourceType, conds []Condition) error { numClauses++ } + if cond.RoleBindingV2 != nil { + numClauses++ + } + if cond.RelationshipAction != nil { numClauses++ } @@ -303,14 +355,34 @@ func (v *policy) validateActionBindings() error { return nil } +// validateRoles validates V2 role resource types to ensure that: +// - role resource type has a valid owner relationship +func (v *policy) validateRoles() error { + if v.p.RBAC == nil { + return nil + } + + for _, roleOwnerName := range v.p.RBAC.RoleOwners { + _, ok := v.rt[roleOwnerName] + + // check if role owner exists + if !ok { + return fmt.Errorf("%w: role owner %s does not exist", ErrorUnknownType, roleOwnerName) + } + } + + return nil +} + func (v *policy) expandActionBindings() { for _, bn := range v.p.ActionBindings { if u, ok := v.un[bn.TypeName]; ok { - for _, typeName := range u.ResourceTypeNames { + for _, resourceType := range u.ResourceTypes { binding := ActionBinding{ - TypeName: typeName, - ActionName: bn.ActionName, - Conditions: bn.Conditions, + TypeName: resourceType.Name, + ActionName: bn.ActionName, + Conditions: bn.Conditions, + ConditionSets: bn.ConditionSets, } v.bn = append(v.bn, binding) } @@ -331,20 +403,262 @@ func (v *policy) expandActionBindings() { } } +// 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. +// This is equivalent to including a resource that looks like: +// +// name: rolev2 +// idprefix: permrv2 +// relationships: +// - relation: owner +// targettypes: +// - name: tenant +// - relation: foo_resource_get_rel +// targettypes: +// - name: user +// subjectidentifier: "*" +func (v *policy) createV2RoleResourceType() { + role := ResourceType{ + Name: v.p.RBAC.RoleResource.Name, + IDPrefix: v.p.RBAC.RoleResource.IDPrefix, + } + + // 1. create a relationship for role owners + roleOwners := Relationship{ + Relation: RoleOwnerRelation, + TargetTypes: make([]types.TargetType, len(v.p.RBAC.RoleOwners)), + } + + for i, owner := range v.p.RBAC.RoleOwners { + roleOwners.TargetTypes[i] = types.TargetType{Name: owner} + } + + // 2. create a list of relationships for all permissions and ownerships + roleRel := make([]Relationship, 0, len(v.ac)+1) + + for _, action := range v.ac { + targettypes := make([]types.TargetType, len(v.p.RBAC.RoleSubjectTypes)) + + for j, subject := range v.p.RBAC.RoleSubjectTypes { + targettypes[j] = types.TargetType{Name: subject, SubjectIdentifier: "*"} + } + + roleRel = append(roleRel, + Relationship{ + Relation: action.Name + PermissionRelationSuffix, + TargetTypes: targettypes, + }, + ) + } + + // 3. create a role resource type containing all the relationships shown above + roleRel = append(roleRel, roleOwners) + + role.Relationships = roleRel + v.rt[role.Name] = role +} + +// 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. +// +// this function effectively creates: +// 1. a resource type like this: +// +// name: rolebinding +// idprefix: permrbn +// relationships: +// - relation: rolev2 +// targettypes: +// - name: rolev2 +// - relation: subject +// targettypes: +// - name: user +// subjectidentifier: "*" +// +// 2. a list of action-bindings representing permissions for all the actions in the policy +// +// actionbindings: +// - actionname: foo_resource_get +// typename: rolebinding +// conditionsets: +// - conditions: +// - relationshipaction: +// relation: rolev2 +// actionname: foo_resource_get_rel +// - conditions: +// - relationshipaction: +// relation: subject +// # ... other action bindings on the rolebinding resource for each +// # action defined in the policy +func (v *policy) createRoleBindingResourceType() { + rolebinding := ResourceType{ + Name: v.p.RBAC.RoleBindingResource.Name, + IDPrefix: v.p.RBAC.RoleBindingResource.IDPrefix, + } + + role := Relationship{ + Relation: RolebindingRoleRelation, + TargetTypes: []types.TargetType{ + {Name: v.p.RBAC.RoleResource.Name}, + }, + } + + // 2. create relationship to subjects + subjects := Relationship{ + Relation: RolebindingSubjectRelation, + TargetTypes: v.p.RBAC.RoleBindingSubjects, + } + + // 3. create a list of action-bindings representing permissions for all the + // actions in the policy + actionbindings := make([]ActionBinding, 0, len(v.ac)) + + for actionName := range v.ac { + ab := ActionBinding{ + ActionName: actionName, + TypeName: v.p.RBAC.RoleBindingResource.Name, + ConditionSets: []types.ConditionSet{ + { + Conditions: []types.Condition{ + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: RolebindingRoleRelation, + ActionName: actionName + PermissionRelationSuffix, + }, + }, + }, + }, + { + Conditions: []types.Condition{ + {RelationshipAction: &types.ConditionRelationshipAction{Relation: RolebindingSubjectRelation}}, + }, + }, + }, + } + + actionbindings = append(actionbindings, ab) + } + + v.bn = append(v.bn, actionbindings...) + + // 4. create role-binding resource type + rolebinding.Relationships = []Relationship{role, subjects} + v.rt[v.p.RBAC.RoleBindingResource.Name] = rolebinding +} + +// 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. +// This function augments the resource types and effectively creating +// resource types like this: +// +// 1. for resource owners, `member_role` relationship is added +// +// ```diff +// resourcetypes: +// name: tenant +// idprefix: tnntten +// rolebindingv2: +// *rolesFromParent +// relationships: +// - relation: parent +// targettypes: +// - name: tenant_parent +// # ... other existing relationships +// + - relation: member_role +// + targettypes: +// + - name: rolev2 +// +// ``` +// +// 2. for resources that inherit permissions from other resources, `available_roles` +// action is added +// +// ```diff +// actionbindings: +// # ... other existing action bindings +// + - actionname: available_roles +// + typename: tenant +// + conditions: +// + - relationshipaction: +// + relation: member_role +// + - relationshipaction: +// + relation: parent +// + actionname: available_roles +// ``` +func (v *policy) expandRBACV2Relationships() { + for name, resourceType := range v.rt { + // 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.RBAC().RoleOwnersSet()[name]; ok { + memberRoleRelation := Relationship{ + Relation: RoleOwnerMemberRoleRelation, + TargetTypes: []types.TargetType{ + {Name: v.p.RBAC.RoleResource.Name}, + }, + } + + resourceType.Relationships = append(resourceType.Relationships, memberRoleRelation) + + // i.e. avail_role = member_role + availableRoles = append(availableRoles, Condition{ + RelationshipAction: &ConditionRelationshipAction{ + Relation: RoleOwnerMemberRoleRelation, + }, + }) + } + + // i.e. avail_role = from[0]->avail_role + from[1]->avail_role ... + if resourceType.RoleBindingV2 != nil { + for _, from := range resourceType.RoleBindingV2.InheritPermissionsFrom { + availableRoles = append(availableRoles, Condition{ + RelationshipAction: &ConditionRelationshipAction{ + Relation: from, + ActionName: AvailableRolesList, + }, + }) + } + } + + // create available role permission + if len(availableRoles) > 0 { + action := ActionBinding{ + ActionName: AvailableRolesList, + TypeName: resourceType.Name, + Conditions: availableRoles, + } + + v.bn = append(v.bn, action) + } + + v.rt[name] = resourceType + } +} + func (v *policy) expandResourceTypes() { for name, resourceType := range v.rt { for i, rel := range resourceType.Relationships { - var typeNames []string + targettypes := []types.TargetType{} - for _, typeName := range rel.TargetTypeNames { - if u, ok := v.un[typeName]; ok { - typeNames = append(typeNames, u.ResourceTypeNames...) + for _, tt := range rel.TargetTypes { + if u, ok := v.un[tt.Name]; ok { + targettypes = append(targettypes, u.ResourceTypes...) } else { - typeNames = append(typeNames, typeName) + targettypes = append(targettypes, tt) } } - resourceType.Relationships[i].TargetTypeNames = typeNames + resourceType.Relationships[i].TargetTypes = targettypes } v.rt[name] = resourceType @@ -364,11 +678,16 @@ func (v *policy) Validate() error { return fmt.Errorf("actionBindings: %w", err) } + if err := v.validateRoles(); err != nil { + return fmt.Errorf("roles: %w", err) + } + return nil } func (v *policy) Schema() []types.ResourceType { typeMap := map[string]*types.ResourceType{} + rbv2Actions := map[string][]types.Action{} for n, rt := range v.rt { out := types.ResourceType{ @@ -379,7 +698,7 @@ func (v *policy) Schema() []types.ResourceType { for _, rel := range rt.Relationships { outRel := types.ResourceTypeRelationship{ Relation: rel.Relation, - Types: rel.TargetTypeNames, + Types: rel.TargetTypes, } out.Relationships = append(out.Relationships, outRel) @@ -389,26 +708,81 @@ func (v *policy) Schema() []types.ResourceType { } for _, b := range v.bn { + actionName := b.ActionName + action := types.Action{ - Name: b.ActionName, + Name: actionName, } + // rbac V2 actions + res := v.rt[b.TypeName] + for _, c := range b.Conditions { - condition := types.Condition{ - RoleBinding: (*types.ConditionRoleBinding)(c.RoleBinding), - RelationshipAction: (*types.ConditionRelationshipAction)(c.RelationshipAction), + var conditions []types.Condition + + switch { + case c.RoleBinding != nil: + conditions = []types.Condition{ + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: actionName + PermissionRelationSuffix, + }, + RoleBinding: &types.ConditionRoleBinding{}, + }, + } + + actionRel := types.ResourceTypeRelationship{ + Relation: actionName + PermissionRelationSuffix, + Types: []types.TargetType{{Name: RolebindingRoleRelation, SubjectRelation: RolebindingSubjectRelation}}, + } + + typeMap[b.TypeName].Relationships = append(typeMap[b.TypeName].Relationships, actionRel) + case c.RoleBindingV2 != nil && res.RoleBindingV2 != nil: + conditions = v.RBAC().CreateRoleBindingConditionsForAction(actionName, res.RoleBindingV2.InheritPermissionsFrom...) + + // add role-binding v2 conditions to the resource, if not exists + if _, ok := rbv2Actions[b.TypeName]; !ok { + rbv2Actions[b.TypeName] = v.RBAC().CreateRoleBindingActionsForResource(res.RoleBindingV2.InheritPermissionsFrom...) + } + default: + conditions = []types.Condition{ + { + RelationshipAction: (*types.ConditionRelationshipAction)(c.RelationshipAction), + }, + } } - action.Conditions = append(action.Conditions, condition) + action.Conditions = append(action.Conditions, conditions...) } + action.ConditionSets = b.ConditionSets + typeMap[b.TypeName].Actions = append(typeMap[b.TypeName].Actions, action) } - out := make([]types.ResourceType, len(v.p.ResourceTypes)) - for i, rt := range v.p.ResourceTypes { - out[i] = *typeMap[rt.Name] + for resType, actions := range rbv2Actions { + typeMap[resType].Actions = append(typeMap[resType].Actions, actions...) + } + + out := make([]types.ResourceType, 0, len(typeMap)) + for _, rt := range typeMap { + out = append(out, *rt) } return out } + +// RBAC returns the RBAC configurations +func (v *policy) RBAC() *RBAC { + return v.p.RBAC +} + +func (v *policy) findRelationship(rels []Relationship, name string) bool { + for _, rel := range rels { + if rel.Relation == name { + return true + } + } + + return false +} diff --git a/internal/iapl/policy_test.go b/internal/iapl/policy_test.go index e01b6121..99718f94 100644 --- a/internal/iapl/policy_test.go +++ b/internal/iapl/policy_test.go @@ -7,10 +7,13 @@ 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) { - cases := []testingx.TestCase[PolicyDocument, struct{}]{ + rbac := defaultRBAC() + + cases := []testingx.TestCase[PolicyDocument, Policy]{ { Name: "TypeExists", Input: PolicyDocument{ @@ -22,13 +25,13 @@ func TestPolicy(t *testing.T) { Unions: []Union{ { Name: "foo", - ResourceTypeNames: []string{ - "foo", + ResourceTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorTypeExists) }, }, @@ -43,13 +46,13 @@ func TestPolicy(t *testing.T) { Unions: []Union{ { Name: "bar", - ResourceTypeNames: []string{ - "baz", + ResourceTypes: []types.TargetType{ + {Name: "baz"}, }, }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownType) }, }, @@ -64,13 +67,13 @@ func TestPolicy(t *testing.T) { Unions: []Union{ { Name: "bar", - ResourceTypeNames: []string{ - "baz", + ResourceTypes: []types.TargetType{ + {Name: "baz"}, }, }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownType) }, }, @@ -83,15 +86,15 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "baz", + TargetTypes: []types.TargetType{ + {Name: "baz"}, }, }, }, }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownType) }, }, @@ -104,8 +107,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -123,7 +126,7 @@ func TestPolicy(t *testing.T) { }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownAction) }, }, @@ -136,8 +139,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -163,7 +166,7 @@ func TestPolicy(t *testing.T) { }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownAction) }, }, @@ -195,7 +198,7 @@ func TestPolicy(t *testing.T) { }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownRelation) }, }, @@ -208,8 +211,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -221,9 +224,9 @@ func TestPolicy(t *testing.T) { Unions: []Union{ { Name: "buzz", - ResourceTypeNames: []string{ - "foo", - "baz", + ResourceTypes: []types.TargetType{ + {Name: "foo"}, + {Name: "baz"}, }, }, }, @@ -247,7 +250,7 @@ func TestPolicy(t *testing.T) { }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownRelation) }, }, @@ -260,8 +263,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -271,8 +274,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -281,9 +284,9 @@ func TestPolicy(t *testing.T) { Unions: []Union{ { Name: "buzz", - ResourceTypeNames: []string{ - "foo", - "baz", + ResourceTypes: []types.TargetType{ + {Name: "foo"}, + {Name: "baz"}, }, }, }, @@ -307,7 +310,7 @@ func TestPolicy(t *testing.T) { }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.ErrorIs(t, res.Err, ErrorUnknownAction) }, }, @@ -320,8 +323,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -331,8 +334,8 @@ func TestPolicy(t *testing.T) { Relationships: []Relationship{ { Relation: "bar", - TargetTypeNames: []string{ - "foo", + TargetTypes: []types.TargetType{ + {Name: "foo"}, }, }, }, @@ -341,9 +344,9 @@ func TestPolicy(t *testing.T) { Unions: []Union{ { Name: "buzz", - ResourceTypeNames: []string{ - "foo", - "baz", + ResourceTypes: []types.TargetType{ + {Name: "foo"}, + {Name: "baz"}, }, }, }, @@ -367,21 +370,98 @@ func TestPolicy(t *testing.T) { }, }, }, - CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { + require.NoError(t, res.Err) + }, + }, + { + Name: "NoRBACProvided", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + }, + { + Name: "rolev2", + IDPrefix: "permrv2", + }, + { + Name: "role_binding", + IDPrefix: "permrbn", + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { + require.NoError(t, res.Err) + require.Nil(t, res.Success.RBAC()) + }, + }, + { + 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 + require.ErrorIs(t, res.Err, ErrorUnknownType) + }, + }, + { + Name: "RBAC_OK", + Input: PolicyDocument{ + RBAC: &RBAC{ + RoleResource: RBACResourceDefinition{"rolev2", "permrv2"}, + RoleBindingResource: RBACResourceDefinition{"role_binding", "permrbn"}, + RoleSubjectTypes: []string{"user"}, + RoleOwners: []string{"tenant"}, + RoleBindingSubjects: []types.TargetType{{Name: "user"}}, + }, + ResourceTypes: []ResourceType{ + { + Name: "tenant", + }, + { + Name: "user", + IDPrefix: "idntusr", + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[Policy]) { require.NoError(t, res.Err) + require.NotNil(t, res.Success.RBAC()) }, }, } - testFn := func(_ context.Context, p PolicyDocument) testingx.TestResult[struct{}] { - policy := NewPolicy(p) - err := policy.Validate() + testFn := func(_ context.Context, doc PolicyDocument) testingx.TestResult[Policy] { + p := NewPolicy(doc) + err := p.Validate() - return testingx.TestResult[struct{}]{ - Success: struct{}{}, + return testingx.TestResult[Policy]{ + Success: p, Err: err, } } testingx.RunTests(context.Background(), t, cases, testFn) } + +func defaultRBAC() RBAC { + return RBAC{ + RoleResource: RBACResourceDefinition{"rolev2", "permrv2"}, + RoleBindingResource: RBACResourceDefinition{"role_binding", "permrbn"}, + RoleSubjectTypes: []string{"user", "client"}, + RoleOwners: []string{"tenant"}, + RoleBindingSubjects: []types.TargetType{{Name: "user"}, {Name: "client"}, {Name: "group", SubjectRelation: "member"}}, + } +} diff --git a/internal/iapl/rbac.go b/internal/iapl/rbac.go new file mode 100644 index 00000000..81b9d21b --- /dev/null +++ b/internal/iapl/rbac.go @@ -0,0 +1,222 @@ +package iapl + +import ( + "go.infratographer.com/permissions-api/internal/types" +) + +const ( + // RoleOwnerRelation is the name of the relationship that connects a role to its owner. + RoleOwnerRelation = "owner" + // RoleOwnerMemberRoleRelation is the name of the relationship that connects a resource + // to a role that it owns + RoleOwnerMemberRoleRelation = "member_role" + // AvailableRolesList is the name of the action in a resource that returns a list + // of roles that are available for the resource + AvailableRolesList = "avail_role" + // RolebindingRoleRelation is the name of the relationship that connects a role binding to a role. + RolebindingRoleRelation = "role" + // RolebindingSubjectRelation is the name of the relationship that connects a role binding to a subject. + 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 { + // InheritPermissionsFrom is the list of resource types that can provide roles + // and grants 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 + InheritPermissionsFrom []string +} + +/* +RBAC represents a role-based access control policy. + +For example, consider the following spicedb schema: +```zed + + definition user {} + definition client {} + + definition group { + relation member: user | client + } + + definition organization { + relation parent: organization + relation member: user | client + relation member_role: role + relation grant: rolebinding + + permissions rolebinding_list: grant->rolebinding_list + permissions rolebinding_create: grant->rolebinding_create + permissions rolebinding_delete: grant->rolebinding_delete + } + + definition role { + relation owner: organization + relation view_organization: user:* | client:* + relation rolebinding_list_rel: user:* | client:* + relation rolebinding_create_rel: user:* | client:* + relation rolebinding_delete_rel: user:* | client:* + } + + definition rolebinding { + relation role: role + relation subject: user | group#member + permission view_organization = subject & role->view_organization + permissions rolebinding_list: subject & role->rolebinding_list + permissions rolebinding_create: subject & role->rolebinding_create + permissions rolebinding_delete: subject & role->rolebinding_delete + } + +``` +in IAPL policy terms: +- the RoleResource would be "{name: role, idprefix: someprefix}" +- the RoleBindingResource would be "{name: rolebinding, idprefix: someprefix}", +- the RoleRelationshipSubject would be `[user, client]`. +- the RoleBindingSubjects would be `[{name: user}, {name: group, subjectrelation: member}]`. +*/ +type RBAC struct { + // RoleResource is the name of the resource type that represents a role. + RoleResource RBACResourceDefinition + // RoleBindingResource is the name of the resource type that represents a role binding. + RoleBindingResource RBACResourceDefinition + // 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{} +} + +// RBACResourceDefinition is a struct to define a resource type for a role +// and role-bindings +type RBACResourceDefinition struct { + Name string + IDPrefix string +} + +// 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, + }, + }) + } + + return conds +} + +// 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, + } + + 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 actions +} + +// 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: AvailableRolesList}) + + 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/internal/query/relations.go b/internal/query/relations.go index c6fb943d..c4be30a9 100644 --- a/internal/query/relations.go +++ b/internal/query/relations.go @@ -49,8 +49,8 @@ func (e *engine) validateRelationship(rel types.Relationship) error { // If we find a relation with a name and type that matches our relationship, // return if rel.Relation == typeRel.Relation { - for _, typeName := range typeRel.Types { - if subjType.Name == typeName { + for _, t := range typeRel.Types { + if subjType.Name == t.Name { return nil } } @@ -806,7 +806,8 @@ func (e *engine) relationshipsToNonRoles(rels []*pb.Relationship) ([]types.Relat var out []types.Relationship for _, rel := range rels { - if rel.Subject.Object.ObjectType == e.namespace+"/role" { + // skip relationships for v1 roles, and wildcard relationships for v2 roles + if rel.Subject.Object.ObjectType == e.namespace+"/role" || rel.Subject.Object.ObjectId == "*" { continue } @@ -895,6 +896,21 @@ func (e *engine) ListRoles(ctx context.Context, resource types.Resource) ([]type return nil, err } + dbRolesv1 := make([]storage.Role, 0, len(dbRoles)) + + for _, dbRole := range dbRoles { + res, err := e.NewResourceFromID(dbRole.ID) + if err != nil { + return nil, err + } + + if res.Type == e.rbac.RoleResource.Name { + continue + } + + dbRolesv1 = append(dbRolesv1, dbRole) + } + resType := e.namespace + "/" + resource.Type roleType := e.namespace + "/role" @@ -922,9 +938,9 @@ func (e *engine) ListRoles(ctx context.Context, resource types.Resource) ([]type rolesByID[role.ID] = role } - out := make([]types.Role, len(dbRoles)) + out := make([]types.Role, len(dbRolesv1)) - for i, dbRole := range dbRoles { + for i, dbRole := range dbRolesv1 { spicedbRole := rolesByID[dbRole.ID] out[i] = types.Role{ diff --git a/internal/query/relations_test.go b/internal/query/relations_test.go index 30da7688..75811dfc 100644 --- a/internal/query/relations_test.go +++ b/internal/query/relations_test.go @@ -75,8 +75,8 @@ func testPolicy() iapl.Policy { Relationships: []iapl.Relationship{ { Relation: "parent", - TargetTypeNames: []string{ - "tenant", + TargetTypes: []types.TargetType{ + {Name: "tenant"}, }, }, }, diff --git a/internal/query/service.go b/internal/query/service.go index 6eacbad0..64a3b929 100644 --- a/internal/query/service.go +++ b/internal/query/service.go @@ -53,6 +53,8 @@ type engine struct { schemaTypeMap map[string]types.ResourceType schemaSubjectRelationMap map[string]map[string][]string schemaRoleables []types.ResourceType + + rbac iapl.RBAC } func (e *engine) cacheSchemaResources() { @@ -67,11 +69,11 @@ func (e *engine) cacheSchemaResources() { for _, relationship := range res.Relationships { for _, t := range relationship.Types { - if _, ok := e.schemaSubjectRelationMap[t]; !ok { - e.schemaSubjectRelationMap[t] = make(map[string][]string) + if _, ok := e.schemaSubjectRelationMap[t.Name]; !ok { + e.schemaSubjectRelationMap[t.Name] = make(map[string][]string) } - e.schemaSubjectRelationMap[t][relationship.Relation] = append(e.schemaSubjectRelationMap[t][relationship.Relation], res.Name) + e.schemaSubjectRelationMap[t.Name][relationship.Relation] = append(e.schemaSubjectRelationMap[t.Name][relationship.Relation], res.Name) } } @@ -111,7 +113,9 @@ func NewEngine(namespace string, client *authzed.Client, kv nats.KeyValue, store } if e.schema == nil { - e.schema = iapl.DefaultPolicy().Schema() + p := iapl.DefaultPolicy() + e.schema = p.Schema() + e.rbac = iapl.RBAC{} e.cacheSchemaResources() } @@ -134,6 +138,13 @@ func WithPolicy(policy iapl.Policy) Option { return func(e *engine) { e.schema = policy.Schema() + rbac := policy.RBAC() + if rbac == nil { + e.rbac = iapl.RBAC{} + } else { + e.rbac = *rbac + } + e.cacheSchemaResources() } } diff --git a/internal/spicedbx/schema.go b/internal/spicedbx/schema.go index 3387a03b..9858f78f 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -8,26 +8,55 @@ import ( "go.infratographer.com/permissions-api/internal/types" ) -var ( - schemaTemplate = template.Must(template.New("schema").Parse(` +var schemaTemplate = template.Must(template.New("schema").Parse(` +{{- define "renderCondition" -}} +{{ $actionName := .Name }} +{{- range $index, $cond := .Conditions -}} + {{- if $index }} + {{end}} + {{- if $cond.RelationshipAction }} + {{- $cond.RelationshipAction.Relation}} + {{- if ne $cond.RelationshipAction.ActionName ""}}->{{ $cond.RelationshipAction.ActionName }}{{- end }} + {{- end }} +{{- end }} +{{- end -}} + +{{- define "renderConditionSet" -}} +{{ $actionName := .Name }} +{{- range $index, $conditionSet := .ConditionSets }} + {{- if $index }} & {{end}} + {{- if gt (len $conditionSet.Conditions) 1 -}} ( {{- end}} + {{- range $index, $cond := .Conditions -}} + {{- if $index }} + {{end}} + {{- if $cond.RelationshipAction }} + {{- $cond.RelationshipAction.Relation}} + {{- if ne $cond.RelationshipAction.ActionName ""}}->{{ $cond.RelationshipAction.ActionName }}{{- end }} + {{- end }} + {{- end }} + {{- if gt (len $conditionSet.Conditions) 1 -}} ) {{- end}} + {{- end}} +{{- end -}} + {{- $namespace := .Namespace -}} {{- range .ResourceTypes -}} definition {{$namespace}}/{{.Name}} { {{- range .Relationships }} - relation {{.Relation}}: {{ range $index, $typeName := .Types -}}{{ if $index }} | {{end}}{{$namespace}}/{{$typeName}}{{- end }} + relation {{.Relation}}: {{ range $index, $type := .Types -}} + {{- if $index }} | {{end}} + {{- $namespace}}/{{$type.Name}} + {{- if $type.SubjectIdentifier}}:{{$type.SubjectIdentifier}}{{end}} + {{- if $type.SubjectRelation}}#{{$type.SubjectRelation}}{{end}} + {{- end }} {{- end }} {{- range .Actions }} - relation {{.Name}}_rel: {{ $namespace }}/role#subject -{{- end }} - -{{- range .Actions }} -{{- $actionName := .Name }} - permission {{ $actionName }} = {{ range $index, $cond := .Conditions -}}{{ if $index }} + {{end}}{{ if $cond.RoleBinding }}{{ $actionName }}_rel{{ end }}{{ if $cond.RelationshipAction }}{{ $cond.RelationshipAction.Relation}}->{{ $cond.RelationshipAction.ActionName }}{{ end }}{{- end }} + permission {{ .Name }} = {{ if gt (len .Conditions) 0 }} + {{- template "renderCondition" . }} + {{- else if gt (len .ConditionSets) 0 }} + {{- template "renderConditionSet" . }} + {{- end}} {{- end }} } {{end}}`)) -) // GenerateSchema generates the spicedb schema from the template func GenerateSchema(namespace string, resourceTypes []types.ResourceType) (string, error) { diff --git a/internal/spicedbx/schema_test.go b/internal/spicedbx/schema_test.go index 3dc2a905..1199f6ea 100644 --- a/internal/spicedbx/schema_test.go +++ b/internal/spicedbx/schema_test.go @@ -39,9 +39,9 @@ func TestSchema(t *testing.T) { Relationships: []types.ResourceTypeRelationship{ { Relation: "subject", - Types: []string{ - "user", - "client", + Types: []types.TargetType{ + {Name: "user"}, + {Name: "client"}, }, }, }, @@ -51,8 +51,32 @@ func TestSchema(t *testing.T) { Relationships: []types.ResourceTypeRelationship{ { Relation: "parent", - Types: []string{ - "tenant", + Types: []types.TargetType{ + {Name: "tenant"}, + }, + }, + { + Relation: "loadbalancer_create_rel", + Types: []types.TargetType{ + {Name: "role", SubjectRelation: "subject"}, + }, + }, + { + Relation: "loadbalancer_get_rel", + Types: []types.TargetType{ + {Name: "role", SubjectRelation: "subject"}, + }, + }, + { + Relation: "port_create_rel", + Types: []types.TargetType{ + {Name: "role", SubjectRelation: "subject"}, + }, + }, + { + Relation: "port_get_rel", + Types: []types.TargetType{ + {Name: "role", SubjectRelation: "subject"}, }, }, }, @@ -61,6 +85,9 @@ func TestSchema(t *testing.T) { Name: "loadbalancer_create", Conditions: []types.Condition{ { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "loadbalancer_create_rel", + }, RoleBinding: &types.ConditionRoleBinding{}, }, { @@ -75,6 +102,9 @@ func TestSchema(t *testing.T) { Name: "loadbalancer_get", Conditions: []types.Condition{ { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "loadbalancer_get_rel", + }, RoleBinding: &types.ConditionRoleBinding{}, }, { @@ -89,6 +119,9 @@ func TestSchema(t *testing.T) { Name: "port_create", Conditions: []types.Condition{ { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "port_create_rel", + }, RoleBinding: &types.ConditionRoleBinding{}, }, { @@ -103,6 +136,9 @@ func TestSchema(t *testing.T) { Name: "port_get", Conditions: []types.Condition{ { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "port_get_rel", + }, RoleBinding: &types.ConditionRoleBinding{}, }, { @@ -120,8 +156,14 @@ func TestSchema(t *testing.T) { Relationships: []types.ResourceTypeRelationship{ { Relation: "owner", - Types: []string{ - "tenant", + Types: []types.TargetType{ + {Name: "tenant"}, + }, + }, + { + Relation: "loadbalancer_get_rel", + Types: []types.TargetType{ + {Name: "role", SubjectRelation: "subject"}, }, }, }, @@ -130,6 +172,9 @@ func TestSchema(t *testing.T) { Name: "loadbalancer_get", Conditions: []types.Condition{ { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "loadbalancer_get_rel", + }, RoleBinding: &types.ConditionRoleBinding{}, }, { @@ -147,8 +192,14 @@ func TestSchema(t *testing.T) { Relationships: []types.ResourceTypeRelationship{ { Relation: "owner", - Types: []string{ - "tenant", + Types: []types.TargetType{ + {Name: "tenant"}, + }, + }, + { + Relation: "port_get_rel", + Types: []types.TargetType{ + {Name: "role", SubjectRelation: "subject"}, }, }, }, @@ -157,6 +208,9 @@ func TestSchema(t *testing.T) { Name: "port_get", Conditions: []types.Condition{ { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "port_get_rel", + }, RoleBinding: &types.ConditionRoleBinding{}, }, { diff --git a/internal/types/types.go b/internal/types/types.go index 34704350..52d2f55e 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -20,15 +20,27 @@ type Role struct { UpdatedAt time.Time } +// TargetType represents a relationship target, as defined in spiceDB's schema +// reference: https://authzed.com/docs/reference/schema-lang#relations +type TargetType struct { + Name string + SubjectIdentifier string + SubjectRelation string +} + // ResourceTypeRelationship is a relationship for a resource type. type ResourceTypeRelationship struct { Relation string - Types []string + Types []TargetType } // ConditionRoleBinding represents a condition where a role binding is necessary to perform an action. 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. +type ConditionRoleBindingV2 struct{} + // ConditionRelationshipAction represents a condition where an action must be able to be performed // on another resource along a relation to perform an action. type ConditionRelationshipAction struct { @@ -39,13 +51,20 @@ type ConditionRelationshipAction struct { // Condition represents a required condition for performing an action. type Condition struct { RoleBinding *ConditionRoleBinding + RoleBindingV2 *ConditionRoleBindingV2 RelationshipAction *ConditionRelationshipAction } +// ConditionSet is a set of conditions that must be met for the action to be performed. +type ConditionSet struct { + Conditions []Condition +} + // Action represents a named thing a subject can do. type Action struct { - Name string - Conditions []Condition + Name string + Conditions []Condition + ConditionSets []ConditionSet } // ResourceType defines a type of resource managed by the api @@ -62,9 +81,26 @@ type Resource struct { ID gidx.PrefixedID } +// RoleBindingSubjectCondition is the object that represents the condition of a +// role binding subject. +type RoleBindingSubjectCondition struct{} + +// RoleBindingSubject is the object that represents the subject of a role binding. +type RoleBindingSubject struct { + SubjectResource Resource + Condition *RoleBindingSubjectCondition +} + // Relationship represents a named association between a resource and a subject. type Relationship struct { Resource Resource Relation string Subject Resource } + +// RoleBinding represents a role binding between a role and a resource. +type RoleBinding struct { + ID gidx.PrefixedID + Role Role + Subjects []RoleBindingSubject +} diff --git a/policies/policy.example.yaml b/policies/policy.example.yaml index 80af7c78..5c52a1f7 100644 --- a/policies/policy.example.yaml +++ b/policies/policy.example.yaml @@ -1,34 +1,120 @@ +rbac: + roleresource: + name: rolev2 + idprefix: permrv2 + rolebindingresource: + name: rolebinding + idprefix: permrbn + rolesubjecttypes: + - user + - client + roleowners: + - tenant + rolebindingsubjects: + - name: user + - name: client + - name: group + subjectrelation: member + +unions: + - name: group_member + resourcetypes: + - name: user + - name: client + - name: group + subjectrelation: member + - name: tenant_member + resourcetypes: + - name: user + - name: client + - name: group + subjectrelation: member + - name: tenant + subjectrelation: member + - name: resourceowner + resourcetypes: + - name: tenant + - name: resourceowner_relationship + resourcetypes: + - name: tenant + - name: tenant + subjectrelation: parent + - name: subject + resourcetypes: + - name: user + - name: client + - name: group_parent + resourcetypes: + - name: group + - name: group + subjectrelation: parent + - name: tenant + - name: tenant + subjectrelation: parent + - name: tenant_parent + resourcetypes: + - name: tenant + - name: tenant + subjectrelation: parent + resourcetypes: - name: role idprefix: permrol relationships: - relation: subject - targettypenames: - - subject + targettypes: + - name: subject + - name: user idprefix: idntusr - name: client - idprefix: idntcli + idprefix: idntclt + + - name: group + idprefix: idntgrp + rolebindingv2: + &rolesFromParent + inheritpermissionsfrom: + - parent + relationships: + - relation: parent + targettypes: + - name: group_parent + - relation: member + targettypes: + - name: group_member + - relation: grant + targettypes: + - name: rolebinding + - name: tenant idprefix: tnntten + rolebindingv2: + *rolesFromParent relationships: - relation: parent - targettypenames: - - tenant + targettypes: + - name: tenant_parent + - relation: member + targettypes: + - name: tenant_member + - relation: grant + targettypes: + - name: rolebinding + - name: loadbalancer idprefix: loadbal + rolebindingv2: + inheritpermissionsfrom: + - owner relationships: - relation: owner - targettypenames: - - resourceowner -unions: - - name: subject - resourcetypenames: - - user - - client - - name: resourceowner - resourcetypenames: - - tenant + targettypes: + - name: resourceowner_relationship + - relation: grant + targettypes: + - name: rolebinding + actions: - name: role_create - name: role_get @@ -40,95 +126,142 @@ actions: - name: loadbalancer_list - name: loadbalancer_update - name: loadbalancer_delete + actionbindings: + # role management - permissions on role + - actionname: role_get + typename: rolev2 + conditions: + - relationshipaction: + relation: owner + actionname: role_get + - actionname: role_update + typename: rolev2 + conditions: + - relationshipaction: + relation: owner + actionname: role_update + - actionname: role_delete + typename: rolev2 + conditions: + - relationshipaction: + relation: owner + actionname: role_delete + # role management - permissions on owner - actionname: role_create typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: role_create + - actionname: role_create + typename: group + conditions: + - rolebindingv2: {} + - actionname: role_get typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: role_get + - actionname: role_get + typename: group + conditions: + - rolebindingv2: {} + - actionname: role_list typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: role_list + - actionname: role_list + typename: group + conditions: + - rolebindingv2: {} + - actionname: role_update typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: role_update + - actionname: role_update + typename: group + conditions: + - rolebindingv2: {} + - actionname: role_delete typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: role_delete - - actionname: loadbalancer_create - typename: resourceowner + - actionname: role_delete + typename: group conditions: - - rolebinding: {} - - relationshipaction: - relation: parent - actionname: loadbalancer_create + - rolebindingv2: {} + + # loadbalancer management - permissions on loadbalancer - actionname: loadbalancer_get - typename: resourceowner + typename: loadbalancer conditions: - rolebinding: {} - - relationshipaction: - relation: parent - actionname: loadbalancer_get + - rolebindingv2: {} - actionname: loadbalancer_update - typename: resourceowner + typename: loadbalancer conditions: - rolebinding: {} - - relationshipaction: - relation: parent - actionname: loadbalancer_update - - actionname: loadbalancer_list + - rolebindingv2: {} + - actionname: loadbalancer_delete + typename: loadbalancer + conditions: + - rolebinding: {} + - rolebindingv2: {} + + # loadbalancer management - permissions on owner + - actionname: loadbalancer_create typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: loadbalancer_list - - actionname: loadbalancer_delete + - actionname: loadbalancer_create + typename: group + conditions: + - rolebindingv2: {} + + - actionname: loadbalancer_get typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: parent - actionname: loadbalancer_delete - actionname: loadbalancer_get - typename: loadbalancer + typename: group conditions: + - rolebindingv2: {} + + - actionname: loadbalancer_list + typename: resourceowner + conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: owner - actionname: loadbalancer_get + - actionname: loadbalancer_list + typename: group + conditions: + - rolebindingv2: {} + - actionname: loadbalancer_update - typename: loadbalancer + typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: owner - actionname: loadbalancer_update + - actionname: loadbalancer_update + typename: group + conditions: + - rolebindingv2: {} + - actionname: loadbalancer_delete - typename: loadbalancer + typename: resourceowner conditions: + - rolebindingv2: {} - rolebinding: {} - - relationshipaction: - relation: owner - actionname: loadbalancer_delete + - actionname: loadbalancer_delete + typename: group + conditions: + - rolebindingv2: {}