---
title: "Security"
manual: "TCA_API"
version: "2.2"
permalink: "https://docs.typo3.org/permalink/maikschneider/tca-api:security@2.2"
source: "Configuration/Security.rst"
rendered: "2026-10-12T13:17:40+00:00"
---

# Security {#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 {#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. |

> [!IMPORTANT]
> Listing an operation in `general.operations` is not enough to enable it.
> Write operations (`create`, `update`, `delete`) also require an
> explicit `security` entry. Without one they return **403 Forbidden**.

## Access roles {#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](https://docs.typo3.org/permalink/maikschneider/tca-api:ownership@2.2) below. |

## Configuration {#configuration}

```php
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,
],
```

## Callable voters {#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):

```php
'security' => [
    'create' => [MyAccessChecker::class, 'checkCreatePermission'],
],
```

The callable must return `true` to grant access or `false` to deny it.

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

## Ownership {#ownership}

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

```php
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
],
```

**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 {#separate-tracking-vs-auth-columns}

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

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

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 {#be-admin-bypass}

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

```php
'ownership' => [
    'column'        => 'fe_user_id',
    'beAdminBypass' => false,           // default: true
],
```

### Ownership config reference {#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 {#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 {#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](https://docs.typo3.org/permalink/maikschneider/tca-api:write-privileges@2.2).

## Denied access {#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.
