@@ -39,6 +39,8 @@ defmodule HttpCapabilityGateway.Gateway do
3939
4040 alias HttpCapabilityGateway.PolicyCompiler
4141 alias HttpCapabilityGateway.Proxy
42+ alias HttpCapabilityGateway.RateLimiter
43+ alias HttpCapabilityGateway.SafeTrust
4244
4345 # Safe HTTP verb conversion with allowlist.
4446 #
@@ -68,6 +70,8 @@ defmodule HttpCapabilityGateway.Gateway do
6870 plug ( Plug.Logger )
6971 plug ( :security_headers )
7072 plug ( :strip_untrusted_headers )
73+ plug ( :extract_trust )
74+ plug ( RateLimiter )
7175 plug ( :match )
7276 plug ( :dispatch )
7377
@@ -160,6 +164,22 @@ defmodule HttpCapabilityGateway.Gateway do
160164 end
161165 end
162166
167+ # Extract trust level from headers/mTLS and store in conn.assigns.
168+ #
169+ # This plug runs BEFORE the RateLimiter plug in the pipeline so that
170+ # rate limiting decisions can be based on the authenticated trust level.
171+ # The trust level is parsed through SafeTrust.parse_trust/1 which
172+ # safely maps strings to atoms from a fixed set (no String.to_atom).
173+ #
174+ # The trust level is stored in conn.assigns[:trust_level] and reused
175+ # by both the rate limiter and the request handler, avoiding duplicate
176+ # extraction work.
177+ defp extract_trust ( conn , _opts ) do
178+ trust_level_str = extract_trust_level ( conn )
179+ trust_level = SafeTrust . parse_trust ( trust_level_str )
180+ Plug.Conn . assign ( conn , :trust_level , trust_level )
181+ end
182+
163183 # Health check endpoint - doesn't require policy
164184 get "/health" do
165185 handle_health_check ( conn )
@@ -265,7 +285,12 @@ defmodule HttpCapabilityGateway.Gateway do
265285
266286 verb ->
267287 # Valid HTTP method -- proceed with policy evaluation.
268- trust_level = extract_trust_level ( conn )
288+ #
289+ # Trust level was already extracted and parsed by the :extract_trust
290+ # plug earlier in the pipeline (stored in conn.assigns[:trust_level]).
291+ # This avoids duplicate header parsing and ensures the rate limiter
292+ # and request handler see the same trust level.
293+ trust_level = Map . get ( conn . assigns , :trust_level , :untrusted )
269294
270295 Logger . info ( "Processing request" ,
271296 path: path ,
@@ -291,20 +316,26 @@ defmodule HttpCapabilityGateway.Gateway do
291316 else
292317 # Lookup policy rule using tiered strategy:
293318 # Tier 1: O(1) exact literal path match
294- # Tier 2: O(r) regex route pattern scan
319+ # Tier 2: O(r) regex route pattern scan (dedicated regex table)
295320 # Tier 3: O(1) global verb rule fallback
296321 case PolicyCompiler . lookup ( policy_table , path , verb ) do
297322 { :ok , rule } ->
298- # Evaluate access decision based on trust level vs exposure requirement
299- case evaluate_access ( trust_level , rule . exposure ) do
300- :allow ->
323+ # Evaluate access decision using SafeTrust.evaluate/2.
324+ # This replaces the ad-hoc evaluate_access/2 function with the
325+ # formally verified trust hierarchy from proven/SafeTrust.idr.
326+ # SafeTrust.evaluate/2 returns {:allow, t, e} or {:deny, t, e}
327+ # providing a structured audit trail for every decision.
328+ exposure = SafeTrust . parse_exposure ( rule . exposure )
329+
330+ case SafeTrust . evaluate ( trust_level , exposure ) do
331+ { :allow , _t , _e } ->
301332 # Forward to backend -- trust level satisfies exposure requirement
302333 duration_us = System . monotonic_time ( ) - start_time
303334 log_decision ( request_id , path , verb , trust_level , :allow , rule , duration_us )
304335
305336 Proxy . forward ( conn , rule )
306337
307- :deny ->
338+ { :deny , _t , _e } ->
308339 # Access denied -- apply stealth profile if configured, otherwise 403
309340 duration_us = System . monotonic_time ( ) - start_time
310341 log_decision ( request_id , path , verb , trust_level , :deny , rule , duration_us )
@@ -506,43 +537,38 @@ defmodule HttpCapabilityGateway.Gateway do
506537 end
507538 end
508539
509- # Evaluate if trust level satisfies exposure requirement
510- @ spec evaluate_access ( trust_level :: String . t ( ) , exposure :: String . t ( ) ) :: :allow | :deny
511- defp evaluate_access ( trust_level , exposure ) do
512- case { trust_level , exposure } do
513- # Public endpoints - anyone can access
514- { _ , "public" } -> :allow
515-
516- # Authenticated endpoints - authenticated or internal only
517- { "authenticated" , "authenticated" } -> :allow
518- { "internal" , "authenticated" } -> :allow
519-
520- # Internal endpoints - internal only
521- { "internal" , "internal" } -> :allow
540+ # NOTE: The previous ad-hoc evaluate_access/2 function has been removed.
541+ # All access decisions now go through SafeTrust.evaluate/2 which implements
542+ # the formally verified trust hierarchy from proven/SafeTrust.idr.
543+ # The access decision is: rank(trust) >= rank(exposure), where ranks are
544+ # untrusted=0, authenticated=1, internal=2 for trust, and
545+ # public=0, authenticated=1, internal=2 for exposure.
546+ # See HttpCapabilityGateway.SafeTrust for the single source of truth.
522547
523- # All other combinations - deny
524- _ -> :deny
525- end
526- end
527-
528- # Handle denied requests - apply stealth profile if configured
548+ # Handle denied requests - apply stealth profile if configured.
549+ #
550+ # trust_level is now a SafeTrust atom (:untrusted, :authenticated, :internal).
551+ # We convert to string for stealth profile map lookups and JSON responses,
552+ # since stealth profiles use string keys matching the DSL v1 format.
529553 defp handle_denial ( conn , rule , trust_level ) do
530554 stealth_profile = get_stealth_profile ( rule . stealth_profile )
555+ trust_str = Atom . to_string ( trust_level )
531556
532557 { status_code , response_body } =
533558 case stealth_profile do
534559 nil ->
535- # No stealth - return clear error
560+ # No stealth - return clear error with trust/exposure atoms as strings
536561 { 403 , % {
537562 error: "Forbidden" ,
538563 message: "Insufficient trust level for this operation" ,
539564 required: rule . exposure ,
540- provided: trust_level
565+ provided: trust_str
541566 } }
542567
543568 profile when is_map ( profile ) ->
544- # Apply stealth - return configured status for trust level
545- code = get_stealth_code ( profile , trust_level , rule . exposure )
569+ # Apply stealth - return configured status for trust level.
570+ # Stealth profile keys are strings matching DSL v1 format.
571+ code = get_stealth_code ( profile , trust_str , rule . exposure )
546572 message = get_stealth_message ( code )
547573 { code , % { error: message } }
548574 end
@@ -689,13 +715,26 @@ defmodule HttpCapabilityGateway.Gateway do
689715 |> send_resp ( 503 , Jason . encode! ( response ) )
690716
691717 true ->
692- # Ready to serve traffic
693- rule_count = :ets . info ( policy_table , :size )
718+ # Ready to serve traffic.
719+ # Report rule counts from both main and regex tables.
720+ regex_table = Application . get_env ( :http_capability_gateway , :policy_regex_table )
721+
722+ main_count = :ets . info ( policy_table , :size )
723+
724+ regex_count =
725+ if regex_table && :ets . whereis ( regex_table ) != :undefined ,
726+ do: :ets . info ( regex_table , :size ) ,
727+ else: 0
728+
729+ rate_limiter_buckets = RateLimiter . bucket_count ( )
694730
695731 response = % {
696732 status: "ready" ,
697733 service: "http-capability-gateway" ,
698- policy_rules: rule_count
734+ policy_rules: main_count + regex_count ,
735+ main_table_rules: main_count ,
736+ regex_table_rules: regex_count ,
737+ rate_limiter_buckets: rate_limiter_buckets
699738 }
700739
701740 conn
0 commit comments