Skip to content

Commit

Permalink
Get role resources (#147)
Browse files Browse the repository at this point in the history
* add getting of a role

Signed-off-by: Mike Mason <[email protected]>

* add endpoint to discover the resource a role is assigned to

Signed-off-by: Mike Mason <[email protected]>

* return error on multiple role resources

Signed-off-by: Mike Mason <[email protected]>

---------

Signed-off-by: Mike Mason <[email protected]>
  • Loading branch information
mikemrm authored Jul 24, 2023
1 parent b1835ab commit 7364ba4
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 2 deletions.
59 changes: 58 additions & 1 deletion internal/api/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down
3 changes: 3 additions & 0 deletions internal/query/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
10 changes: 10 additions & 0 deletions internal/query/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions internal/query/relations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
55 changes: 54 additions & 1 deletion internal/query/relations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions internal/query/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 7364ba4

Please sign in to comment.