Skip to content

EN Authorization Tenants and Ownership

Cavanshirpro edited this page Sep 19, 2026 · 1 revision

RBAC, tenants and row ownership

English · Türkçe · Languages / Diller

Scope: JSON API Forge 0.5.1 · Source review: 17 September 2026 · Language: English

Authentication establishes who is calling. Authorization determines which action that identity can perform on which data. A Forge principal carries identity kind, subject, roles, permissions and an optional tenant.

Roles and permissions

{
  "roles": {
    "reader": {"permissions": ["notes.list", "notes.read"]},
    "writer": {"inherits": ["reader"], "permissions": ["notes.create", "notes.update"]}
  }
}

Version permission names as part of your application contract. notes.* and * are broad grants. Role inheritance is transitive; cycles and unknown roles require validation. Leaving permission empty is not a general way to make an endpoint public. Where supported, use explicit public=true.

Row policies

tenant_field uses the principal's tenant; owner_field uses its subject. For example:

{
  "tenant_field": "tenant_id",
  "owner_field": "owner_id",
  "owner_actions": ["list", "read", "update", "delete"],
  "owner_bypass_permission": "notes.owner.bypass"
}

This is a resource fragment, not a complete resource. The corresponding columns must exist. Exclude tenant/owner/soft-delete fields from writable_fields. A client-provided tenant_id in the body is not an authority source.

When a tenant field is configured, a principal missing the required tenant context is denied access. Tenant and owner answer different questions: which customer's data, and which identity's record? They can be enforced together.

SQL and custom-endpoint boundaries

Generic CRUD's row policies are not automatically injected into every custom SQL statement or hook. An RPC can apply explicit constraints:

{
  "sql": "UPDATE notes SET title = :title WHERE id = :id AND tenant_id = :tenant AND owner_id = :owner",
  "mode": "execute",
  "params": {
    "id": "$body.id", "title": "$body.title",
    "tenant": "$principal.tenant_id", "owner": "$principal.subject"
  },
  "require_rowcount_min": 1,
  "require_rowcount_max": 1
}

This statement requires a suitable table, input schema and operation permission. Taking the tenant/owner from the request body does not provide the same trust boundary as using the authenticated principal.

Four cases to test

  1. An authorized principal can read/update its own record in its own tenant.
  2. Another tenant with the same role cannot see that record.
  3. A different owner within the same tenant receives the intended restriction.
  4. Bypass works only for explicitly granted identities; cached results do not leak between callers.

Verify actual endpoint behavior, including 404 for invisible rows and 403 for insufficient permissions where applicable. Editor roles use a separate rank/scope model; see the team guide.

Sources and verification

This page describes the behavior at the pinned commits below. Recheck these contracts when upgrading.


English home · All English pages · Version maintenance

Clone this wiki locally