Skip to content

Commit

Permalink
feat: allow optional idp arg into auth to allow provided auth role or…
Browse files Browse the repository at this point in the history
… authenticated identity (#8609)
  • Loading branch information
SwaySway committed Nov 2, 2021
1 parent 6b4994d commit bf843b9
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 85 deletions.
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`iam checks test that admin roles are added when functions have access to the graphql api 1`] = `
"## [Start] Authorization Steps. **
$util.qr($ctx.stash.put(\\"hasAuth\\", true))
#set( $inputFields = $util.parseJson($util.toJson($ctx.args.input.keySet())) )
#set( $isAuthorized = false )
#set( $allowedFields = [] )
#if( $util.authType() == \\"IAM Authorization\\" )
#set( $adminRoles = [\\"helloWorldFunction\\",\\"echoMessageFunction\\"] )
#foreach( $adminRole in $adminRoles )
#if( $ctx.identity.userArn.contains($adminRole) )
#return($util.toJson({}))
#end
#end
#if( $ctx.identity.userArn == $ctx.stash.authRole )
#set( $isAuthorized = true )
#end
#end
#if( !$isAuthorized && $allowedFields.isEmpty() )
$util.unauthorized()
#end
#if( !$isAuthorized )
#set( $deniedFields = $util.list.copyAndRemoveAll($inputFields, $allowedFields) )
#if( $deniedFields.size() > 0 )
$util.error(\\"Unauthorized on \${deniedFields}\\", \\"Unauthorized\\")
#end
#end
$util.toJson({\\"version\\":\\"2018-05-29\\",\\"payload\\":{}})
## [End] Authorization Steps. **"
`;
Expand Up @@ -605,3 +605,53 @@ describe('schema generation directive tests', () => {
}
});
});

describe('iam checks', () => {
const identityPoolId = 'us-fake-1:1234abc';
const adminRoles = ['helloWorldFunction', 'echoMessageFunction'];

test('identity pool check gets added when using private rule', () => {
const schema = getSchema(privateIAMDirective);
const transformer = new GraphQLTransform({
authConfig: iamDefaultConfig,
transformers: [new ModelTransformer(), new AuthTransformer({ identityPoolId })],
});
const out = transformer.transform(schema);
expect(out).toBeDefined();
const createResolver = out.pipelineFunctions['Mutation.createPost.auth.1.req.vtl'];
expect(createResolver).toContain(
`#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == \"${identityPoolId}\" && $ctx.identity.cognitoIdentityAuthType == \"authenticated\") )`,
);
const queryResolver = out.pipelineFunctions['Query.listPosts.auth.1.req.vtl'];
expect(queryResolver).toContain(
`#if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == \"${identityPoolId}\" && $ctx.identity.cognitoIdentityAuthType == \"authenticated\") )`,
);
});

test('identity pool check does not get added when using public rule', () => {
const schema = getSchema(publicIAMAuthDirective);
const transformer = new GraphQLTransform({
authConfig: iamDefaultConfig,
transformers: [new ModelTransformer(), new AuthTransformer({ identityPoolId })],
});
const out = transformer.transform(schema);
expect(out).toBeDefined();
const createResolver = out.pipelineFunctions['Mutation.createPost.auth.1.req.vtl'];
expect(createResolver).toContain(`#if( $ctx.identity.userArn == $ctx.stash.unauthRole )`);
const queryResolver = out.pipelineFunctions['Query.listPosts.auth.1.req.vtl'];
expect(queryResolver).toContain(`#if( $ctx.identity.userArn == $ctx.stash.unauthRole )`);
});

test('test that admin roles are added when functions have access to the graphql api', () => {
const schema = getSchema(privateIAMDirective);
const transformer = new GraphQLTransform({
authConfig: iamDefaultConfig,
transformers: [new ModelTransformer(), new AuthTransformer({ adminRoles })],
});
const out = transformer.transform(schema);
expect(out).toBeDefined();
const createResolver = out.pipelineFunctions['Mutation.createPost.auth.1.req.vtl'];
expect(createResolver).toContain(`#set( $adminRoles = [\"helloWorldFunction\",\"echoMessageFunction\"] )`);
expect(createResolver).toMatchSnapshot();
});
});
Expand Up @@ -121,7 +121,11 @@ export class AuthTransformer extends TransformerAuthBase implements TransformerA
private authPolicyResources = new Set<string>();
private unauthPolicyResources = new Set<string>();

constructor(config: AuthTransformerConfig = { adminRoles: [] }) {
/**
*
* @param config settings to configure the auth transformer during transpilation
*/
constructor(config: AuthTransformerConfig = {}) {
super('amplify-auth-transformer', authDirectiveDefinition);
this.config = config;
this.modelDirectiveConfig = new Map();
Expand All @@ -135,7 +139,7 @@ export class AuthTransformer extends TransformerAuthBase implements TransformerA

before = (context: TransformerBeforeStepContextProvider): void => {
// if there was no auth config in the props we add the authConfig from the context
this.config.authConfig = this.config.authConfig || context.authConfig;
this.config.authConfig = this.config.authConfig ?? context.authConfig;
this.configuredAuthProviders = getConfiguredAuthProviders(this.config);
};

Expand Down
14 changes: 7 additions & 7 deletions packages/amplify-graphql-auth-transformer/src/resolvers/field.ts
Expand Up @@ -93,23 +93,23 @@ const generateDynamicAuthReadExpression = (roles: Array<RoleDefinition>, fields:
};

export const generateAuthExpressionForField = (
provider: ConfiguredAuthProviders,
providers: ConfiguredAuthProviders,
roles: Array<RoleDefinition>,
fields: ReadonlyArray<FieldDefinitionNode>,
): string => {
const { cognitoStaticRoles, cognitoDynamicRoles, oidcStaticRoles, oidcDynamicRoles, iamRoles, apiKeyRoles, lambdaRoles } =
splitRoles(roles);
const totalAuthExpressions: Array<Expression> = [set(ref(IS_AUTHORIZED_FLAG), bool(false))];
if (provider.hasApiKey) {
if (providers.hasApiKey) {
totalAuthExpressions.push(apiKeyExpression(apiKeyRoles));
}
if (provider.hasLambda) {
if (providers.hasLambda) {
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
}
if (provider.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, provider.hasAdminRolesEnabled, provider.adminRoles));
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (provider.hasUserPools) {
if (providers.hasUserPools) {
totalAuthExpressions.push(
iff(
equals(ref('util.authType()'), str(COGNITO_AUTH_TYPE)),
Expand All @@ -120,7 +120,7 @@ export const generateAuthExpressionForField = (
),
);
}
if (provider.hasOIDC) {
if (providers.hasOIDC) {
totalAuthExpressions.push(
iff(
equals(ref('util.authType()'), str(OIDC_AUTH_TYPE)),
Expand Down
29 changes: 26 additions & 3 deletions packages/amplify-graphql-auth-transformer/src/resolvers/helpers.ts
Expand Up @@ -17,6 +17,9 @@ import {
forEach,
list,
equals,
or,
and,
parens,
} from 'graphql-mapping-template';
import { NONE_VALUE } from 'graphql-transformer-common';
import {
Expand Down Expand Up @@ -51,7 +54,22 @@ export const addAllowedFieldsIfElse = (fieldKey: string, breakLoop: boolean = fa
};

// iam check
export const iamCheck = (claim: string, exp: Expression) => iff(equals(ref('ctx.identity.userArn'), ref(`ctx.stash.${claim}`)), exp);
export const iamCheck = (claim: string, exp: Expression, identityPoolId?: string) => {
let iamExp: Expression = equals(ref('ctx.identity.userArn'), ref(`ctx.stash.${claim}`));
// only include the additional check if we have a private rule and a provided identityPoolId
if (identityPoolId && claim === 'authRole') {
iamExp = or([
parens(iamExp),
parens(
and([
equals(ref('ctx.identity.cognitoIdentityPoolId'), str(identityPoolId)),
equals(ref('ctx.identity.cognitoIdentityAuthType'), str('authenticated')),
]),
),
]);
}
return iff(iamExp, exp);
};

/**
* Behavior of auth v1
Expand Down Expand Up @@ -118,15 +136,20 @@ export const lambdaExpression = (roles: Array<RoleDefinition>) => {
);
};

export const iamExpression = (roles: Array<RoleDefinition>, adminRolesEnabled: boolean, adminRoles: Array<string> = []) => {
export const iamExpression = (
roles: Array<RoleDefinition>,
adminRolesEnabled: boolean,
adminRoles: Array<string> = [],
identityPoolId?: string,
) => {
const expression = new Array<Expression>();
// allow if using an admin role
if (adminRolesEnabled) {
expression.push(iamAdminRoleCheckExpression(adminRoles));
}
if (roles.length > 0) {
for (let role of roles) {
expression.push(iff(not(ref(IS_AUTHORIZED_FLAG)), iamCheck(role.claim!, set(ref(IS_AUTHORIZED_FLAG), bool(true)))));
expression.push(iff(not(ref(IS_AUTHORIZED_FLAG)), iamCheck(role.claim!, set(ref(IS_AUTHORIZED_FLAG), bool(true)), identityPoolId)));
}
} else {
expression.push(ref('util.unauthorized()'));
Expand Down
Expand Up @@ -66,7 +66,12 @@ const apiKeyExpression = (roles: Array<RoleDefinition>) => {
* @param roles
* @returns
*/
const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boolean = false, adminRoles: Array<string> = []) => {
const iamExpression = (
roles: Array<RoleDefinition>,
hasAdminRolesEnabled: boolean = false,
adminRoles: Array<string> = [],
identityPoolId?: string,
) => {
const expression = new Array<Expression>();
// allow if using an admin role
if (hasAdminRolesEnabled) {
Expand All @@ -79,7 +84,7 @@ const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boole
iamCheck(role.claim!, compoundExpression([set(ref(`${ALLOWED_FIELDS}`), raw(JSON.stringify(role.allowedFields)))])),
);
} else {
expression.push(iamCheck(role.claim!, set(ref(IS_AUTHORIZED_FLAG), bool(true))));
expression.push(iamCheck(role.claim!, set(ref(IS_AUTHORIZED_FLAG), bool(true)), identityPoolId));
}
}
} else {
Expand Down Expand Up @@ -232,7 +237,7 @@ export const generateAuthExpressionForCreate = (
totalAuthExpressions.push(apiKeyExpression(apiKeyRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasLambda) {
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
Expand All @@ -241,10 +246,7 @@ export const generateAuthExpressionForCreate = (
totalAuthExpressions.push(
iff(
equals(ref('util.authType()'), str(COGNITO_AUTH_TYPE)),
compoundExpression([
...generateStaticRoleExpression(cognitoStaticRoles),
...dynamicRoleExpression(cognitoDynamicRoles, fields),
]),
compoundExpression([...generateStaticRoleExpression(cognitoStaticRoles), ...dynamicRoleExpression(cognitoDynamicRoles, fields)]),
),
);
}
Expand Down
Expand Up @@ -49,15 +49,20 @@ const apiKeyExpression = (roles: Array<RoleDefinition>) => {
* @param roles
* @returns
*/
const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boolean = false, adminRoles: Array<string> = []) => {
const iamExpression = (
roles: Array<RoleDefinition>,
hasAdminRolesEnabled: boolean = false,
adminRoles: Array<string> = [],
identityPoolId?: string,
) => {
const expression = new Array<Expression>();
// allow if using an admin role
if (hasAdminRolesEnabled) {
expression.push(iamAdminRoleCheckExpression(adminRoles));
}
if (roles.length > 0) {
for (let role of roles) {
expression.push(iamCheck(role.claim!, set(ref(IS_AUTHORIZED_FLAG), bool(true))));
expression.push(iamCheck(role.claim!, set(ref(IS_AUTHORIZED_FLAG), bool(true)), identityPoolId));
}
} else {
expression.push(ref('util.unauthorized()'));
Expand Down Expand Up @@ -168,7 +173,7 @@ export const geneateAuthExpressionForDelete = (
totalAuthExpressions.push(apiKeyExpression(apiKeyRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasLambda) {
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
Expand Down
Expand Up @@ -89,7 +89,12 @@ const lambdaExpression = (roles: Array<RoleDefinition>) => {
return iff(equals(ref('util.authType()'), str(LAMBDA_AUTH_TYPE)), compoundExpression(expression));
};

const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boolean = false, adminRoles: Array<string> = []) => {
const iamExpression = (
roles: Array<RoleDefinition>,
hasAdminRolesEnabled: boolean = false,
adminRoles: Array<string> = [],
identityPoolId?: string,
) => {
const expression = new Array<Expression>();
// allow if using an admin role
if (hasAdminRolesEnabled) {
Expand All @@ -105,6 +110,7 @@ const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boole
set(ref(`${ALLOWED_FIELDS}`), raw(JSON.stringify(role.allowedFields))),
set(ref(`${NULL_ALLOWED_FIELDS}`), raw(JSON.stringify(role.nullAllowedFields))),
]),
identityPoolId,
),
);
} else {
Expand Down Expand Up @@ -292,7 +298,7 @@ export const generateAuthExpressionForUpdate = (
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasUserPools) {
totalAuthExpressions.push(
Expand Down
Expand Up @@ -271,7 +271,7 @@ export const generateAuthExpressionForQueries = (
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasUserPools) {
totalAuthExpressions.push(
Expand Down Expand Up @@ -327,7 +327,7 @@ export const generateAuthExpressionForRelationQuery = (
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasUserPools) {
totalAuthExpressions.push(
Expand Down
Expand Up @@ -69,7 +69,12 @@ const lambdaExpression = (roles: Array<RoleDefinition>): Expression => {
return iff(equals(ref('util.authType()'), str(LAMBDA_AUTH_TYPE)), compoundExpression(expression));
};

const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boolean = false, adminRoles: Array<string> = []) => {
const iamExpression = (
roles: Array<RoleDefinition>,
hasAdminRolesEnabled: boolean = false,
adminRoles: Array<string> = [],
identityPoolId?: string,
) => {
const expression = new Array<Expression>();
// allow if using an admin role
if (hasAdminRolesEnabled) {
Expand All @@ -85,7 +90,7 @@ const iamExpression = (roles: Array<RoleDefinition>, hasAdminRolesEnabled: boole
} else {
exp.push(set(ref(allowedAggFieldsList), ref(totalFields)));
}
expression.push(iff(not(ref(IS_AUTHORIZED_FLAG)), iamCheck(role.claim!, compoundExpression(exp))));
expression.push(iff(not(ref(IS_AUTHORIZED_FLAG)), iamCheck(role.claim!, compoundExpression(exp), identityPoolId)));
}
}
return iff(equals(ref('util.authType()'), str(IAM_AUTH_TYPE)), compoundExpression(expression));
Expand Down Expand Up @@ -248,7 +253,7 @@ export const generateAuthExpressionForSearchQueries = (
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasUserPools) {
totalAuthExpressions.push(
Expand Down
Expand Up @@ -55,7 +55,7 @@ export const generateAuthExpressionForSubscriptions = (providers: ConfiguredAuth
totalAuthExpressions.push(lambdaExpression(lambdaRoles));
}
if (providers.hasIAM) {
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles));
totalAuthExpressions.push(iamExpression(iamRoles, providers.hasAdminRolesEnabled, providers.adminRoles, providers.identityPoolId));
}
if (providers.hasUserPools)
totalAuthExpressions.push(
Expand Down
16 changes: 10 additions & 6 deletions packages/amplify-graphql-auth-transformer/src/utils/definitions.ts
Expand Up @@ -12,6 +12,15 @@ export interface SearchableConfig {
};
}

export interface AuthTransformerConfig {
/** used mainly in the before step to pass the authConfig from the transformer core down to the directive */
authConfig?: AppSyncAuthConfiguration;
/** using the iam provider the resolvers checks will lets the roles in this list passthrough the acm */
adminRoles?: Array<string>;
/** when authorizing private/public @auth can also check authenticated/unauthenticated status for a given identityPoolId */
identityPoolId?: string;
}

export interface RolesByProvider {
cognitoStaticRoles: Array<RoleDefinition>;
cognitoDynamicRoles: Array<RoleDefinition>;
Expand Down Expand Up @@ -60,12 +69,7 @@ export interface ConfiguredAuthProviders {
hasLambda: boolean;
hasAdminRolesEnabled: boolean;
adminRoles: Array<string>;
adminUserPoolID?: string;
}

export interface AuthTransformerConfig {
adminRoles?: Array<string>;
authConfig?: AppSyncAuthConfiguration;
identityPoolId?: string;
}

export const authDirectiveDefinition = `
Expand Down

0 comments on commit bf843b9

Please sign in to comment.