From 0fdcf47b97e8379c9e058a64bf2785d623caf2b4 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Tue, 13 Jun 2023 22:48:26 +0000 Subject: [PATCH 01/14] [hax] First pass at IAPL schema generation Signed-off-by: John Schaeffer --- internal/iapl/doc.go | 3 + internal/iapl/errors.go | 16 ++ internal/iapl/policy.go | 300 ++++++++++++++++++++++++++++++++++++ internal/query/tenants.go | 20 ++- internal/spicedbx/schema.go | 287 ++++++++++++++++++++++++---------- internal/types/types.go | 26 +++- 6 files changed, 559 insertions(+), 93 deletions(-) create mode 100644 internal/iapl/doc.go create mode 100644 internal/iapl/errors.go create mode 100644 internal/iapl/policy.go diff --git a/internal/iapl/doc.go b/internal/iapl/doc.go new file mode 100644 index 00000000..f24029f1 --- /dev/null +++ b/internal/iapl/doc.go @@ -0,0 +1,3 @@ +// Package iapl contains functions and data for the Infratographer Authorization Policy Language, a +// domain-specific language for defining authorization policies based on resource relationships. +package iapl diff --git a/internal/iapl/errors.go b/internal/iapl/errors.go new file mode 100644 index 00000000..17f347c7 --- /dev/null +++ b/internal/iapl/errors.go @@ -0,0 +1,16 @@ +package iapl + +import "errors" + +var ( + // ErrorUnknownType represents an error where a resource type is unknown in the authorization policy. + ErrorUnknownType = errors.New("unknown resource type") + // ErrorInvalidAlias represents an error where a type alias is invalid. + ErrorInvalidAlias = errors.New("invalid type alias") + // ErrorInvalidCondition represents an error where an action binding condition is invalid. + ErrorInvalidCondition = errors.New("invalid condition") + // ErrorUnknownRelation represents an error where a relation is not defined for a resource type. + ErrorUnknownRelation = errors.New("unknown relation") + // ErrorUnknownAction represents an error where an action is not defined. + ErrorUnknownAction = errors.New("unknown action") +) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go new file mode 100644 index 00000000..962451d8 --- /dev/null +++ b/internal/iapl/policy.go @@ -0,0 +1,300 @@ +package iapl + +import ( + "fmt" + + "go.infratographer.com/permissions-api/internal/types" +) + +// PolicyDocument represents a partial authorization policy. +type PolicyDocument struct { + ResourceTypes []ResourceType + TypeAliases []TypeAlias + Actions []Action + ActionBindings []ActionBinding +} + +// ResourceType represents a resource type in the authorization policy. +type ResourceType struct { + Name string + IDPrefix string + Relationships []Relationship +} + +// Relationship represents a named relation between two resources. +type Relationship struct { + Relation string + TargetTypeName string +} + +// TypeAlias represents a named alias to multiple concrete resource types. +type TypeAlias struct { + Name string + ResourceTypeNames []string +} + +// Action represents an action that can be taken in an authorization policy. +type Action struct { + Name string +} + +// ActionBinding represents a binding of an action to a resource type. +type ActionBinding struct { + ActionName string + ResourceTypeName string + Conditions []Condition +} + +// Condition represents a necessary condition for performing an action. +type Condition struct { + RoleBinding *ConditionRoleBinding + RelationshipAction *ConditionRelationshipAction +} + +// ConditionRoleBinding represents a condition where a role binding is necessary to perform an action. +type ConditionRoleBinding struct { +} + +// ConditionRelationshipAction represents a condition where another action must be allowed on a resource +// along a relation to perform an action. +type ConditionRelationshipAction struct { + Relation string + ActionName string +} + +// Policy represents an authorization policy as defined by IAPL. +type Policy interface { + Validate() error + Schema() []types.ResourceType +} + +var _ Policy = &policy{} + +type policy struct { + rt map[string]ResourceType + ta map[string]TypeAlias + ac map[string]Action + rb map[string]map[string]struct{} + p PolicyDocument +} + +// NewPolicy creates a policy from the given policy document. +func NewPolicy(p PolicyDocument) Policy { + rt := make(map[string]ResourceType, len(p.ResourceTypes)) + for _, r := range p.ResourceTypes { + rt[r.Name] = r + } + + ta := make(map[string]TypeAlias, len(p.TypeAliases)) + for _, t := range p.TypeAliases { + ta[t.Name] = t + } + + ac := make(map[string]Action, len(p.Actions)) + for _, a := range p.Actions { + ac[a.Name] = a + } + + rb := make(map[string]map[string]struct{}, len(p.ResourceTypes)) + for _, ab := range p.ActionBindings { + b, ok := rb[ab.ResourceTypeName] + if !ok { + b = make(map[string]struct{}) + rb[ab.ResourceTypeName] = b + } + + b[ab.ActionName] = struct{}{} + } + + out := policy{ + rt: rt, + ta: ta, + ac: ac, + rb: rb, + p: p, + } + + return &out +} + +func (v *policy) validateTypeAliases() error { + for _, typeAlias := range v.p.TypeAliases { + if _, ok := v.rt[typeAlias.Name]; ok { + return fmt.Errorf("%s: %w", typeAlias.Name, ErrorInvalidAlias) + } + + for _, rtName := range typeAlias.ResourceTypeNames { + if _, ok := v.rt[rtName]; !ok { + return fmt.Errorf("%s: resourceTypeNames: %s: %w", typeAlias.Name, rtName, ErrorUnknownType) + } + } + } + + return nil +} + +func (v *policy) validateResourceTypes() error { + for _, resourceType := range v.p.ResourceTypes { + for _, rel := range resourceType.Relationships { + if _, ok := v.rt[rel.TargetTypeName]; ok { + continue + } + + if _, ok := v.ta[rel.TargetTypeName]; ok { + continue + } + + return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, rel.TargetTypeName, ErrorUnknownType) + } + } + + return nil +} + +func (v *policy) validateConditionRelationshipAction(rt ResourceType, c ConditionRelationshipAction) error { + var ( + rel Relationship + found bool + ) + + for _, candidate := range rt.Relationships { + if c.Relation == candidate.Relation { + rel = candidate + found = true + + break + } + } + + if !found { + return fmt.Errorf("%s: %w", c.Relation, ErrorUnknownRelation) + } + + var typeNames []string + + if _, ok := v.rt[rel.TargetTypeName]; ok { + typeNames = []string{ + rel.TargetTypeName, + } + } else { + typeNames = v.ta[rel.TargetTypeName].ResourceTypeNames + } + + for _, tn := range typeNames { + if _, ok := v.rb[tn][c.ActionName]; !ok { + return fmt.Errorf("%s: %s: %s: %w", c.Relation, tn, c.ActionName, ErrorUnknownAction) + } + } + + return nil +} + +func (v *policy) validateConditions(rt ResourceType, conds []Condition) error { + for i, cond := range conds { + var numClauses int + if cond.RoleBinding != nil { + numClauses++ + } + + if cond.RelationshipAction != nil { + numClauses++ + } + + if numClauses != 1 { + return fmt.Errorf("%d: %w", i, ErrorInvalidCondition) + } + + if cond.RelationshipAction != nil { + if err := v.validateConditionRelationshipAction(rt, *cond.RelationshipAction); err != nil { + return fmt.Errorf("%d: %w", i, err) + } + } + } + + return nil +} + +func (v *policy) validateActionBindings() error { + for i, binding := range v.p.ActionBindings { + if _, ok := v.ac[binding.ActionName]; !ok { + return fmt.Errorf("%d: %s: %w", i, binding.ActionName, ErrorUnknownAction) + } + + rt, ok := v.rt[binding.ResourceTypeName] + if !ok { + return fmt.Errorf("%d: %s: %w", i, binding.ResourceTypeName, ErrorUnknownType) + } + + if err := v.validateConditions(rt, binding.Conditions); err != nil { + return fmt.Errorf("%d: conditions: %w", i, err) + } + } + + return nil +} + +func (v *policy) Validate() error { + if err := v.validateTypeAliases(); err != nil { + return fmt.Errorf("typeAliases: %w", err) + } + + if err := v.validateResourceTypes(); err != nil { + return fmt.Errorf("resourceTypes: %w", err) + } + + if err := v.validateActionBindings(); err != nil { + return fmt.Errorf("actionBindings: %w", err) + } + + return nil +} + +func (v *policy) Schema() []types.ResourceType { + typeMap := map[string]*types.ResourceType{} + + for n, rt := range v.rt { + out := types.ResourceType{ + Name: rt.Name, + } + + for _, rel := range rt.Relationships { + outRel := types.ResourceTypeRelationship{ + Relation: rel.Relation, + } + + if alias, ok := v.ta[rel.TargetTypeName]; ok { + outRel.Types = alias.ResourceTypeNames + } else { + outRel.Types = []string{ + rel.TargetTypeName, + } + } + + out.Relationships = append(out.Relationships, outRel) + } + + typeMap[n] = &out + } + + for _, b := range v.p.ActionBindings { + action := types.Action{ + Name: b.ActionName, + } + for _, c := range b.Conditions { + condition := types.Condition{ + RoleBinding: (*types.ConditionRoleBinding)(c.RoleBinding), + RelationshipAction: (*types.ConditionRelationshipAction)(c.RelationshipAction), + } + + action.Conditions = append(action.Conditions, condition) + } + typeMap[b.ResourceTypeName].Actions = append(typeMap[b.ResourceTypeName].Actions, action) + } + + out := make([]types.ResourceType, len(v.p.ResourceTypes)) + for i, rt := range v.p.ResourceTypes { + out[i] = *typeMap[rt.Name] + } + + return out +} diff --git a/internal/query/tenants.go b/internal/query/tenants.go index 4550a2de..a6afe9bc 100644 --- a/internal/query/tenants.go +++ b/internal/query/tenants.go @@ -46,7 +46,7 @@ func validateRelationship(rel types.Relationship) error { for _, typeRel := range subjType.Relationships { // If we find a relation with a name and type that matches our relationship, // return - if rel.Relation == typeRel.Name && resType.Name == typeRel.Type { + if rel.Relation == typeRel.Relation && resType.Name == typeRel.Relation { return nil } } @@ -444,8 +444,10 @@ func GetResourceTypes() []types.ResourceType { Name: "loadbalancer", Relationships: []types.ResourceTypeRelationship{ { - Name: "tenant", - Type: "tenant", + Relation: "tenant", + Types: []string{ + "tenant", + }, }, }, }, @@ -453,8 +455,10 @@ func GetResourceTypes() []types.ResourceType { Name: "role", Relationships: []types.ResourceTypeRelationship{ { - Name: "tenant", - Type: "tenant", + Relation: "tenant", + Types: []string{ + "tenant", + }, }, }, }, @@ -462,8 +466,10 @@ func GetResourceTypes() []types.ResourceType { Name: "tenant", Relationships: []types.ResourceTypeRelationship{ { - Name: "tenant", - Type: "tenant", + Relation: "tenant", + Types: []string{ + "tenant", + }, }, }, }, diff --git a/internal/spicedbx/schema.go b/internal/spicedbx/schema.go index 97b98640..e3fa9fb9 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -4,83 +4,29 @@ import ( "bytes" "text/template" + "go.infratographer.com/permissions-api/internal/iapl" "go.infratographer.com/permissions-api/internal/types" ) var ( schemaTemplate = template.Must(template.New("schema").Parse(` {{- $namespace := .Namespace -}} -definition {{$namespace}}/user {} - -definition {{$namespace}}/client {} - -definition {{$namespace}}/role { - relation tenant: {{$namespace}}/tenant - relation subject: {{$namespace}}/user | {{$namespace}}/client +{{- range .ResourceTypes -}} +definition {{$namespace}}/{{.Name}} { +{{- range .Relationships }} + relation {{.Relation}}: {{ range $index, $typeName := .Types -}}{{ if $index }} | {{end}}{{$namespace}}/{{$typeName}}{{- end }} +{{- end }} - relation role_get_rel: {{$namespace}}/role#subject - relation role_update_rel: {{$namespace}}/role#subject - relation role_delete_rel: {{$namespace}}/role#subject +{{- range .Actions }} + relation {{.Name}}_rel: {{ $namespace }}/role#subject +{{- end }} - permission role_get = role_get_rel + tenant->role_get - permission role_update = role_update_rel + tenant->role_update - permission role_delete = role_delete_rel + tenant->role_delete +{{- 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 }} +{{- end }} } -definition {{$namespace}}/tenant { - relation tenant: {{$namespace}}/tenant - - relation tenant_create_rel: {{$namespace}}/role#subject - relation tenant_get_rel: {{$namespace}}/role#subject - relation tenant_list_rel: {{$namespace}}/role#subject - relation tenant_update_rel: {{$namespace}}/role#subject - relation tenant_delete_rel: {{$namespace}}/role#subject - - permission tenant_create = tenant_create_rel + tenant->tenant_create - permission tenant_get = tenant_get_rel + tenant->tenant_get - permission tenant_list = tenant_list_rel + tenant->tenant_list - permission tenant_update = tenant_update_rel + tenant->tenant_update - permission tenant_delete = tenant_delete_rel + tenant->tenant_delete - - relation role_create_rel: {{$namespace}}/role#subject - relation role_get_rel: {{$namespace}}/role#subject - relation role_list_rel: {{$namespace}}/role#subject - relation role_update_rel: {{$namespace}}/role#subject - relation role_delete_rel: {{$namespace}}/role#subject - - permission role_create = role_create_rel + tenant->role_create - permission role_get = role_get_rel + tenant->role_get - permission role_list = role_list_rel + tenant->role_list - permission role_update = role_update_rel + tenant->role_update - permission role_delete = role_delete_rel + tenant->role_delete - -{{- range .ResourceTypes -}} -{{$typeName := .Name}} -{{range .Actions}} - relation {{$typeName}}_{{.}}_rel: {{$namespace}}/role#subject -{{- end}} -{{range .Actions}} - permission {{$typeName}}_{{.}} = {{$typeName}}_{{.}}_rel + tenant->{{$typeName}}_{{.}} -{{- end}} -{{range .TenantActions}} - relation {{$typeName}}_{{.}}_rel: {{$namespace}}/role#subject -{{- end}} -{{range .TenantActions}} - permission {{$typeName}}_{{.}} = {{$typeName}}_{{.}}_rel + tenant->{{$typeName}}_{{.}} -{{- end}} -{{- end}} -} -{{range .ResourceTypes -}} -{{$typeName := .Name}} -definition {{$namespace}}/{{$typeName}} { - relation tenant: {{$namespace}}/tenant -{{range .Actions}} - relation {{$typeName}}_{{.}}_rel: {{$namespace}}/role#subject -{{- end}} -{{range .Actions}} - permission {{$typeName}}_{{.}} = {{$typeName}}_{{.}}_rel + tenant->{{$typeName}}_{{.}} -{{- end}} -} {{end}}`)) ) @@ -108,24 +54,205 @@ func GenerateSchema(namespace string, resourceTypes []types.ResourceType) (strin return out.String(), nil } -// GeneratedSchema generated the schema for a namespace +// GeneratedSchema generates the schema for a namespace func GeneratedSchema(namespace string) string { - resourceTypes := []types.ResourceType{ - { - Name: "loadbalancer", - Actions: []string{ - "get", - "update", - "delete", - }, - TenantActions: []string{ - "create", - "list", + policyDocument := iapl.PolicyDocument{ + ResourceTypes: []iapl.ResourceType{ + { + Name: "role", + IDPrefix: "idenrol", + Relationships: []iapl.Relationship{ + { + Relation: "subject", + TargetTypeName: "subject", + }, + }, + }, + { + Name: "user", + IDPrefix: "idenusr", + }, + { + Name: "client", + IDPrefix: "idencli", + }, + { + Name: "tenant", + IDPrefix: "identen", + Relationships: []iapl.Relationship{ + { + Relation: "tenant", + TargetTypeName: "tenant", + }, + }, + }, + { + Name: "loadbalancer", + IDPrefix: "loadbal", + Relationships: []iapl.Relationship{ + { + Relation: "tenant", + TargetTypeName: "tenant", + }, + }, + }, + }, + TypeAliases: []iapl.TypeAlias{ + { + Name: "subject", + ResourceTypeNames: []string{ + "user", + "client", + }, }, }, + Actions: []iapl.Action{ + { + Name: "loadbalancer_create", + }, + { + Name: "loadbalancer_get", + }, + { + Name: "loadbalancer_list", + }, + { + Name: "loadbalancer_update", + }, + { + Name: "loadbalancer_delete", + }, + }, + ActionBindings: []iapl.ActionBinding{ + { + ActionName: "loadbalancer_create", + ResourceTypeName: "tenant", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_create", + }, + }, + }, + }, + { + ActionName: "loadbalancer_get", + ResourceTypeName: "tenant", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_get", + }, + }, + }, + }, + { + ActionName: "loadbalancer_update", + ResourceTypeName: "tenant", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_update", + }, + }, + }, + }, + { + ActionName: "loadbalancer_list", + ResourceTypeName: "tenant", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_list", + }, + }, + }, + }, + { + ActionName: "loadbalancer_delete", + ResourceTypeName: "tenant", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_delete", + }, + }, + }, + }, + { + ActionName: "loadbalancer_get", + ResourceTypeName: "loadbalancer", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_get", + }, + }, + }, + }, + { + ActionName: "loadbalancer_update", + ResourceTypeName: "loadbalancer", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_update", + }, + }, + }, + }, + { + ActionName: "loadbalancer_delete", + ResourceTypeName: "loadbalancer", + Conditions: []iapl.Condition{ + { + RoleBinding: &iapl.ConditionRoleBinding{}, + }, + { + RelationshipAction: &iapl.ConditionRelationshipAction{ + Relation: "tenant", + ActionName: "loadbalancer_delete", + }, + }, + }, + }, + }, + } + + policy := iapl.NewPolicy(policyDocument) + if err := policy.Validate(); err != nil { + panic(err) } - schema, err := GenerateSchema(namespace, resourceTypes) + schema, err := GenerateSchema(namespace, policy.Schema()) if err != nil { panic(err) } diff --git a/internal/types/types.go b/internal/types/types.go index c434d206..2d41d993 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -11,18 +11,32 @@ type Role struct { // ResourceTypeRelationship is a relationship for a resource type. type ResourceTypeRelationship struct { - Name string - Type string + Relation string + Types []string +} + +type ConditionRoleBinding struct{} + +type ConditionRelationshipAction struct { + Relation string + ActionName string +} + +type Condition struct { + RoleBinding *ConditionRoleBinding + RelationshipAction *ConditionRelationshipAction +} + +type Action struct { + Name string + Conditions []Condition } // ResourceType defines a type of resource managed by the api type ResourceType struct { Name string Relationships []ResourceTypeRelationship - // Actions represents actions that can be taken on the resource directly - Actions []string - // TenantActions represents actions that can be taken on the resource's tenant context - TenantActions []string + Actions []Action } // Resource is the object to be acted upon by an subject From 5d5ec8372269831f37a3bf628a0754718e9a2609 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Wed, 14 Jun 2023 16:28:23 +0000 Subject: [PATCH 02/14] [hax] Use better DSL Signed-off-by: John Schaeffer --- internal/iapl/policy.go | 140 ++++++++++++++++++++---------------- internal/spicedbx/schema.go | 74 +++++++++++-------- 2 files changed, 122 insertions(+), 92 deletions(-) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index 962451d8..bd69d8bb 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -9,7 +9,7 @@ import ( // PolicyDocument represents a partial authorization policy. type PolicyDocument struct { ResourceTypes []ResourceType - TypeAliases []TypeAlias + Unions []Union Actions []Action ActionBindings []ActionBinding } @@ -23,12 +23,12 @@ type ResourceType struct { // Relationship represents a named relation between two resources. type Relationship struct { - Relation string - TargetTypeName string + Relation string + TargetTypeNames []string } -// TypeAlias represents a named alias to multiple concrete resource types. -type TypeAlias struct { +// Union represents a named union of multiple concrete resource types. +type Union struct { Name string ResourceTypeNames []string } @@ -38,11 +38,11 @@ type Action struct { Name string } -// ActionBinding represents a binding of an action to a resource type. +// ActionBinding represents a binding of an action to a resource type or union. type ActionBinding struct { - ActionName string - ResourceTypeName string - Conditions []Condition + ActionName string + TypeName string + Conditions []Condition } // Condition represents a necessary condition for performing an action. @@ -72,9 +72,10 @@ var _ Policy = &policy{} type policy struct { rt map[string]ResourceType - ta map[string]TypeAlias + un map[string]Union ac map[string]Action rb map[string]map[string]struct{} + bn []ActionBinding p PolicyDocument } @@ -85,9 +86,9 @@ func NewPolicy(p PolicyDocument) Policy { rt[r.Name] = r } - ta := make(map[string]TypeAlias, len(p.TypeAliases)) - for _, t := range p.TypeAliases { - ta[t.Name] = t + un := make(map[string]Union, len(p.Unions)) + for _, t := range p.Unions { + un[t.Name] = t } ac := make(map[string]Action, len(p.Actions)) @@ -95,30 +96,18 @@ func NewPolicy(p PolicyDocument) Policy { ac[a.Name] = a } - rb := make(map[string]map[string]struct{}, len(p.ResourceTypes)) - for _, ab := range p.ActionBindings { - b, ok := rb[ab.ResourceTypeName] - if !ok { - b = make(map[string]struct{}) - rb[ab.ResourceTypeName] = b - } - - b[ab.ActionName] = struct{}{} - } - out := policy{ rt: rt, - ta: ta, + un: un, ac: ac, - rb: rb, p: p, } return &out } -func (v *policy) validateTypeAliases() error { - for _, typeAlias := range v.p.TypeAliases { +func (v *policy) validateUnions() error { + for _, typeAlias := range v.p.Unions { if _, ok := v.rt[typeAlias.Name]; ok { return fmt.Errorf("%s: %w", typeAlias.Name, ErrorInvalidAlias) } @@ -136,15 +125,11 @@ func (v *policy) validateTypeAliases() error { func (v *policy) validateResourceTypes() error { for _, resourceType := range v.p.ResourceTypes { for _, rel := range resourceType.Relationships { - if _, ok := v.rt[rel.TargetTypeName]; ok { - continue - } - - if _, ok := v.ta[rel.TargetTypeName]; ok { - continue + for _, name := range rel.TargetTypeNames { + if _, ok := v.rt[name]; !ok { + return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, name, ErrorUnknownType) + } } - - return fmt.Errorf("%s: relationships: %s: %w", resourceType.Name, rel.TargetTypeName, ErrorUnknownType) } } @@ -170,17 +155,7 @@ func (v *policy) validateConditionRelationshipAction(rt ResourceType, c Conditio return fmt.Errorf("%s: %w", c.Relation, ErrorUnknownRelation) } - var typeNames []string - - if _, ok := v.rt[rel.TargetTypeName]; ok { - typeNames = []string{ - rel.TargetTypeName, - } - } else { - typeNames = v.ta[rel.TargetTypeName].ResourceTypeNames - } - - for _, tn := range typeNames { + 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) } @@ -215,14 +190,14 @@ func (v *policy) validateConditions(rt ResourceType, conds []Condition) error { } func (v *policy) validateActionBindings() error { - for i, binding := range v.p.ActionBindings { + for i, binding := range v.bn { if _, ok := v.ac[binding.ActionName]; !ok { return fmt.Errorf("%d: %s: %w", i, binding.ActionName, ErrorUnknownAction) } - rt, ok := v.rt[binding.ResourceTypeName] + rt, ok := v.rt[binding.TypeName] if !ok { - return fmt.Errorf("%d: %s: %w", i, binding.ResourceTypeName, ErrorUnknownType) + return fmt.Errorf("%d: %s: %w", i, binding.TypeName, ErrorUnknownType) } if err := v.validateConditions(rt, binding.Conditions); err != nil { @@ -233,8 +208,58 @@ func (v *policy) validateActionBindings() error { 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 { + binding := ActionBinding{ + TypeName: typeName, + ActionName: bn.ActionName, + Conditions: bn.Conditions, + } + v.bn = append(v.bn, binding) + } + } else { + v.bn = append(v.bn, bn) + } + } + + v.rb = make(map[string]map[string]struct{}, len(v.p.ResourceTypes)) + for _, ab := range v.bn { + b, ok := v.rb[ab.TypeName] + if !ok { + b = make(map[string]struct{}) + v.rb[ab.TypeName] = b + } + + b[ab.ActionName] = struct{}{} + } +} + +func (v *policy) expandResourceTypes() { + for name, resourceType := range v.rt { + for i, rel := range resourceType.Relationships { + var typeNames []string + for _, typeName := range rel.TargetTypeNames { + if u, ok := v.un[typeName]; ok { + typeNames = append(typeNames, u.ResourceTypeNames...) + } else { + typeNames = append(typeNames, typeName) + } + } + + resourceType.Relationships[i].TargetTypeNames = typeNames + } + + v.rt[name] = resourceType + } +} + func (v *policy) Validate() error { - if err := v.validateTypeAliases(); err != nil { + v.expandActionBindings() + v.expandResourceTypes() + + if err := v.validateUnions(); err != nil { return fmt.Errorf("typeAliases: %w", err) } @@ -260,14 +285,7 @@ func (v *policy) Schema() []types.ResourceType { for _, rel := range rt.Relationships { outRel := types.ResourceTypeRelationship{ Relation: rel.Relation, - } - - if alias, ok := v.ta[rel.TargetTypeName]; ok { - outRel.Types = alias.ResourceTypeNames - } else { - outRel.Types = []string{ - rel.TargetTypeName, - } + Types: rel.TargetTypeNames, } out.Relationships = append(out.Relationships, outRel) @@ -276,7 +294,7 @@ func (v *policy) Schema() []types.ResourceType { typeMap[n] = &out } - for _, b := range v.p.ActionBindings { + for _, b := range v.bn { action := types.Action{ Name: b.ActionName, } @@ -288,7 +306,7 @@ func (v *policy) Schema() []types.ResourceType { action.Conditions = append(action.Conditions, condition) } - typeMap[b.ResourceTypeName].Actions = append(typeMap[b.ResourceTypeName].Actions, action) + typeMap[b.TypeName].Actions = append(typeMap[b.TypeName].Actions, action) } out := make([]types.ResourceType, len(v.p.ResourceTypes)) diff --git a/internal/spicedbx/schema.go b/internal/spicedbx/schema.go index e3fa9fb9..52910291 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -63,8 +63,10 @@ func GeneratedSchema(namespace string) string { IDPrefix: "idenrol", Relationships: []iapl.Relationship{ { - Relation: "subject", - TargetTypeName: "subject", + Relation: "subject", + TargetTypeNames: []string{ + "subject", + }, }, }, }, @@ -81,8 +83,10 @@ func GeneratedSchema(namespace string) string { IDPrefix: "identen", Relationships: []iapl.Relationship{ { - Relation: "tenant", - TargetTypeName: "tenant", + Relation: "parent", + TargetTypeNames: []string{ + "tenant", + }, }, }, }, @@ -91,13 +95,15 @@ func GeneratedSchema(namespace string) string { IDPrefix: "loadbal", Relationships: []iapl.Relationship{ { - Relation: "tenant", - TargetTypeName: "tenant", + Relation: "owner", + TargetTypeNames: []string{ + "resourceowner", + }, }, }, }, }, - TypeAliases: []iapl.TypeAlias{ + Unions: []iapl.Union{ { Name: "subject", ResourceTypeNames: []string{ @@ -105,6 +111,12 @@ func GeneratedSchema(namespace string) string { "client", }, }, + { + Name: "resourceowner", + ResourceTypeNames: []string{ + "tenant", + }, + }, }, Actions: []iapl.Action{ { @@ -125,120 +137,120 @@ func GeneratedSchema(namespace string) string { }, ActionBindings: []iapl.ActionBinding{ { - ActionName: "loadbalancer_create", - ResourceTypeName: "tenant", + ActionName: "loadbalancer_create", + TypeName: "resourceowner", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "parent", ActionName: "loadbalancer_create", }, }, }, }, { - ActionName: "loadbalancer_get", - ResourceTypeName: "tenant", + ActionName: "loadbalancer_get", + TypeName: "resourceowner", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "parent", ActionName: "loadbalancer_get", }, }, }, }, { - ActionName: "loadbalancer_update", - ResourceTypeName: "tenant", + ActionName: "loadbalancer_update", + TypeName: "resourceowner", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "parent", ActionName: "loadbalancer_update", }, }, }, }, { - ActionName: "loadbalancer_list", - ResourceTypeName: "tenant", + ActionName: "loadbalancer_list", + TypeName: "resourceowner", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "parent", ActionName: "loadbalancer_list", }, }, }, }, { - ActionName: "loadbalancer_delete", - ResourceTypeName: "tenant", + ActionName: "loadbalancer_delete", + TypeName: "resourceowner", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "parent", ActionName: "loadbalancer_delete", }, }, }, }, { - ActionName: "loadbalancer_get", - ResourceTypeName: "loadbalancer", + ActionName: "loadbalancer_get", + TypeName: "loadbalancer", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "owner", ActionName: "loadbalancer_get", }, }, }, }, { - ActionName: "loadbalancer_update", - ResourceTypeName: "loadbalancer", + ActionName: "loadbalancer_update", + TypeName: "loadbalancer", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "owner", ActionName: "loadbalancer_update", }, }, }, }, { - ActionName: "loadbalancer_delete", - ResourceTypeName: "loadbalancer", + ActionName: "loadbalancer_delete", + TypeName: "loadbalancer", Conditions: []iapl.Condition{ { RoleBinding: &iapl.ConditionRoleBinding{}, }, { RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "tenant", + Relation: "owner", ActionName: "loadbalancer_delete", }, }, From ffac92e1b988f5e4984aa0549780d4b52c208357 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Wed, 14 Jun 2023 21:39:34 +0000 Subject: [PATCH 03/14] Fix SpiceDB schema generation tests Signed-off-by: John Schaeffer --- internal/spicedbx/schema.go | 1 - internal/spicedbx/schema_test.go | 217 +++++++++++++++++++++---------- 2 files changed, 146 insertions(+), 72 deletions(-) diff --git a/internal/spicedbx/schema.go b/internal/spicedbx/schema.go index 52910291..b331b8a0 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -26,7 +26,6 @@ definition {{$namespace}}/{{.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 }} {{- end }} } - {{end}}`)) ) diff --git a/internal/spicedbx/schema_test.go b/internal/spicedbx/schema_test.go index 8cfba133..3dc2a905 100644 --- a/internal/spicedbx/schema_test.go +++ b/internal/spicedbx/schema_test.go @@ -28,101 +28,176 @@ func TestSchema(t *testing.T) { } resourceTypes := []types.ResourceType{ + { + Name: "user", + }, + { + Name: "client", + }, + { + Name: "role", + Relationships: []types.ResourceTypeRelationship{ + { + Relation: "subject", + Types: []string{ + "user", + "client", + }, + }, + }, + }, + { + Name: "tenant", + Relationships: []types.ResourceTypeRelationship{ + { + Relation: "parent", + Types: []string{ + "tenant", + }, + }, + }, + Actions: []types.Action{ + { + Name: "loadbalancer_create", + Conditions: []types.Condition{ + { + RoleBinding: &types.ConditionRoleBinding{}, + }, + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_create", + }, + }, + }, + }, + { + Name: "loadbalancer_get", + Conditions: []types.Condition{ + { + RoleBinding: &types.ConditionRoleBinding{}, + }, + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_get", + }, + }, + }, + }, + { + Name: "port_create", + Conditions: []types.Condition{ + { + RoleBinding: &types.ConditionRoleBinding{}, + }, + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "parent", + ActionName: "port_create", + }, + }, + }, + }, + { + Name: "port_get", + Conditions: []types.Condition{ + { + RoleBinding: &types.ConditionRoleBinding{}, + }, + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "parent", + ActionName: "port_get", + }, + }, + }, + }, + }, + }, { Name: "loadbalancer", - Actions: []string{ - "get", + Relationships: []types.ResourceTypeRelationship{ + { + Relation: "owner", + Types: []string{ + "tenant", + }, + }, }, - TenantActions: []string{ - "create", + Actions: []types.Action{ + { + Name: "loadbalancer_get", + Conditions: []types.Condition{ + { + RoleBinding: &types.ConditionRoleBinding{}, + }, + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "owner", + ActionName: "loadbalancer_get", + }, + }, + }, + }, }, }, { Name: "port", - Actions: []string{ - "get", + Relationships: []types.ResourceTypeRelationship{ + { + Relation: "owner", + Types: []string{ + "tenant", + }, + }, }, - TenantActions: []string{ - "create", + Actions: []types.Action{ + { + Name: "port_get", + Conditions: []types.Condition{ + { + RoleBinding: &types.ConditionRoleBinding{}, + }, + { + RelationshipAction: &types.ConditionRelationshipAction{ + Relation: "owner", + ActionName: "port_get", + }, + }, + }, + }, }, }, } - schemaOutput := `definition foo/user {} - -definition foo/client {} - + schemaOutput := `definition foo/user { +} +definition foo/client { +} definition foo/role { - relation tenant: foo/tenant relation subject: foo/user | foo/client - - relation role_get_rel: foo/role#subject - relation role_update_rel: foo/role#subject - relation role_delete_rel: foo/role#subject - - permission role_get = role_get_rel + tenant->role_get - permission role_update = role_update_rel + tenant->role_update - permission role_delete = role_delete_rel + tenant->role_delete } - definition foo/tenant { - relation tenant: foo/tenant - - relation tenant_create_rel: foo/role#subject - relation tenant_get_rel: foo/role#subject - relation tenant_list_rel: foo/role#subject - relation tenant_update_rel: foo/role#subject - relation tenant_delete_rel: foo/role#subject - - permission tenant_create = tenant_create_rel + tenant->tenant_create - permission tenant_get = tenant_get_rel + tenant->tenant_get - permission tenant_list = tenant_list_rel + tenant->tenant_list - permission tenant_update = tenant_update_rel + tenant->tenant_update - permission tenant_delete = tenant_delete_rel + tenant->tenant_delete - - relation role_create_rel: foo/role#subject - relation role_get_rel: foo/role#subject - relation role_list_rel: foo/role#subject - relation role_update_rel: foo/role#subject - relation role_delete_rel: foo/role#subject - - permission role_create = role_create_rel + tenant->role_create - permission role_get = role_get_rel + tenant->role_get - permission role_list = role_list_rel + tenant->role_list - permission role_update = role_update_rel + tenant->role_update - permission role_delete = role_delete_rel + tenant->role_delete - - relation loadbalancer_get_rel: foo/role#subject - - permission loadbalancer_get = loadbalancer_get_rel + tenant->loadbalancer_get - + relation parent: foo/tenant relation loadbalancer_create_rel: foo/role#subject - - permission loadbalancer_create = loadbalancer_create_rel + tenant->loadbalancer_create - - relation port_get_rel: foo/role#subject - - permission port_get = port_get_rel + tenant->port_get - + relation loadbalancer_get_rel: foo/role#subject relation port_create_rel: foo/role#subject - - permission port_create = port_create_rel + tenant->port_create + relation port_get_rel: foo/role#subject + permission loadbalancer_create = loadbalancer_create_rel + parent->loadbalancer_create + permission loadbalancer_get = loadbalancer_get_rel + parent->loadbalancer_get + permission port_create = port_create_rel + parent->port_create + permission port_get = port_get_rel + parent->port_get } - definition foo/loadbalancer { - relation tenant: foo/tenant - + relation owner: foo/tenant relation loadbalancer_get_rel: foo/role#subject - - permission loadbalancer_get = loadbalancer_get_rel + tenant->loadbalancer_get + permission loadbalancer_get = loadbalancer_get_rel + owner->loadbalancer_get } - definition foo/port { - relation tenant: foo/tenant - + relation owner: foo/tenant relation port_get_rel: foo/role#subject - - permission port_get = port_get_rel + tenant->port_get + permission port_get = port_get_rel + owner->port_get } ` From cf7f969efd2574d711f7f67a04c20330daee0f9e Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Wed, 14 Jun 2023 22:09:17 +0000 Subject: [PATCH 04/14] Use iapl package as source of default policy, fix tests Signed-off-by: John Schaeffer --- internal/query/service.go | 5 + internal/query/tenants.go | 67 ++--------- internal/query/tenants_test.go | 6 +- internal/spicedbx/schema.go | 209 +-------------------------------- 4 files changed, 21 insertions(+), 266 deletions(-) diff --git a/internal/query/service.go b/internal/query/service.go index 9007405c..0d1a57f8 100644 --- a/internal/query/service.go +++ b/internal/query/service.go @@ -6,6 +6,7 @@ import ( "github.com/authzed/authzed-go/v1" "go.infratographer.com/x/urnx" + "go.infratographer.com/permissions-api/internal/iapl" "go.infratographer.com/permissions-api/internal/types" ) @@ -26,12 +27,16 @@ type Engine interface { type engine struct { namespace string client *authzed.Client + schema []types.ResourceType } // NewEngine returns a new client for making permissions queries. func NewEngine(namespace string, client *authzed.Client) Engine { + policy := iapl.DefaultPolicy() + return &engine{ namespace: namespace, client: client, + schema: policy.Schema(), } } diff --git a/internal/query/tenants.go b/internal/query/tenants.go index a6afe9bc..066c36f8 100644 --- a/internal/query/tenants.go +++ b/internal/query/tenants.go @@ -20,10 +20,8 @@ var ( errorInvalidRelationship = errors.New("invalid relationship") ) -func getTypeForResource(res types.Resource) (types.ResourceType, error) { - resTypes := GetResourceTypes() - - for _, resType := range resTypes { +func (e *engine) getTypeForResource(res types.Resource) (types.ResourceType, error) { + for _, resType := range e.schema { if res.Type == resType.Name { return resType, nil } @@ -32,13 +30,13 @@ func getTypeForResource(res types.Resource) (types.ResourceType, error) { return types.ResourceType{}, errorInvalidType } -func validateRelationship(rel types.Relationship) error { - subjType, err := getTypeForResource(rel.Subject) +func (e *engine) validateRelationship(rel types.Relationship) error { + subjType, err := e.getTypeForResource(rel.Subject) if err != nil { return err } - resType, err := getTypeForResource(rel.Resource) + resType, err := e.getTypeForResource(rel.Resource) if err != nil { return err } @@ -46,8 +44,12 @@ func validateRelationship(rel types.Relationship) error { for _, typeRel := range subjType.Relationships { // If we find a relation with a name and type that matches our relationship, // return - if rel.Relation == typeRel.Relation && resType.Name == typeRel.Relation { - return nil + if rel.Relation == typeRel.Relation { + for _, typeName := range typeRel.Types { + if resType.Name == typeName { + return nil + } + } } } @@ -175,7 +177,7 @@ func (e *engine) checkPermission(ctx context.Context, req *pb.CheckPermissionReq // CreateRelationships atomically creates the given relationships in SpiceDB. func (e *engine) CreateRelationships(ctx context.Context, rels []types.Relationship) (string, error) { for _, rel := range rels { - err := validateRelationship(rel) + err := e.validateRelationship(rel) if err != nil { return "", err } @@ -437,51 +439,6 @@ func (e *engine) ListRoles(ctx context.Context, resource types.Resource, queryTo return out, nil } -// GetResourceTypes returns the list of resource types. -func GetResourceTypes() []types.ResourceType { - return []types.ResourceType{ - { - Name: "loadbalancer", - Relationships: []types.ResourceTypeRelationship{ - { - Relation: "tenant", - Types: []string{ - "tenant", - }, - }, - }, - }, - { - Name: "role", - Relationships: []types.ResourceTypeRelationship{ - { - Relation: "tenant", - Types: []string{ - "tenant", - }, - }, - }, - }, - { - Name: "tenant", - Relationships: []types.ResourceTypeRelationship{ - { - Relation: "tenant", - Types: []string{ - "tenant", - }, - }, - }, - }, - { - Name: "user", - }, - { - Name: "client", - }, - } -} - // NewResourceFromURN returns a new resource struct from a given urn func (e *engine) NewResourceFromURN(urn *urnx.URN) (types.Resource, error) { if urn.Namespace != e.namespace { diff --git a/internal/query/tenants_test.go b/internal/query/tenants_test.go index 803d812a..4474a0c8 100644 --- a/internal/query/tenants_test.go +++ b/internal/query/tenants_test.go @@ -199,7 +199,7 @@ func TestRelationships(t *testing.T) { Input: []types.Relationship{ { Resource: childRes, - Relation: "tenant", + Relation: "parent", Subject: parentRes, }, }, @@ -207,12 +207,12 @@ func TestRelationships(t *testing.T) { expRels := []types.Relationship{ { Resource: childRes, - Relation: "tenant", + Relation: "parent", Subject: parentRes, }, } - assert.NoError(t, res.Err) + require.NoError(t, res.Err) assert.Equal(t, expRels, res.Success) }, }, diff --git a/internal/spicedbx/schema.go b/internal/spicedbx/schema.go index b331b8a0..518e6fc9 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -53,215 +53,8 @@ func GenerateSchema(namespace string, resourceTypes []types.ResourceType) (strin return out.String(), nil } -// GeneratedSchema generates the schema for a namespace func GeneratedSchema(namespace string) string { - policyDocument := iapl.PolicyDocument{ - ResourceTypes: []iapl.ResourceType{ - { - Name: "role", - IDPrefix: "idenrol", - Relationships: []iapl.Relationship{ - { - Relation: "subject", - TargetTypeNames: []string{ - "subject", - }, - }, - }, - }, - { - Name: "user", - IDPrefix: "idenusr", - }, - { - Name: "client", - IDPrefix: "idencli", - }, - { - Name: "tenant", - IDPrefix: "identen", - Relationships: []iapl.Relationship{ - { - Relation: "parent", - TargetTypeNames: []string{ - "tenant", - }, - }, - }, - }, - { - Name: "loadbalancer", - IDPrefix: "loadbal", - Relationships: []iapl.Relationship{ - { - Relation: "owner", - TargetTypeNames: []string{ - "resourceowner", - }, - }, - }, - }, - }, - Unions: []iapl.Union{ - { - Name: "subject", - ResourceTypeNames: []string{ - "user", - "client", - }, - }, - { - Name: "resourceowner", - ResourceTypeNames: []string{ - "tenant", - }, - }, - }, - Actions: []iapl.Action{ - { - Name: "loadbalancer_create", - }, - { - Name: "loadbalancer_get", - }, - { - Name: "loadbalancer_list", - }, - { - Name: "loadbalancer_update", - }, - { - Name: "loadbalancer_delete", - }, - }, - ActionBindings: []iapl.ActionBinding{ - { - ActionName: "loadbalancer_create", - TypeName: "resourceowner", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "parent", - ActionName: "loadbalancer_create", - }, - }, - }, - }, - { - ActionName: "loadbalancer_get", - TypeName: "resourceowner", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "parent", - ActionName: "loadbalancer_get", - }, - }, - }, - }, - { - ActionName: "loadbalancer_update", - TypeName: "resourceowner", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "parent", - ActionName: "loadbalancer_update", - }, - }, - }, - }, - { - ActionName: "loadbalancer_list", - TypeName: "resourceowner", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "parent", - ActionName: "loadbalancer_list", - }, - }, - }, - }, - { - ActionName: "loadbalancer_delete", - TypeName: "resourceowner", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "parent", - ActionName: "loadbalancer_delete", - }, - }, - }, - }, - { - ActionName: "loadbalancer_get", - TypeName: "loadbalancer", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "owner", - ActionName: "loadbalancer_get", - }, - }, - }, - }, - { - ActionName: "loadbalancer_update", - TypeName: "loadbalancer", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "owner", - ActionName: "loadbalancer_update", - }, - }, - }, - }, - { - ActionName: "loadbalancer_delete", - TypeName: "loadbalancer", - Conditions: []iapl.Condition{ - { - RoleBinding: &iapl.ConditionRoleBinding{}, - }, - { - RelationshipAction: &iapl.ConditionRelationshipAction{ - Relation: "owner", - ActionName: "loadbalancer_delete", - }, - }, - }, - }, - }, - } - - policy := iapl.NewPolicy(policyDocument) - if err := policy.Validate(); err != nil { - panic(err) - } + policy := iapl.DefaultPolicy() schema, err := GenerateSchema(namespace, policy.Schema()) if err != nil { From c3ff0691ebff72e10497143d5915fa8d0ea05e10 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Wed, 14 Jun 2023 22:12:55 +0000 Subject: [PATCH 05/14] Fix linting issues Signed-off-by: John Schaeffer --- internal/iapl/policy.go | 3 +++ internal/spicedbx/schema.go | 1 + internal/types/types.go | 5 +++++ main.go | 1 + 4 files changed, 10 insertions(+) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index bd69d8bb..512e481e 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -240,6 +240,7 @@ func (v *policy) expandResourceTypes() { for name, resourceType := range v.rt { for i, rel := range resourceType.Relationships { var typeNames []string + for _, typeName := range rel.TargetTypeNames { if u, ok := v.un[typeName]; ok { typeNames = append(typeNames, u.ResourceTypeNames...) @@ -298,6 +299,7 @@ func (v *policy) Schema() []types.ResourceType { action := types.Action{ Name: b.ActionName, } + for _, c := range b.Conditions { condition := types.Condition{ RoleBinding: (*types.ConditionRoleBinding)(c.RoleBinding), @@ -306,6 +308,7 @@ func (v *policy) Schema() []types.ResourceType { action.Conditions = append(action.Conditions, condition) } + typeMap[b.TypeName].Actions = append(typeMap[b.TypeName].Actions, action) } diff --git a/internal/spicedbx/schema.go b/internal/spicedbx/schema.go index 518e6fc9..3387a03b 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -53,6 +53,7 @@ func GenerateSchema(namespace string, resourceTypes []types.ResourceType) (strin return out.String(), nil } +// GeneratedSchema produces a namespaced SpiceDB schema based on the default IAPL policy. func GeneratedSchema(namespace string) string { policy := iapl.DefaultPolicy() diff --git a/internal/types/types.go b/internal/types/types.go index 2d41d993..bd1548cf 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -15,18 +15,23 @@ type ResourceTypeRelationship struct { Types []string } +// ConditionRoleBinding represents a condition where a role binding is necessary to perform an action. type ConditionRoleBinding 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 { Relation string ActionName string } +// Condition represents a required condition for performing an action. type Condition struct { RoleBinding *ConditionRoleBinding RelationshipAction *ConditionRelationshipAction } +// Action represents a named thing a subject can do. type Action struct { Name string Conditions []Condition diff --git a/main.go b/main.go index da39e61e..bb37afde 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// Package main provides the executable logic for permissions-api. package main //go:generate sqlboiler crdb --add-soft-deletes From 62cd18cc07725d19e0507b89139d8587554fd9ef Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Wed, 14 Jun 2023 18:15:13 -0400 Subject: [PATCH 06/14] Apply suggestions from code review Co-authored-by: E Camden Fisher Signed-off-by: John Schaeffer --- internal/iapl/policy.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index 512e481e..7ec8ed53 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -52,8 +52,7 @@ type Condition struct { } // ConditionRoleBinding represents a condition where a role binding is necessary to perform an action. -type ConditionRoleBinding struct { -} +type ConditionRoleBinding struct {} // ConditionRelationshipAction represents a condition where another action must be allowed on a resource // along a relation to perform an action. From dc86f5f0d87e8de24a449b6a841f0dcc47d23149 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 02:56:20 +0000 Subject: [PATCH 07/14] Add IAPL default policy definition Signed-off-by: John Schaeffer --- internal/iapl/default.go | 214 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 internal/iapl/default.go diff --git a/internal/iapl/default.go b/internal/iapl/default.go new file mode 100644 index 00000000..d825d224 --- /dev/null +++ b/internal/iapl/default.go @@ -0,0 +1,214 @@ +package iapl + +// DefaultPolicy generates the default policy for permissions-api. +func DefaultPolicy() Policy { + policyDocument := PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "role", + IDPrefix: "idenrol", + Relationships: []Relationship{ + { + Relation: "subject", + TargetTypeNames: []string{ + "subject", + }, + }, + }, + }, + { + Name: "user", + IDPrefix: "idenusr", + }, + { + Name: "client", + IDPrefix: "idencli", + }, + { + Name: "tenant", + IDPrefix: "identen", + Relationships: []Relationship{ + { + Relation: "parent", + TargetTypeNames: []string{ + "tenant", + }, + }, + }, + }, + { + Name: "loadbalancer", + IDPrefix: "loadbal", + Relationships: []Relationship{ + { + Relation: "owner", + TargetTypeNames: []string{ + "resourceowner", + }, + }, + }, + }, + }, + Unions: []Union{ + { + Name: "subject", + ResourceTypeNames: []string{ + "user", + "client", + }, + }, + { + Name: "resourceowner", + ResourceTypeNames: []string{ + "tenant", + }, + }, + }, + Actions: []Action{ + { + Name: "loadbalancer_create", + }, + { + Name: "loadbalancer_get", + }, + { + Name: "loadbalancer_list", + }, + { + Name: "loadbalancer_update", + }, + { + Name: "loadbalancer_delete", + }, + }, + ActionBindings: []ActionBinding{ + { + ActionName: "loadbalancer_create", + TypeName: "resourceowner", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_create", + }, + }, + }, + }, + { + ActionName: "loadbalancer_get", + TypeName: "resourceowner", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_get", + }, + }, + }, + }, + { + ActionName: "loadbalancer_update", + TypeName: "resourceowner", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_update", + }, + }, + }, + }, + { + ActionName: "loadbalancer_list", + TypeName: "resourceowner", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_list", + }, + }, + }, + }, + { + ActionName: "loadbalancer_delete", + TypeName: "resourceowner", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "parent", + ActionName: "loadbalancer_delete", + }, + }, + }, + }, + { + ActionName: "loadbalancer_get", + TypeName: "loadbalancer", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "owner", + ActionName: "loadbalancer_get", + }, + }, + }, + }, + { + ActionName: "loadbalancer_update", + TypeName: "loadbalancer", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "owner", + ActionName: "loadbalancer_update", + }, + }, + }, + }, + { + ActionName: "loadbalancer_delete", + TypeName: "loadbalancer", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "owner", + ActionName: "loadbalancer_delete", + }, + }, + }, + }, + }, + } + + policy := NewPolicy(policyDocument) + if err := policy.Validate(); err != nil { + panic(err) + } + + return policy +} From 96df0760d4db7067d70e34e7fa3c4a88001dcbc3 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 14:07:09 +0000 Subject: [PATCH 08/14] Fix more linting Signed-off-by: John Schaeffer --- internal/iapl/policy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index 7ec8ed53..e841682c 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -52,7 +52,7 @@ type Condition struct { } // ConditionRoleBinding represents a condition where a role binding is necessary to perform an action. -type ConditionRoleBinding struct {} +type ConditionRoleBinding struct{} // ConditionRelationshipAction represents a condition where another action must be allowed on a resource // along a relation to perform an action. From 6653a037dd2ecb6e461fc1ddd13db5b4bc3599d1 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 14:26:54 +0000 Subject: [PATCH 09/14] Expand action bindings and resource types during NewPolicy Signed-off-by: John Schaeffer --- internal/iapl/policy.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index e841682c..6eff4664 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -102,6 +102,9 @@ func NewPolicy(p PolicyDocument) Policy { p: p, } + out.expandActionBindings() + out.expandResourceTypes() + return &out } @@ -256,9 +259,6 @@ func (v *policy) expandResourceTypes() { } func (v *policy) Validate() error { - v.expandActionBindings() - v.expandResourceTypes() - if err := v.validateUnions(); err != nil { return fmt.Errorf("typeAliases: %w", err) } From 0a3841ca47e8d36b98550f9f6a0549faf6d68ec3 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 14:29:37 +0000 Subject: [PATCH 10/14] Use correct ID prefixes in default policy Signed-off-by: John Schaeffer --- internal/iapl/default.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/iapl/default.go b/internal/iapl/default.go index d825d224..8daacd96 100644 --- a/internal/iapl/default.go +++ b/internal/iapl/default.go @@ -6,7 +6,7 @@ func DefaultPolicy() Policy { ResourceTypes: []ResourceType{ { Name: "role", - IDPrefix: "idenrol", + IDPrefix: "permrol", Relationships: []Relationship{ { Relation: "subject", @@ -18,15 +18,15 @@ func DefaultPolicy() Policy { }, { Name: "user", - IDPrefix: "idenusr", + IDPrefix: "idntusr", }, { Name: "client", - IDPrefix: "idencli", + IDPrefix: "idntcli", }, { Name: "tenant", - IDPrefix: "identen", + IDPrefix: "tnntten", Relationships: []Relationship{ { Relation: "parent", From 8cfe77beec3ebf7eac13dcf3779fc99e4652007a Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 14:32:40 +0000 Subject: [PATCH 11/14] Use more informative error when defining duplicate types Signed-off-by: John Schaeffer --- internal/iapl/errors.go | 2 ++ internal/iapl/policy.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/iapl/errors.go b/internal/iapl/errors.go index 17f347c7..cf330b60 100644 --- a/internal/iapl/errors.go +++ b/internal/iapl/errors.go @@ -3,6 +3,8 @@ package iapl import "errors" var ( + // ErrorTypeExists represents an error where a duplicate type or union was declared. + ErrorTypeExists = errors.New("type already exists") // ErrorUnknownType represents an error where a resource type is unknown in the authorization policy. ErrorUnknownType = errors.New("unknown resource type") // ErrorInvalidAlias represents an error where a type alias is invalid. diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index 6eff4664..96148f83 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -111,7 +111,7 @@ func NewPolicy(p PolicyDocument) Policy { func (v *policy) validateUnions() error { for _, typeAlias := range v.p.Unions { if _, ok := v.rt[typeAlias.Name]; ok { - return fmt.Errorf("%s: %w", typeAlias.Name, ErrorInvalidAlias) + return fmt.Errorf("%s: %w", typeAlias.Name, ErrorTypeExists) } for _, rtName := range typeAlias.ResourceTypeNames { From 5d3fdb3ea1c9139dc48a635ade127cdc6306f235 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 14:36:25 +0000 Subject: [PATCH 12/14] Clean up old references to type alias, rename to union Signed-off-by: John Schaeffer --- internal/iapl/errors.go | 4 ++-- internal/iapl/policy.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/iapl/errors.go b/internal/iapl/errors.go index cf330b60..c2c9ca40 100644 --- a/internal/iapl/errors.go +++ b/internal/iapl/errors.go @@ -7,8 +7,8 @@ var ( ErrorTypeExists = errors.New("type already exists") // ErrorUnknownType represents an error where a resource type is unknown in the authorization policy. ErrorUnknownType = errors.New("unknown resource type") - // ErrorInvalidAlias represents an error where a type alias is invalid. - ErrorInvalidAlias = errors.New("invalid type alias") + // ErrorInvalidUnion represents an error where a union type is invalid. + ErrorInvalidUnion = errors.New("invalid union") // ErrorInvalidCondition represents an error where an action binding condition is invalid. ErrorInvalidCondition = errors.New("invalid condition") // ErrorUnknownRelation represents an error where a relation is not defined for a resource type. diff --git a/internal/iapl/policy.go b/internal/iapl/policy.go index 96148f83..7e895e14 100644 --- a/internal/iapl/policy.go +++ b/internal/iapl/policy.go @@ -109,14 +109,14 @@ func NewPolicy(p PolicyDocument) Policy { } func (v *policy) validateUnions() error { - for _, typeAlias := range v.p.Unions { - if _, ok := v.rt[typeAlias.Name]; ok { - return fmt.Errorf("%s: %w", typeAlias.Name, ErrorTypeExists) + for _, union := range v.p.Unions { + if _, ok := v.rt[union.Name]; ok { + return fmt.Errorf("%s: %w", union.Name, ErrorTypeExists) } - for _, rtName := range typeAlias.ResourceTypeNames { + for _, rtName := range union.ResourceTypeNames { if _, ok := v.rt[rtName]; !ok { - return fmt.Errorf("%s: resourceTypeNames: %s: %w", typeAlias.Name, rtName, ErrorUnknownType) + return fmt.Errorf("%s: resourceTypeNames: %s: %w", union.Name, rtName, ErrorUnknownType) } } } @@ -260,7 +260,7 @@ func (v *policy) expandResourceTypes() { func (v *policy) Validate() error { if err := v.validateUnions(); err != nil { - return fmt.Errorf("typeAliases: %w", err) + return fmt.Errorf("unions: %w", err) } if err := v.validateResourceTypes(); err != nil { From 9c85b4bd7a5e12d15d526521425e255f3b90706c Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 18:23:28 +0000 Subject: [PATCH 13/14] Add IAPL tests Signed-off-by: John Schaeffer --- internal/iapl/policy_test.go | 386 +++++++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 internal/iapl/policy_test.go diff --git a/internal/iapl/policy_test.go b/internal/iapl/policy_test.go new file mode 100644 index 00000000..4dfd21fb --- /dev/null +++ b/internal/iapl/policy_test.go @@ -0,0 +1,386 @@ +package iapl + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.infratographer.com/permissions-api/internal/testingx" +) + +func TestPolicy(t *testing.T) { + cases := []testingx.TestCase[PolicyDocument, struct{}]{ + { + Name: "TypeExists", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + }, + }, + Unions: []Union{ + { + Name: "foo", + ResourceTypeNames: []string{ + "foo", + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorTypeExists) + }, + }, + { + Name: "UnknownTypeInUnion", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + }, + }, + Unions: []Union{ + { + Name: "bar", + ResourceTypeNames: []string{ + "baz", + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownType) + }, + }, + { + Name: "UnknownTypeInUnion", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + }, + }, + Unions: []Union{ + { + Name: "bar", + ResourceTypeNames: []string{ + "baz", + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownType) + }, + }, + { + Name: "UnknownTypeInRelationship", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "baz", + }, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownType) + }, + }, + { + Name: "UnknownActionInCondition", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + }, + ActionBindings: []ActionBinding{ + { + TypeName: "foo", + ActionName: "qux", + Conditions: []Condition{ + { + RoleBinding: &ConditionRoleBinding{}, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownAction) + }, + }, + { + Name: "UnknownActionInCondition", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + }, + Actions: []Action{ + { + Name: "qux", + }, + }, + ActionBindings: []ActionBinding{ + { + TypeName: "foo", + ActionName: "qux", + Conditions: []Condition{ + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "bar", + ActionName: "baz", + }, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownAction) + }, + }, + { + Name: "UnknownRelationInCondition", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + }, + }, + Actions: []Action{ + { + Name: "qux", + }, + }, + ActionBindings: []ActionBinding{ + { + TypeName: "foo", + ActionName: "qux", + Conditions: []Condition{ + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "bar", + ActionName: "qux", + }, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownRelation) + }, + }, + { + Name: "UnknownRelationInUnion", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + { + Name: "baz", + }, + }, + Unions: []Union{ + { + Name: "buzz", + ResourceTypeNames: []string{ + "foo", + "baz", + }, + }, + }, + Actions: []Action{ + { + Name: "qux", + }, + }, + ActionBindings: []ActionBinding{ + { + TypeName: "buzz", + ActionName: "qux", + Conditions: []Condition{ + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "bar", + ActionName: "qux", + }, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownRelation) + }, + }, + { + Name: "UnknownActionInUnion", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + { + Name: "baz", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + }, + Unions: []Union{ + { + Name: "buzz", + ResourceTypeNames: []string{ + "foo", + "baz", + }, + }, + }, + Actions: []Action{ + { + Name: "qux", + }, + }, + ActionBindings: []ActionBinding{ + { + TypeName: "buzz", + ActionName: "qux", + Conditions: []Condition{ + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "bar", + ActionName: "fizz", + }, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.ErrorIs(t, res.Err, ErrorUnknownAction) + }, + }, + { + Name: "Success", + Input: PolicyDocument{ + ResourceTypes: []ResourceType{ + { + Name: "foo", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + { + Name: "baz", + Relationships: []Relationship{ + { + Relation: "bar", + TargetTypeNames: []string{ + "foo", + }, + }, + }, + }, + }, + Unions: []Union{ + { + Name: "buzz", + ResourceTypeNames: []string{ + "foo", + "baz", + }, + }, + }, + Actions: []Action{ + { + Name: "qux", + }, + }, + ActionBindings: []ActionBinding{ + { + TypeName: "buzz", + ActionName: "qux", + Conditions: []Condition{ + { + RelationshipAction: &ConditionRelationshipAction{ + Relation: "bar", + ActionName: "qux", + }, + }, + }, + }, + }, + }, + CheckFn: func(_ context.Context, t *testing.T, res testingx.TestResult[struct{}]) { + require.NoError(t, res.Err) + }, + }, + } + + testFn := func(_ context.Context, p PolicyDocument) testingx.TestResult[struct{}] { + policy := NewPolicy(p) + err := policy.Validate() + + return testingx.TestResult[struct{}]{ + Success: struct{}{}, + Err: err, + } + } + + testingx.RunTests(context.Background(), t, cases, testFn) +} From 62415595d28f064f49323263418fb668d21636fc Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 18:23:41 +0000 Subject: [PATCH 14/14] Remove unused error from IAPL package Signed-off-by: John Schaeffer --- internal/iapl/errors.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/iapl/errors.go b/internal/iapl/errors.go index c2c9ca40..c88c51f8 100644 --- a/internal/iapl/errors.go +++ b/internal/iapl/errors.go @@ -7,8 +7,6 @@ var ( ErrorTypeExists = errors.New("type already exists") // ErrorUnknownType represents an error where a resource type is unknown in the authorization policy. ErrorUnknownType = errors.New("unknown resource type") - // ErrorInvalidUnion represents an error where a union type is invalid. - ErrorInvalidUnion = errors.New("invalid union") // ErrorInvalidCondition represents an error where an action binding condition is invalid. ErrorInvalidCondition = errors.New("invalid condition") // ErrorUnknownRelation represents an error where a relation is not defined for a resource type.