Security 

The security section assigns an access role to each operation. Access is checked by the AccessController before the request reaches the operation handler.

How a client authenticates as a frontend user, a backend user or with an API key is described in Authentication.

Default access roles 

Operations not listed in security fall back to sensible defaults:

Operation Default Explanation
list PUBLIC Read operations are publicly accessible without authentication.
show PUBLIC Read operations are publicly accessible without authentication.
create DISABLED Write operations are disabled until explicitly configured.
update DISABLED Write operations are disabled until explicitly configured.
delete DISABLED Write operations are disabled until explicitly configured.

Access roles 

Role Description
AccessRole::PUBLIC No authentication required. Anyone can access the endpoint.
AccessRole::DISABLED Always denied regardless of authentication. Used as the implicit default for write operations that have no explicit security configuration.
AccessRole::FE_USER Requires a logged-in frontend user.
AccessRole::FE_GROUP Requires a frontend user belonging to a specific group. Use [AccessRole::FE_GROUP, [1,2]] to restrict to specific group IDs.
AccessRole::BE_USER Requires any authenticated backend user.
AccessRole::BE_ADMIN Requires an admin backend user.
AccessRole::OWNER Authenticated FE user whose UID matches the record's ownership column. See Ownership below.
AccessRole::API_KEY Requires a credential sent with the request (API key, token) instead of a session. Use [AccessRole::API_KEY, ['articles:write']] to also require scopes. See Scopes below.

Configuration 

use MaikSchneider\TcaApi\Enum\AccessRole;

'security' => [
    // list and show are PUBLIC by default — only specify to restrict them
    'create' => AccessRole::FE_USER,
    'update' => AccessRole::OWNER,      // Only the record owner may update
    'delete' => AccessRole::OWNER,
],
Copied!

Callable voters 

For custom access logic, use a callable instead of an AccessRole enum value. The callable receives the PSR-7 server request and an optional record array (for object-level security):

'security' => [
    'create' => [MyAccessChecker::class, 'checkCreatePermission'],
],
Copied!

The callable must return true to grant access or false to deny it. The caller is available as Principal::fromRequest($request), see Authenticators.

class MyAccessChecker
{
    public static function checkCreatePermission(
        \Psr\Http\Message\ServerRequestInterface $request,
        ?array $record = null
    ): bool {
        // Custom access logic
        return true;
    }
}
Copied!

Scopes 

Credentials can carry scopes that limit what they may do. A scope names a resource and an access level:

Scope Grants
articles:read list, show and userinfo on articles
articles:write create, update and delete on articles
articles:* All operations on articles
*:read Read access to every resource

Scopes always restrict. A credential with scopes may only perform the operations they cover, even when its role would allow more, so a key bound to a frontend user is never more powerful than its scopes. This also applies to PUBLIC operations and to nested creates into another resource. Sessions and credentials without scopes are unrestricted.

A role can additionally require scopes:

'security' => [
    'show'   => AccessRole::API_KEY,                       // any credential
    'create' => [AccessRole::API_KEY, ['articles:write']], // a credential with the scope
    'update' => AccessRole::FE_USER,                       // session or user-bound credential
],
Copied!

Invalid scope strings fail when the configuration is loaded. Which scopes a credential carries is decided by the extension that authenticates it, see Authenticators.

Ownership 

The ownership section enables declarative record-level security for write operations. It pairs with AccessRole::OWNER in the security config.

use MaikSchneider\TcaApi\Enum\AccessRole;

'security' => [
    'create' => AccessRole::FE_USER,
    'update' => AccessRole::OWNER,
    'delete' => AccessRole::OWNER,
],
'ownership' => [
    'column' => 'fe_user_id',   // DB column holding the owner's FE user UID
],
Copied!

What this does:

  • On create — the fe_user_id column is automatically set to the authenticated FE user's UID server-side. The client cannot supply this value; it is stripped from the request body regardless of the column's groups config.
  • On update/delete — the record's fe_user_id is compared to the current FE user's UID. If they don't match the request returns 403.

Separate tracking vs. auth columns 

Use setOnCreate when you want an additional tracking column alongside the auth column:

'ownership' => [
    'column'      => 'fe_user_id',      // column checked on update/delete (also written on create)
    'setOnCreate' => 'fe_creator_id',   // additional column written on create only
],
Copied!

On create, both column and setOnCreate receive the FE user UID. column must be populated for OWNER auth to work on subsequent update/delete; setOnCreate provides an immutable "created by" audit trail in a separate DB column.

BE_ADMIN bypass 

Backend admins bypass ownership checks by default. Set beAdminBypass: false to enforce ownership for admins too:

'ownership' => [
    'column'        => 'fe_user_id',
    'beAdminBypass' => false,           // default: true
],
Copied!

Ownership config reference 

Key Required Default Description
column When using OWNER — DB column holding owner UID; compared on update/delete.
setOnCreate No Same as column Column auto-set on create (if different from column).
beAdminBypass No true When true, BE_ADMIN skips the ownership check.

Behaviour notes 

  • AccessRole::OWNER without ownership.column configured → 403 (fail-secure).
  • Unauthenticated request + OWNER → 403 (no FE user found).
  • setOnCreate without a logged-in FE user → column is not set (no injection if user is null).
  • Ownership columns are always stripped from client input regardless of groups config.
  • OWNER is only meaningful on update and delete; using it on list/show will always deny (no single record to compare against).

Table write restrictions 

The API enforces a built-in deny list for tables that must never be written through the API regardless of security configuration. Attempting to write to any of these tables returns 403 Forbidden:

  • be_users, be_groups, be_sessions
  • fe_sessions
  • sys_filemounts, sys_be_shortcuts, sys_action, sys_log

This protection is unconditional — no resource configuration can unblock these tables. The policy can be tightened further (an allow list, or additional denied tables) and every write is audit-logged; see Write privilege model.

Denied access 

When access is denied, the API returns:

  • 401 Unauthorized — when the request carries credentials that are invalid, e.g. a wrong, expired or revoked API key.
  • 403 Forbidden — when the caller is anonymous or does not have sufficient privileges.

See How tca_api sees identity.