Security 

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

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.

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.

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

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 + OWNER403 (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 and cannot be overridden by configuration.

Denied access 

When access is denied, the API returns:

  • 401 Unauthorized — when no user is authenticated but one is required.
  • 403 Forbidden — when the authenticated user does not have sufficient privileges.