Skip to content

Commit

Permalink
Add Bulk Permission Checks Endpoint (#294)
Browse files Browse the repository at this point in the history
* Add Bulk Permission Checks Endpoint

`POST /v1/allow/bulk`

This endpoint takes a list of `<resource, action>` as input, and returns
the permission checks outcome of each input

---------

Signed-off-by: Bailin He <[email protected]>
  • Loading branch information
bailinhe authored Oct 24, 2024
1 parent 169ee48 commit 6fe19eb
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 0 deletions.
124 changes: 124 additions & 0 deletions internal/api/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"sync"
"time"

"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -129,6 +130,17 @@ type checkResult struct {
Error error
}

type bulkCheckActionsRequest []checkAction

type checkActionResponse struct {
ResourceID string `json:"resource_id"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
Error string `json:"error,omitempty"`
}

type bulkCheckActionsResponse []checkActionResponse

// checkAllActions will check if a subject is allowed to perform an action on a list of resources.
// This is the permissions check endpoint.
// It will return a 200 if the subject is allowed to perform all requested resource actions.
Expand Down Expand Up @@ -312,3 +324,115 @@ func getParam(c echo.Context, name string) (string, bool) {

return values[0], true
}

// bulkCheckActions will check if a subject is allowed to perform a list of
// actions on a list of resources provided in the request body.
//
// This endpoint will always return 200 on successful checks, regardless of the
// outcome of the checks.
// It will return a 400 if the request is invalid.
func (r *Router) bulkCheckActions(c echo.Context) error {
ctx, span := tracer.Start(c.Request().Context(), "api.bulkCheckAction")
defer span.End()

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

var reqBody bulkCheckActionsRequest

if err := c.Bind(&reqBody); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "error parsing request body").SetInternal(err)
}

// validate requests
var (
validationResp bulkCheckActionsResponse = make([]checkActionResponse, 0, len(reqBody))
validationErrors = make([]error, 0, len(reqBody))
checks = make([]checkRequest, 0, len(reqBody))
)

for _, req := range reqBody {
resourceID, err := gidx.Parse(req.ResourceID)
if err != nil {
err = fmt.Errorf("error parsing resource ID: %w", err)

validationResp = append(validationResp, checkActionResponse{
ResourceID: req.ResourceID,
Action: req.Action,
Error: err.Error(),
})

validationErrors = append(validationErrors, err)

continue
}

resource, err := r.engine.NewResourceFromID(resourceID)
if err != nil {
err = fmt.Errorf("error creating resource from ID: %w", err)

validationResp = append(validationResp, checkActionResponse{
ResourceID: req.ResourceID,
Action: req.Action,
Error: err.Error(),
})

validationErrors = append(validationErrors, err)

continue
}

checks = append(checks, checkRequest{
Resource: resource,
Action: req.Action,
})
}

if len(validationErrors) != 0 {
return echo.NewHTTPError(http.StatusBadRequest, validationResp).SetInternal(multierr.Combine(validationErrors...))
}

// check permissions
var (
responses bulkCheckActionsResponse = make([]checkActionResponse, len(checks))
wg = &sync.WaitGroup{}
)

for i, check := range checks {
wg.Add(1)

go func(ctx context.Context, i int, action string, resource types.Resource) {
defer wg.Done()

ctxWithCancel, cancel := context.WithTimeout(ctx, maxCheckDuration)
defer cancel()

resp := checkActionResponse{
ResourceID: resource.ID.String(),
Action: action,
}

err := r.engine.SubjectHasPermission(ctxWithCancel, subjectResource, action, resource)

switch {
case errors.Is(err, query.ErrActionNotAssigned):
// do nothing
case errors.Is(err, query.ErrInvalidAction):
resp.Error = fmt.Sprintf("invalid action '%s' for resource '%s'", action, resource.ID.String())
case err != nil:
resp.Error = err.Error()
default:
resp.Allowed = true
}

responses[i] = resp
}(ctx, i, check.Action, check.Resource)
}

wg.Wait()

return c.JSON(http.StatusOK, responses)
}
1 change: 1 addition & 0 deletions internal/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func (r *Router) Routes(rg *echo.Group) {
// /allow is the permissions check endpoint
v1.GET("/allow", r.checkAction)
v1.POST("/allow", r.checkAllActions)
v1.POST("/allow/bulk", r.bulkCheckActions)
}

v2 := rg.Group("api/v2")
Expand Down

0 comments on commit 6fe19eb

Please sign in to comment.