@@ -138,42 +138,75 @@ defmodule HttpCapabilityGateway.PolicyCompiler do
138138 @ spec lookup ( table :: ets_table ( ) , path :: String . t ( ) , verb :: atom ( ) ) ::
139139 { :ok , CompiledRule . t ( ) } | { :error , :no_match }
140140 def lookup ( table , path , verb ) when is_atom ( verb ) do
141- # Iterate through all rules and find first matching pattern
141+ # Lookup strategy: route-specific rules OVERRIDE global rules
142+ # 1. Check if path matches any route-specific patterns
143+ # 2. If yes, only allow verbs defined for that route
144+ # 3. If no route match, fall back to global rules
142145 case :ets . tab2list ( table ) do
143146 [ ] ->
144147 { :error , :no_match }
145148
146149 rules ->
147- Enum . find_value ( rules , { :error , :no_match } , fn { _key , rule } ->
148- if rule . verb == verb and Regex . match? ( rule . path_regex , path ) do
149- { :ok , rule }
150- else
151- nil
152- end
153- end )
150+ # Split rules into route-specific and global
151+ { route_rules , global_rules } =
152+ Enum . split_with ( rules , fn
153+ { { :global , _ } , _rule } -> false
154+ _ -> true
155+ end )
156+
157+ # Check if path matches any route pattern
158+ matching_route_pattern =
159+ Enum . find_value ( route_rules , fn { { pattern , _verb } , rule } ->
160+ if Regex . match? ( rule . path_regex , path ) , do: pattern , else: nil
161+ end )
162+
163+ case matching_route_pattern do
164+ nil ->
165+ # No route-specific pattern matches, use global rules
166+ find_matching_rule ( global_rules , path , verb )
167+
168+ pattern ->
169+ # Route-specific pattern matches, only allow verbs for this route
170+ # Find rule for this pattern and verb
171+ case Enum . find ( route_rules , fn { { p , v } , _rule } -> p == pattern and v == verb end ) do
172+ { _key , rule } -> { :ok , rule }
173+ nil -> { :error , :no_match }
174+ end
175+ end
154176 end
155177 end
156178
179+ # Helper to find matching rule in a list
180+ defp find_matching_rule ( rules , path , verb ) do
181+ Enum . find_value ( rules , { :error , :no_match } , fn { _key , rule } ->
182+ if rule . verb == verb and Regex . match? ( rule . path_regex , path ) do
183+ { :ok , rule }
184+ else
185+ nil
186+ end
187+ end )
188+ end
189+
157190 # Compile global verb rules that apply to all paths (unless overridden)
158191 defp compile_global_verbs ( errors , policy , table ) do
159- global_verbs = Map . get ( policy , "verbs" , % { } )
192+ # DSL v1 format: governance.global_verbs is a list of verb strings
193+ global_verbs = get_in ( policy , [ "governance" , "global_verbs" ] ) || [ ]
160194
161- Enum . reduce ( global_verbs , errors , fn { verb_str , config } , acc ->
195+ Enum . reduce ( global_verbs , errors , fn verb_str , acc ->
162196 verb_atom = String . to_existing_atom ( verb_str )
163197
164198 if verb_atom not in @ valid_http_verbs do
165199 [ { :global_verb , "Invalid HTTP verb: #{ verb_str } " } | acc ]
166200 else
167- exposure = Map . get ( config , "exposure" )
168-
169- # Global rules match any path
201+ # DSL v1: global verbs have no specific exposure level, default to "public"
202+ # (Gateway will handle access control based on trust levels)
170203 rule = % CompiledRule {
171204 path_pattern: ".*" ,
172205 path_regex: ~r/ .*/ ,
173206 verb: verb_atom ,
174- exposure: exposure ,
175- stealth_profile: get_default_stealth_profile ( policy ) ,
176- narrative: Map . get ( config , "narrative" )
207+ exposure: "public" , # Default for global verbs
208+ stealth_profile: get_stealth_enabled ( policy ) ,
209+ narrative: nil
177210 }
178211
179212 # Use verb atom as part of key for global rules
@@ -185,31 +218,32 @@ defmodule HttpCapabilityGateway.PolicyCompiler do
185218
186219 # Compile route-specific overrides that take precedence over globals
187220 defp compile_route_overrides ( errors , policy , table ) do
188- routes = Map . get ( policy , "routes" , [ ] )
221+ # DSL v1 format: governance.routes is a list of route configs
222+ routes = get_in ( policy , [ "governance" , "routes" ] ) || [ ]
189223
190224 Enum . reduce ( routes , errors , fn route , acc ->
191225 path_pattern = Map . get ( route , "path" )
192- route_verbs = Map . get ( route , "verbs" , % { } )
226+ # DSL v1: route.verbs is a list of verb strings
227+ route_verbs = Map . get ( route , "verbs" , [ ] )
193228
194229 # Compile the regex pattern
195230 case Regex . compile ( path_pattern ) do
196231 { :ok , path_regex } ->
197- # Compile each verb override for this route
198- Enum . reduce ( route_verbs , acc , fn { verb_str , config } , verb_acc ->
232+ # Compile each verb for this route
233+ Enum . reduce ( route_verbs , acc , fn verb_str , verb_acc ->
199234 verb_atom = String . to_existing_atom ( verb_str )
200235
201236 if verb_atom not in @ valid_http_verbs do
202237 [ { :route_verb , "Invalid HTTP verb in route: #{ verb_str } " } | verb_acc ]
203238 else
204- exposure = Map . get ( config , "exposure" )
205-
239+ # DSL v1: route-specific verbs override globals
206240 rule = % CompiledRule {
207241 path_pattern: path_pattern ,
208242 path_regex: path_regex ,
209243 verb: verb_atom ,
210- exposure: exposure ,
211- stealth_profile: Map . get ( config , "stealth_profile" ) || get_default_stealth_profile ( policy ) ,
212- narrative: Map . get ( config , "narrative" )
244+ exposure: "public" , # DSL v1 doesn't specify exposure per-route
245+ stealth_profile: get_stealth_enabled ( policy ) ,
246+ narrative: nil
213247 }
214248
215249 # Route-specific rules override globals - use path pattern in key
@@ -224,14 +258,21 @@ defmodule HttpCapabilityGateway.PolicyCompiler do
224258 end )
225259 end
226260
227- # Extract default stealth profile name from policy
228- defp get_default_stealth_profile ( policy ) do
229- case get_in ( policy , [ "stealth" , "default" ] ) do
230- nil -> nil
231- profile when is_binary ( profile ) -> profile
261+ # Extract stealth configuration from DSL v1 policy
262+ # DSL v1: stealth = %{"enabled" => bool, "status_code" => int}
263+ # Return "default" if stealth is enabled, nil otherwise
264+ defp get_stealth_enabled ( policy ) do
265+ case get_in ( policy , [ "stealth" , "enabled" ] ) do
266+ true -> "default" # Use "default" as profile name for enabled stealth
267+ _ -> nil
232268 end
233269 end
234270
271+ # Get stealth status code from DSL v1 policy
272+ defp get_stealth_status_code ( policy ) do
273+ get_in ( policy , [ "stealth" , "status_code" ] ) || 404
274+ end
275+
235276 @ doc """
236277 Returns statistics about a compiled policy table.
237278
0 commit comments