How does service security posture work with 'open' and 'constrain by'?
← Web Services · Ref: Q1292
Every EK9 service operation must have a security posture. The posture is either inherited from the parent service or declared on the operation itself.
SERVICE-LEVEL POSTURE
Declare 'open' or 'constrain by X' on the service. All operations inherit:
PublicApi :/api open SecureApi :/api constrain by JwtGate
OPERATION-LEVEL POSTURE
When the service has no posture, each operation must declare its own:
MixedApi :/api health() as GET for :/status open data() as GET for :/data constrain by JwtGate
WHY REQUIRED (E12050)
Security must be a conscious decision. A service that silently defaults to 'open' creates bugs where developers forget authentication. The compiler enforces explicit posture on every endpoint.
SECURITYGATE AND CORSPOLICY
Constraints reference functions extending SecurityGate or CORSPolicy:
MyGate is SecurityGate — authenticates requests MyCors is CORSPolicy — validates CORS origins
Use 'constrain by MyGate' for auth only, or 'constrain by MyGate and MyCors' for auth plus CORS.
See Q1293 for constraint type resolution. See Q1294 for constraint type validation. See Q1295 for duplicate constraint roles. See Q657 for URI mapping. See Q685 for method bodies.
Example
defines module qa.webdeep.securityposture defines constant jsonType <- "application/json" noCache <- "no-cache" langEn <- "en" okStatus <- 200 defines function <?- SecurityGate implementation that accepts all authenticated requests. -?> JwtGate is SecurityGate -> context as HTTPContext <- rtn as HTTPContext: context <?- CORSPolicy that allows specific origins. -?> AllowedOrigins is CORSPolicy -> origin as String <- rtn as Boolean: true defines service <?- Service-level posture: all operations inherit 'open'. -?> PublicApi :/api/public open health() as GET for :/health <- response as HTTPResponse: () with trait HTTPResponse override content() <- rtn as String: `{"status": "healthy"}` override status() as pure <- rtn as Integer: okStatus override contentType() as pure <- rtn as String: jsonType override cacheControl() as pure <- rtn as String: noCache override contentLanguage() as pure <- rtn as String: langEn default operator ? <?- Service-level posture: all operations inherit 'constrain by'. -?> SecureApi :/api/secure constrain by JwtGate data() as GET for :/data <- response as HTTPResponse: () with trait HTTPResponse override content() <- rtn as String: `{"items": []}` override status() as pure <- rtn as Integer: okStatus override contentType() as pure <- rtn as String: jsonType override cacheControl() as pure <- rtn as String: noCache override contentLanguage() as pure <- rtn as String: langEn default operator ? <?- Service with both SecurityGate and CORSPolicy. -?> FullApi :/api/full constrain by JwtGate and AllowedOrigins info() as GET for :/info <- response as HTTPResponse: () with trait HTTPResponse override content() <- rtn as String: `{"name": "FullApi"}` override status() as pure <- rtn as Integer: okStatus override contentType() as pure <- rtn as String: jsonType override cacheControl() as pure <- rtn as String: noCache override contentLanguage() as pure <- rtn as String: langEn default operator ? <?- Operation-level posture: service has none, each operation declares its own. -?> MixedApi :/api/mixed health() as GET for :/health open <- response as HTTPResponse: () with trait HTTPResponse override content() <- rtn as String: "ok" override status() as pure <- rtn as Integer: okStatus override contentType() as pure <- rtn as String: "text/plain" override cacheControl() as pure <- rtn as String: noCache override contentLanguage() as pure <- rtn as String: langEn default operator ? secure() as GET for :/secure constrain by JwtGate <- response as HTTPResponse: () with trait HTTPResponse override content() <- rtn as String: `{"secure": true}` override status() as pure <- rtn as Integer: okStatus override contentType() as pure <- rtn as String: jsonType override cacheControl() as pure <- rtn as String: noCache override contentLanguage() as pure <- rtn as String: langEn default operator ? defines application SecurityPostureApp register PublicApi() register SecureApi() register FullApi() register MixedApi() defines program ServiceSecurityPostureDemo() stdout <- Stdout() stdout.println("Service security posture patterns:") stdout.println(" PublicApi: service-level 'open'") stdout.println(" SecureApi: service-level 'constrain by JwtGate'") stdout.println(" FullApi: service-level 'constrain by JwtGate and AllowedOrigins'") stdout.println(" MixedApi: operation-level mixed postures")
Common mistakes
E12050 — A service with no security posture whose operation also lacks its own posture is rejected — add 'open' or 'constrain by X'. See ek9 -h E12050 for details.
Incorrect:
PublicApi :/api/public
Correct:
PublicApi :/api/public open
Other ways to ask this
- What is E12050 service security not declared?
- How do I declare security on an EK9 service?
- What is the difference between open and constrain by on a service?
- Can service operations have their own security posture?
Coming from another language?
Java: Spring Security uses @PreAuthorize or SecurityFilterChain — separate from controllers. Python: Flask-Login or decorators, optional and easily forgotten. Go: middleware wrapping, no compile-time check. Rust: Actix middleware, runtime only. EK9: security posture is part of the service grammar, enforced at compile time. No endpoint can exist without an explicit posture.
Keywords: posture, constrain, CORS, authentication, CORSPolicy, SecurityGate, security, service, open, http, E12050