Skip to content

Commit

Permalink
list related resources (#140)
Browse files Browse the repository at this point in the history
Expose endpoints to list related resources From and To a given resource.

Signed-off-by: Mike Mason <[email protected]>
  • Loading branch information
mikemrm authored Jul 18, 2023
1 parent 211c29f commit 00f5ac1
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 27 deletions.
45 changes: 40 additions & 5 deletions internal/api/relationships.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ func (r *Router) relationshipsCreate(c echo.Context) error {
return c.JSON(http.StatusCreated, resp)
}

func (r *Router) relationshipsList(c echo.Context) error {
func (r *Router) relationshipListFrom(c echo.Context) error {
resourceIDStr := c.Param("id")

ctx, span := tracer.Start(c.Request().Context(), "api.relationshipsList", trace.WithAttributes(attribute.String("id", resourceIDStr)))
ctx, span := tracer.Start(c.Request().Context(), "api.relationshipListFrom", trace.WithAttributes(attribute.String("id", resourceIDStr)))
defer span.End()

resourceID, err := gidx.Parse(resourceIDStr)
Expand All @@ -102,20 +102,55 @@ func (r *Router) relationshipsList(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "error listing relationships").SetInternal(err)
}

rels, err := r.engine.ListRelationships(ctx, resource, "")
rels, err := r.engine.ListRelationshipsFrom(ctx, resource, "")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "error listing relationships").SetInternal(err)
}

items := make([]relationshipItem, len(rels))

for i, rel := range rels {
item := relationshipItem{
items[i] = relationshipItem{
Relation: rel.Relation,
SubjectID: rel.Subject.ID.String(),
}
}

out := listRelationshipsResponse{
Data: items,
}

return c.JSON(http.StatusOK, out)
}

func (r *Router) relationshipListTo(c echo.Context) error {
resourceIDStr := c.Param("id")

ctx, span := tracer.Start(c.Request().Context(), "api.relationshipListTo", trace.WithAttributes(attribute.String("id", resourceIDStr)))
defer span.End()

resourceID, err := gidx.Parse(resourceIDStr)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "error parsing resource ID").SetInternal(err)
}

resource, err := r.engine.NewResourceFromID(resourceID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "error listing relationships").SetInternal(err)
}

items[i] = item
rels, err := r.engine.ListRelationshipsTo(ctx, resource, "")
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "error listing relationships").SetInternal(err)
}

items := make([]relationshipItem, len(rels))

for i, rel := range rels {
items[i] = relationshipItem{
ResourceID: rel.Resource.ID.String(),
Relation: rel.Relation,
}
}

out := listRelationshipsResponse{
Expand Down
4 changes: 3 additions & 1 deletion internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ func (r *Router) Routes(rg *echo.Group) {
v1.GET("/resources/:id/roles", r.rolesList)
v1.POST("/resources/:id/relationships", r.relationshipsCreate)
v1.DELETE("/resources/:id/relationships", r.relationshipDelete)
v1.GET("/resources/:id/relationships", r.relationshipsList)
v1.GET("/resources/:id/relationships", r.relationshipListFrom)
v1.GET("/relationships/from/:id", r.relationshipListFrom)
v1.GET("/relationships/to/:id", r.relationshipListTo)
v1.DELETE("/roles/:id", r.roleDelete)
v1.POST("/roles/:role_id/assignments", r.assignmentCreate)
v1.DELETE("/roles/:role_id/assignments", r.assignmentDelete)
Expand Down
5 changes: 3 additions & 2 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ type deleteRelationshipsResponse struct {
}

type relationshipItem struct {
Relation string `json:"relation"`
SubjectID string `json:"subject_id"`
ResourceID string `json:"resource_id,omitempty"`
Relation string `json:"relation"`
SubjectID string `json:"subject_id,omitempty"`
}

type listRelationshipsResponse struct {
Expand Down
9 changes: 7 additions & 2 deletions internal/query/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ func (e *Engine) ListAssignments(ctx context.Context, role types.Role, queryToke
return nil, nil
}

// ListRelationships returns nothing but satisfies the Engine interface.
func (e *Engine) ListRelationships(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error) {
// ListRelationshipsFrom returns nothing but satisfies the Engine interface.
func (e *Engine) ListRelationshipsFrom(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error) {
return nil, nil
}

// ListRelationshipsTo returns nothing but satisfies the Engine interface.
func (e *Engine) ListRelationshipsTo(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error) {
return nil, nil
}

Expand Down
51 changes: 45 additions & 6 deletions internal/query/relations.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,20 +413,30 @@ func relationshipsToRoles(rels []*pb.Relationship) []types.Role {
return out
}

func (e *engine) relationshipsToNonRoles(rels []*pb.Relationship, res types.Resource) ([]types.Relationship, error) {
func (e *engine) relationshipsToNonRoles(rels []*pb.Relationship) ([]types.Relationship, error) {
var out []types.Relationship

for _, rel := range rels {
if rel.Subject.Object.ObjectType == e.namespace+"/role" {
continue
}

id, err := gidx.Parse(rel.Subject.Object.ObjectId)
resID, err := gidx.Parse(rel.Resource.ObjectId)
if err != nil {
return nil, err
}

res, err := e.NewResourceFromID(resID)
if err != nil {
return nil, err
}

subj, err := e.NewResourceFromID(id)
subjID, err := gidx.Parse(rel.Subject.Object.ObjectId)
if err != nil {
return nil, err
}

subj, err := e.NewResourceFromID(subjID)
if err != nil {
return nil, err
}
Expand All @@ -443,8 +453,8 @@ func (e *engine) relationshipsToNonRoles(rels []*pb.Relationship, res types.Reso
return out, nil
}

// ListRelationships returns all non-role relationships bound to a given resource.
func (e *engine) ListRelationships(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error) {
// ListRelationshipsFrom returns all non-role relationships bound to a given resource.
func (e *engine) ListRelationshipsFrom(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error) {
resType := e.namespace + "/" + resource.Type

filter := &pb.RelationshipFilter{
Expand All @@ -457,7 +467,36 @@ func (e *engine) ListRelationships(ctx context.Context, resource types.Resource,
return nil, err
}

return e.relationshipsToNonRoles(relationships, resource)
return e.relationshipsToNonRoles(relationships)
}

// ListRelationshipsTo returns all non-role relationships destined for a given resource.
func (e *engine) ListRelationshipsTo(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error) {
relTypes, ok := e.schemaSubjectRelationMap[resource.Type]
if !ok {
return nil, ErrInvalidType
}

var relationships []*pb.Relationship

for _, types := range relTypes {
for _, relType := range types {
rels, err := e.readRelationships(ctx, &pb.RelationshipFilter{
ResourceType: e.namespace + "/" + relType,
OptionalSubjectFilter: &pb.SubjectFilter{
SubjectType: e.namespace + "/" + resource.Type,
OptionalSubjectId: resource.ID.String(),
},
}, queryToken)
if err != nil {
return nil, err
}

relationships = append(relationships, rels...)
}
}

return e.relationshipsToNonRoles(relationships)
}

// ListRoles returns all roles bound to a given resource.
Expand Down
6 changes: 3 additions & 3 deletions internal/query/relations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ func TestRelationships(t *testing.T) {
}
}

rels, err := e.ListRelationships(ctx, input.Resource, queryToken)
rels, err := e.ListRelationshipsFrom(ctx, input.Resource, queryToken)

return testingx.TestResult[[]types.Relationship]{
Success: rels,
Expand Down Expand Up @@ -449,7 +449,7 @@ func TestRelationshipDelete(t *testing.T) {
queryToken, err := e.CreateRelationships(ctx, []types.Relationship{relReq})
require.NoError(t, err)

createdResources, err := e.ListRelationships(ctx, childRes, queryToken)
createdResources, err := e.ListRelationshipsFrom(ctx, childRes, queryToken)
require.NoError(t, err)
require.NotEmpty(t, createdResources)

Expand Down Expand Up @@ -487,7 +487,7 @@ func TestRelationshipDelete(t *testing.T) {
}
}

rels, err := e.ListRelationships(ctx, input.Resource, queryToken)
rels, err := e.ListRelationshipsFrom(ctx, input.Resource, queryToken)

return testingx.TestResult[[]types.Relationship]{
Success: rels,
Expand Down
29 changes: 21 additions & 8 deletions internal/query/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type Engine interface {
CreateRelationships(ctx context.Context, rels []types.Relationship) (string, error)
CreateRole(ctx context.Context, res types.Resource, actions []string) (types.Role, string, error)
ListAssignments(ctx context.Context, role types.Role, queryToken string) ([]types.Resource, error)
ListRelationships(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error)
ListRelationshipsFrom(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error)
ListRelationshipsTo(ctx context.Context, resource types.Resource, queryToken string) ([]types.Relationship, error)
ListRoles(ctx context.Context, resource types.Resource, queryToken string) ([]types.Role, error)
DeleteRelationship(ctx context.Context, rel types.Relationship) (string, error)
DeleteRole(ctx context.Context, roleResource types.Resource, queryToken string) (string, error)
Expand All @@ -29,24 +30,36 @@ type Engine interface {
}

type engine struct {
logger *zap.SugaredLogger
namespace string
client *authzed.Client
schema []types.ResourceType
schemaPrefixMap map[string]types.ResourceType
schemaTypeMap map[string]types.ResourceType
schemaRoleables []types.ResourceType
logger *zap.SugaredLogger
namespace string
client *authzed.Client
schema []types.ResourceType
schemaPrefixMap map[string]types.ResourceType
schemaTypeMap map[string]types.ResourceType
schemaSubjectRelationMap map[string]map[string][]string
schemaRoleables []types.ResourceType
}

func (e *engine) cacheSchemaResources() {
e.schemaPrefixMap = make(map[string]types.ResourceType, len(e.schema))
e.schemaTypeMap = make(map[string]types.ResourceType, len(e.schema))
e.schemaSubjectRelationMap = make(map[string]map[string][]string)
e.schemaRoleables = []types.ResourceType{}

for _, res := range e.schema {
e.schemaPrefixMap[res.IDPrefix] = res
e.schemaTypeMap[res.Name] = res

for _, relationship := range res.Relationships {
for _, t := range relationship.Types {
if _, ok := e.schemaSubjectRelationMap[t]; !ok {
e.schemaSubjectRelationMap[t] = make(map[string][]string)
}

e.schemaSubjectRelationMap[t][relationship.Relation] = append(e.schemaSubjectRelationMap[t][relationship.Relation], res.Name)
}
}

if resourceHasRoleBindings(res) {
e.schemaRoleables = append(e.schemaRoleables, res)
}
Expand Down

0 comments on commit 00f5ac1

Please sign in to comment.