Security
The security section assigns an access role to each operation. Access is
checked by the
Access 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. |
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
| 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,
],
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'],
],
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;
}
}
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
],
What this does:
- On create — the
fe_user_idcolumn 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'sgroupsconfig. - On update/delete — the record's
fe_user_idis 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
],
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
],
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::OWNERwithoutownership.columnconfigured → 403 (fail-secure).- Unauthenticated request +
OWNER→ 403 (no FE user found). setOnCreatewithout 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
groupsconfig. OWNERis only meaningful onupdateanddelete; using it onlist/showwill 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_sessionsfe_sessionssys_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.