# Permissions

Authorisation is enforced **on the server for every action**. Hiding a control is presentation, never a control.

## Roles

| Capability | Guest | Member | Moderator | Admin |
|---|---|---|---|---|
| Read public content | ✓ | ✓ | ✓ | ✓ |
| Ask, answer, comment | | ✓ | ✓ | ✓ |
| Vote | | ✓ | ✓ | ✓ |
| Follow | | ✓ | ✓ | ✓ |
| Report | | ✓ | ✓ | ✓ |
| Accept an answer on own question | | ✓ | ✓ | ✓ |
| Edit own profile | | ✓ | ✓ | ✓ |
| Manage API tokens | | ✓ | ✓ | ✓ |
| Accept any answer | | | ✓ | ✓ |
| Publish, hide, delete content | | | ✓ | ✓ |
| Lock and pin topics | | | ✓ | ✓ |
| Resolve reports | | | ✓ | ✓ |
| Manage users and roles | | | | ✓ |
| Settings, appearance, languages | | | | ✓ |
| Plugins, system, audit | | | | ✓ |
| Editorial personas and seeding | | | | ✓ |

## Account status gates

| Status | Effect |
|---|---|
| `active` | Normal |
| `suspended` | Cannot sign in or post |
| `banned` | Cannot sign in |

## Account type gates

| Type | Can sign in | Counts as a member | Profile indexable |
|---|---|---|---|
| `member` | ✓ | ✓ | if it meets the threshold |
| `editorial` | ✗ | ✗ | `noindex` by default |
| `system` | ✗ | ✗ | `noindex` |

`login_disabled` is checked during authentication, so an editorial account cannot sign in even with a valid password hash.

## Additional gates on every write

1. POST method
2. Valid CSRF token
3. Active signed-in account where the action requires one
4. Role and ownership check
5. Rate limit
6. Posting policy and spam assessment

All six apply to the API as well as to the web forms.

## Trust level

`trust_level` influences the spam assessment. It is not a role and never grants a capability from this table.

## For plugin authors

A filter cannot grant a permission. Returning `true` from a generic filter does not bypass CSRF, authorisation, or moderation policy — those decisions are made by the core after filters run.

## Verified behaviour

`tests/Security/AuthorizationContractRegression.php` exercises these decisions rather than trusting the code to read correctly:

- `canLogin()` allows an active member and refuses editorial, system, suspended and banned accounts — and refuses any account with `login_disabled`, whatever its type.
- Authentication and transient status pages render `noindex`; public content pages stay indexable.
- An unknown route answers 404 and `POST /` answers 405.
- Write handlers validate CSRF.

Sign-in eligibility is decided by account type, status and `login_disabled` together. Hiding a control is presentation; these checks are the control.
