GraphQL Gateway
GraphQL Gateway
Section titled “GraphQL Gateway”GraphQL Gateway vystavuje jeden sjednocený GraphQL endpoint nad hrstkou core Strapi entit. Každý resolver deleguje na podkladové Strapi REST API se service-level hlavičkou Authorization: Bearer ${STRAPI_API_TOKEN} — gateway sám žádná data nepersistuje.
Klíčové údaje
Section titled “Klíčové údaje”- Port: 4000
- Tech: Fastify 5, Apollo Server 5 (přes
@as-integrations/fastify), GraphQL 16, TypeScript - Feature flag:
GRAPHQL_ENABLEDmusí býttrue, jinak/graphqlvrací503 - Auth: Keycloak Bearer JWT je vyžadován na každém requestu — ověřuje se proti KC JWKS endpointu
- Introspection: zakázáno, když
NODE_ENV=production
Schéma
Section titled “Schéma”type CloudInstance { id: ID! name: String! provider: String! status: String! region: String! org_id: ID!}
type Incident { id: ID! title: String! severity: String! status: String! org_id: ID! created_at: String!}
type AuditLog { id: ID! action: String! actor_email: String resource_type: String! created_at: String!}
type OrgMember { id: ID! email: String! role: String! org_id: ID!}
type Query { cloudInstances(orgId: ID!): [CloudInstance!]! incidents(orgId: ID!, limit: Int): [Incident!]! auditLogs(orgId: ID!, limit: Int): [AuditLog!]! orgMembers(orgId: ID!): [OrgMember!]!}Schéma je záměrně ploché — mezi čtyřmi typy neexistují žádná vnořená/relační pole. incidents a auditLogs mají výchozí limit 20, respektive 50, pokud argument není zadán.
Ukázkové query
Section titled “Ukázkové query”query OrgOverview($orgId: ID!) { cloudInstances(orgId: $orgId) { id name provider status region } incidents(orgId: $orgId, limit: 5) { id title severity status created_at }}curl -X POST http://localhost:4000/graphql \ -H "Authorization: Bearer $KC_JWT" \ -H "Content-Type: application/json" \ -d '{"query":"query($orgId: ID!){ orgMembers(orgId:$orgId){ id email role } }","variables":{"orgId":"1"}}'Auth a org-scoping
Section titled “Auth a org-scoping”Každý request musí nést platný Keycloak JWT (Authorization: Bearer <token>), ověřovaný v src/auth.ts:
- Podpis + issuer — ověřuje se proti Keycloak JWKS endpointu (pouze RS256).
- Audience/
azpkontrola — token musí nést claimazpneboaudodpovídajícíKEYCLOAK_CLIENT_ID(výchozísencai-frontend). Token vydaný pro jiného klienta ve stejném realmu je odmítnut, i když je platně podepsán.
Nad rámec autentizace všechny čtyři resolvery (cloudInstances, incidents, auditLogs, orgMembers) volají assertOrgAccess() před dotazem na Strapi: ověřený Keycloak sub volajícího musí být skutečným členem orgId předaného jako argument query, jinak resolver vyhodí GraphQLError s extensions.code = "FORBIDDEN". Bez této kontroly by byl orgId triviální cross-tenant IDOR — kterýkoliv autentizovaný volající by mohl předat id cizí organizace a číst její data. Platform admini (KC realm role ADMIN_KC_REALM_ROLE, výchozí sencai-admin) kontrolu členství obcházejí.
Limity proti zneužití
Section titled “Limity proti zneužití”Protože schéma nemá žádné vnoření, samotný depth limit nechrání proti batchingu přes aliasy (např. vydání stejné query desítkykrát pod různými aliasy v jednom requestu). Gateway proto skládá tři nezávislé limity:
| Limit | Env proměnná | Výchozí | Mechanismus |
|---|---|---|---|
| Hloubka query | QUERY_DEPTH_LIMIT | 10 | graphql-depth-limit |
| Složitost query | QUERY_COMPLEXITY_LIMIT | 1000 | graphql-query-complexity |
| Aliasovaná root pole | QUERY_ALIAS_LIMIT | 15 | vlastní validation rule, src/validation-rules.ts |
Requesty překračující kterýkoliv z těchto limitů jsou odmítnuty dřív, než se dostanou k resolveru.
Další ochrany: @fastify/rate-limit (100 req/min per IP), @fastify/helmet, @fastify/cors.
Každé query — úspěšné i neúspěšné — emituje gateway.graphql.query audit event přes @sencai/audit, a to ještě před kontrolou org-access. Payload obsahuje:
query_hash— prvních 16 hex znaků SHA-256 hashe surového query stringuorg_id— argumentorgIdz query
Endpoints
Section titled “Endpoints”| Metoda | Cesta | Auth | Popis |
|---|---|---|---|
| GET/POST | /graphql | Bearer KC JWT | GraphQL endpoint (vyžaduje GRAPHQL_ENABLED=true) |
| GET | /health | Žádná | Health check |
Konfigurace
Section titled “Konfigurace”Kompletní šablona je v .env.example. Klíčové proměnné:
PORT=4000GRAPHQL_ENABLED=false # musí být true pro aktivaci endpointu
QUERY_DEPTH_LIMIT=10QUERY_COMPLEXITY_LIMIT=1000QUERY_ALIAS_LIMIT=15
STRAPI_URL=http://sencai-backend:1337STRAPI_API_TOKEN= # read přístup k cloud-instances, incidents, audit-logs, organisation-members
KEYCLOAK_URL=http://keycloak:8080/kcKEYCLOAK_REALM=sencaiKEYCLOAK_CLIENT_ID=sencai-frontendADMIN_KC_REALM_ROLE=sencai-admin
RABBITMQ_URL=amqp://admin:admin@sencai-mq:5672NODE_ENV=developmentČasté problémy
Section titled “Časté problémy”Q: 503 na /graphql
A: GRAPHQL_ENABLED není true. Nastavit a restartovat službu.
Q: 401 na /graphql
A: Bearer token chybí, je expirovaný, nebo neprojde JWKS/audience validací. Zkontrolovat, že KEYCLOAK_CLIENT_ID odpovídá azp/aud v tokenu.
Q: FORBIDDEN (ekvivalent 403) GraphQL chyba na query
A: Volající je autentizovaný, ale není členem požadovaného orgId. Použít účet, který je skutečně členem dané organizace, nebo účet s realm role ADMIN_KC_REALM_ROLE.
Q: “Query exceeds maximum allowed aliases” / chyby depth / complexity
A: Query narazila na QUERY_ALIAS_LIMIT, QUERY_DEPTH_LIMIT nebo QUERY_COMPLEXITY_LIMIT. Zmenšit tvar query, nebo zvýšit příslušnou env proměnnou, pokud je limit pro legitimní use-case skutečně příliš přísný.
Q: Audit eventy se neobjevují
A: RABBITMQ_URL není nastavená nebo je nedostupná — gateway vypíše warning a event zahodí, request ale neselže.