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

Add README with initial concepts and persist them throughout the repo #54

Merged
merged 1 commit into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,48 @@
# permissionapi
# 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
20 changes: 10 additions & 10 deletions internal/api/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}

Expand Down
10 changes: 5 additions & 5 deletions internal/api/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions internal/query/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
42 changes: 21 additions & 21 deletions internal/query/tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,28 @@ import (
"go.infratographer.com/x/urnx"
)

var roleActorRelation = "subject"
var roleSubjectRelation = "subject"

var (
BuiltInRoleAdmins = "Admins"
BuiltInRoleEditors = "Editors"
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 {
Expand All @@ -41,17 +41,17 @@ 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{
Resource: &pb.ObjectReference{
ObjectType: "role",
ObjectId: dbRoleName(role, object),
},
Relation: roleActorRelation,
Relation: roleSubjectRelation,
Subject: &pb.SubjectReference{
Object: actor.spiceDBObjectReference(),
Object: subject.spiceDBObjectReference(),
},
},
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
},
},
})
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 6 additions & 6 deletions internal/query/tenants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
})
}
8 changes: 4 additions & 4 deletions pkg/client/v1/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/v1/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)