|
5 | 5 | "errors"
|
6 | 6 | "fmt"
|
7 | 7 | "net/http"
|
| 8 | + "sync" |
8 | 9 | "time"
|
9 | 10 |
|
10 | 11 | "github.com/labstack/echo/v4"
|
@@ -129,6 +130,17 @@ type checkResult struct {
|
129 | 130 | Error error
|
130 | 131 | }
|
131 | 132 |
|
| 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 | + |
132 | 144 | // checkAllActions will check if a subject is allowed to perform an action on a list of resources.
|
133 | 145 | // This is the permissions check endpoint.
|
134 | 146 | // 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) {
|
312 | 324 |
|
313 | 325 | return values[0], true
|
314 | 326 | }
|
| 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 | +} |
0 commit comments