Security & Authentication
Security is enforced at three layers: identity (Cognito), the API gateway edge (a pre-process authorizer), and per-handler authorization (fine-grained permission checks). Infrastructure access is scoped by least-privilege links in the serverless configuration.
Identity — Auth_Service (Amazon Cognito)
- Login provider: email only.
- Self-registration: disabled — accounts are created by administrators.
- MFA: optional (per user), delivered via Email_Service/SMS_Service.
- Password policy: minimum length 12, requiring upper-case, numbers and symbols.
- Tokens: Cognito issues JWT access/refresh tokens. The admin UI sends the access token as Authorization: Bearer <token>; auth-refresh exchanges a refresh token for a new access token.
Groups
Cognito holds group memberships that drive authorization:
Platform (system) groups
Group | Capability |
|---|---|
admin | Full platform administrator — access to everything |
site-creator | Can create and manage sites |
site-editor | Can edit settings across sites |
authentication-admin | Can manage users and groups |
Per-site group types (a membership names a siteId + a role)
Site role | Capability on that site |
|---|---|
admin | Full control of the site (implies author + templates + publish) |
author | Create/edit content |
templates | Manage templates |
publish | Publish content |
Edge enforcement — the pre-process authorizer
CMS_API declares preProcessFunction: cms-pre-process. This Lambda runs before the routed handler and:
- Validates the caller's Cognito JWT.
- Resolves the caller's group memberships into an effective permissions object.
- Attaches those permissions to the request context so downstream handlers don't each re-parse the token.
The auth endpoints (/auth/*) and /platform/status are the only routes without auth: Auth_Service; everything else requires a valid token.
Per-handler authorization
Handlers call shared helpers in lib/ to make an explicit decision:
- permissionResolver.resolvePermissions(groups) turns a list of Cognito groups into a structured object:
{
isAdmin, isSiteCreator, isSiteEditor, isAuthAdmin,
sitePermissions: {
"<siteId>": { isAdmin, isAuthor, isTemplates, isPublish }
}
}
- authorizationChecker enforces access, e.g. hasSiteAccess(permissions, siteId):
- platform admin and site-editor implicitly have access to all sites;
- otherwise the caller must hold a per-site group for that siteId.
If permissions are missing the request is rejected 401 UNAUTHORIZED; if present but insufficient, 403 FORBIDDEN.
This two-step design (resolve once at the edge, enforce in each handler) keeps authorization decisions close to the action being performed.
Infrastructure least-privilege — links & permissions
The platform never grants blanket IAM access. Each function declares the specific resources it uses via typed links, and the plugin derives the matching IAM policy:
Link type | Grants |
|---|---|
FUNCTION_TO_DATABASE | Access to the linked DynamoDB table (Content_Store) |
FUNCTION_TO_STORAGE | Access to a specific S3 store (asset/component/publish/backup) |
FUNCTION_TO_AUTHENTICATION | Access to the Cognito user pool |
FUNCTION_TO_FUNCTION | Permission to invoke another Lambda |
API_GATEWAY_TO_FUNCTION / API_GATEWAY_TO_AUTHENTICATION | Wire the API to its handlers and authorizer |
WEBSITE_TO_API_GATEWAY / WEBSITE_TO_STORAGE | Wire the admin site to the API and media |
Cross-cutting actions that aren't covered by a link are named explicitly in a function's permissions array, for example:
- provision-site: dns:create/delete/list, certificate:create/read, website:create/read, storage:*, event-bus:manage.
- publish-page / bulk-publish-worker: website:invalidate.
- backup-*: storage:read/write/list, event-bus:manage.
- sites-delete / delete-site-step: authentication:delete, dns:delete, certificate:delete, website:delete, storage:delete.
So a handler that only reads content (e.g. pages-list) is limited to the content table, while a privileged handler (e.g. provision-site) receives only the additional DNS/certificate/CDN permissions it needs — and nothing more.
Transport & tenancy boundaries
- HTTPS everywhere — the admin UI, both APIs and every site are served over TLS (ACM-issued certificates).
- CORS — CMS_API only accepts the admin origin (https://admin.<rootDomain>).
- Plane isolation — the public delivery plane (Public_API, public-functions, CloudFront) is unauthenticated and read-only; it cannot reach the admin control plane.
- Tenant scoping — per-site data shares the SITE#{siteId} partition and every site-scoped route carries {siteId}, checked against the caller's resolved site permissions before any read or write.
Continue to Technical specifications »