Přeskočit na obsah

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.

  • Port: 4000
  • Tech: Fastify 5, Apollo Server 5 (přes @as-integrations/fastify), GraphQL 16, TypeScript
  • Feature flag: GRAPHQL_ENABLED musí být true, jinak /graphql vrací 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
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.

query OrgOverview($orgId: ID!) {
cloudInstances(orgId: $orgId) {
id
name
provider
status
region
}
incidents(orgId: $orgId, limit: 5) {
id
title
severity
status
created_at
}
}
Terminál
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"}}'

Každý request musí nést platný Keycloak JWT (Authorization: Bearer <token>), ověřovaný v src/auth.ts:

  1. Podpis + issuer — ověřuje se proti Keycloak JWKS endpointu (pouze RS256).
  2. Audience/azp kontrola — token musí nést claim azp nebo aud odpoví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í.

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:

LimitEnv proměnnáVýchozíMechanismus
Hloubka queryQUERY_DEPTH_LIMIT10graphql-depth-limit
Složitost queryQUERY_COMPLEXITY_LIMIT1000graphql-query-complexity
Aliasovaná root poleQUERY_ALIAS_LIMIT15vlastní 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 stringu
  • org_id — argument orgId z query
MetodaCestaAuthPopis
GET/POST/graphqlBearer KC JWTGraphQL endpoint (vyžaduje GRAPHQL_ENABLED=true)
GET/healthŽádnáHealth check

Kompletní šablona je v .env.example. Klíčové proměnné:

Terminál
PORT=4000
GRAPHQL_ENABLED=false # musí být true pro aktivaci endpointu
QUERY_DEPTH_LIMIT=10
QUERY_COMPLEXITY_LIMIT=1000
QUERY_ALIAS_LIMIT=15
STRAPI_URL=http://sencai-backend:1337
STRAPI_API_TOKEN= # read přístup k cloud-instances, incidents, audit-logs, organisation-members
KEYCLOAK_URL=http://keycloak:8080/kc
KEYCLOAK_REALM=sencai
KEYCLOAK_CLIENT_ID=sencai-frontend
ADMIN_KC_REALM_ROLE=sencai-admin
RABBITMQ_URL=amqp://admin:admin@sencai-mq:5672
NODE_ENV=development

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.