Skip to content

Commit

Permalink
lots of docs
Browse files Browse the repository at this point in the history
  • Loading branch information
FoseFx committed Aug 19, 2024
1 parent 7350b93 commit eec2913
Show file tree
Hide file tree
Showing 16 changed files with 228 additions and 215 deletions.
175 changes: 45 additions & 130 deletions libs/hwauthz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,39 @@ import (
"common"
"common/locale"
"context"
"errors"
"fmt"
"go.opentelemetry.io/otel/attribute"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
"telemetry"
)

// StatusErrorPermissionDenied should be returned when a necessary permission check has failed
func StatusErrorPermissionDenied(ctx context.Context, check Relationship) error {
msg := fmt.Sprintf(
"subject '%s:%s' does not have permission '%s' on resource '%s:%s'",
check.Subject.Type(),
check.Subject.ID(),
check.Relation,
check.Resource.Type(),
check.Resource.ID(),
)
return common.NewStatusError(ctx, codes.PermissionDenied, msg, locale.PermissionDeniedError(ctx))
}

// TODO: explain ct
type ConsistencyToken = string

// TODO: explain Relation
// A Relation defines how two objects (or an object and subject) can relate to one another.
// For example, a reader on a document, or a member of a group.
// See https://authzed.com/docs/spicedb/concepts/schema#relations
type Relation = string

// A Permission defines a computed set of subjects that have a permission of some kind on its object.
// For example, is a user within the set of users that can edit a document.
// For the most part Relation and Permission can be used interchangeably, however, you can not write to permissions!
// See https://authzed.com/docs/spicedb/concepts/schema#permissions
type Permission = Relation

// An Object must relate to an [Object Definition](https://authzed.com/docs/spicedb/concepts/schema#object-type-definitions) in the spicedb schema.
//
// TODO: see spicedb.yaml
//
// We use this to build a v1.ObjectReference used for a Relationship
type Object interface {
// Type of the Object definition, (e.g., "user")
Expand All @@ -33,18 +48,20 @@ type Object interface {
// A Relationship is a tuple relating a Subject with a Resource
//
// Is <this subject> allowed to perform <this action> on <this resource>?
// The action is a relation or permission defined on the Resource's type
// The action is a relation or permission defined on the Resource's type.
//
// Read: https://authzed.com/docs/spicedb/concepts/relationships#relationships
type Relationship struct {
// TODO: comment fields
Subject Object
// Subject that should be added to a relation on the Resource, for example a user
Subject Object
// Relation of the Resource
Relation Relation
// Resource, for example a task
Resource Object

SubjectRelation string // TODO??
}

// NewRelationship constructs a new Relationship object
// To store a new Relationship Tuple in SpiceDB, see AuthZ.Write
func NewRelationship(subject Object, relation Relation, resource Object) Relationship {
return Relationship{
Subject: subject,
Expand All @@ -53,135 +70,33 @@ func NewRelationship(subject Object, relation Relation, resource Object) Relatio
}
}

// A PermissionCheck is used to check if a Relationship exists
// See AuthZ.Check for usage
type PermissionCheck = Relationship

// NewPermissionCheck constructs a new PermissionCheck
// Used to check if a Relationship exists, see AuthZ.Check
func NewPermissionCheck(subject Object, relation Relation, resource Object) PermissionCheck {
return NewRelationship(subject, relation, resource)
}

func (r *Relationship) SpanAttributeKeyValue() []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String("spice.resource.type", r.Resource.Type()),
attribute.String("spice.resource.id", r.Resource.ID()),
attribute.String("spice.relation", r.Relation),
attribute.String("spice.subject.type", r.Subject.Type()),
attribute.String("spice.subject.id", r.Subject.ID()),
attribute.String("spice.subjectRelation", r.SubjectRelation),
}
}

// A Permission defines a computed set of subjects that have a permission of some kind on the parent object.
// For us, they are a special kind of Relationship. TODO: violation of ubiqu. language! A Permission is like a Relation. not like a Relationship, rename this
// TODO: mention authz.Check
// See: https://authzed.com/docs/spicedb/concepts/schema#permissions
type Permission struct {
Relationship
}

func NewPermission(subject Object, relation Relation, resource Object) Permission {
return Permission{
NewRelationship(subject, relation, resource),
}
}

func (p *Permission) SpanAttributeKeyValue() []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String("spice.resource.type", p.Resource.Type()),
attribute.String("spice.resource.id", p.Resource.ID()),
attribute.String("spice.permission", p.Relation),
attribute.String("spice.subject.type", p.Subject.Type()),
attribute.String("spice.subject.id", p.Subject.ID()),
attribute.String("spice.subjectRelation", p.SubjectRelation),
}
}

// todo: new comment, that explains What it is, How to use it, who implements is
// AuthZ interfaces our provider Google Zanzibar like provider for authorization
// AuthZ is a Zanzibar-like Fine-Grained Authorization Provider
// Implemented by most notably spicedb.SpiceDBAuthZ, for testing use test.TrueAuthZ
type AuthZ interface {
// Write writes one or many Relationship Tuples to the Permissions Graph
Write(ctx context.Context, relationships ...Relationship) (ConsistencyToken, error)
// Delete removes one or many Relationship Tuples to the Permissions Graph
Delete(ctx context.Context, relationships ...Relationship) (ConsistencyToken, error)
Check(ctx context.Context, permission Permission) (bool, error)
}

// CheckMany calls authz.Check in parallel for every permission
// If the first bool param is false and the third error param is not nil
// The second *Permission param contains the falsy/invalid permission
// TODO: there is a bulk check endpoint, refactor this
// TODO: also discourage multiple lookups, a single action should be expressed by a single permission (which itself might be a composite of other permissions)
func CheckMany(ctx context.Context, authz AuthZ, permissions ...Permission) (bool, *Permission, error) {
ctx, span, _ := telemetry.StartSpan(ctx, "hwauthz.CheckMany")
defer span.End()

type res struct {
Permission Permission
HasPermission bool
}

resChan := make(chan res, len(permissions))

var eg errgroup.Group

for _, permission := range permissions {
eg.Go(func() error {
ctx, span, _ := telemetry.StartSpan(ctx, "hwauthz.CheckMany.Check")
defer span.End()

hasPermission, err := authz.Check(ctx, permission)
if err != nil {
return err
}

resChan <- res{
Permission: permission,
HasPermission: hasPermission,
}

return nil
})
}

err := eg.Wait()
close(resChan)
if err != nil {
return false, nil, err
}

for res := range resChan {
if !res.HasPermission {
return false, &res.Permission, nil
}
}

return true, nil, nil
}

// TODO: the only thing that makes this grpc specific is the error message, the name is shit and we should just provide a default error
func CheckGrpcWrapper(ctx context.Context, authz AuthZ, permissions ...Permission) error {
ctx, span, _ := telemetry.StartSpan(ctx, "hwauthz.CheckGrpcWrapper")
defer span.End()

var hasPermission bool
var permission *Permission
var err error

if len(permissions) > 1 {
// Distribute many auth.Check() via CheckMany()
hasPermission, permission, err = CheckMany(ctx, authz, permissions...)
} else if len(permissions) == 1 {
// Fast path. Directly call auth.Check() for one permission
permission = &permissions[0]
hasPermission, err = authz.Check(ctx, *permission)
} else {
return errors.New("you need to pass one or many permissions")
}

if err != nil {
return err
} else if !hasPermission {
msg := fmt.Sprintf(
"subject '%s:%s' does not have permission '%s' on resource '%s:%s'",
permission.Subject.Type(),
permission.Subject.ID(),
permission.Relation,
permission.Resource.Type(),
permission.Resource.ID(),
)
return common.NewStatusError(ctx, codes.PermissionDenied, msg, locale.PermissionDeniedError(ctx))
}

return nil
// Check queries the Permission Graph for the existence of a PermissionCheck (i.e., a Relationship)
Check(ctx context.Context, check PermissionCheck) (permissionGranted bool, err error)
}
51 changes: 0 additions & 51 deletions libs/hwauthz/perm/permission.go

This file was deleted.

12 changes: 6 additions & 6 deletions libs/hwauthz/spicedb/spicedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,26 +148,26 @@ func (s *SpiceDBAuthZ) Delete(ctx context.Context, relations ...hwauthz.Relation
}

// TODO: consistency token?
func (s *SpiceDBAuthZ) Check(ctx context.Context, permission hwauthz.Permission) (bool, error) {
func (s *SpiceDBAuthZ) Check(ctx context.Context, permissionCheck hwauthz.PermissionCheck) (bool, error) {
ctx, span, log := telemetry.StartSpan(ctx, "hwauthz.SpiceDB.Check")
defer span.End()

telemetry.SetSpanAttributes(ctx, permission.SpanAttributeKeyValue()...)
telemetry.SetSpanAttributes(ctx, permissionCheck.SpanAttributeKeyValue()...)

// convert internal Representation to gRPC body
req := &v1.CheckPermissionRequest{
Subject: &v1.SubjectReference{
Object: fromObject(permission.Subject),
Object: fromObject(permissionCheck.Subject),
},
Permission: permission.Relation,
Resource: fromObject(permission.Resource),
Permission: permissionCheck.Relation,
Resource: fromObject(permissionCheck.Resource),
}

// make request
res, err := s.client.CheckPermission(ctx, req)
if err != nil {
log.Error().Err(err).Msg("spicedb: error while checking permissions")
return false, fmt.Errorf("spicedb: could not check permission: %w", err)
return false, fmt.Errorf("spicedb: could not check permissionCheck: %w", err)
}

hasPermission := res.Permissionship == v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION
Expand Down
2 changes: 1 addition & 1 deletion libs/hwauthz/test/true_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func (*TrueAuthZ) Delete(ctx context.Context, relations ...hwauthz.Relationship)
return "", nil
}

func (*TrueAuthZ) Check(ctx context.Context, permissions hwauthz.Permission) (bool, error) {
func (*TrueAuthZ) Check(ctx context.Context, check hwauthz.PermissionCheck) (bool, error) {
// Always returns true
return true, nil
}
38 changes: 38 additions & 0 deletions services/tasks-svc/internal/perm/permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package perm

import (
"github.com/google/uuid"
"hwauthz"
)

// Types

type Task uuid.UUID

func (t Task) Type() string { return "task" }
func (t Task) ID() string { return uuid.UUID(t).String() }

type User uuid.UUID

func (t User) Type() string { return "user" }
func (t User) ID() string { return uuid.UUID(t).String() }

type Patient uuid.UUID

func (p Patient) Type() string { return "patient" }
func (p Patient) ID() string { return uuid.UUID(p).String() }

// Direct Relations

const TaskAssignee hwauthz.Relation = "assignee"
const TaskPatient hwauthz.Relation = "patient"

// Permission

const CanUserViewTask hwauthz.Permission = "view"
const CanUserUpdateTask hwauthz.Permission = "update_task"
const CanUserCreateSubtaskOnTask hwauthz.Permission = "create_subtask"
const CanUserDeleteSubtaskOnTask hwauthz.Permission = "delete_subtask"
const CanUserUpdateSubtaskOnTask hwauthz.Permission = "update_subtask"
const CanUserAssignTask hwauthz.Permission = "assign"
const CanUserCompleteSubtask hwauthz.Permission = "assign"
14 changes: 10 additions & 4 deletions services/tasks-svc/internal/task/commands/v1/assign_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package v1
import (
"common"
"context"
"fmt"
"github.com/google/uuid"
"hwauthz"
"hwauthz/perm"
"hwes"
"tasks-svc/internal/perm"
"tasks-svc/internal/task/aggregate"
)

Expand All @@ -18,12 +19,17 @@ func NewAssignTaskCommandHandler(as hwes.AggregateStore, authz hwauthz.AuthZ) As
if err != nil {
return err
}
user := perm.User(requestUserID)
authzUser := perm.User(requestUserID)
authzTask := perm.Task(taskID)

// TODO: We need to check both, target user and request user!. Right now, we are just checking the request user.
if err := hwauthz.CheckGrpcWrapper(ctx, authz, perm.NewCanUserAssignTaskPermission(user, authzTask)); err != nil {
return err
check := hwauthz.NewPermissionCheck(authzUser, perm.CanUserAssignTask, authzTask)
allowed, err := authz.Check(ctx, check)
if err != nil {
return fmt.Errorf("could not check permissions: %w", err)
}
if !allowed {
return hwauthz.StatusErrorPermissionDenied(ctx, check)
}

task, err := aggregate.LoadTaskAggregate(ctx, as, taskID)
Expand Down
Loading

0 comments on commit eec2913

Please sign in to comment.