Security
The security section assigns an access role to each operation. Access is
checked by the
Access 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. |
Important
Listing an operation in general. 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 |
|---|---|
Access | No authentication required. Anyone can access the endpoint. |
Access | Always denied regardless of authentication. Used as the implicit default for write operations that have no explicit security configuration. |
Access | Requires a logged-in frontend user. |
Access | Requires a frontend user belonging to a specific group. Use
[Access to restrict to specific group IDs. |
Access | Requires any authenticated backend user. |
Access | Requires an admin backend user. |
Access | Authenticated FE user whose UID matches the record's ownership column. See Ownership below. |
Access | Requires a credential sent with the request (API key, token) instead of
a session. Use [Access 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,
],
Callable voters
For custom access logic, use a callable instead of an Access 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. The
caller is available as Principal::, see
Authenticators.
class MyAccessChecker
{
public static function checkCreatePermission(
\Psr\Http\Message\ServerRequestInterface $request,
?array $record = null
): bool {
// Custom access logic
return true;
}
}
Scopes
Credentials can carry scopes that limit what they may do. A scope names a resource and an access level:
| Scope | Grants |
|---|---|
articles: | list, show and userinfo on articles |
articles: | create, update and delete on articles |
articles:* | All operations on articles |
*: | 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
],
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 Access 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_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'suser_ id groupsconfig. - On update/delete — the record's
fe_is compared to the current FE user's UID. If they don't match the request returns 403.user_ id
Separate tracking vs. auth columns
Use set 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 set receive the FE user UID.
column must be populated for OWNER auth to work on subsequent
update/delete; set provides an immutable "created by" audit trail in
a separate DB column.
BE_ADMIN bypass
Backend admins bypass ownership checks by default. Set be
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. |
set | No | Same as column | Column auto-set on create (if different from column). |
be | No | true | When true, BE_ skips the ownership check. |
Behaviour notes
AccesswithoutRole:: OWNER ownership.configured → 403 (fail-secure).column - Unauthenticated request +
OWNER→ 403 (no FE user found). setwithout a logged-in FE user → column is not set (no injection if user is null).On Create - 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_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.