@@ -4,9 +4,13 @@ import { roleAccessBuilder } from './access_builder.js';
44import {
55 ConstructContainer ,
66 ConstructFactoryGetInstanceProps ,
7+ ResourceAccessAcceptor ,
78 ResourceAccessAcceptorFactory ,
89 ResourceProvider ,
10+ SsmEnvironmentEntry ,
911} from '@aws-amplify/plugin-types' ;
12+ import { App , Stack } from 'aws-cdk-lib' ;
13+ import { Policy , PolicyStatement } from 'aws-cdk-lib/aws-iam' ;
1014
1115void describe ( 'storageAccessBuilder' , ( ) => {
1216 const resourceAccessAcceptorMock = mock . fn ( ) ;
@@ -115,13 +119,21 @@ void describe('storageAccessBuilder', () => {
115119 accessDefinition . idSubstitution ,
116120 '${cognito-identity.amazonaws.com:sub}' ,
117121 ) ;
118- assert . deepStrictEqual (
119- accessDefinition . getResourceAccessAcceptors . map (
120- ( getResourceAccessAcceptor ) =>
121- getResourceAccessAcceptor ( stubGetInstanceProps ) ,
122- ) ,
123- [ resourceAccessAcceptorMock ] ,
124- ) ;
122+
123+ // Should have 2 resource access acceptors now: authenticated role + compound group acceptor
124+ assert . equal ( accessDefinition . getResourceAccessAcceptors . length , 2 ) ;
125+
126+ // First acceptor should be the authenticated role
127+ const authAcceptor =
128+ accessDefinition . getResourceAccessAcceptors [ 0 ] ( stubGetInstanceProps ) ;
129+ assert . equal ( authAcceptor , resourceAccessAcceptorMock ) ;
130+
131+ // Second acceptor should be the compound group acceptor
132+ const groupAcceptor =
133+ accessDefinition . getResourceAccessAcceptors [ 1 ] ( stubGetInstanceProps ) ;
134+ assert . equal ( typeof groupAcceptor . identifier , 'string' ) ;
135+ assert . equal ( typeof groupAcceptor . acceptResourceAccess , 'function' ) ;
136+
125137 assert . equal (
126138 getConstructFactoryMock . mock . calls [ 0 ] . arguments [ 0 ] ,
127139 'AuthResources' ,
@@ -172,4 +184,130 @@ void describe('storageAccessBuilder', () => {
172184 [ group1AccessAcceptorMock , group2AccessAcceptorMock ] ,
173185 ) ;
174186 } ) ;
187+
188+ void it ( 'entity permissions apply to all group roles when groups are defined' , ( ) => {
189+ // Mock acceptResourceAccess calls to verify they're made
190+ const adminAcceptResourceAccessMock = mock . fn ( ) ;
191+ const usersAcceptResourceAccessMock = mock . fn ( ) ;
192+ const adminAcceptorMock : ResourceAccessAcceptor = {
193+ identifier : 'adminAcceptor' ,
194+ acceptResourceAccess : adminAcceptResourceAccessMock ,
195+ } ;
196+ const usersAcceptorMock : ResourceAccessAcceptor = {
197+ identifier : 'usersAcceptor' ,
198+ acceptResourceAccess : usersAcceptResourceAccessMock ,
199+ } ;
200+
201+ // Create a group-specific mock that returns the correct types
202+ const groupSpecificMock = mock . fn ( ( roleName : string ) => {
203+ if ( roleName === 'Admins' ) return adminAcceptorMock ;
204+ if ( roleName === 'Users' ) return usersAcceptorMock ;
205+ return resourceAccessAcceptorMock ;
206+ } ) ;
207+
208+ // Mock auth resources with groups
209+ const mockAuthResources = {
210+ resources : {
211+ groups : {
212+ Admins : { role : { } } ,
213+ Users : { role : { } } ,
214+ } ,
215+ } ,
216+ getResourceAccessAcceptor : groupSpecificMock ,
217+ } ;
218+
219+ const mockConstructFactory = mock . fn (
220+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
221+ < T extends ResourceProvider > ( _ : string ) => ( {
222+ getInstance : ( ) => mockAuthResources as unknown as T ,
223+ } ) ,
224+ ) ;
225+
226+ const mockGetInstanceProps : ConstructFactoryGetInstanceProps = {
227+ constructContainer : {
228+ getConstructFactory : mockConstructFactory ,
229+ } as unknown as ConstructContainer ,
230+ } as unknown as ConstructFactoryGetInstanceProps ;
231+
232+ const accessDefinition = roleAccessBuilder
233+ . entity ( 'identity' )
234+ . to ( [ 'read' , 'write' , 'delete' ] ) ;
235+
236+ // Should have 2 resource access acceptors: authenticated role + compound group acceptor
237+ assert . equal ( accessDefinition . getResourceAccessAcceptors . length , 2 ) ;
238+
239+ // Get the compound group acceptor
240+ const groupAcceptor =
241+ accessDefinition . getResourceAccessAcceptors [ 1 ] ( mockGetInstanceProps ) ;
242+
243+ // Verify it has the expected structure
244+ assert . ok ( groupAcceptor . identifier . includes ( 'entityAccessForAllGroups' ) ) ;
245+ assert . ok ( groupAcceptor . identifier . includes ( 'Admins' ) ) ;
246+ assert . ok ( groupAcceptor . identifier . includes ( 'Users' ) ) ;
247+ assert . equal ( typeof groupAcceptor . acceptResourceAccess , 'function' ) ;
248+
249+ // Call acceptResourceAccess on the compound acceptor
250+ const stack = new Stack ( new App ( ) , 'TestStack' ) ;
251+ const mockPolicy = new Policy ( stack , 'TestPolicy' , {
252+ statements : [ new PolicyStatement ( ) ] ,
253+ } ) ;
254+ const mockSsmEntries : SsmEnvironmentEntry [ ] = [ ] ;
255+ groupAcceptor . acceptResourceAccess ( mockPolicy , mockSsmEntries ) ;
256+
257+ // Verify both group roles had the policy applied
258+ assert . equal ( adminAcceptResourceAccessMock . mock . callCount ( ) , 1 ) ;
259+ assert . equal ( usersAcceptResourceAccessMock . mock . callCount ( ) , 1 ) ;
260+ assert . deepStrictEqual (
261+ adminAcceptResourceAccessMock . mock . calls [ 0 ] . arguments ,
262+ [ mockPolicy , mockSsmEntries ] ,
263+ ) ;
264+ assert . deepStrictEqual (
265+ usersAcceptResourceAccessMock . mock . calls [ 0 ] . arguments ,
266+ [ mockPolicy , mockSsmEntries ] ,
267+ ) ;
268+ } ) ;
269+
270+ void it ( 'entity permissions handle case when no groups are defined' , ( ) => {
271+ // Mock auth resources without groups
272+ const mockAuthResources = {
273+ resources : {
274+ groups : undefined ,
275+ } ,
276+ getResourceAccessAcceptor : getResourceAccessAcceptorMock ,
277+ } ;
278+
279+ const mockConstructFactory = mock . fn (
280+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
281+ < T extends ResourceProvider > ( _ : string ) => ( {
282+ getInstance : ( ) => mockAuthResources as unknown as T ,
283+ } ) ,
284+ ) ;
285+
286+ const mockGetInstanceProps : ConstructFactoryGetInstanceProps = {
287+ constructContainer : {
288+ getConstructFactory : mockConstructFactory ,
289+ } as unknown as ConstructContainer ,
290+ } as unknown as ConstructFactoryGetInstanceProps ;
291+
292+ const accessDefinition = roleAccessBuilder
293+ . entity ( 'identity' )
294+ . to ( [ 'read' , 'write' , 'delete' ] ) ;
295+
296+ // Should still have 2 resource access acceptors
297+ assert . equal ( accessDefinition . getResourceAccessAcceptors . length , 2 ) ;
298+
299+ // Get the compound group acceptor (should be empty/no-op)
300+ const groupAcceptor =
301+ accessDefinition . getResourceAccessAcceptors [ 1 ] ( mockGetInstanceProps ) ;
302+ assert . equal ( groupAcceptor . identifier , 'entityAccessForAllGroups-empty' ) ;
303+
304+ // Should not throw when called (no-op behavior)
305+ const stack = new Stack ( new App ( ) , 'TestStack' ) ;
306+ const emptyPolicy = new Policy ( stack , 'EmptyPolicy' , {
307+ statements : [ new PolicyStatement ( ) ] ,
308+ } ) ;
309+ assert . doesNotThrow ( ( ) => {
310+ groupAcceptor . acceptResourceAccess ( emptyPolicy , [ ] ) ;
311+ } ) ;
312+ } ) ;
175313} ) ;
0 commit comments