Skip to content

Commit 6fe19eb

Browse files
authored
Add Bulk Permission Checks Endpoint (#294)
* 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]>
1 parent 169ee48 commit 6fe19eb

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

internal/api/permissions.go

+124
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"sync"
89
"time"
910

1011
"github.com/labstack/echo/v4"
@@ -129,6 +130,17 @@ type checkResult struct {
129130
Error error
130131
}
131132

133+
type bulkCheckActionsRequest []checkAction
134+
135+
type checkActionResponse struct {
136+
ResourceID string `json:"resource_id"`
137+
Action string `json:"action"`
138+
Allowed bool `json:"allowed"`
139+
Error string `json:"error,omitempty"`
140+
}
141+
142+
type bulkCheckActionsResponse []checkActionResponse
143+
132144
// checkAllActions will check if a subject is allowed to perform an action on a list of resources.
133145
// This is the permissions check endpoint.
134146
// It will return a 200 if the subject is allowed to perform all requested resource actions.
@@ -312,3 +324,115 @@ func getParam(c echo.Context, name string) (string, bool) {
312324

313325
return values[0], true
314326
}
327+
328+
// bulkCheckActions will check if a subject is allowed to perform a list of
329+
// actions on a list of resources provided in the request body.
330+
//
331+
// This endpoint will always return 200 on successful checks, regardless of the
332+
// outcome of the checks.
333+
// It will return a 400 if the request is invalid.
334+
func (r *Router) bulkCheckActions(c echo.Context) error {
335+
ctx, span := tracer.Start(c.Request().Context(), "api.bulkCheckAction")
336+
defer span.End()
337+
338+
// Subject validation
339+
subjectResource, err := r.currentSubject(c)
340+
if err != nil {
341+
return err
342+
}
343+
344+
var reqBody bulkCheckActionsRequest
345+
346+
if err := c.Bind(&reqBody); err != nil {
347+
return echo.NewHTTPError(http.StatusBadRequest, "error parsing request body").SetInternal(err)
348+
}
349+
350+
// validate requests
351+
var (
352+
validationResp bulkCheckActionsResponse = make([]checkActionResponse, 0, len(reqBody))
353+
validationErrors = make([]error, 0, len(reqBody))
354+
checks = make([]checkRequest, 0, len(reqBody))
355+
)
356+
357+
for _, req := range reqBody {
358+
resourceID, err := gidx.Parse(req.ResourceID)
359+
if err != nil {
360+
err = fmt.Errorf("error parsing resource ID: %w", err)
361+
362+
validationResp = append(validationResp, checkActionResponse{
363+
ResourceID: req.ResourceID,
364+
Action: req.Action,
365+
Error: err.Error(),
366+
})
367+
368+
validationErrors = append(validationErrors, err)
369+
370+
continue
371+
}
372+
373+
resource, err := r.engine.NewResourceFromID(resourceID)
374+
if err != nil {
375+
err = fmt.Errorf("error creating resource from ID: %w", err)
376+
377+
validationResp = append(validationResp, checkActionResponse{
378+
ResourceID: req.ResourceID,
379+
Action: req.Action,
380+
Error: err.Error(),
381+
})
382+
383+
validationErrors = append(validationErrors, err)
384+
385+
continue
386+
}
387+
388+
checks = append(checks, checkRequest{
389+
Resource: resource,
390+
Action: req.Action,
391+
})
392+
}
393+
394+
if len(validationErrors) != 0 {
395+
return echo.NewHTTPError(http.StatusBadRequest, validationResp).SetInternal(multierr.Combine(validationErrors...))
396+
}
397+
398+
// check permissions
399+
var (
400+
responses bulkCheckActionsResponse = make([]checkActionResponse, len(checks))
401+
wg = &sync.WaitGroup{}
402+
)
403+
404+
for i, check := range checks {
405+
wg.Add(1)
406+
407+
go func(ctx context.Context, i int, action string, resource types.Resource) {
408+
defer wg.Done()
409+
410+
ctxWithCancel, cancel := context.WithTimeout(ctx, maxCheckDuration)
411+
defer cancel()
412+
413+
resp := checkActionResponse{
414+
ResourceID: resource.ID.String(),
415+
Action: action,
416+
}
417+
418+
err := r.engine.SubjectHasPermission(ctxWithCancel, subjectResource, action, resource)
419+
420+
switch {
421+
case errors.Is(err, query.ErrActionNotAssigned):
422+
// do nothing
423+
case errors.Is(err, query.ErrInvalidAction):
424+
resp.Error = fmt.Sprintf("invalid action '%s' for resource '%s'", action, resource.ID.String())
425+
case err != nil:
426+
resp.Error = err.Error()
427+
default:
428+
resp.Allowed = true
429+
}
430+
431+
responses[i] = resp
432+
}(ctx, i, check.Action, check.Resource)
433+
}
434+
435+
wg.Wait()
436+
437+
return c.JSON(http.StatusOK, responses)
438+
}

internal/api/router.go

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func (r *Router) Routes(rg *echo.Group) {
7676
// /allow is the permissions check endpoint
7777
v1.GET("/allow", r.checkAction)
7878
v1.POST("/allow", r.checkAllActions)
79+
v1.POST("/allow/bulk", r.bulkCheckActions)
7980
}
8081

8182
v2 := rg.Group("api/v2")

0 commit comments

Comments
 (0)