From 94ca36297b6afada9aa3cfe5e5ca957fdf92ec61 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Tue, 28 Mar 2023 11:32:29 +0300 Subject: [PATCH] Add README with initial concepts and persist them throughout the repo This adds an initial README that describes the concepts that Permissions API will cover. This allows us to have more consistent conversations in the future about functionality and logic within the project. This also persists the concepts and replaces the relevant wording in functions and variables throughout the repository. Note that the worker has not yet been touched, as we need to revisit how that piece will work. Signed-off-by: Juan Antonio Osorio --- README.md | 49 +++++++++++++++++++++++++++++++++- internal/api/permissions.go | 20 +++++++------- internal/api/resources.go | 10 +++---- internal/api/router.go | 4 +-- internal/query/errors.go | 4 +-- internal/query/tenants.go | 42 ++++++++++++++--------------- internal/query/tenants_test.go | 12 ++++----- pkg/client/v1/auth.go | 8 +++--- pkg/client/v1/errors.go | 2 +- 9 files changed, 99 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0d45a132..a7108064 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# permissionapi \ No newline at end of file +# Permissions API + +Hello, and welcome to the Permissions API! This API is designed to allow you to +manage permissions for services in the Infratographer ecosystem. The intent is for +this project to contain both the API to manage permissions, as well as the +policy engine that will be used to enforce permissions. + +## Concepts + +Before we get into the details of the API, let's talk about some of the concepts +that are important to understanding how the API works. + +### Subject + +A subject is a user, group, or service that is requesting access to a resource. + +### Resource + +A resource is a tenant or object that is being accessed. + +In an initial implementation, the resource will be a tenant. In the future, we +will add support for objects. + +### Action + +An action is a verb that describes what the subject is trying to do to the +resource. For example, "read", "write", "delete", "create", etc. + +Given that Permissions API is designed to be used by multiple services, the +actions are currently defined by the service that is using the API. e.g. a +Load Balancer service may define the actions "loadbalancers_get", "loadbalancers_create", +"loadbalancers_delete", etc. + +### Role + +A role is a collection of actions that are allowed to be performed on a resource. + +### Role Assignment + +A role assignment is a mapping of a subject to a role. This is how a subject is +granted access to a resource. + +# Components + +The Permissions API is made up of two components: + +* Management API +* Policy Engine diff --git a/internal/api/permissions.go b/internal/api/permissions.go index 829dafc9..78288633 100644 --- a/internal/api/permissions.go +++ b/internal/api/permissions.go @@ -9,11 +9,11 @@ import ( "go.infratographer.com/x/urnx" ) -func (r *Router) checkScope(c *gin.Context) { +func (r *Router) checkAction(c *gin.Context) { resourceURNStr := c.Param("urn") - scope := c.Param("scope") + action := c.Param("action") - ctx, span := tracer.Start(c.Request.Context(), "api.checkScope") + ctx, span := tracer.Start(c.Request.Context(), "api.checkAction") defer span.End() resourceURN, err := urnx.Parse(resourceURNStr) @@ -28,22 +28,22 @@ func (r *Router) checkScope(c *gin.Context) { return } - actor, err := currentActor(c) + subject, err := currentSubject(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": "failed to get the actor"}) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": "failed to get the subject"}) return } - actorResource, err := query.NewResourceFromURN(actor) + subjectResource, err := query.NewResourceFromURN(subject) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "error processing actor URN", "error": err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"message": "error processing subject URN", "error": err.Error()}) return } - err = query.ActorHasPermission(ctx, r.authzedClient, actorResource, scope, resource, "") + err = query.SubjectHasPermission(ctx, r.authzedClient, subjectResource, action, resource, "") if err != nil { - if errors.Is(err, query.ErrScopeNotAssigned) { - c.JSON(http.StatusForbidden, gin.H{"message": "actor does not have requested scope"}) + if errors.Is(err, query.ErrActionNotAssigned) { + c.JSON(http.StatusForbidden, gin.H{"message": "subject does not have requested action"}) return } diff --git a/internal/api/resources.go b/internal/api/resources.go index 7e8d74f6..7a0227cd 100644 --- a/internal/api/resources.go +++ b/internal/api/resources.go @@ -33,19 +33,19 @@ func (r *Router) resourceCreate(c *gin.Context) { return } - actor, err := currentActor(c) + subject, err := currentSubject(c) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": "failed to get the actor"}) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "message": "failed to get the subject"}) return } - actorResource, err := query.NewResourceFromURN(actor) + subjectResource, err := query.NewResourceFromURN(subject) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"message": "error processing actor URN", "error": err.Error()}) + c.JSON(http.StatusBadRequest, gin.H{"message": "error processing subject URN", "error": err.Error()}) return } - zedToken, err := query.CreateSpiceDBRelationships(ctx, r.authzedClient, resource, actorResource) + zedToken, err := query.CreateSpiceDBRelationships(ctx, r.authzedClient, resource, subjectResource) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"message": "failed to create relationship", "error": err.Error()}) return diff --git a/internal/api/router.go b/internal/api/router.go index 1c9eb79d..588f14cd 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -42,11 +42,11 @@ func (r *Router) Routes(rg *gin.RouterGroup) { v1.POST("/resources/:urn", r.resourceCreate) v1.DELETE("/resources/:urn", r.resourceDelete) // Check resource access - v1.GET("/has/:scope/on/:urn", r.checkScope) + v1.GET("/has/:action/on/:urn", r.checkAction) } } -func currentActor(c *gin.Context) (*urnx.URN, error) { +func currentSubject(c *gin.Context) (*urnx.URN, error) { subject := ginjwt.GetSubject(c) return urnx.Parse(subject) diff --git a/internal/query/errors.go b/internal/query/errors.go index 4bafc58e..3f373431 100644 --- a/internal/query/errors.go +++ b/internal/query/errors.go @@ -3,7 +3,7 @@ package query import "errors" var ( - // ErrScopeNotAssigned represents an error condition where the actor is not able to complete + // ErrActionNotAssigned represents an error condition where the subject is not able to complete // the given request. - ErrScopeNotAssigned = errors.New("the actor does not have permissions to complete this request") + ErrActionNotAssigned = errors.New("the subject does not have permissions to complete this request") ) diff --git a/internal/query/tenants.go b/internal/query/tenants.go index 86bff984..dd86c307 100644 --- a/internal/query/tenants.go +++ b/internal/query/tenants.go @@ -10,7 +10,7 @@ import ( "go.infratographer.com/x/urnx" ) -var roleActorRelation = "subject" +var roleSubjectRelation = "subject" var ( BuiltInRoleAdmins = "Admins" @@ -18,20 +18,20 @@ var ( BuiltInRoleViewers = "Viewers" ) -func ActorHasPermission(ctx context.Context, db *authzed.Client, actor *Resource, scope string, object *Resource, queryToken string) error { +func SubjectHasPermission(ctx context.Context, db *authzed.Client, subject *Resource, action string, object *Resource, queryToken string) error { req := &pb.CheckPermissionRequest{ Resource: object.spiceDBObjectReference(), - Permission: scope, + Permission: action, Subject: &pb.SubjectReference{ - Object: actor.spiceDBObjectReference(), + Object: subject.spiceDBObjectReference(), }, } return checkPermission(ctx, db, req, queryToken) } -func AssignActorRole(ctx context.Context, db *authzed.Client, actor *Resource, role string, object *Resource) (string, error) { - request := &pb.WriteRelationshipsRequest{Updates: []*pb.RelationshipUpdate{actorRoleRel(actor, role, object)}} +func AssignSubjectRole(ctx context.Context, db *authzed.Client, subject *Resource, role string, object *Resource) (string, error) { + request := &pb.WriteRelationshipsRequest{Updates: []*pb.RelationshipUpdate{subjectRoleRel(subject, role, object)}} r, err := db.WriteRelationships(ctx, request) if err != nil { @@ -41,7 +41,7 @@ func AssignActorRole(ctx context.Context, db *authzed.Client, actor *Resource, r return r.WrittenAt.GetToken(), nil } -func actorRoleRel(actor *Resource, role string, object *Resource) *pb.RelationshipUpdate { +func subjectRoleRel(subject *Resource, role string, object *Resource) *pb.RelationshipUpdate { return &pb.RelationshipUpdate{ Operation: pb.RelationshipUpdate_OPERATION_CREATE, Relationship: &pb.Relationship{ @@ -49,9 +49,9 @@ func actorRoleRel(actor *Resource, role string, object *Resource) *pb.Relationsh ObjectType: "role", ObjectId: dbRoleName(role, object), }, - Relation: roleActorRelation, + Relation: roleSubjectRelation, Subject: &pb.SubjectReference{ - Object: actor.spiceDBObjectReference(), + Object: subject.spiceDBObjectReference(), }, }, } @@ -71,7 +71,7 @@ func checkPermission(ctx context.Context, db *authzed.Client, req *pb.CheckPermi return nil } - return ErrScopeNotAssigned + return ErrActionNotAssigned } func dbRoleName(role string, res *Resource) string { @@ -116,43 +116,43 @@ func builtInRoles(res *Resource) []*pb.RelationshipUpdate { rels := []*pb.RelationshipUpdate{} - for _, scope := range adminAssignments { + for _, action := range adminAssignments { rels = append(rels, &pb.RelationshipUpdate{ Operation: pb.RelationshipUpdate_OPERATION_CREATE, Relationship: &pb.Relationship{ Resource: res.spiceDBObjectReference(), - Relation: scope, + Relation: action, Subject: &pb.SubjectReference{ Object: adminRole, - OptionalRelation: roleActorRelation, + OptionalRelation: roleSubjectRelation, }, }, }) } - for _, scope := range editorAssignments { + for _, action := range editorAssignments { rels = append(rels, &pb.RelationshipUpdate{ Operation: pb.RelationshipUpdate_OPERATION_CREATE, Relationship: &pb.Relationship{ Resource: res.spiceDBObjectReference(), - Relation: scope, + Relation: action, Subject: &pb.SubjectReference{ Object: editorRole, - OptionalRelation: roleActorRelation, + OptionalRelation: roleSubjectRelation, }, }, }) } - for _, scope := range viewerAssignments { + for _, action := range viewerAssignments { rels = append(rels, &pb.RelationshipUpdate{ Operation: pb.RelationshipUpdate_OPERATION_CREATE, Relationship: &pb.Relationship{ Resource: res.spiceDBObjectReference(), - Relation: scope, + Relation: action, Subject: &pb.SubjectReference{ Object: viewerRole, - OptionalRelation: roleActorRelation, + OptionalRelation: roleSubjectRelation, }, }, }) @@ -244,13 +244,13 @@ func (r *Resource) spiceDBObjectReference() *pb.ObjectReference { } } -func CreateSpiceDBRelationships(ctx context.Context, db *authzed.Client, r *Resource, actor *Resource) (string, error) { +func CreateSpiceDBRelationships(ctx context.Context, db *authzed.Client, r *Resource, subject *Resource) (string, error) { rels := []*pb.RelationshipUpdate{} if r.ResourceType.URNResourceType == "tenant" { rels = append(rels, builtInRoles(r)...) - rels = append(rels, actorRoleRel(actor, BuiltInRoleAdmins, r)) + rels = append(rels, subjectRoleRel(subject, BuiltInRoleAdmins, r)) } for _, rr := range r.ResourceType.Relationships { diff --git a/internal/query/tenants_test.go b/internal/query/tenants_test.go index 54c753dc..be5563fe 100644 --- a/internal/query/tenants_test.go +++ b/internal/query/tenants_test.go @@ -59,7 +59,7 @@ func cleanDB(ctx context.Context, t *testing.T, client *authzed.Client) { } } -func TestActorScopes(t *testing.T) { +func TestSubjectActions(t *testing.T) { ctx := context.Background() s := dbTest(ctx, t) @@ -78,23 +78,23 @@ func TestActorScopes(t *testing.T) { assert.NoError(t, err) t.Run("allow a user to view an ou", func(t *testing.T) { - queryToken, err = query.AssignActorRole(ctx, s.SpiceDB, userRes, "Editors", tenRes) + queryToken, err = query.AssignSubjectRole(ctx, s.SpiceDB, userRes, "Editors", tenRes) assert.NoError(t, err) }) t.Run("check that the user has edit access to an ou", func(t *testing.T) { - err := query.ActorHasPermission(ctx, s.SpiceDB, userRes, "loadbalancer_get", tenRes, queryToken) + err := query.SubjectHasPermission(ctx, s.SpiceDB, userRes, "loadbalancer_get", tenRes, queryToken) assert.NoError(t, err) }) - t.Run("error returned when the user doesn't have the global scope", func(t *testing.T) { + t.Run("error returned when the user doesn't have the global action", func(t *testing.T) { subjURN, err := urnx.Build("infratographer", "subject", uuid.New()) require.NoError(t, err) otherUserRes, err := query.NewResourceFromURN(subjURN) require.NoError(t, err) - err = query.ActorHasPermission(ctx, s.SpiceDB, otherUserRes, "loadbalancer_get", tenRes, queryToken) + err = query.SubjectHasPermission(ctx, s.SpiceDB, otherUserRes, "loadbalancer_get", tenRes, queryToken) assert.Error(t, err) - assert.ErrorIs(t, err, query.ErrScopeNotAssigned) + assert.ErrorIs(t, err, query.ErrActionNotAssigned) }) } diff --git a/pkg/client/v1/auth.go b/pkg/client/v1/auth.go index 0e7844b8..6055f4f5 100644 --- a/pkg/client/v1/auth.go +++ b/pkg/client/v1/auth.go @@ -55,14 +55,14 @@ func New(url string, doerClient Doer) (*Client, error) { return c, nil } -func (c *Client) Allowed(ctx context.Context, scope string, resourceURNPrefix string) (bool, error) { - ctx, span := tracer.Start(ctx, "ActorHasScope", trace.WithAttributes( - attribute.String("scope", scope), +func (c *Client) Allowed(ctx context.Context, action string, resourceURNPrefix string) (bool, error) { + ctx, span := tracer.Start(ctx, "SubjectHasAction", trace.WithAttributes( + attribute.String("action", action), attribute.String("resource", resourceURNPrefix), )) defer span.End() - err := c.get(ctx, fmt.Sprintf("/has/%s/on/%s", scope, resourceURNPrefix), map[string]string{}) + err := c.get(ctx, fmt.Sprintf("/has/%s/on/%s", action, resourceURNPrefix), map[string]string{}) if err != nil { if errors.Is(err, ErrPermissionDenied) { return false, nil diff --git a/pkg/client/v1/errors.go b/pkg/client/v1/errors.go index 8303230a..7bdc24c2 100644 --- a/pkg/client/v1/errors.go +++ b/pkg/client/v1/errors.go @@ -10,5 +10,5 @@ var ( ErrNoAuthToken = errors.New("no auth token provided for client") // ErrPermissionDenied is the error returned when permission is denied to a call - ErrPermissionDenied = errors.New("actor doesn't have access") + ErrPermissionDenied = errors.New("subject doesn't have access") )