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:

  1. Validates the caller's Cognito JWT.
  2. Resolves the caller's group memberships into an effective permissions object.
  3. 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.