Document batch authorization
Describe AuthorizeBatch semantics (all-or-nothing, single-entity-type, single-organization, batch attribute requirement, audit logging), the authz.NewBatchAuthorizeFunc / WithBatch* helpers, the MCP Resolver.AuthorizeBatch entry point, and the new batch-style AuthorizationAttributes implementation contract (non-empty, deduplicated, same-entity-type input; only found rows returned). Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -54,6 +54,35 @@ The flow:
|
|||||||
6. Return an authorization scope (`*coredata.Scope`) for downstream data access
|
6. Return an authorization scope (`*coredata.Scope`) for downstream data access
|
||||||
7. Return `ErrInsufficientPermissions` if no allow match
|
7. Return `ErrInsufficientPermissions` if no allow match
|
||||||
|
|
||||||
|
## Batch authorization
|
||||||
|
|
||||||
|
Use batch authorization when a caller needs all-or-nothing authorization across
|
||||||
|
multiple resources for the same action:
|
||||||
|
|
||||||
|
```go
|
||||||
|
scope, err := iamService.Authorizer.AuthorizeBatch(ctx, iam.AuthorizeBatchParams{
|
||||||
|
Principal: identityID,
|
||||||
|
Action: probo.ActionTaskDelete,
|
||||||
|
Resources: taskIDs, // all resources must have same entity type + organization
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch semantics:
|
||||||
|
- **All-or-nothing** — the first denied resource returns `ErrInsufficientPermissions`
|
||||||
|
- **Single-entity-type batch** — mixed entity types return `ErrMixedEntityTypeBatch`
|
||||||
|
- **Single-organization batch** — mixed or missing `organization_id` attributes return `ErrMixedOrganizationBatch`
|
||||||
|
- **Empty resource list** returns `ErrEmptyResourceBatch`
|
||||||
|
- **Batch attributes are required** — each resource type in `AuthorizeBatch` must implement batch attributes loading or it returns `ErrBatchAuthorizationUnsupportedResourceType`
|
||||||
|
- **Shared `ResourceAttributes` map** is applied to every resource in the batch
|
||||||
|
- **Audit logs** are written per resource only when all resources are authorized (`DryRun` skips logs)
|
||||||
|
|
||||||
|
GraphQL wrappers can use `authz.NewBatchAuthorizeFunc(...)` with
|
||||||
|
`authz.WithBatchAttr`, `authz.WithBatchDryRun`, and
|
||||||
|
`authz.WithBatchSkipAssumptionCheck`.
|
||||||
|
|
||||||
|
MCP resolvers can use `Resolver.AuthorizeBatch(ctx, resourceIDs, action)` for
|
||||||
|
the same behavior and error mapping as single-resource authorization.
|
||||||
|
|
||||||
## PolicySet
|
## PolicySet
|
||||||
|
|
||||||
Policies are organized into identity-scoped (applied to all authenticated users) and role-based:
|
Policies are organized into identity-scoped (applied to all authenticated users) and role-based:
|
||||||
@@ -98,20 +127,42 @@ Key paths use `principal.ATTR` or `resource.ATTR` (e.g., `principal.organization
|
|||||||
Resources that support authorization must implement this interface in `pkg/coredata/`:
|
Resources that support authorization must implement this interface in `pkg/coredata/`:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (v *ThirdParty) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
|
func (v *ThirdParty) AuthorizationAttributes(
|
||||||
q := `SELECT organization_id FROM thirdParties WHERE id = $1 LIMIT 1;`
|
ctx context.Context,
|
||||||
var organizationID gid.GID
|
conn pg.Querier,
|
||||||
if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil {
|
resourceIDs []gid.GID,
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
) (map[gid.GID]map[string]string, error) {
|
||||||
return nil, ErrResourceNotFound
|
q := `SELECT id, organization_id FROM third_parties WHERE id = ANY(@resource_ids::text[])`
|
||||||
}
|
|
||||||
|
rows, err := conn.Query(ctx, q, pgx.StrictNamedArgs{"resource_ids": resourceIDs})
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot query third party authorization attributes: %w", err)
|
return nil, fmt.Errorf("cannot query third party authorization attributes: %w", err)
|
||||||
}
|
}
|
||||||
return map[string]string{"organization_id": organizationID.String()}, nil
|
defer rows.Close()
|
||||||
|
|
||||||
|
attrsByID := make(map[gid.GID]map[string]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var id, organizationID gid.GID
|
||||||
|
if err := rows.Scan(&id, &organizationID); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan third party authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
attrsByID[id] = map[string]string{"organization_id": organizationID.String()}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot iterate third party authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrsByID, nil
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The returned map provides attributes for condition evaluation (e.g., `resource.organization_id`).
|
The returned map provides attributes for condition evaluation (e.g., `resource.organization_id`).
|
||||||
|
`AuthorizationAttributes` implementers should assume caller-side preconditions:
|
||||||
|
- `resourceIDs` is non-empty
|
||||||
|
- `resourceIDs` is deduplicated
|
||||||
|
- in `AuthorizeBatch`, all resources are the same entity type
|
||||||
|
|
||||||
|
Implementers should return only found rows keyed by id. Missing resources are handled by caller-side per-resource existence checks.
|
||||||
|
|
||||||
## Error types
|
## Error types
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user