From 00f5ac1ccd5900df2121643bab8d289be14b64e9 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Tue, 18 Jul 2023 14:17:18 -0500 Subject: [PATCH] list related resources (#140) Expose endpoints to list related resources From and To a given resource. Signed-off-by: Mike Mason --- internal/api/relationships.go | 45 ++++++++++++++++++++++++---- internal/api/router.go | 4 ++- internal/api/types.go | 5 ++-- internal/query/mock/mock.go | 9 ++++-- internal/query/relations.go | 51 ++++++++++++++++++++++++++++---- internal/query/relations_test.go | 6 ++-- internal/query/service.go | 29 +++++++++++++----- 7 files changed, 122 insertions(+), 27 deletions(-) diff --git a/internal/api/relationships.go b/internal/api/relationships.go index b1bef468..fec54c61 100644 --- a/internal/api/relationships.go +++ b/internal/api/relationships.go @@ -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) @@ -102,7 +102,7 @@ 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) } @@ -110,12 +110,47 @@ func (r *Router) relationshipsList(c echo.Context) error { 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{ diff --git a/internal/api/router.go b/internal/api/router.go index 746b6622..c47e7ed5 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -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) diff --git a/internal/api/types.go b/internal/api/types.go index 2a41ca7d..5db354c5 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -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 { diff --git a/internal/query/mock/mock.go b/internal/query/mock/mock.go index 72b1a6ad..4fa939ca 100644 --- a/internal/query/mock/mock.go +++ b/internal/query/mock/mock.go @@ -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 } diff --git a/internal/query/relations.go b/internal/query/relations.go index 82c177ef..f047185f 100644 --- a/internal/query/relations.go +++ b/internal/query/relations.go @@ -413,7 +413,7 @@ 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 { @@ -421,12 +421,22 @@ func (e *engine) relationshipsToNonRoles(rels []*pb.Relationship, res types.Reso 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 } @@ -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{ @@ -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. diff --git a/internal/query/relations_test.go b/internal/query/relations_test.go index 4a79ad00..f0300888 100644 --- a/internal/query/relations_test.go +++ b/internal/query/relations_test.go @@ -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, @@ -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) @@ -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, diff --git a/internal/query/service.go b/internal/query/service.go index a802cb44..c4561c8e 100644 --- a/internal/query/service.go +++ b/internal/query/service.go @@ -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) @@ -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) }