Skip to content

Commit ab05ae0

Browse files
bombguyAmplifiyer
andauthored
feat: attach policy & ssm params to access userpool from auth resource (#1090)
* feat: attach policy & ssm params to acces userpool from auth resource * remove action scope, introduce meta actions and granular iam action * update type naming to match other access pattern * Update packages/backend-auth/src/access_builder.ts Co-authored-by: Amplifiyer <[email protected]> * fix comments * fix name * update jsdoc * rename to manageUsers * addressing nit * update permission mapping * update API.md * removed type enforcement between meta & iam actions, updated list * use flatmap --------- Co-authored-by: Amplifiyer <[email protected]>
1 parent 615a3e6 commit ab05ae0

11 files changed

+642
-7
lines changed

.changeset/good-wasps-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend-auth': minor
3+
---
4+
5+
attach policy & ssm params to acces userpool from auth resource

packages/backend-auth/API.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,23 @@ import { AuthResources } from '@aws-amplify/plugin-types';
1111
import { AuthRoleName } from '@aws-amplify/plugin-types';
1212
import { BackendSecret } from '@aws-amplify/plugin-types';
1313
import { ConstructFactory } from '@aws-amplify/plugin-types';
14+
import { ConstructFactoryGetInstanceProps } from '@aws-amplify/plugin-types';
1415
import { ExternalProviderOptions } from '@aws-amplify/auth-construct-alpha';
1516
import { FacebookProviderProps } from '@aws-amplify/auth-construct-alpha';
1617
import { FunctionResources } from '@aws-amplify/plugin-types';
1718
import { GoogleProviderProps } from '@aws-amplify/auth-construct-alpha';
1819
import { OidcProviderProps } from '@aws-amplify/auth-construct-alpha';
20+
import { ResourceAccessAcceptor } from '@aws-amplify/plugin-types';
1921
import { ResourceAccessAcceptorFactory } from '@aws-amplify/plugin-types';
2022
import { ResourceProvider } from '@aws-amplify/plugin-types';
2123
import { TriggerEvent } from '@aws-amplify/auth-construct-alpha';
2224

25+
// @public
26+
export type ActionIam = 'addUserToGroup' | 'createUser' | 'deleteUser' | 'deleteUserAttributes' | 'disableUser' | 'enableUser' | 'forgetDevice' | 'getDevice' | 'getUser' | 'listDevices' | 'listGroupsForUser' | 'removeUserFromGroup' | 'resetUserPassword' | 'setUserMfaPreference' | 'setUserPassword' | 'setUserSettings' | 'updateDeviceStatus' | 'updateUserAttributes';
27+
28+
// @public
29+
export type ActionMeta = 'manageUsers' | 'manageGroupMembership' | 'manageUserDevices' | 'managePasswordRecovery';
30+
2331
// @public
2432
export type AmazonProviderFactoryProps = Omit<AmazonProviderProps, 'clientId' | 'clientSecret'> & {
2533
clientId: BackendSecret;
@@ -30,6 +38,7 @@ export type AmazonProviderFactoryProps = Omit<AmazonProviderProps, 'clientId' |
3038
export type AmplifyAuthProps = Expand<Omit<AuthProps, 'outputStorageStrategy' | 'loginWith'> & {
3139
loginWith: Expand<AuthLoginWithFactoryProps>;
3240
triggers?: Partial<Record<TriggerEvent, ConstructFactory<ResourceProvider<FunctionResources>>>>;
41+
access?: AuthAccessGenerator;
3342
}>;
3443

3544
// @public
@@ -40,6 +49,28 @@ export type AppleProviderFactoryProps = Omit<AppleProviderProps, 'clientId' | 't
4049
privateKey: BackendSecret;
4150
};
4251

52+
// @public (undocumented)
53+
export type AuthAccessBuilder = {
54+
resource: (other: ConstructFactory<ResourceProvider & ResourceAccessAcceptorFactory>) => AuthActionBuilder;
55+
};
56+
57+
// @public (undocumented)
58+
export type AuthAccessDefinition = {
59+
getResourceAccessAcceptor: (getInstanceProps: ConstructFactoryGetInstanceProps) => ResourceAccessAcceptor;
60+
actions: AuthAction[];
61+
};
62+
63+
// @public (undocumented)
64+
export type AuthAccessGenerator = (allow: AuthAccessBuilder) => AuthAccessDefinition[];
65+
66+
// @public (undocumented)
67+
export type AuthAction = ActionIam | ActionMeta;
68+
69+
// @public (undocumented)
70+
export type AuthActionBuilder = {
71+
to: (actions: AuthAction[]) => AuthAccessDefinition;
72+
};
73+
4374
// @public
4475
export type AuthLoginWithFactoryProps = Omit<AuthProps['loginWith'], 'externalProviders'> & {
4576
externalProviders?: ExternalProviderSpecificFactoryProps;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, it, mock } from 'node:test';
2+
import { authAccessBuilder } from './access_builder.js';
3+
import {
4+
ConstructContainer,
5+
ConstructFactoryGetInstanceProps,
6+
ResourceAccessAcceptorFactory,
7+
ResourceProvider,
8+
} from '@aws-amplify/plugin-types';
9+
import assert from 'node:assert';
10+
11+
void describe('allowAccessBuilder', () => {
12+
const resourceAccessAcceptorMock = mock.fn();
13+
14+
const getResourceAccessAcceptorMock = mock.fn(
15+
// allows us to get proper typing on the mock args
16+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
17+
(_: string) => resourceAccessAcceptorMock
18+
);
19+
20+
const getConstructFactoryMock = mock.fn(
21+
// this lets us get proper typing on the mock args
22+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
23+
<T extends ResourceProvider>(_: string) => ({
24+
getInstance: () =>
25+
({
26+
getResourceAccessAcceptor: getResourceAccessAcceptorMock,
27+
} as unknown as T),
28+
})
29+
);
30+
31+
const stubGetInstanceProps = {
32+
constructContainer: {
33+
getConstructFactory: getConstructFactoryMock,
34+
} as unknown as ConstructContainer,
35+
} as unknown as ConstructFactoryGetInstanceProps;
36+
37+
void it('builds access definition for resource', () => {
38+
const accessDefinition = authAccessBuilder
39+
.resource({
40+
getInstance: () =>
41+
({
42+
getResourceAccessAcceptor: getResourceAccessAcceptorMock,
43+
} as unknown as ResourceProvider & ResourceAccessAcceptorFactory),
44+
})
45+
.to(['createUser', 'deleteUser', 'setUserPassword']);
46+
47+
assert.deepStrictEqual(accessDefinition.actions, [
48+
'createUser',
49+
'deleteUser',
50+
'setUserPassword',
51+
]);
52+
assert.equal(
53+
accessDefinition.getResourceAccessAcceptor(stubGetInstanceProps),
54+
resourceAccessAcceptorMock
55+
);
56+
});
57+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { AuthAccessBuilder } from './types.js';
2+
3+
export const authAccessBuilder: AuthAccessBuilder = {
4+
resource: (grantee) => ({
5+
to: (actions) => ({
6+
getResourceAccessAcceptor: (getInstanceProps) =>
7+
grantee.getInstance(getInstanceProps).getResourceAccessAcceptor(),
8+
actions,
9+
}),
10+
}),
11+
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { beforeEach, describe, it, mock } from 'node:test';
2+
import { App, Stack } from 'aws-cdk-lib';
3+
import {
4+
BackendOutputEntry,
5+
BackendOutputStorageStrategy,
6+
ConstructContainer,
7+
ConstructFactoryGetInstanceProps,
8+
ImportPathVerifier,
9+
} from '@aws-amplify/plugin-types';
10+
import {
11+
ConstructContainerStub,
12+
ImportPathVerifierStub,
13+
StackResolverStub,
14+
} from '@aws-amplify/backend-platform-test-stubs';
15+
import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage';
16+
import { UserPool } from 'aws-cdk-lib/aws-cognito';
17+
import { AuthAccessPolicyArbiter } from './auth_access_policy_arbiter.js';
18+
import assert from 'node:assert';
19+
import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js';
20+
21+
void describe('AuthAccessPolicyArbiter', () => {
22+
void describe('arbitratePolicies', () => {
23+
let stack: Stack;
24+
let constructContainer: ConstructContainer;
25+
let outputStorageStrategy: BackendOutputStorageStrategy<BackendOutputEntry>;
26+
let importPathVerifier: ImportPathVerifier;
27+
let getInstanceProps: ConstructFactoryGetInstanceProps;
28+
29+
beforeEach(() => {
30+
stack = createStackAndSetContext();
31+
32+
constructContainer = new ConstructContainerStub(
33+
new StackResolverStub(stack)
34+
);
35+
36+
outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy(
37+
stack
38+
);
39+
40+
importPathVerifier = new ImportPathVerifierStub();
41+
42+
getInstanceProps = {
43+
constructContainer,
44+
outputStorageStrategy,
45+
importPathVerifier,
46+
};
47+
});
48+
49+
void it('passes expected policy and ssm context to resource access acceptor', () => {
50+
const userpool = new UserPool(stack, 'testUserPool');
51+
const acceptResourceAccessMock = mock.fn();
52+
const authAccessPolicyArbiter = new AuthAccessPolicyArbiter(
53+
[
54+
{
55+
actions: ['manageUsers'],
56+
getResourceAccessAcceptor: () => ({
57+
identifier: 'testResourceAccessAcceptor',
58+
acceptResourceAccess: acceptResourceAccessMock,
59+
}),
60+
},
61+
{
62+
actions: ['deleteUser', 'disableUser', 'deleteUserAttributes'],
63+
getResourceAccessAcceptor: () => ({
64+
identifier: 'testResourceAccessAcceptor',
65+
acceptResourceAccess: acceptResourceAccessMock,
66+
}),
67+
},
68+
],
69+
getInstanceProps,
70+
[{ name: 'TEST_USERPOOL_ID', path: 'test/ssm/path/to/userpool/id' }],
71+
new UserPoolAccessPolicyFactory(userpool)
72+
);
73+
74+
authAccessPolicyArbiter.arbitratePolicies();
75+
assert.equal(acceptResourceAccessMock.mock.callCount(), 2);
76+
assert.deepStrictEqual(
77+
acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(),
78+
{
79+
Statement: [
80+
{
81+
Action: [
82+
'cognito-idp:AdminConfirmSignUp',
83+
'cognito-idp:AdminCreateUser',
84+
'cognito-idp:AdminDeleteUser',
85+
'cognito-idp:AdminDeleteUserAttributes',
86+
'cognito-idp:AdminDisableUser',
87+
'cognito-idp:AdminEnableUser',
88+
'cognito-idp:AdminGetUser',
89+
'cognito-idp:AdminListGroupsForUser',
90+
'cognito-idp:AdminRespondToAuthChallenge',
91+
'cognito-idp:AdminSetUserMFAPreference',
92+
'cognito-idp:AdminSetUserSettings',
93+
'cognito-idp:AdminUpdateUserAttributes',
94+
'cognito-idp:AdminUserGlobalSignOut',
95+
],
96+
Effect: 'Allow',
97+
Resource: `${userpool.userPoolArn}`,
98+
},
99+
],
100+
Version: '2012-10-17',
101+
}
102+
);
103+
assert.deepStrictEqual(
104+
acceptResourceAccessMock.mock.calls[1].arguments[0].document.toJSON(),
105+
{
106+
Statement: [
107+
{
108+
Action: [
109+
'cognito-idp:AdminDeleteUser',
110+
'cognito-idp:AdminDisableUser',
111+
'cognito-idp:AdminDeleteUserAttributes',
112+
],
113+
Effect: 'Allow',
114+
Resource: `${userpool.userPoolArn}`,
115+
},
116+
],
117+
Version: '2012-10-17',
118+
}
119+
);
120+
assert.deepStrictEqual(
121+
acceptResourceAccessMock.mock.calls[0].arguments[1],
122+
[{ name: 'TEST_USERPOOL_ID', path: 'test/ssm/path/to/userpool/id' }]
123+
);
124+
});
125+
});
126+
});
127+
128+
const createStackAndSetContext = (): Stack => {
129+
const app = new App();
130+
const stack = new Stack(app);
131+
return stack;
132+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
ConstructFactoryGetInstanceProps,
3+
SsmEnvironmentEntry,
4+
} from '@aws-amplify/plugin-types';
5+
import { AuthAccessDefinition } from './types.js';
6+
import { UserPoolAccessPolicyFactory } from './userpool_access_policy_factory.js';
7+
8+
/**
9+
* Middleman between creating bucket policies and attaching those policies to corresponding roles
10+
*/
11+
export class AuthAccessPolicyArbiter {
12+
/**
13+
* Instantiate with context from the auth factory
14+
*/
15+
constructor(
16+
private readonly accessDefinition: AuthAccessDefinition[],
17+
private readonly getInstanceProps: ConstructFactoryGetInstanceProps,
18+
private readonly ssmEnvironmentEntries: SsmEnvironmentEntry[],
19+
private readonly userPoolAccessPolicyFactory: UserPoolAccessPolicyFactory
20+
) {}
21+
22+
/**
23+
* Responsible for creating policies corresponding to the definition,
24+
* then invoking the corresponding ResourceAccessAcceptor to accept the policies
25+
*/
26+
arbitratePolicies = () => {
27+
this.accessDefinition.forEach(this.acceptResourceAccess);
28+
};
29+
30+
acceptResourceAccess = (accessDefinition: AuthAccessDefinition) => {
31+
const accessAcceptor = accessDefinition.getResourceAccessAcceptor(
32+
this.getInstanceProps
33+
);
34+
const policy = this.userPoolAccessPolicyFactory.createPolicy(
35+
accessDefinition.actions
36+
);
37+
38+
accessAcceptor.acceptResourceAccess(policy, this.ssmEnvironmentEntries);
39+
};
40+
}
41+
42+
/**
43+
*
44+
*/
45+
export class AuthAccessPolicyArbiterFactory {
46+
getInstance = (
47+
accessDefinition: AuthAccessDefinition[],
48+
getInstanceProps: ConstructFactoryGetInstanceProps,
49+
ssmEnvironmentEntries: SsmEnvironmentEntry[],
50+
userpoolAccessPolicyFactory: UserPoolAccessPolicyFactory
51+
) =>
52+
new AuthAccessPolicyArbiter(
53+
accessDefinition,
54+
getInstanceProps,
55+
ssmEnvironmentEntries,
56+
userpoolAccessPolicyFactory
57+
);
58+
}

0 commit comments

Comments
 (0)