From d80de5a8c2fb6ec9c96fa97be38059e172edb428 Mon Sep 17 00:00:00 2001 From: John Schaeffer Date: Thu, 15 Jun 2023 14:58:33 -0400 Subject: [PATCH] IAPL schema generation (#106) * [hax] First pass at IAPL schema generation Signed-off-by: John Schaeffer * [hax] Use better DSL Signed-off-by: John Schaeffer * Fix SpiceDB schema generation tests Signed-off-by: John Schaeffer * Use iapl package as source of default policy, fix tests Signed-off-by: John Schaeffer * Fix linting issues Signed-off-by: John Schaeffer * Apply suggestions from code review Co-authored-by: E Camden Fisher Signed-off-by: John Schaeffer * Add IAPL default policy definition Signed-off-by: John Schaeffer * Fix more linting Signed-off-by: John Schaeffer * Expand action bindings and resource types during NewPolicy Signed-off-by: John Schaeffer * Use correct ID prefixes in default policy Signed-off-by: John Schaeffer * Use more informative error when defining duplicate types Signed-off-by: John Schaeffer * Clean up old references to type alias, rename to union Signed-off-by: John Schaeffer * Add IAPL tests Signed-off-by: John Schaeffer * Remove unused error from IAPL package Signed-off-by: John Schaeffer --------- Signed-off-by: John Schaeffer Signed-off-by: John Schaeffer Co-authored-by: E Camden Fisher --- internal/iapl/default.go | 214 +++++++++++++++++ internal/iapl/doc.go | 3 + internal/iapl/errors.go | 16 ++ internal/iapl/policy.go | 320 +++++++++++++++++++++++++ internal/iapl/policy_test.go | 386 +++++++++++++++++++++++++++++++ internal/query/service.go | 5 + internal/query/tenants.go | 61 +---- internal/query/tenants_test.go | 6 +- internal/spicedbx/schema.go | 102 ++------ internal/spicedbx/schema_test.go | 217 +++++++++++------ internal/types/types.go | 31 ++- main.go | 1 + 12 files changed, 1148 insertions(+), 214 deletions(-) create mode 100644 internal/iapl/default.go create mode 100644 internal/iapl/doc.go create mode 100644 internal/iapl/errors.go create mode 100644 internal/iapl/policy.go create mode 100644 internal/iapl/policy_test.go diff --git a/internal/iapl/default.go b/internal/iapl/default.go new file mode 100644 index 00000000..8daacd96 --- /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: "permrol", + Relationships: []Relationship{ + { + Relation: "subject", + TargetTypeNames: []string{ + "subject", + }, + }, + }, + }, + { + Name: "user", + IDPrefix: "idntusr", + }, + { + Name: "client", + IDPrefix: "idntcli", + }, + { + Name: "tenant", + IDPrefix: "tnntten", + 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 +} 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..c88c51f8 --- /dev/null +++ b/internal/iapl/errors.go @@ -0,0 +1,16 @@ +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") + // 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..7e895e14 --- /dev/null +++ b/internal/iapl/policy.go @@ -0,0 +1,320 @@ +package iapl + +import ( + "fmt" + + "go.infratographer.com/permissions-api/internal/types" +) + +// PolicyDocument represents a partial authorization policy. +type PolicyDocument struct { + ResourceTypes []ResourceType + Unions []Union + 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 + TargetTypeNames []string +} + +// Union represents a named union of multiple concrete resource types. +type Union 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 or union. +type ActionBinding struct { + ActionName string + TypeName 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 + un map[string]Union + ac map[string]Action + rb map[string]map[string]struct{} + bn []ActionBinding + 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 + } + + 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)) + for _, a := range p.Actions { + ac[a.Name] = a + } + + out := policy{ + rt: rt, + un: un, + ac: ac, + p: p, + } + + out.expandActionBindings() + out.expandResourceTypes() + + return &out +} + +func (v *policy) validateUnions() error { + for _, union := range v.p.Unions { + if _, ok := v.rt[union.Name]; ok { + 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) + } + } + } + + return nil +} + +func (v *policy) validateResourceTypes() error { + for _, resourceType := range v.p.ResourceTypes { + 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) + } + } + } + } + + 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) + } + + 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) + } + } + + 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.bn { + if _, ok := v.ac[binding.ActionName]; !ok { + return fmt.Errorf("%d: %s: %w", i, binding.ActionName, ErrorUnknownAction) + } + + rt, ok := v.rt[binding.TypeName] + if !ok { + return fmt.Errorf("%d: %s: %w", i, binding.TypeName, ErrorUnknownType) + } + + if err := v.validateConditions(rt, binding.Conditions); err != nil { + return fmt.Errorf("%d: conditions: %w", i, err) + } + } + + 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.validateUnions(); err != nil { + return fmt.Errorf("unions: %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, + Types: rel.TargetTypeNames, + } + + out.Relationships = append(out.Relationships, outRel) + } + + typeMap[n] = &out + } + + for _, b := range v.bn { + 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.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] + } + + return out +} 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) +} 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 4550a2de..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.Name && resType.Name == typeRel.Type { - 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,45 +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{ - { - Name: "tenant", - Type: "tenant", - }, - }, - }, - { - Name: "role", - Relationships: []types.ResourceTypeRelationship{ - { - Name: "tenant", - Type: "tenant", - }, - }, - }, - { - Name: "tenant", - Relationships: []types.ResourceTypeRelationship{ - { - Name: "tenant", - Type: "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 97b98640..3387a03b 100644 --- a/internal/spicedbx/schema.go +++ b/internal/spicedbx/schema.go @@ -4,82 +4,27 @@ 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 - - relation role_get_rel: {{$namespace}}/role#subject - relation role_update_rel: {{$namespace}}/role#subject - relation role_delete_rel: {{$namespace}}/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 {{$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}} +definition {{$namespace}}/{{.Name}} { +{{- range .Relationships }} + relation {{.Relation}}: {{ range $index, $typeName := .Types -}}{{ if $index }} | {{end}}{{$namespace}}/{{$typeName}}{{- 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 }} +{{- end }} } {{end}}`)) ) @@ -108,24 +53,11 @@ func GenerateSchema(namespace string, resourceTypes []types.ResourceType) (strin return out.String(), nil } -// GeneratedSchema generated the schema for a namespace +// GeneratedSchema produces a namespaced SpiceDB schema based on the default IAPL policy. func GeneratedSchema(namespace string) string { - resourceTypes := []types.ResourceType{ - { - Name: "loadbalancer", - Actions: []string{ - "get", - "update", - "delete", - }, - TenantActions: []string{ - "create", - "list", - }, - }, - } + policy := iapl.DefaultPolicy() - schema, err := GenerateSchema(namespace, resourceTypes) + schema, err := GenerateSchema(namespace, policy.Schema()) if err != nil { panic(err) } 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 } ` diff --git a/internal/types/types.go b/internal/types/types.go index c434d206..bd1548cf 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -11,18 +11,37 @@ type Role struct { // ResourceTypeRelationship is a relationship for a resource type. type ResourceTypeRelationship struct { - Name string - Type string + Relation string + 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 } // 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 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