Přeskočit na obsah

Strapi backend

Strapi je headless CMS a REST API backend pro Sencai. Spravuje veškerá platformní data: uživatele, organizace, instance, registrace a další.

  • Verze: v5
  • Port: 1337 (dev), routováno přes Traefik
  • Admin: http://localhost:1337/admin
  • Databáze: MySQL (backend-db:3306)
  • Tech: Node.js 20+, TypeScript, Knex (migrace)

Upozornění: Strapi aktuálně definuje 212 content-types (stav Fáze 3, viz src/api/ v sencai.space). Seznam níže je malý ilustrativní výběr pokrývající hlavní domény — není vyčerpávající. Před spoléháním na konkrétní název nebo tvar content-type vždy zkontrolujte src/api/ jako autoritativní aktuální zdroj.

  • User — Synchronizováno s Keycloakem, profilová pole (name, surname, email, jobTitle, adresa)
  • Registration — Registrační požadavky, čekající na ověření emailu
  • Organisation — Workspace pro týmy
  • Organisation-Member (pivot) — Propojuje uživatele s organizacemi přes RBAC role (owner, admin, member, viewer)
  • CloudInstance — Provisionované VM/kontejnery u cloud providerů
  • CloudProvider — Přihlašovací údaje a konfigurace cloud providera
  • CustomRegistry — Docker registry (privátní nebo veřejné)
  • DockerHubCache — Cache oblíbených Docker images
  • GitTemplate — Předpřipravené Git šablony pro rychlý setup
  • Invitation — Pozvánky do organizace s tokenovým přijetím
  • ApiToken — Uživatelské API tokeny pro programový přístup
  • Billing, Billing-Event — Záznamy usage/subscription billingu a event log
  • Billing-Portal — Stav zákaznického billing portálu
  • Organisation-Billing — Billing konfigurace/tarif per organizace
  • Agent-Config, Agent-Policy, Agent-Release — Konfigurace, politika a release tracking on-host fleet agenta (sencai-agent)
  • Agent-Metric-Snapshot — Snímky fleet telemetrie
  • Fleet-Cohort — Seskupení fleet agentů pro rollout/policy targeting
  • K8s-Agent — Registrace/stav Kubernetes fleet agenta (k8s-agent)
  • Scim, Scim-Token — SCIM provisioning záznamy a bearer tokeny
  • Llm-Token-Usage, Llm-Usage — Usage/token metering záznamy LLM fasády (llm-service)
  • Onboarding-State — Průběh onboarding wizardu per organizace/uživatel
  • Feedback, Nps-Response — In-app feedback a odpovědi na NPS průzkum

Každý content-type má:

  • REST endpointy: GET /api/<plural>, POST /api/<plural>, PUT /api/<plural>/:id, DELETE /api/<plural>/:id
  • Filtrování a stránkování: ?filters[field][$eq]=value&populate=*&sort=-updatedAt&pagination[pageSize]=10
  • Politiky: RBAC přes src/policies/ (např. is-organisation-member) — celkový přehled viz Policies (RBAC) níže
  • Webhooky: Spouštěné při create/update/delete pro event streaming

Strapi spouští hooky v klíčových fázích životního cyklu. Sencai je používá pro cross-service synchronizaci:

Soubor: src/lifecycles/user-kc-sync.ts

Při vytvoření nebo aktualizaci Strapi Usera:

plugin::users-permissions.user afterCreate/afterUpdate
runWithKcSync() ověří AsyncLocalStorage flag
→ Pokud není potlačeno: POST webhook-publisher/webhook-publisher
→ Event: { eventName: 'user.update', entity: user, ... }
→ RabbitMQ user.events exchange
→ auth-service-consumer zpracuje
KC admin API aktualizuje atributy uživatele

Ochrana proti smyčce: Používá AsyncLocalStorage k prevenci nekonečných smyček:

  • runWithoutKcSync() — Deaktivuje KC sync pro daný async kontext
  • Používá se při KC → Strapi synchronizaci, aby se zabránilo zpětnému promítnutí do KC

Při aktualizaci Strapi Usera se tato pole synchronizují do Keycloaku:

  • email ↔ KC email
  • username ↔ KC sub (UUID, read-only)
  • confirmed/emailVerified ↔ KC emailVerified
  • blocked/enabled ↔ KC enabled
  • Profil: name, surname, jobTitle, street, city, zip_code, country

Strapi validuje JWT tokeny z Keycloaku pomocí jwks-rsa:

Soubor: src/middlewares/keycloak-jwt.ts

// Validuje JWT podpis přes Keycloak JWKS endpoint
// Dekóduje uživatelské údaje (sub, email, name, atd.)
// Pokud uživatel v Strapi neexistuje nebo je profil neúplný:
// → Synchronizovat z KC před povolením přístupu
// → Zavolat runWithoutKcSync, aby se předešlo smyčce

Obsah JWT: Keycloak vydává RS256-signované tokeny s:

  • sub — UUID uživatele (stává se Strapi username)
  • email, name, family_name — Profilová pole
  • exp — 12hodinová expirace
  • iat, iss, aud — Standardní claims
Terminál
curl -H "Authorization: Bearer <JWT>" http://localhost:1337/api/users/me
Terminál
curl -X POST http://localhost:1337/api/organisations \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp","description":"AI team"}'
Terminál
curl http://localhost:1337/api/cloud-instances?populate=* \
-H "Authorization: Bearer <JWT>"
Terminál
curl "http://localhost:1337/api/organisations?pagination[page]=2&pagination[pageSize]=10&sort=-createdAt"

Strapi umí posílat webhooky externím službám. Sencai používá Webhook Publisher jako příjemce:

  1. Jít do Strapi Admin → Settings → Webhooks

  2. Vytvořit nový webhook:

    • URL: http://webhook-publisher:1347/webhook-publisher
    • Events: Zaškrtnout Entry Create, Entry Update, Entry Delete pro cílové content-types
    • Headers: Volitelné auth hlavičky
  3. Uložit

{
"event": "entry.create",
"createdAt": "2024-01-01T12:00:00Z",
"model": {
"uid": "plugin::users-permissions.user"
},
"entry": {
"id": 1,
"email": "user@example.com",
"username": "user",
"name": "John",
...
}
}

Sencai vynucuje politiky přes Strapi policy systém. src/policies/ obsahuje sadu znovupoužitelných, globálních politik — is-organisation-member (ukázka níže) je jen jedna z nich, ne jediný mechanismus. Celá sada pro org/tenant scoping zahrnuje:

  • is-organisation-member — Základní kontrola členství (jakákoliv role) pro danou organizaci
  • is-organisation-role — Kontrola členství s minimální rolí v hierarchii (owner (4) > admin (3) > member (2) > viewer (1))
  • is-organisation-scoped — Parametrizovaná, znovupoužitelná izolační politika (config.minRole, výchozí viewer) s bypassem pro platform-admin; organizaci pro routy s record-id (:id/:documentId) odvozuje ze skutečné relace organisation daného záznamu, ne z klientem dodaných query filtrů
  • is-organisation-scoped-or-cross-tenant — Varianta, která navíc povoluje přístup přes schválený cross-tenant organisation-relationship (MSP/agenturní model)
  • is-org-auditor / is-org-or-admin-auditor — Politiky s read-scope pro přístup k audit-logu/auditor roli v rámci organizace (nebo platform admin)

Soubor: src/policies/is-organisation-member.ts

module.exports = async (policyContext, config, { strapi }) => {
const { auth } = policyContext.state;
const { organisationId } = policyContext.params;
// Ověřit, že uživatel je členem organizace
const member = await strapi.entityService.findMany(
'api::organisation-member.organisation-member',
{ filters: { user: auth.user.id, organisation: organisationId } }
);
if (!member) throw new Error('Not a member of this organisation');
};

Aplikace na routy:

src/api/cloud-instance/routes/cloud-instance.js
{
method: 'GET',
path: '/cloud-instances/:id',
handler: 'cloud-instance.findOne',
config: {
policies: ['is-organisation-member']
}
}

Org-scoping není vyřešený problém — berte to jako defense-in-depth

Section titled “Org-scoping není vyřešený problém — berte to jako defense-in-depth”

is-organisation-scoped dříve mělo bypass IDOR třídy: útočník mohl odvodit organizaci z jím kontrolované hodnoty query.filters[organisation], zatímco operoval nad záznamem identifikovaným jiným, obětí vlastněným :id/:documentId v cestě. Šlo o jeden z nálezů remediovaných během bezpečnostních prací Fáze 3 (F3.SECREM, evidováno jako S0-1 v SECURITY-AUDIT.md).

Opravená politika nyní odvozuje organizaci pro routy s record-id výhradně z vlastní relace organisation cílového záznamu (nikdy z klientem dodaných query filtrů) a při nemožnosti organizaci odvodit selhává uzavřeně (přístup zamítne). Nicméně:

  • Pro collection routy (find/create bez path :id) není politika sama autoritativním zdrojem pravdy pro to, které řádky se vrátí — samotný controller musí query stále scopovat na straně serveru (načíst ID organizací volajícího a aplikovat explicitní where filtr).
  • Některé controllery content-types implementují vlastní autoritativní izolaci nad rámec (nebo místo) sdílené politiky; v takových případech je is-organisation-scoped jen druhou vrstvou obrany, ne jediným strážcem.

Nepředpokládejte, že org-scoping je plně a obecně vyřešen pouhým připojením politiky k routě — vždy ověřte konkrétní chování scopování query v daném controlleru, zejména u collection endpointů.

Strapi v5 používá Knex pro migrace.

Soubory: database/migrations/

database/migrations/[timestamp]_add_new_field.js
cd sencai.space
npm run knex -- migrate:make add_new_field
exports.up = async (knex) => {
await knex.schema.createTable('users_permissions_users', (table) => {
table.increments('id');
table.string('email').notNullable().unique();
table.timestamps();
});
};
exports.down = async (knex) => {
await knex.schema.dropTable('users_permissions_users');
};

Důležité: Nikdy neupravujte ani nemažte existující migrace. Vždy vytvořte nové.

Migrace se spouští automaticky při startu služby. Ruční spuštění:

Terminál
npm run knex -- migrate:latest

Soubor: src/api/organisation/controllers/organisation.ts

import { factories } from '@strapi/strapi';
export default factories.createCoreController(
'api::organisation.organisation',
({ strapi }) => ({
// Přepsat nebo rozšířit výchozí akce
async findOne(ctx) {
// Vlastní logika zde
const { id } = ctx.params;
const entity = await strapi.entityService.findOne('api::organisation.organisation', id);
return entity;
}
})
);

Soubor: src/api/organisation/services/organisation.ts

import { factories } from '@strapi/strapi';
export default factories.createCoreService(
'api::organisation.organisation',
({ strapi }) => ({
// Znovupoužitelná business logika
async createWithDefaults(data) {
return strapi.entityService.create('api::organisation.organisation', {
data: { ...data, status: 'active' }
});
}
})
);

Citlivá pole (hesla, tokeny) jsou šifrována pomocí ADMIN_ENCRYPTION_KEY:

// Ve schématu content-type
{
type: 'password',
encrypted: true // Používá ADMIN_ENCRYPTION_KEY pro uložení
}
Terminál
curl http://localhost:1337/api/health
  1. http://localhost:1337/admin
  2. Vytvořit admin uživatele při prvním přihlášení
  3. Definovat content-types, spravovat data, konfigurovat webhooky
Terminál
docker logs -f backend
# Nebo při lokálním běhu:
npm run develop
Terminál
# MySQL (Strapi databáze)
mysql -h 127.0.0.1 -u strapi -p sencai_db
# Výpis uživatelů
SELECT id, email, username, confirmed FROM up_users;
# Kontrola organizací
SELECT id, name, description FROM organisations;

Q: Chyba “Model webhook not found”

A: Strapi v5 odstranilo model webhook. Webhooky musí být vytvořeny přes admin UI (Settings → Webhooks), ne přes kód.

Q: Migrace se nespouští

A: Ujistěte se, že je databáze dostupná. Zkontrolujte logy MySQL kontejneru: docker logs backend-db

Q: CORS chyby při requestech z frontendu

A: Strapi CORS je nakonfigurován v config/middlewares.ts. Přidejte URL frontendu do allowedOrigins.

Q: JWT validace selhává

A: Ověřte Keycloak URL v .env: KEYCLOAK_URL=http://keycloak:8080/kc (interní, s prefixem /kc).

Q: Uživatel se nesynchronizuje do Keycloaku

A: Zkontrolujte, že běží webhook-publisher a je dostupné RabbitMQ. Chyby zpracování eventů zjistíte v logách RabbitMQ.