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