-
Notifications
You must be signed in to change notification settings - Fork 825
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: graphql auth v2 add auth on mutation and subscription resolvers
- Loading branch information
Showing
26 changed files
with
3,034 additions
and
698 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
342 changes: 207 additions & 135 deletions
342
packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts
Large diffs are not rendered by default.
Oops, something went wrong.
137 changes: 137 additions & 0 deletions
137
packages/amplify-graphql-auth-transformer/src/resolvers/field.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { OPERATION_KEY } from '@aws-amplify/graphql-model-transformer'; | ||
import { FieldDefinitionNode } from 'graphql'; | ||
import { | ||
Expression, | ||
iff, | ||
not, | ||
ref, | ||
equals, | ||
str, | ||
compoundExpression, | ||
printBlock, | ||
toJson, | ||
obj, | ||
set, | ||
methodCall, | ||
nul, | ||
ifElse, | ||
bool, | ||
raw, | ||
forEach, | ||
} from 'graphql-mapping-template'; | ||
import { | ||
RoleDefinition, | ||
splitRoles, | ||
COGNITO_AUTH_TYPE, | ||
OIDC_AUTH_TYPE, | ||
ConfiguredAuthProviders, | ||
fieldIsList, | ||
IS_AUTHORIZED_FLAG, | ||
} from '../utils'; | ||
import { getOwnerClaim, staticGroupRoleExpression, apiKeyExpression, iamExpression } from './helpers'; | ||
|
||
// Field Read VTL Functions | ||
const generateDynamicAuthReadExpression = (roles: Array<RoleDefinition>, fields: ReadonlyArray<FieldDefinitionNode>) => { | ||
const ownerExpressions = new Array<Expression>(); | ||
const dynamicGroupExpressions = new Array<Expression>(); | ||
roles.forEach((role, idx) => { | ||
const entityIsList = fieldIsList(fields, role.entity!); | ||
if (role.strategy === 'owner') { | ||
ownerExpressions.push( | ||
iff( | ||
not(ref(IS_AUTHORIZED_FLAG)), | ||
compoundExpression([ | ||
set(ref(`ownerEntity${idx}`), methodCall(ref('util.defaultIfNull'), ref(`ctx.source.${role.entity!}`), nul())), | ||
set(ref(`ownerClaim${idx}`), getOwnerClaim(role.claim!)), | ||
...(entityIsList | ||
? [ | ||
forEach(ref('allowedOwner'), ref(`ownerEntity${idx}`), [ | ||
iff( | ||
equals(ref('allowedOwner'), ref(`ownerClaim${idx}`)), | ||
compoundExpression([set(ref(IS_AUTHORIZED_FLAG), bool(true)), raw('#break')]), | ||
), | ||
]), | ||
] | ||
: [iff(equals(ref('ownerEntity'), ref(`ownerClaim${idx}`)), set(ref(IS_AUTHORIZED_FLAG), bool(true)))]), | ||
]), | ||
), | ||
); | ||
} | ||
if (role.strategy === 'groups') { | ||
dynamicGroupExpressions.push( | ||
iff( | ||
not(ref(IS_AUTHORIZED_FLAG)), | ||
compoundExpression([ | ||
set(ref(`groupEntity${idx}`), methodCall(ref('util.defaultIfNull'), ref(`ctx.source.${role.entity!}`), nul())), | ||
set(ref(`groupClaim${idx}`), getOwnerClaim(role.claim!)), | ||
forEach(ref('userGroup'), ref('dynamicGroupClaim'), [ | ||
iff( | ||
entityIsList | ||
? methodCall(ref(`groupEntity${idx}.contains`), ref('userGroup')) | ||
: equals(ref(`groupEntity${idx}`), ref('userGroup')), | ||
compoundExpression([set(ref(IS_AUTHORIZED_FLAG), bool(true)), raw('#break')]), | ||
), | ||
]), | ||
]), | ||
), | ||
); | ||
} | ||
}); | ||
return [...(ownerExpressions.length > 0 ? ownerExpressions : []), ...(dynamicGroupExpressions.length > 0 ? dynamicGroupExpressions : [])]; | ||
}; | ||
|
||
export const generateAuthExpressionForField = ( | ||
provider: ConfiguredAuthProviders, | ||
roles: Array<RoleDefinition>, | ||
fields: ReadonlyArray<FieldDefinitionNode>, | ||
): string => { | ||
const { cognitoStaticGroupRoles, cognitoDynamicRoles, oidcStaticGroupRoles, oidcDynamicRoles, iamRoles, apiKeyRoles } = splitRoles(roles); | ||
const totalAuthExpressions: Array<Expression> = [set(ref(IS_AUTHORIZED_FLAG), bool(false))]; | ||
if (provider.hasApiKey) { | ||
totalAuthExpressions.push(apiKeyExpression(apiKeyRoles)); | ||
} | ||
if (provider.hasIAM) { | ||
totalAuthExpressions.push(iamExpression(iamRoles, provider.hasAdminUIEnabled)); | ||
} | ||
if (provider.hasUserPools) { | ||
totalAuthExpressions.push( | ||
iff( | ||
equals(ref('util.authType()'), str(COGNITO_AUTH_TYPE)), | ||
compoundExpression([ | ||
...staticGroupRoleExpression(cognitoStaticGroupRoles), | ||
...generateDynamicAuthReadExpression(cognitoDynamicRoles, fields), | ||
]), | ||
), | ||
); | ||
} | ||
if (provider.hasOIDC) { | ||
totalAuthExpressions.push( | ||
iff( | ||
equals(ref('util.authType()'), str(OIDC_AUTH_TYPE)), | ||
compoundExpression([ | ||
...staticGroupRoleExpression(oidcStaticGroupRoles), | ||
...generateDynamicAuthReadExpression(oidcDynamicRoles, fields), | ||
]), | ||
), | ||
); | ||
} | ||
totalAuthExpressions.push(iff(not(ref(IS_AUTHORIZED_FLAG)), ref('util.unauthorized()'))); | ||
return printBlock('Field Authorization Steps')(compoundExpression([...totalAuthExpressions, toJson(obj({}))])); | ||
}; | ||
|
||
/** | ||
* This is the response resolver for fields to protect subscriptions | ||
* @param subscriptionsEnabled | ||
* @returns | ||
*/ | ||
export const generateFieldAuthResponse = (operation: string, fieldName: string, subscriptionsEnabled: boolean): string => { | ||
if (subscriptionsEnabled) { | ||
return printBlock('Checking for allowed operations which can return this field')( | ||
compoundExpression([ | ||
set(ref('operation'), methodCall(ref('util.defaultIfNull'), methodCall(ref('ctx.source.get'), str(OPERATION_KEY)), nul())), | ||
ifElse(equals(ref('operation'), str(operation)), toJson(nul()), toJson(ref(`context.source.${fieldName}`))), | ||
]), | ||
); | ||
} | ||
return printBlock('Return Source Field')(toJson(ref(`context.source.${fieldName}`))); | ||
}; |
142 changes: 142 additions & 0 deletions
142
packages/amplify-graphql-auth-transformer/src/resolvers/helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { | ||
qref, | ||
Expression, | ||
ifElse, | ||
iff, | ||
methodCall, | ||
not, | ||
ref, | ||
set, | ||
str, | ||
raw, | ||
obj, | ||
bool, | ||
compoundExpression, | ||
printBlock, | ||
toJson, | ||
forEach, | ||
list, | ||
equals, | ||
or, | ||
} from 'graphql-mapping-template'; | ||
import { NONE_VALUE } from 'graphql-transformer-common'; | ||
import { | ||
DEFAULT_COGNITO_IDENTITY_CLAIM, | ||
RoleDefinition, | ||
IS_AUTHORIZED_FLAG, | ||
ALLOWED_FIELDS, | ||
API_KEY_AUTH_TYPE, | ||
ADMIN_ROLE, | ||
IAM_AUTH_TYPE, | ||
MANAGE_ROLE, | ||
} from '../utils'; | ||
|
||
// since the keySet returns a set we can convert it to a list by converting to json and parsing back as a list | ||
export const getInputFields = () => | ||
compoundExpression([ | ||
set(ref('inputFields'), methodCall(ref('util.parseJson'), methodCall(ref('util.toJson'), ref('ctx.args.input.keySet()')))), | ||
iff( | ||
ref('ctx.stash.metadata.modelObjectKey'), | ||
forEach(ref('entry'), ref('ctx.stash.metadata.modelObjectKey.keySet()'), [qref(methodCall(ref('inputFields.remove'), ref('entry')))]), | ||
), | ||
]); | ||
|
||
export const getIdentityClaimExp = (value: Expression, defaultValueExp: Expression) => { | ||
return methodCall(ref('util.defaultIfNull'), methodCall(ref('ctx.identity.claims.get'), value), defaultValueExp); | ||
}; | ||
|
||
// for create mutations and subscriptions | ||
export const addAllowedFieldsIfElse = (fieldKey: string, breakLoop: boolean = false) => | ||
ifElse( | ||
not(ref(`${fieldKey}.isEmpty()`)), | ||
qref(methodCall(ref(`${ALLOWED_FIELDS}.addAll`), ref(fieldKey))), | ||
compoundExpression([set(ref(IS_AUTHORIZED_FLAG), bool(true)), ...(breakLoop ? [raw('#break')] : [])]), | ||
); | ||
|
||
/** | ||
* Behavior of auth v1 | ||
* Order of how the owner value is retrieved from the jwt | ||
* if claim is username | ||
* 1. username | ||
* 2. cognito:username | ||
* 3. none value | ||
* | ||
* if claim is custom | ||
* 1. custom | ||
* 2. none value | ||
*/ | ||
export const getOwnerClaim = (ownerClaim: string): Expression => { | ||
if (ownerClaim === 'username') { | ||
return getIdentityClaimExp(str(ownerClaim), getIdentityClaimExp(str(DEFAULT_COGNITO_IDENTITY_CLAIM), str(NONE_VALUE))); | ||
} | ||
return getIdentityClaimExp(str(ownerClaim), str(NONE_VALUE)); | ||
}; | ||
|
||
export const responseCheckForErrors = () => | ||
iff(ref('ctx.error'), methodCall(ref('util.error'), ref('ctx.error.message'), ref('ctx.error.type'))); | ||
|
||
// Common Expressions | ||
|
||
export const staticGroupRoleExpression = (roles: Array<RoleDefinition>): Array<Expression> => { | ||
return roles.length > 0 | ||
? [ | ||
set(ref('staticGroupRoles'), raw(JSON.stringify(roles.map(r => ({ claim: r.claim, entity: r.entity }))))), | ||
forEach(ref('groupRole'), ref('staticGroupRoles'), [ | ||
set(ref('groupsInToken'), getIdentityClaimExp(ref('groupRole.claim'), list([]))), | ||
iff( | ||
methodCall(ref('groupsInToken.contains'), ref('groupRole.entity')), | ||
compoundExpression([set(ref(IS_AUTHORIZED_FLAG), bool(true)), raw(`#break`)]), | ||
), | ||
]), | ||
] | ||
: []; | ||
}; | ||
|
||
export const apiKeyExpression = (roles: Array<RoleDefinition>) => | ||
iff( | ||
equals(ref('util.authType()'), str(API_KEY_AUTH_TYPE)), | ||
compoundExpression([ | ||
...(roles.length > 0 ? [set(ref(IS_AUTHORIZED_FLAG), bool(true))] : []), | ||
iff(not(ref(IS_AUTHORIZED_FLAG)), methodCall(ref('util.unauthorized'))), | ||
]), | ||
); | ||
|
||
export const iamExpression = (roles: Array<RoleDefinition>, adminuiEnabled: boolean = false) => { | ||
const iamCheck = (claim: string, exp: Expression) => | ||
iff(equals(methodCall(ref('ctx.identity.get'), str('cognitoIdentityAuthType')), str(claim)), exp); | ||
const expression = new Array<Expression>(); | ||
// allow if using admin ui | ||
if (adminuiEnabled) { | ||
expression.push( | ||
iff( | ||
or([ | ||
methodCall(ref('ctx.identity.userArn.contains'), str(ADMIN_ROLE)), | ||
methodCall(ref('ctx.identity.userArn.contains'), str(MANAGE_ROLE)), | ||
]), | ||
raw('#return($util.toJson({})'), | ||
), | ||
); | ||
} | ||
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)), methodCall(ref('util.unauthorized')))); | ||
return iff(equals(ref('util.authType()'), str(IAM_AUTH_TYPE)), compoundExpression(expression)); | ||
}; | ||
|
||
// Get Request for Update and Delete | ||
export const generateAuthRequestExpression = () => { | ||
const statements = [ | ||
set(ref('GetRequest'), obj({ version: str('2018-05-29'), operation: str('GetItem') })), | ||
ifElse( | ||
ref('ctx.stash.metadata.modelObjectKey'), | ||
set(ref('key'), ref('ctx.stash.metadata.modelObjectKey')), | ||
compoundExpression([set(ref('key'), obj({ id: methodCall(ref('util.dynamodb.toDynamoDB'), ref('ctx.args.input.id')) }))]), | ||
), | ||
qref(methodCall(ref('GetRequest.put'), str('key'), ref('key'))), | ||
toJson(ref('GetRequest')), | ||
]; | ||
return printBlock('Get Request template')(compoundExpression(statements)); | ||
}; |
8 changes: 7 additions & 1 deletion
8
packages/amplify-graphql-auth-transformer/src/resolvers/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
export * from './query'; | ||
export { generateAuthExpressionForQueries } from './query'; | ||
export { generateAuthExpressionForCreate } from './mutation.create'; | ||
export { generateAuthExpressionForUpdate } from './mutation.update'; | ||
export { geneateAuthExpressionForDelete } from './mutation.delete'; | ||
export { generateAuthExpressionForField, generateFieldAuthResponse } from './field'; | ||
export { generateAuthExpressionForSubscriptions } from './subscriptions'; | ||
export { generateAuthRequestExpression } from './helpers'; |
Oops, something went wrong.