@@ -3,7 +3,8 @@ import { CONSOLE_SCOPES } from "./console-permissions"
33
44export interface ConsoleStatement {
55 Effect : "Allow" | "Deny"
6- Action : string [ ]
6+ Action ?: string [ ]
7+ NotAction ?: string [ ]
78 Resource ?: string [ ]
89}
910
@@ -12,10 +13,36 @@ export interface ConsolePolicy {
1213 Statement : ConsoleStatement [ ]
1314}
1415
15- function matchAction ( policyActions : string [ ] , requestAction : string ) : boolean {
16+ /**
17+ * Check if an action matches the policy actions
18+ * @param policyActions - Array of action patterns (empty array means match all, undefined means no match)
19+ * @param requestAction - The action to check
20+ * @returns true if the action matches
21+ */
22+ function matchAction ( policyActions : string [ ] | undefined , requestAction : string ) : boolean {
23+ // Undefined means no match; explicit empty array means match all actions
24+ if ( policyActions === undefined ) {
25+ return false
26+ }
27+ if ( policyActions . length === 0 ) {
28+ return true
29+ }
1630 return policyActions . some ( ( pattern ) => resourceMatch ( pattern , requestAction ) )
1731}
1832
33+ /**
34+ * Check if an action matches the NotAction patterns
35+ * @param notActions - Array of action patterns to exclude
36+ * @param requestAction - The action to check
37+ * @returns true if the action should be excluded
38+ */
39+ function matchNotAction ( notActions : string [ ] | undefined , requestAction : string ) : boolean {
40+ if ( ! notActions || notActions . length === 0 ) {
41+ return false
42+ }
43+ return notActions . some ( ( pattern ) => resourceMatch ( pattern , requestAction ) )
44+ }
45+
1946const IMPLIED_SCOPES : Record < string , string [ ] > = {
2047 [ CONSOLE_SCOPES . VIEW_BROWSER ] : [
2148 "s3:ListAllMyBuckets" ,
@@ -88,6 +115,13 @@ function matchResource(policyResources: string[] | undefined, requestResource: s
88115 return policyResources . some ( ( pattern ) => resourceMatch ( pattern , requestResource ) )
89116}
90117
118+ /**
119+ * Check if an action is a console scope (starts with "console:" or is "consoleAdmin")
120+ */
121+ function isConsoleScope ( action : string ) : boolean {
122+ return action . startsWith ( "console:" ) || action === CONSOLE_SCOPES . CONSOLE_ADMIN
123+ }
124+
91125export function hasConsolePermission (
92126 policy : ConsolePolicy | ConsoleStatement [ ] | undefined ,
93127 action : string ,
@@ -98,28 +132,100 @@ export function hasConsolePermission(
98132 const statements = Array . isArray ( policy ) ? policy : policy . Statement || [ ]
99133 if ( statements . length === 0 ) return false
100134
101- const denied = statements . some (
102- ( s ) => s . Effect === "Deny" && matchAction ( s . Action , action ) && matchResource ( s . Resource , resource ) ,
103- )
135+ // For console scopes, we should ignore Resource restrictions if Resource doesn't match "console"
136+ // This allows policies with S3 resources to still grant console permissions
137+ const isConsoleAction = isConsoleScope ( action )
138+ const shouldCheckResource = ( s : ConsoleStatement ) : boolean => {
139+ // If action is a console scope and Resource is specified but doesn't match "console",
140+ // we should still allow if Action matches (console scopes are management permissions)
141+ if ( isConsoleAction && s . Resource && s . Resource . length > 0 ) {
142+ // Check if Resource contains console-related resources
143+ const hasConsoleResource = s . Resource . some ( ( r ) => r === "console" || r === "*" || r . includes ( "console" ) )
144+ // If Resource doesn't contain console resources, skip resource check for console actions
145+ if ( ! hasConsoleResource ) {
146+ return false
147+ }
148+ }
149+ return true
150+ }
151+
152+ // Check Deny statements first
153+ const denied = statements . some ( ( s ) => {
154+ if ( s . Effect !== "Deny" ) return false
155+
156+ // If NotAction is present, deny applies to all actions EXCEPT those in NotAction
157+ if ( s . NotAction && s . NotAction . length > 0 ) {
158+ // Deny if action is NOT in NotAction list
159+ if ( ! matchNotAction ( s . NotAction , action ) ) {
160+ return shouldCheckResource ( s ) ? matchResource ( s . Resource , resource ) : false
161+ }
162+ return false
163+ }
164+
165+ // If Action is present (or empty array), deny applies to matching actions
166+ if ( matchAction ( s . Action , action ) ) {
167+ return shouldCheckResource ( s ) ? matchResource ( s . Resource , resource ) : false
168+ }
169+
170+ return false
171+ } )
104172
105173 if ( denied ) return false
106174
175+ // Check Allow statements
107176 const allowed = statements . some ( ( s ) => {
108177 if ( s . Effect !== "Allow" ) return false
109178
179+ // Handle NotAction: allow all actions EXCEPT those in NotAction
180+ if ( s . NotAction && s . NotAction . length > 0 ) {
181+ // If Action is also present, first check if action matches Action
182+ if ( s . Action && s . Action . length > 0 ) {
183+ // Both Action and NotAction present: action must match Action AND not be in NotAction
184+ const actionMatches = matchAction ( s . Action , action )
185+ const adminMatch = matchAction ( s . Action , CONSOLE_SCOPES . CONSOLE_ADMIN )
186+ const wildcardMatch = matchAction ( s . Action , "console:*" )
187+ const adminStarMatch = matchAction ( s . Action , "admin:*" )
188+
189+ if ( ! ( actionMatches || adminMatch || wildcardMatch || adminStarMatch ) ) {
190+ // Check implied actions
191+ const impliedActions = IMPLIED_SCOPES [ action ]
192+ if ( ! impliedActions || ! impliedActions . some ( ( implied ) => matchAction ( s . Action , implied ) ) ) {
193+ return false // Action doesn't match Action list
194+ }
195+ }
196+
197+ // Action matches Action list, now check NotAction exclusion
198+ if ( matchNotAction ( s . NotAction , action ) ) {
199+ return false // Action is excluded by NotAction
200+ }
201+ } else {
202+ // Only NotAction present: allow all actions except those in NotAction
203+ if ( matchNotAction ( s . NotAction , action ) ) {
204+ return false // Action is excluded
205+ }
206+ }
207+ // Action is allowed (matches Action if present, and not in NotAction), check resource
208+ return shouldCheckResource ( s ) ? matchResource ( s . Resource , resource ) : true
209+ }
210+
211+ // Only Action is present (or empty array), allow applies to matching actions
110212 const actionMatch = matchAction ( s . Action , action )
111213 const adminMatch = matchAction ( s . Action , CONSOLE_SCOPES . CONSOLE_ADMIN )
112214 const wildcardMatch = matchAction ( s . Action , "console:*" )
113215 const adminStarMatch = matchAction ( s . Action , "admin:*" )
114216
115217 const explicitMatch =
116- ( actionMatch || adminMatch || wildcardMatch || adminStarMatch ) && matchResource ( s . Resource , resource )
218+ actionMatch || adminMatch || wildcardMatch || adminStarMatch
219+ ? shouldCheckResource ( s )
220+ ? matchResource ( s . Resource , resource )
221+ : true
222+ : false
117223 if ( explicitMatch ) return true
118224
119225 const impliedActions = IMPLIED_SCOPES [ action ]
120226 if ( impliedActions ) {
121227 if ( impliedActions . some ( ( implied ) => matchAction ( s . Action , implied ) ) ) {
122- return true
228+ return shouldCheckResource ( s ) ? matchResource ( s . Resource , resource ) : true
123229 }
124230 }
125231
0 commit comments