Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IAPL schema generation #106

Merged
merged 14 commits into from
Jun 15, 2023
Merged
3 changes: 3 additions & 0 deletions internal/iapl/doc.go
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions internal/iapl/errors.go
Original file line number Diff line number Diff line change
@@ -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")
mikemrm marked this conversation as resolved.
Show resolved Hide resolved
// 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")
)
300 changes: 300 additions & 0 deletions internal/iapl/policy.go
Original file line number Diff line number Diff line change
@@ -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 {
fishnix marked this conversation as resolved.
Show resolved Hide resolved
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 {
}
jnschaeffer marked this conversation as resolved.
Show resolved Hide resolved

// 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{}
fishnix marked this conversation as resolved.
Show resolved Hide resolved

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
fishnix marked this conversation as resolved.
Show resolved Hide resolved
}

func (v *policy) validateTypeAliases() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for a nil policy? here and below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I don't like to do nil checks for pointer receivers unless there's a legitimate reason they could be nil. In this case there isn't, so a panic would be the expected result for me if someone tried to call validateTypeAliases() on a nil policy.

for _, typeAlias := range v.p.TypeAliases {
if _, ok := v.rt[typeAlias.Name]; ok {
return fmt.Errorf("%s: %w", typeAlias.Name, ErrorInvalidAlias)
mikemrm marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
20 changes: 13 additions & 7 deletions internal/query/tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -444,26 +444,32 @@ func GetResourceTypes() []types.ResourceType {
Name: "loadbalancer",
Relationships: []types.ResourceTypeRelationship{
{
Name: "tenant",
Type: "tenant",
Relation: "tenant",
Types: []string{
"tenant",
},
},
},
},
{
Name: "role",
Relationships: []types.ResourceTypeRelationship{
{
Name: "tenant",
Type: "tenant",
Relation: "tenant",
Types: []string{
"tenant",
},
},
},
},
{
Name: "tenant",
Relationships: []types.ResourceTypeRelationship{
{
Name: "tenant",
Type: "tenant",
Relation: "tenant",
Types: []string{
"tenant",
},
},
},
},
Expand Down
Loading