diff --git a/internal/api/roles.go b/internal/api/roles.go index 1a325457..9893a43a 100644 --- a/internal/api/roles.go +++ b/internal/api/roles.go @@ -45,10 +45,39 @@ func (r *Router) roleCreate(c echo.Context) error { return c.JSON(http.StatusCreated, resp) } +func (r *Router) roleGet(c echo.Context) error { + roleIDStr := c.Param("role_id") + + ctx, span := tracer.Start(c.Request().Context(), "api.roleGet", trace.WithAttributes(attribute.String("id", roleIDStr))) + defer span.End() + + roleResourceID, err := gidx.Parse(roleIDStr) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "error getting resource").SetInternal(err) + } + + roleResource, err := r.engine.NewResourceFromID(roleResourceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "error getting resource").SetInternal(err) + } + + role, err := r.engine.GetRole(ctx, roleResource, "") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "error getting resource").SetInternal(err) + } + + resp := roleResponse{ + ID: role.ID, + Actions: role.Actions, + } + + return c.JSON(http.StatusOK, resp) +} + func (r *Router) rolesList(c echo.Context) error { resourceIDStr := c.Param("id") - ctx, span := tracer.Start(c.Request().Context(), "api.roleGet", trace.WithAttributes(attribute.String("id", resourceIDStr))) + ctx, span := tracer.Start(c.Request().Context(), "api.rolesList", trace.WithAttributes(attribute.String("id", resourceIDStr))) defer span.End() resourceID, err := gidx.Parse(resourceIDStr) @@ -109,3 +138,31 @@ func (r *Router) roleDelete(c echo.Context) error { return c.JSON(http.StatusOK, resp) } + +func (r *Router) roleGetResource(c echo.Context) error { + roleIDStr := c.Param("role_id") + + ctx, span := tracer.Start(c.Request().Context(), "api.roleGetResource", trace.WithAttributes(attribute.String("id", roleIDStr))) + defer span.End() + + roleResourceID, err := gidx.Parse(roleIDStr) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "error getting resource").SetInternal(err) + } + + roleResource, err := r.engine.NewResourceFromID(roleResourceID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "error getting resource").SetInternal(err) + } + + resource, err := r.engine.GetRoleResource(ctx, roleResource, "") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "error getting resource").SetInternal(err) + } + + resp := resourceResponse{ + ID: resource.ID, + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/internal/api/router.go b/internal/api/router.go index c47e7ed5..458071bd 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -49,7 +49,9 @@ func (r *Router) Routes(rg *echo.Group) { v1.GET("/resources/:id/relationships", r.relationshipListFrom) v1.GET("/relationships/from/:id", r.relationshipListFrom) v1.GET("/relationships/to/:id", r.relationshipListTo) + v1.GET("/roles/:role_id", r.roleGet) v1.DELETE("/roles/:id", r.roleDelete) + v1.GET("/roles/:role_id/resource", r.roleGetResource) v1.POST("/roles/:role_id/assignments", r.assignmentCreate) v1.DELETE("/roles/:role_id/assignments", r.assignmentDelete) v1.GET("/roles/:role_id/assignments", r.assignmentsList) diff --git a/internal/api/types.go b/internal/api/types.go index 5db354c5..dbab6e9a 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -13,6 +13,10 @@ type roleResponse struct { Actions []string `json:"actions"` } +type resourceResponse struct { + ID gidx.PrefixedID `json:"id"` +} + type deleteRoleResponse struct { Success bool `json:"success"` } diff --git a/internal/query/errors.go b/internal/query/errors.go index 4fe09700..7373102e 100644 --- a/internal/query/errors.go +++ b/internal/query/errors.go @@ -21,4 +21,7 @@ var ( // ErrRoleNotFound represents an error when no matching role was found on resource ErrRoleNotFound = errors.New("role not found") + + // ErrRoleHasTooManyResources represents an error which a role has too many resources + ErrRoleHasTooManyResources = errors.New("role has too many resources") ) diff --git a/internal/query/mock/mock.go b/internal/query/mock/mock.go index 4fa939ca..5105f077 100644 --- a/internal/query/mock/mock.go +++ b/internal/query/mock/mock.go @@ -57,6 +57,16 @@ func (e *Engine) CreateRole(ctx context.Context, res types.Resource, actions []s return role, "", nil } +// GetRole returns nothing but satisfies the Engine interface. +func (e *Engine) GetRole(ctx context.Context, roleResource types.Resource, queryToken string) (types.Role, error) { + return types.Role{}, nil +} + +// GetRoleResource returns nothing but satisfies the Engine interface. +func (e *Engine) GetRoleResource(ctx context.Context, roleResource types.Resource, queryToken string) (types.Resource, error) { + return types.Resource{}, nil +} + // ListAssignments returns nothing but satisfies the Engine interface. func (e *Engine) ListAssignments(ctx context.Context, role types.Role, queryToken string) ([]types.Resource, error) { return nil, nil diff --git a/internal/query/relations.go b/internal/query/relations.go index f047185f..dfcc1cc1 100644 --- a/internal/query/relations.go +++ b/internal/query/relations.go @@ -572,6 +572,75 @@ func (e *engine) listRoleResourceActions(ctx context.Context, role types.Resourc return resourceActions, nil } +// GetRole gets the role with it's actions. +func (e *engine) GetRole(ctx context.Context, roleResource types.Resource, queryToken string) (types.Role, error) { + var ( + resActions map[types.Resource][]string + err error + ) + + for _, resType := range e.schemaRoleables { + resActions, err = e.listRoleResourceActions(ctx, roleResource, resType.Name, queryToken) + if err != nil { + return types.Role{}, err + } + + // roles are only ever created for a single resource, so we can break after the first one is found. + if len(resActions) != 0 { + break + } + } + + if len(resActions) > 1 { + return types.Role{}, ErrRoleHasTooManyResources + } + + // returns the first resources actions. + for _, actions := range resActions { + for i, action := range actions { + actions[i] = relationToAction(action) + } + + return types.Role{ + ID: roleResource.ID, + Actions: actions, + }, nil + } + + return types.Role{}, ErrRoleNotFound +} + +// GetRoleResource gets the role's assigned resource. +func (e *engine) GetRoleResource(ctx context.Context, roleResource types.Resource, queryToken string) (types.Resource, error) { + var ( + resActions map[types.Resource][]string + err error + ) + + for _, resType := range e.schemaRoleables { + resActions, err = e.listRoleResourceActions(ctx, roleResource, resType.Name, queryToken) + if err != nil { + return types.Resource{}, err + } + + // roles are only ever created for a single resource, so we can break after the first one is found. + if len(resActions) != 0 { + break + } + } + + if len(resActions) > 1 { + return types.Resource{}, ErrRoleHasTooManyResources + } + + // returns the first resources actions. + for resource := range resActions { + return resource, nil + } + + return types.Resource{}, ErrRoleNotFound +} + // DeleteRole removes all role actions from the assigned resource. func (e *engine) DeleteRole(ctx context.Context, roleResource types.Resource, queryToken string) (string, error) { var ( diff --git a/internal/query/relations_test.go b/internal/query/relations_test.go index f0300888..d568c630 100644 --- a/internal/query/relations_test.go +++ b/internal/query/relations_test.go @@ -83,7 +83,7 @@ func cleanDB(ctx context.Context, t *testing.T, client *authzed.Client, namespac } } -func TestRoles(t *testing.T) { +func TestCreateRoles(t *testing.T) { namespace := "testroles" ctx := context.Background() e := testEngine(ctx, t, namespace) @@ -141,6 +141,59 @@ func TestRoles(t *testing.T) { testingx.RunTests(ctx, t, testCases, testFn) } +func TestGetRoles(t *testing.T) { + namespace := "testroles" + ctx := context.Background() + e := testEngine(ctx, t, namespace) + tenID, err := gidx.NewID("tnntten") + require.NoError(t, err) + tenRes, err := e.NewResourceFromID(tenID) + require.NoError(t, err) + + role, queryToken, err := e.CreateRole(ctx, tenRes, []string{"loadbalancer_get"}) + require.NoError(t, err) + roleRes, err := e.NewResourceFromID(role.ID) + require.NoError(t, err) + + missingRes, err := e.NewResourceFromID(gidx.PrefixedID("permrol-notfound")) + require.NoError(t, err) + + testCases := []testingx.TestCase[types.Resource, types.Role]{ + { + Name: "GetRoleNotFound", + Input: missingRes, + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[types.Role]) { + assert.ErrorIs(t, res.Err, ErrRoleNotFound) + }, + }, + { + Name: "GetSuccess", + Input: roleRes, + CheckFn: func(ctx context.Context, t *testing.T, res testingx.TestResult[types.Role]) { + expActions := []string{ + "loadbalancer_get", + } + + assert.NoError(t, res.Err) + require.NotEmpty(t, res.Success.ID) + + assert.Equal(t, expActions, res.Success.Actions) + }, + }, + } + + testFn := func(ctx context.Context, roleResource types.Resource) testingx.TestResult[types.Role] { + roles, err := e.GetRole(ctx, roleResource, queryToken) + + return testingx.TestResult[types.Role]{ + Success: roles, + Err: err, + } + } + + testingx.RunTests(ctx, t, testCases, testFn) +} + func TestRoleDelete(t *testing.T) { namespace := "testroles" ctx := context.Background() diff --git a/internal/query/service.go b/internal/query/service.go index c4561c8e..c984b65a 100644 --- a/internal/query/service.go +++ b/internal/query/service.go @@ -17,6 +17,8 @@ type Engine interface { UnassignSubjectRole(ctx context.Context, subject types.Resource, role types.Role) (string, error) CreateRelationships(ctx context.Context, rels []types.Relationship) (string, error) CreateRole(ctx context.Context, res types.Resource, actions []string) (types.Role, string, error) + GetRole(ctx context.Context, roleResource types.Resource, queryToken string) (types.Role, error) + GetRoleResource(ctx context.Context, roleResource types.Resource, queryToken string) (types.Resource, error) ListAssignments(ctx context.Context, role types.Role, queryToken string) ([]types.Resource, 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)