Skip to content

Commit

Permalink
RBAC v2 role bindings (#249)
Browse files Browse the repository at this point in the history
* update query pkg to support role-binding V2, add v2 APIs
* add rolebinding storage
* Apply suggestions from code review

---------

Signed-off-by: Bailin He <[email protected]>
Signed-off-by: Bailin He <[email protected]>
Co-authored-by: John Schaeffer <[email protected]>
  • Loading branch information
bailinhe and jnschaeffer authored May 7, 2024
1 parent ca13cbc commit 22a688b
Show file tree
Hide file tree
Showing 25 changed files with 4,271 additions and 155 deletions.
33 changes: 20 additions & 13 deletions cmd/createrole.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go.infratographer.com/permissions-api/internal/query"
"go.infratographer.com/permissions-api/internal/spicedbx"
"go.infratographer.com/permissions-api/internal/storage"
"go.infratographer.com/permissions-api/internal/types"
)

const (
Expand All @@ -24,15 +25,13 @@ const (
createRoleFlagName = "name"
)

var (
createRoleCmd = &cobra.Command{
Use: "create-role",
Short: "create role in SpiceDB directly",
Run: func(cmd *cobra.Command, _ []string) {
createRole(cmd.Context(), globalCfg)
},
}
)
var createRoleCmd = &cobra.Command{
Use: "create-role",
Short: "create role in SpiceDB directly",
Run: func(cmd *cobra.Command, _ []string) {
createRole(cmd.Context(), globalCfg)
},
}

func init() {
rootCmd.AddCommand(createRoleCmd)
Expand Down Expand Up @@ -125,14 +124,22 @@ func createRole(ctx context.Context, cfg *config.AppConfig) {
logger.Fatalw("error creating subject resource", "error", err)
}

role, err := engine.CreateRole(ctx, subjectResource, resource, name, actions)
role, err := engine.CreateRoleV2(ctx, subjectResource, resource, name, actions)
if err != nil {
logger.Fatalw("error creating role", "error", err)
}

if err = engine.AssignSubjectRole(ctx, subjectResource, role); err != nil {
logger.Fatalw("error creating role", "error", err)
rbsubj := []types.RoleBindingSubject{{SubjectResource: subjectResource}}

roleres, err := engine.NewResourceFromID(role.ID)
if err != nil {
logger.Fatalw("error creating role resource", "error", err)
}

rb, err := engine.CreateRoleBinding(ctx, subjectResource, resource, roleres, rbsubj)
if err != nil {
logger.Fatalw("error creating role binding", "error", err)
}

logger.Infow("role successfully created", "role_id", role.ID)
logger.Infof("created role %s[%s] and role-binding %s", role.Name, role.ID, rb.ID)
}
331 changes: 331 additions & 0 deletions internal/api/rolebindings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
package api

import (
"fmt"
"net/http"
"time"

"github.com/labstack/echo/v4"
"go.infratographer.com/x/gidx"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"

"go.infratographer.com/permissions-api/internal/iapl"
"go.infratographer.com/permissions-api/internal/types"
)

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

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

resourceID, err := gidx.Parse(resourceIDStr)
if err != nil {
return r.errorResponse("error parsing resource ID", fmt.Errorf("%w: %s", ErrInvalidID, err.Error()))
}

var body roleBindingRequest

err = c.Bind(&body)
if err != nil {
return r.errorResponse(err.Error(), ErrParsingRequestBody)
}

resource, err := r.engine.NewResourceFromID(resourceID)
if err != nil {
return r.errorResponse("error creating resource", err)
}

actor, err := r.currentSubject(c)
if err != nil {
return err
}

// permissions on role binding actions, similar to roles v1, are granted on the resources
if err := r.checkActionWithResponse(ctx, actor, string(iapl.RoleBindingActionCreate), resource); err != nil {
return err
}

roleID, err := gidx.Parse(body.RoleID)
if err != nil {
return r.errorResponse("error parsing role ID", fmt.Errorf("%w: %s", ErrInvalidID, err.Error()))
}

roleResource, err := r.engine.NewResourceFromID(roleID)
if err != nil {
return r.errorResponse("error creating role resource", err)
}

subjects := make([]types.RoleBindingSubject, len(body.SubjectIDs))

for i, sid := range body.SubjectIDs {
subj, err := r.engine.NewResourceFromID(sid)
if err != nil {
return r.errorResponse("error creating subject resource", err)
}

subjects[i] = types.RoleBindingSubject{
SubjectResource: subj,
}
}

rb, err := r.engine.CreateRoleBinding(ctx, actor, resource, roleResource, subjects)
if err != nil {
return r.errorResponse("error creating role-binding", err)
}

return c.JSON(
http.StatusOK,
roleBindingResponse{
ID: rb.ID,
ResourceID: rb.ResourceID,
SubjectIDs: rb.SubjectIDs,
RoleID: rb.RoleID,

CreatedBy: rb.CreatedBy,
UpdatedBy: rb.UpdatedBy,
CreatedAt: rb.CreatedAt.Format(time.RFC3339),
UpdatedAt: rb.UpdatedAt.Format(time.RFC3339),
},
)
}

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

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

resourceID, err := gidx.Parse(resourceIDStr)
if err != nil {
return r.errorResponse("error parsing resource ID", fmt.Errorf("%w: %s", ErrInvalidID, err.Error()))
}

resource, err := r.engine.NewResourceFromID(resourceID)
if err != nil {
return r.errorResponse("error creating resource", err)
}

subjectResource, err := r.currentSubject(c)
if err != nil {
return err
}

if err := r.checkActionWithResponse(ctx, subjectResource, string(iapl.RoleBindingActionList), resource); err != nil {
return err
}

rbs, err := r.engine.ListRoleBindings(ctx, resource, nil)
if err != nil {
return r.errorResponse("error listing role-binding", err)
}

resp := listRoleBindingsResponse{
Data: make([]roleBindingResponse, len(rbs)),
}

for i, rb := range rbs {
resp.Data[i] = roleBindingResponse{
ID: rb.ID,
ResourceID: rb.ResourceID,
SubjectIDs: rb.SubjectIDs,
RoleID: rb.RoleID,

CreatedBy: rb.CreatedBy,
UpdatedBy: rb.UpdatedBy,
CreatedAt: rb.CreatedAt.Format(time.RFC3339),
UpdatedAt: rb.UpdatedAt.Format(time.RFC3339),
}
}

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

func (r *Router) roleBindingDelete(c echo.Context) error {
rbID := c.Param("rb_id")

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

// role-binding
rolebindingID, err := gidx.Parse(rbID)
if err != nil {
return r.errorResponse("error parsing resource ID", fmt.Errorf("%w: %s", ErrInvalidID, err.Error()))
}

rbRes, err := r.engine.NewResourceFromID(rolebindingID)
if err != nil {
return r.errorResponse("error creating resource", err)
}

actor, err := r.currentSubject(c)
if err != nil {
return err
}

// resource
resource, err := r.engine.GetRoleBindingResource(ctx, rbRes)
if err != nil {
return r.errorResponse("error getting role-binding owner resource", err)
}

// permissions on role binding actions, similar to roles v1, are granted on the resources
if err := r.checkActionWithResponse(ctx, actor, string(iapl.RoleBindingActionDelete), resource); err != nil {
return err
}

if err := r.engine.DeleteRoleBinding(ctx, rbRes); err != nil {
return r.errorResponse("error updating role-binding", err)
}

resp := deleteRoleBindingResponse{Success: true}

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

func (r *Router) roleBindingGet(c echo.Context) error {
rbID := c.Param("rb_id")

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

// role-binding
rolebindingID, err := gidx.Parse(rbID)
if err != nil {
return r.errorResponse("error parsing resource ID", fmt.Errorf("%w: %s", ErrInvalidID, err.Error()))
}

rbRes, err := r.engine.NewResourceFromID(rolebindingID)
if err != nil {
return r.errorResponse("error creating resource", err)
}

actor, err := r.currentSubject(c)
if err != nil {
return err
}

rb, err := r.engine.GetRoleBinding(ctx, rbRes)
if err != nil {
return r.errorResponse("error getting role-binding", err)
}

// permissions on role binding actions, similar to roles v1, are granted on the resources
// since the rolebinding is returning the resource ID that it belongs to, we
// will use this resource ID to check the permissions
resource, err := r.engine.NewResourceFromID(rb.ResourceID)
if err != nil {
return r.errorResponse("error creating resource", err)
}

if err := r.checkActionWithResponse(ctx, actor, string(iapl.RoleBindingActionGet), resource); err != nil {
return err
}

return c.JSON(
http.StatusOK,
roleBindingResponse{
ID: rb.ID,
ResourceID: rb.ResourceID,
SubjectIDs: rb.SubjectIDs,
RoleID: rb.RoleID,

CreatedBy: rb.CreatedBy,
UpdatedBy: rb.UpdatedBy,
CreatedAt: rb.CreatedAt.Format(time.RFC3339),
UpdatedAt: rb.UpdatedAt.Format(time.RFC3339),
},
)
}

func (r *Router) roleBindingUpdate(c echo.Context) error {
rbID := c.Param("rb_id")

ctx, span := tracer.Start(
c.Request().Context(), "api.roleBindingUpdate",
trace.WithAttributes(attribute.String("rolebinding_id", rbID)),
)
defer span.End()

// resource

// role-binding
rolebindingID, err := gidx.Parse(rbID)
if err != nil {
return r.errorResponse("error parsing resource ID", fmt.Errorf("%w: %s", ErrInvalidID, err.Error()))
}

rbRes, err := r.engine.NewResourceFromID(rolebindingID)
if err != nil {
return r.errorResponse("error creating resource", err)
}

actor, err := r.currentSubject(c)
if err != nil {
return err
}

// resource
resource, err := r.engine.GetRoleBindingResource(ctx, rbRes)
if err != nil {
return r.errorResponse("error getting role-binding owner resource", err)
}

// permissions on role binding actions, similar to roles v1, are granted on the resources
if err := r.checkActionWithResponse(ctx, actor, string(iapl.RoleBindingActionUpdate), resource); err != nil {
return err
}

body := &rolebindingUpdateRequest{}

err = c.Bind(body)
if err != nil {
return r.errorResponse(err.Error(), ErrParsingRequestBody)
}

subjects := make([]types.RoleBindingSubject, len(body.SubjectIDs))

for i, sid := range body.SubjectIDs {
subj, err := r.engine.NewResourceFromID(sid)
if err != nil {
return r.errorResponse("error creating subject resource", err)
}

subjects[i] = types.RoleBindingSubject{
SubjectResource: subj,
}
}

rb, err := r.engine.UpdateRoleBinding(ctx, actor, rbRes, subjects)
if err != nil {
return r.errorResponse("error updating role-binding", err)
}

return c.JSON(
http.StatusOK,
roleBindingResponse{
ID: rb.ID,
ResourceID: rb.ResourceID,
SubjectIDs: rb.SubjectIDs,
RoleID: rb.RoleID,

CreatedBy: rb.CreatedBy,
UpdatedBy: rb.UpdatedBy,
CreatedAt: rb.CreatedAt.Format(time.RFC3339),
UpdatedAt: rb.UpdatedAt.Format(time.RFC3339),
},
)
}
2 changes: 1 addition & 1 deletion internal/api/roles_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (r *Router) roleV2Update(c echo.Context) error {
return r.errorResponse("error creating resource", err)
}

if err := r.checkActionWithResponse(ctx, subjectResource, string(iapl.RoleActionGet), roleResource); err != nil {
if err := r.checkActionWithResponse(ctx, subjectResource, string(iapl.RoleActionUpdate), roleResource); err != nil {
return err
}

Expand Down
Loading

0 comments on commit 22a688b

Please sign in to comment.