@@ -16,7 +16,7 @@ const action: ActionDefinition = {
1616
1717describe ( "ActionPolicyService" , ( ) => {
1818 it ( "allows actions by default" , ( ) => {
19- expect ( new ActionPolicyService ( ) . evaluate ( action ) ) . toEqual ( { allowed : true } ) ;
19+ expect ( new ActionPolicyService ( ) . evaluate ( action ) ) . toEqual ( { allowed : true , checks : [ ] } ) ;
2020 } ) ;
2121
2222 it ( "enforces exact and provider-wide allowlists" , ( ) => {
@@ -26,15 +26,18 @@ describe("ActionPolicyService", () => {
2626 } ) ;
2727 expect ( new ActionPolicyService ( { allowedActions : [ "github.*" ] } ) . evaluate ( action ) ) . toEqual ( {
2828 allowed : true ,
29+ checks : [ { source : "deployment" , outcome : "allow_match" , rule : "github.*" } ] ,
2930 } ) ;
3031 expect ( new ActionPolicyService ( { allowedActions : [ "github.create_issue" ] } ) . evaluate ( action ) ) . toEqual ( {
3132 allowed : true ,
33+ checks : [ { source : "deployment" , outcome : "allow_match" , rule : "github.create_issue" } ] ,
3234 } ) ;
3335 } ) ;
3436
3537 it ( "supports bare wildcard to match all actions" , ( ) => {
3638 expect ( new ActionPolicyService ( { allowedActions : [ "*" ] } ) . evaluate ( action ) ) . toEqual ( {
3739 allowed : true ,
40+ checks : [ { source : "deployment" , outcome : "allow_match" , rule : "*" } ] ,
3841 } ) ;
3942 expect ( new ActionPolicyService ( { blockedActions : [ "*" ] } ) . evaluate ( action ) ) . toMatchObject ( {
4043 allowed : false ,
@@ -55,23 +58,37 @@ describe("ActionPolicyService", () => {
5558 } ) ;
5659
5760 it ( "allows proxies by default" , ( ) => {
58- expect ( new ActionPolicyService ( ) . evaluateProxy ( "github" ) ) . toEqual ( { allowed : true } ) ;
61+ expect ( new ActionPolicyService ( ) . evaluateProxy ( "github" ) ) . toEqual ( { allowed : true , checks : [ ] } ) ;
5962 } ) ;
6063
6164 it ( "ignores action policy when evaluating proxies" , ( ) => {
6265 expect ( new ActionPolicyService ( { allowedActions : [ "github.get_current_user" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( {
6366 allowed : true ,
67+ checks : [ ] ,
6468 } ) ;
6569 expect ( new ActionPolicyService ( { blockedActions : [ "github.delete_repository" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( {
6670 allowed : true ,
71+ checks : [ ] ,
72+ } ) ;
73+ expect ( new ActionPolicyService ( { allowedActions : [ "*" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( {
74+ allowed : true ,
75+ checks : [ ] ,
76+ } ) ;
77+ expect ( new ActionPolicyService ( { blockedActions : [ "*" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( {
78+ allowed : true ,
79+ checks : [ ] ,
6780 } ) ;
68- expect ( new ActionPolicyService ( { allowedActions : [ "*" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( { allowed : true } ) ;
69- expect ( new ActionPolicyService ( { blockedActions : [ "*" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( { allowed : true } ) ;
7081 } ) ;
7182
7283 it ( "ignores proxy policy when evaluating actions" , ( ) => {
73- expect ( new ActionPolicyService ( { blockedProxies : [ "*" ] } ) . evaluate ( action ) ) . toEqual ( { allowed : true } ) ;
74- expect ( new ActionPolicyService ( { allowedProxies : [ "slack" ] } ) . evaluate ( action ) ) . toEqual ( { allowed : true } ) ;
84+ expect ( new ActionPolicyService ( { blockedProxies : [ "*" ] } ) . evaluate ( action ) ) . toEqual ( {
85+ allowed : true ,
86+ checks : [ ] ,
87+ } ) ;
88+ expect ( new ActionPolicyService ( { allowedProxies : [ "slack" ] } ) . evaluate ( action ) ) . toEqual ( {
89+ allowed : true ,
90+ checks : [ ] ,
91+ } ) ;
7592 } ) ;
7693
7794 it ( "disables every proxy with a blocked wildcard" , ( ) => {
@@ -88,9 +105,11 @@ describe("ActionPolicyService", () => {
88105 } ) ;
89106 expect ( new ActionPolicyService ( { allowedProxies : [ "github" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( {
90107 allowed : true ,
108+ checks : [ { source : "deployment" , outcome : "allow_match" , rule : "github" } ] ,
91109 } ) ;
92110 expect ( new ActionPolicyService ( { allowedProxies : [ "*" ] } ) . evaluateProxy ( "github" ) ) . toEqual ( {
93111 allowed : true ,
112+ checks : [ { source : "deployment" , outcome : "allow_match" , rule : "*" } ] ,
94113 } ) ;
95114 } ) ;
96115
@@ -109,4 +128,91 @@ describe("ActionPolicyService", () => {
109128 it ( "parses comma-separated environment lists" , ( ) => {
110129 expect ( parseActionPolicyList ( " github.* , gmail.send_email ,, " ) ) . toEqual ( [ "github.*" , "gmail.send_email" ] ) ;
111130 } ) ;
131+
132+ it ( "intersects deployment, runtime, and token action allowlists" , ( ) => {
133+ const snapshot = new ActionPolicyService ( { allowedActions : [ "github.*" ] } ) . createSnapshot (
134+ {
135+ allowedActions : [ "github.create_issue" ] ,
136+ blockedActions : [ ] ,
137+ allowedProxies : [ ] ,
138+ blockedProxies : [ ] ,
139+ } ,
140+ { allowedActions : [ "github.*" ] , blockedActions : [ ] } ,
141+ ) ;
142+
143+ expect ( snapshot . evaluate ( action ) ) . toEqual ( {
144+ allowed : true ,
145+ checks : [
146+ { source : "deployment" , outcome : "allow_match" , rule : "github.*" } ,
147+ { source : "runtime" , outcome : "allow_match" , rule : "github.create_issue" } ,
148+ { source : "token" , outcome : "allow_match" , rule : "github.*" } ,
149+ ] ,
150+ } ) ;
151+ } ) ;
152+
153+ it ( "reports the decisive layer when a lower allowlist rejects" , ( ) => {
154+ const snapshot = new ActionPolicyService ( { allowedActions : [ "github.*" ] } ) . createSnapshot ( {
155+ allowedActions : [ "gmail.*" ] ,
156+ blockedActions : [ ] ,
157+ allowedProxies : [ ] ,
158+ blockedProxies : [ ] ,
159+ } ) ;
160+
161+ expect ( snapshot . evaluate ( action ) ) . toMatchObject ( {
162+ allowed : false ,
163+ code : "action_not_allowed" ,
164+ checks : [
165+ { source : "deployment" , outcome : "allow_match" , rule : "github.*" } ,
166+ { source : "runtime" , outcome : "allow_miss" } ,
167+ ] ,
168+ } ) ;
169+ } ) ;
170+
171+ it ( "applies Runtime and token block rules before every allowlist" , ( ) => {
172+ const service = new ActionPolicyService ( { allowedActions : [ "*" ] } ) ;
173+ const runtimeBlocked = service . createSnapshot ( {
174+ allowedActions : [ "github.*" ] ,
175+ blockedActions : [ "github.create_issue" ] ,
176+ allowedProxies : [ ] ,
177+ blockedProxies : [ ] ,
178+ } ) ;
179+ expect ( runtimeBlocked . evaluate ( action ) ) . toMatchObject ( {
180+ allowed : false ,
181+ code : "action_blocked" ,
182+ checks : [ { source : "runtime" , outcome : "block_match" , rule : "github.create_issue" } ] ,
183+ } ) ;
184+
185+ const tokenBlocked = service . createSnapshot (
186+ {
187+ allowedActions : [ "github.*" ] ,
188+ blockedActions : [ ] ,
189+ allowedProxies : [ ] ,
190+ blockedProxies : [ ] ,
191+ } ,
192+ { allowedActions : [ "github.*" ] , blockedActions : [ "github.create_issue" ] } ,
193+ ) ;
194+ expect ( tokenBlocked . evaluate ( action ) ) . toMatchObject ( {
195+ allowed : false ,
196+ checks : [ { source : "token" , outcome : "block_match" , rule : "github.create_issue" } ] ,
197+ } ) ;
198+ } ) ;
199+
200+ it ( "records only the first matching rule from each layer" , ( ) => {
201+ const decision = new ActionPolicyService ( { allowedActions : [ "github.*" , "*" ] } )
202+ . createSnapshot ( {
203+ allowedActions : [ "github.create_issue" , "github.*" ] ,
204+ blockedActions : [ ] ,
205+ allowedProxies : [ ] ,
206+ blockedProxies : [ ] ,
207+ } )
208+ . evaluate ( action ) ;
209+
210+ expect ( decision ) . toEqual ( {
211+ allowed : true ,
212+ checks : [
213+ { source : "deployment" , outcome : "allow_match" , rule : "github.*" } ,
214+ { source : "runtime" , outcome : "allow_match" , rule : "github.create_issue" } ,
215+ ] ,
216+ } ) ;
217+ } ) ;
112218} ) ;
0 commit comments