From 1a0636d72d010b9d0ed18d511f853bcbffa9d421 Mon Sep 17 00:00:00 2001 From: Josue Ruiz <7465495+SwaySway@users.noreply.github.com> Date: Fri, 5 Nov 2021 11:44:57 -0700 Subject: [PATCH] fix: allow duplicate auth rules when creating the join type (#8680) --- .../src/__tests__/accesscontrol.test.ts | 28 + .../src/accesscontrol/acm.ts | 5 +- .../src/graphql-auth-transformer.ts | 23 +- ...phql-many-to-many-transformer.test.ts.snap | 507 ++++++++++++++++++ ...y-graphql-many-to-many-transformer.test.ts | 46 +- .../src/graphql-many-to-many-transformer.ts | 4 + 6 files changed, 602 insertions(+), 11 deletions(-) diff --git a/packages/amplify-graphql-auth-transformer/src/__tests__/accesscontrol.test.ts b/packages/amplify-graphql-auth-transformer/src/__tests__/accesscontrol.test.ts index e34eb85cac0..172312190e8 100644 --- a/packages/amplify-graphql-auth-transformer/src/__tests__/accesscontrol.test.ts +++ b/packages/amplify-graphql-auth-transformer/src/__tests__/accesscontrol.test.ts @@ -129,4 +129,32 @@ test('that adding a role again without a resource is not allowed', () => { expect(acm.isAllowed(blogOwnerRole, field, 'delete')).toBe(true); } expect(() => acm.setRole({ role: blogOwnerRole, operations: ['read'] })).toThrow(`@auth ${blogOwnerRole} already exists for Blog`); + // field overwrites should still be allowed + acm.setRole({ role: blogOwnerRole, operations: ['read'], resource: 'name' }); + acm.setRole({ role: blogOwnerRole, operations: ['read'], resource: 'id' }); + expect(acm.isAllowed(blogOwnerRole, 'id', 'read')).toBe(true); +}); + +test('that adding a role again without a resource is allowed with overwrite flag enabled', () => { + const blogOwnerRole = 'userPools:owner'; + const blogFields = ['id', 'owner', 'name', 'content']; + const acm = new AccessControlMatrix({ + name: 'Blog', + resources: blogFields, + operations: MODEL_OPERATIONS, + }); + acm.setRole({ role: blogOwnerRole, operations: MODEL_OPERATIONS }); + for (let field of blogFields) { + expect(acm.isAllowed(blogOwnerRole, field, 'create')).toBe(true); + expect(acm.isAllowed(blogOwnerRole, field, 'read')).toBe(true); + expect(acm.isAllowed(blogOwnerRole, field, 'update')).toBe(true); + expect(acm.isAllowed(blogOwnerRole, field, 'delete')).toBe(true); + } + acm.setRole({ role: blogOwnerRole, operations: ['read'], allowRoleOverwrite: true }); + for (let field of blogFields) { + expect(acm.isAllowed(blogOwnerRole, field, 'create')).toBe(false); + expect(acm.isAllowed(blogOwnerRole, field, 'read')).toBe(true); + expect(acm.isAllowed(blogOwnerRole, field, 'update')).toBe(false); + expect(acm.isAllowed(blogOwnerRole, field, 'delete')).toBe(false); + } }); diff --git a/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts b/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts index 8b8269f5c92..45457af1598 100644 --- a/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts +++ b/packages/amplify-graphql-auth-transformer/src/accesscontrol/acm.ts @@ -11,6 +11,7 @@ type SetRoleInput = { role: string; operations: Array; resource?: string; + allowRoleOverwrite?: boolean; }; type ValidateInput = { @@ -48,7 +49,7 @@ export class AccessControlMatrix { } public setRole(input: SetRoleInput): void { - const { role, resource, operations } = input; + const { role, resource, operations, allowRoleOverwrite = false } = input; this.validate({ resource, operations }); let allowedVector: Array>; if (!this.roles.includes(role)) { @@ -56,7 +57,7 @@ export class AccessControlMatrix { this.roles.push(input.role); this.matrix.push(allowedVector); assert(this.roles.length === this.matrix.length, 'Roles are not aligned with Roles added in Matrix'); - } else if (this.roles.includes(role) && resource) { + } else if (this.roles.includes(role) && (resource || allowRoleOverwrite)) { allowedVector = this.getResourceOperationMatrix({ operations, resource, role }); const roleIndex = this.roles.indexOf(role); this.matrix[roleIndex] = allowedVector; diff --git a/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts b/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts index 91351eb850f..58a550ce6b3 100644 --- a/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts +++ b/packages/amplify-graphql-auth-transformer/src/graphql-auth-transformer.ts @@ -149,6 +149,11 @@ export class AuthTransformer extends TransformerAuthBase implements TransformerA throw new TransformerContractError('Types annotated with @auth must also be annotated with @model.'); } const typeName = def.name.value; + let isJoinType = false; + // check if type is a joinedType + if (context.metadata.has('joinTypeList')) { + isJoinType = context.metadata.get>('joinTypeList')!.includes(typeName); + } const authDir = new DirectiveWrapper(directive); const rules: AuthRule[] = authDir.getArguments<{ rules: Array }>({ rules: [] }).rules; ensureAuthRuleDefaults(rules); @@ -166,7 +171,7 @@ export class AuthTransformer extends TransformerAuthBase implements TransformerA // add object into policy this.addTypeToResourceReferences(def.name.value, rules); // turn rules into roles and add into acm and roleMap - this.convertRulesToRoles(acm, rules); + this.convertRulesToRoles(acm, rules, isJoinType); this.modelDirectiveConfig.set(typeName, getModelConfig(modelDirective, typeName, context.isProjectUsingDataStore())); this.authModelConfig.set(typeName, acm); }; @@ -226,7 +231,7 @@ Static group authorization should perform as expected.`, acm = this.authModelConfig.get(typeName) as AccessControlMatrix; acm.resetAccessForResource(fieldName); } - this.convertRulesToRoles(acm, rules, fieldName); + this.convertRulesToRoles(acm, rules, false, fieldName); this.authModelConfig.set(typeName, acm); } else { // if @auth is used without @model only generate static group rules in the resolver @@ -239,7 +244,7 @@ Static group authorization should perform as expected.`, operations: ['read'], resources: [typeFieldName], }); - this.convertRulesToRoles(acm, staticRules, typeFieldName, ['read']); + this.convertRulesToRoles(acm, staticRules, false, typeFieldName, ['read']); this.authNonModelConfig.set(typeFieldName, acm); } }; @@ -761,7 +766,13 @@ Static group authorization should perform as expected.`, /* Role Helpers */ - private convertRulesToRoles(acm: AccessControlMatrix, authRules: AuthRule[], field?: string, overideOperations?: ModelOperation[]) { + private convertRulesToRoles( + acm: AccessControlMatrix, + authRules: AuthRule[], + allowRoleOverwrite: boolean, + field?: string, + overideOperations?: ModelOperation[], + ) { for (let rule of authRules) { let operations: ModelOperation[] = overideOperations ? overideOperations : rule.operations || MODEL_OPERATIONS; if (rule.groups && !rule.groupsField) { @@ -777,7 +788,7 @@ Static group authorization should perform as expected.`, entity: group, }); } - acm.setRole({ role: roleName, resource: field, operations }); + acm.setRole({ role: roleName, resource: field, operations, allowRoleOverwrite }); }); } else { let roleName: string; @@ -841,7 +852,7 @@ Static group authorization should perform as expected.`, if (!(roleName in this.roleMap)) { this.roleMap.set(roleName, roleDefinition); } - acm.setRole({ role: roleName, resource: field, operations }); + acm.setRole({ role: roleName, resource: field, operations, allowRoleOverwrite }); } } } diff --git a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-many-to-many-transformer.test.ts.snap b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-many-to-many-transformer.test.ts.snap index 1ec7f89b791..1d0344bf163 100644 --- a/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-many-to-many-transformer.test.ts.snap +++ b/packages/amplify-graphql-relational-transformer/src/__tests__/__snapshots__/amplify-graphql-many-to-many-transformer.test.ts.snap @@ -440,6 +440,513 @@ $util.qr($ctx.result.put(\\"__operation\\", \\"Mutation\\")) ## [End] ResponseTemplate. **" `; +exports[`join table inherits auth from tables with similar rules 1`] = ` +"## [Start] Authorization Steps. ** +$util.qr($ctx.stash.put(\\"hasAuth\\", true)) +#set( $isAuthorized = false ) +#set( $primaryFieldMap = {} ) +#if( $util.authType() == \\"API Key Authorization\\" ) + #set( $isAuthorized = true ) +#end +#if( $util.authType() == \\"IAM Authorization\\" ) + #if( !$isAuthorized ) + #if( $ctx.identity.userArn == $ctx.stash.authRole ) + #set( $isAuthorized = true ) + #end + #end +#end +#if( $util.authType() == \\"User Pool Authorization\\" ) + #if( !$isAuthorized ) + #set( $authFilter = [{ + \\"owner\\": { + \\"eq\\": $util.defaultIfNull($ctx.identity.claims.get(\\"username\\"), $util.defaultIfNull($ctx.identity.claims.get(\\"cognito:username\\"), \\"___xamznone____\\")) + } +}] ) + $util.qr($ctx.stash.put(\\"authFilter\\", { \\"or\\": $authFilter })) + #end +#end +#if( !$isAuthorized && $util.isNull($ctx.stash.authFilter) && $primaryFieldMap.isEmpty() ) +$util.unauthorized() +#end +$util.toJson({\\"version\\":\\"2018-05-29\\",\\"payload\\":{}}) +## [End] Authorization Steps. **" +`; + +exports[`join table inherits auth from tables with similar rules 2`] = ` +"## [Start] Sandbox Mode Disabled. ** +#if( !$ctx.stash.get(\\"hasAuth\\") ) + $util.unauthorized() +#end +$util.toJson({}) +## [End] Sandbox Mode Disabled. **" +`; + +exports[`join table inherits auth from tables with similar rules 3`] = ` +"## [Start] Get Response template. ** +#if( $ctx.error ) + $util.error($ctx.error.message, $ctx.error.type) +#end +#if( !$ctx.result.items.isEmpty() && $ctx.result.scannedCount == 1 ) + $util.toJson($ctx.result.items[0]) +#else + #if( $ctx.result.items.isEmpty() && $ctx.result.scannedCount == 1 ) +$util.unauthorized() + #end + $util.toJson(null) +#end +## [End] Get Response template. **" +`; + +exports[`join table inherits auth from tables with similar rules 4`] = ` +"## [Start] Authorization Steps. ** +$util.qr($ctx.stash.put(\\"hasAuth\\", true)) +#set( $isAuthorized = false ) +#set( $primaryFieldMap = {} ) +#if( $util.authType() == \\"API Key Authorization\\" ) + #set( $isAuthorized = true ) +#end +#if( $util.authType() == \\"IAM Authorization\\" ) + #if( !$isAuthorized ) + #if( $ctx.identity.userArn == $ctx.stash.authRole ) + #set( $isAuthorized = true ) + #end + #end +#end +#if( $util.authType() == \\"User Pool Authorization\\" ) + #if( !$isAuthorized ) + #set( $authFilter = [{ + \\"owner\\": { + \\"eq\\": $util.defaultIfNull($ctx.identity.claims.get(\\"username\\"), $util.defaultIfNull($ctx.identity.claims.get(\\"cognito:username\\"), \\"___xamznone____\\")) + } +}] ) + $util.qr($ctx.stash.put(\\"authFilter\\", { \\"or\\": $authFilter })) + #end +#end +#if( !$isAuthorized && $util.isNull($ctx.stash.authFilter) && $primaryFieldMap.isEmpty() ) +$util.unauthorized() +#end +$util.toJson({\\"version\\":\\"2018-05-29\\",\\"payload\\":{}}) +## [End] Authorization Steps. **" +`; + +exports[`join table inherits auth from tables with similar rules 5`] = ` +"## [Start] Sandbox Mode Disabled. ** +#if( !$ctx.stash.get(\\"hasAuth\\") ) + $util.unauthorized() +#end +$util.toJson({}) +## [End] Sandbox Mode Disabled. **" +`; + +exports[`join table inherits auth from tables with similar rules 6`] = ` +"## [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() == \\"API Key Authorization\\" ) + #set( $isAuthorized = true ) +#end +#if( $util.authType() == \\"IAM Authorization\\" ) + #if( $ctx.identity.userArn == $ctx.stash.authRole ) + #set( $isAuthorized = true ) + #end +#end +#if( $util.authType() == \\"User Pool Authorization\\" ) + #if( !$isAuthorized ) + #set( $ownerEntity0 = $util.defaultIfNull($ctx.args.input.owner, null) ) + #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get(\\"username\\"), $util.defaultIfNull($ctx.identity.claims.get(\\"cognito:username\\"), \\"___xamznone____\\")) ) + #set( $ownerAllowedFields0 = [] ) + #if( $ownerClaim0 == $ownerEntity0 ) + #if( !$ownerAllowedFields0.isEmpty() ) + $util.qr($allowedFields.addAll($ownerAllowedFields0)) + #else + #set( $isAuthorized = true ) + #end + #end + #if( $util.isNull($ownerEntity0) && !$ctx.args.input.containsKey(\\"owner\\") ) + $util.qr($ctx.args.input.put(\\"owner\\", $ownerClaim0)) + #if( !$ownerAllowedFields0.isEmpty() ) + $util.qr($allowedFields.addAll($ownerAllowedFields0)) + #else + #set( $isAuthorized = true ) + #end + #end + #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. **" +`; + +exports[`join table inherits auth from tables with similar rules 7`] = ` +"## [Start] Sandbox Mode Disabled. ** +#if( !$ctx.stash.get(\\"hasAuth\\") ) + $util.unauthorized() +#end +$util.toJson({}) +## [End] Sandbox Mode Disabled. **" +`; + +exports[`join table inherits auth from tables with similar rules 8`] = ` +"## [Start] Get Request template. ** +#set( $GetRequest = { + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"GetItem\\" +} ) +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $key = $ctx.stash.metadata.modelObjectKey ) +#else + #set( $key = { + \\"id\\": $util.dynamodb.toDynamoDB($ctx.args.input.id) +} ) +#end +$util.qr($GetRequest.put(\\"key\\", $key)) +$util.toJson($GetRequest) +## [End] Get Request template. **" +`; + +exports[`join table inherits auth from tables with similar rules 9`] = ` +"## [Start] Sandbox Mode Disabled. ** +#if( !$ctx.stash.get(\\"hasAuth\\") ) + $util.unauthorized() +#end +$util.toJson({}) +## [End] Sandbox Mode Disabled. **" +`; + +exports[`join table inherits auth from tables with similar rules 10`] = ` +"## [Start] Authorization Steps. ** +$util.qr($ctx.stash.put(\\"hasAuth\\", true)) +#set( $isAuthorized = false ) +#if( $util.authType() == \\"API Key Authorization\\" ) + #set( $isAuthorized = true ) +#end +#if( $util.authType() == \\"IAM Authorization\\" ) + #if( $ctx.identity.userArn == $ctx.stash.authRole ) + #set( $isAuthorized = true ) + #end +#end +#if( $util.authType() == \\"User Pool Authorization\\" ) + #if( !$isAuthorized ) + #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.owner, null) ) + #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get(\\"username\\"), $util.defaultIfNull($ctx.identity.claims.get(\\"cognito:username\\"), \\"___xamznone____\\")) ) + #if( $ownerEntity0 == $ownerClaim0 ) + #set( $isAuthorized = true ) + #end + #end +#end +#if( !$isAuthorized ) +$util.unauthorized() +#end +$util.toJson({\\"version\\":\\"2018-05-29\\",\\"payload\\":{}}) +## [End] Authorization Steps. **" +`; + +exports[`join table inherits auth from tables with similar rules 11`] = ` +"## [Start] Delete Request template. ** +#set( $DeleteRequest = { + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"DeleteItem\\" +} ) +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $Key = $ctx.stash.metadata.modelObjectKey ) +#else + #set( $Key = { + \\"id\\": $util.dynamodb.toDynamoDB($ctx.args.input.id) +} ) +#end +$util.qr($DeleteRequest.put(\\"key\\", $Key)) +## Begin - key condition ** +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $keyConditionExpr = {} ) + #set( $keyConditionExprNames = {} ) + #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() ) + $util.qr($keyConditionExpr.put(\\"keyCondition$velocityCount\\", { + \\"attributeExists\\": true +})) + $util.qr($keyConditionExprNames.put(\\"#keyCondition$velocityCount\\", \\"$entry.key\\")) + #end + $util.qr($ctx.stash.conditions.add($keyConditionExpr)) +#else + $util.qr($ctx.stash.conditions.add({ + \\"id\\": { + \\"attributeExists\\": true + } +})) +#end +## End - key condition ** +#if( $context.args.condition ) + $util.qr($ctx.stash.conditions.add($context.args.condition)) +#end +## Start condition block ** +#if( $ctx.stash.conditions && $ctx.stash.conditions.size() != 0 ) + #set( $mergedConditions = { + \\"and\\": $ctx.stash.conditions +} ) + #set( $Conditions = $util.parseJson($util.transform.toDynamoDBConditionExpression($mergedConditions)) ) + #if( $Conditions.expressionValues && $Conditions.expressionValues.size() == 0 ) + #set( $Conditions = { + \\"expression\\": $Conditions.expression, + \\"expressionNames\\": $Conditions.expressionNames +} ) + #end + ## End condition block ** +#end +#if( $Conditions ) + #if( $keyConditionExprNames ) + $util.qr($Conditions.expressionNames.putAll($keyConditionExprNames)) + #end + $util.qr($DeleteRequest.put(\\"condition\\", $Conditions)) +#end +$util.toJson($DeleteRequest) +## [End] Delete Request template. **" +`; + +exports[`join table inherits auth from tables with similar rules 12`] = ` +"## [Start] ResponseTemplate. ** +$util.qr($ctx.result.put(\\"__operation\\", \\"Mutation\\")) +#if( $ctx.error ) + $util.error($ctx.error.message, $ctx.error.type) +#else + $util.toJson($ctx.result) +#end +## [End] ResponseTemplate. **" +`; + +exports[`join table inherits auth from tables with similar rules 13`] = ` +"## [Start] Get Request template. ** +#set( $GetRequest = { + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"GetItem\\" +} ) +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $key = $ctx.stash.metadata.modelObjectKey ) +#else + #set( $key = { + \\"id\\": $util.dynamodb.toDynamoDB($ctx.args.input.id) +} ) +#end +$util.qr($GetRequest.put(\\"key\\", $key)) +$util.toJson($GetRequest) +## [End] Get Request template. **" +`; + +exports[`join table inherits auth from tables with similar rules 14`] = ` +"## [Start] Sandbox Mode Disabled. ** +#if( !$ctx.stash.get(\\"hasAuth\\") ) + $util.unauthorized() +#end +$util.toJson({}) +## [End] Sandbox Mode Disabled. **" +`; + +exports[`join table inherits auth from tables with similar rules 15`] = ` +"## [Start] Authorization Steps. ** +$util.qr($ctx.stash.put(\\"hasAuth\\", true)) +#if( $ctx.error ) + $util.error($ctx.error.message, $ctx.error.type) +#end +#set( $inputFields = $util.parseJson($util.toJson($ctx.args.input.keySet())) ) +#set( $isAuthorized = false ) +#set( $allowedFields = [] ) +#set( $nullAllowedFields = [] ) +#set( $deniedFields = {} ) +#if( $util.authType() == \\"API Key Authorization\\" ) + #set( $isAuthorized = true ) +#end +#if( $util.authType() == \\"IAM Authorization\\" ) + #if( $ctx.identity.userArn == $ctx.stash.authRole ) + #set( $isAuthorized = true ) + #end +#end +#if( $util.authType() == \\"User Pool Authorization\\" ) + #if( !$isAuthorized ) + #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.owner, null) ) + #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get(\\"username\\"), $util.defaultIfNull($ctx.identity.claims.get(\\"cognito:username\\"), \\"___xamznone____\\")) ) + #set( $ownerAllowedFields0 = [] ) + #set( $ownerNullAllowedFields0 = [] ) + #if( $ownerEntity0 == $ownerClaim0 ) + #if( !$ownerAllowedFields0.isEmpty() || !$ownerNullAllowedFields0.isEmpty() ) + $util.qr($allowedFields.addAll($ownerAllowedFields0)) + $util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0)) + #else + #set( $isAuthorized = true ) + #end + #end + #end +#end +#if( !$isAuthorized && $allowedFields.isEmpty() && $nullAllowedFields.isEmpty() ) +$util.unauthorized() +#end +#if( !$isAuthorized ) + #foreach( $entry in $util.map.copyAndRetainAllKeys($ctx.args.input, $inputFields).entrySet() ) + #if( $util.isNull($entry.value) && !$nullAllowedFields.contains($entry.key) ) + $util.qr($deniedFields.put($entry.key, \\"\\")) + #end + #end + #foreach( $deniedField in $util.list.copyAndRemoveAll($inputFields, $allowedFields) ) + $util.qr($deniedFields.put($deniedField, \\"\\")) + #end +#end +#if( $deniedFields.keySet().size() > 0 ) + $util.error(\\"Unauthorized on \${deniedFields.keySet()}\\", \\"Unauthorized\\") +#end +$util.toJson({}) +## [End] Authorization Steps. **" +`; + +exports[`join table inherits auth from tables with similar rules 16`] = ` +"## [Start] Mutation Update resolver. ** +## Set the default values to put request ** +#set( $mergedValues = $util.defaultIfNull($ctx.stash.defaultValues, {}) ) +## copy the values from input ** +$util.qr($mergedValues.putAll($util.defaultIfNull($ctx.args.input, {}))) +## set the typename ** +## Initialize the vars for creating ddb expression ** +#set( $expNames = {} ) +#set( $expValues = {} ) +#set( $expSet = {} ) +#set( $expAdd = {} ) +#set( $expRemove = [] ) +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $Key = $ctx.stash.metadata.modelObjectKey ) +#else + #set( $Key = { + \\"id\\": $util.dynamodb.toDynamoDB($ctx.args.input.id) +} ) +#end +## Model key ** +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $keyFields = [] ) + #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() ) + $util.qr($keyFields.add(\\"$entry.key\\")) + #end +#else + #set( $keyFields = [\\"id\\"] ) +#end +#foreach( $entry in $util.map.copyAndRemoveAllKeys($mergedValues, $keyFields).entrySet() ) + #if( !$util.isNull($ctx.stash.metadata.dynamodbNameOverrideMap) && $ctx.stash.metadata.dynamodbNameOverrideMap.containsKey(\\"$entry.key\\") ) + #set( $entryKeyAttributeName = $ctx.stash.metadata.dynamodbNameOverrideMap.get(\\"$entry.key\\") ) + #else + #set( $entryKeyAttributeName = $entry.key ) + #end + #if( $util.isNull($entry.value) ) + #set( $discard = $expRemove.add(\\"#$entryKeyAttributeName\\") ) + $util.qr($expNames.put(\\"#$entryKeyAttributeName\\", \\"$entry.key\\")) + #else + $util.qr($expSet.put(\\"#$entryKeyAttributeName\\", \\":$entryKeyAttributeName\\")) + $util.qr($expNames.put(\\"#$entryKeyAttributeName\\", \\"$entry.key\\")) + $util.qr($expValues.put(\\":$entryKeyAttributeName\\", $util.dynamodb.toDynamoDB($entry.value))) + #end +#end +#set( $expression = \\"\\" ) +#if( !$expSet.isEmpty() ) + #set( $expression = \\"SET\\" ) + #foreach( $entry in $expSet.entrySet() ) + #set( $expression = \\"$expression $entry.key = $entry.value\\" ) + #if( $foreach.hasNext() ) + #set( $expression = \\"$expression,\\" ) + #end + #end +#end +#if( !$expAdd.isEmpty() ) + #set( $expression = \\"$expression ADD\\" ) + #foreach( $entry in $expAdd.entrySet() ) + #set( $expression = \\"$expression $entry.key $entry.value\\" ) + #if( $foreach.hasNext() ) + #set( $expression = \\"$expression,\\" ) + #end + #end +#end +#if( !$expRemove.isEmpty() ) + #set( $expression = \\"$expression REMOVE\\" ) + #foreach( $entry in $expRemove ) + #set( $expression = \\"$expression $entry\\" ) + #if( $foreach.hasNext() ) + #set( $expression = \\"$expression,\\" ) + #end + #end +#end +#set( $update = {} ) +$util.qr($update.put(\\"expression\\", \\"$expression\\")) +#if( !$expNames.isEmpty() ) + $util.qr($update.put(\\"expressionNames\\", $expNames)) +#end +#if( !$expValues.isEmpty() ) + $util.qr($update.put(\\"expressionValues\\", $expValues)) +#end +## Begin - key condition ** +#if( $ctx.stash.metadata.modelObjectKey ) + #set( $keyConditionExpr = {} ) + #set( $keyConditionExprNames = {} ) + #foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() ) + $util.qr($keyConditionExpr.put(\\"keyCondition$velocityCount\\", { + \\"attributeExists\\": true +})) + $util.qr($keyConditionExprNames.put(\\"#keyCondition$velocityCount\\", \\"$entry.key\\")) + #end + $util.qr($ctx.stash.conditions.add($keyConditionExpr)) +#else + $util.qr($ctx.stash.conditions.add({ + \\"id\\": { + \\"attributeExists\\": true + } +})) +#end +## End - key condition ** +#if( $context.args.condition ) + $util.qr($ctx.stash.conditions.add($context.args.condition)) +#end +## Start condition block ** +#if( $ctx.stash.conditions && $ctx.stash.conditions.size() != 0 ) + #set( $mergedConditions = { + \\"and\\": $ctx.stash.conditions +} ) + #set( $Conditions = $util.parseJson($util.transform.toDynamoDBConditionExpression($mergedConditions)) ) + #if( $Conditions.expressionValues && $Conditions.expressionValues.size() == 0 ) + #set( $Conditions = { + \\"expression\\": $Conditions.expression, + \\"expressionNames\\": $Conditions.expressionNames +} ) + #end + ## End condition block ** +#end +#set( $UpdateItem = { + \\"version\\": \\"2018-05-29\\", + \\"operation\\": \\"UpdateItem\\", + \\"key\\": $Key, + \\"update\\": $update +} ) +#if( $Conditions ) + #if( $keyConditionExprNames ) + $util.qr($Conditions.expressionNames.putAll($keyConditionExprNames)) + #end + $util.qr($UpdateItem.put(\\"condition\\", $Conditions)) +#end +$util.toJson($UpdateItem) +## [End] Mutation Update resolver. **" +`; + +exports[`join table inherits auth from tables with similar rules 17`] = ` +"## [Start] ResponseTemplate. ** +$util.qr($ctx.result.put(\\"__operation\\", \\"Mutation\\")) +#if( $ctx.error ) + $util.error($ctx.error.message, $ctx.error.type) +#else + $util.toJson($ctx.result) +#end +## [End] ResponseTemplate. **" +`; + exports[`valid schema 1`] = ` " type Foo { diff --git a/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-many-to-many-transformer.test.ts b/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-many-to-many-transformer.test.ts index 2888a6ba154..0c539457163 100644 --- a/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-many-to-many-transformer.test.ts +++ b/packages/amplify-graphql-relational-transformer/src/__tests__/amplify-graphql-many-to-many-transformer.test.ts @@ -291,8 +291,48 @@ test('join table inherits auth from both tables', () => { expect(out.pipelineFunctions['Mutation.updateFooBar.res.vtl']).toMatchSnapshot(); }); -function createTransformer() { - const authConfig: AppSyncAuthConfiguration = { +test('join table inherits auth from tables with similar rules', () => { + const inputSchema = ` + type Foo @model @auth(rules: [{ allow: owner }, { allow: private, provider: iam }]) { + id: ID! + bars: [Bar] @manyToMany(relationName: "FooBar") + } + type Bar @model @auth(rules: [{ allow: owner }, { allow: public, provider: apiKey }]) { + id: ID! + foos: [Foo] @manyToMany(relationName: "FooBar") + }`; + const transformer = createTransformer({ + defaultAuthentication: { + authenticationType: 'API_KEY', + }, + additionalAuthenticationProviders: [{ authenticationType: 'AWS_IAM' }, { authenticationType: 'AMAZON_COGNITO_USER_POOLS' }], + }); + const out = transformer.transform(inputSchema); + expect(out).toBeDefined(); + const schema = parse(out.schema); + validateModelSchema(schema); + + expect(out.pipelineFunctions['Query.getFooBar.auth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Query.getFooBar.postAuth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Query.getFooBar.res.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Query.listFooBars.auth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Query.listFooBars.postAuth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.createFooBar.auth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.createFooBar.postAuth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.deleteFooBar.auth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.deleteFooBar.postAuth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.deleteFooBar.auth.1.res.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.deleteFooBar.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.deleteFooBar.res.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.updateFooBar.auth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.updateFooBar.postAuth.1.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.updateFooBar.auth.1.res.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.updateFooBar.req.vtl']).toMatchSnapshot(); + expect(out.pipelineFunctions['Mutation.updateFooBar.res.vtl']).toMatchSnapshot(); +}); + +function createTransformer(authConfig?: AppSyncAuthConfiguration) { + const transformerAuthConfig: AppSyncAuthConfiguration = authConfig ?? { defaultAuthentication: { authenticationType: 'API_KEY', }, @@ -303,7 +343,7 @@ function createTransformer() { const indexTransformer = new IndexTransformer(); const hasOneTransformer = new HasOneTransformer(); const transformer = new GraphQLTransform({ - authConfig, + authConfig: transformerAuthConfig, transformers: [ modelTransformer, indexTransformer, diff --git a/packages/amplify-graphql-relational-transformer/src/graphql-many-to-many-transformer.ts b/packages/amplify-graphql-relational-transformer/src/graphql-many-to-many-transformer.ts index ef3c04399fd..eced83b963f 100644 --- a/packages/amplify-graphql-relational-transformer/src/graphql-many-to-many-transformer.ts +++ b/packages/amplify-graphql-relational-transformer/src/graphql-many-to-many-transformer.ts @@ -116,9 +116,13 @@ export class ManyToManyTransformer extends TransformerPluginBase { prepare = (ctx: TransformerPrepareStepContextProvider): void => { // The @manyToMany directive creates a join table, injects it into the existing transformer, and then functions like one to many. const context = ctx as TransformerContextProvider; + if (!ctx.metadata.has('joinTypeList')) { + ctx.metadata.set('joinTypeList', []); + } this.relationMap.forEach(relation => { const { directive1, directive2, name } = relation; + ctx.metadata.get>('joinTypeList')!.push(name); const d1TypeName = directive1.object.name.value; const d2TypeName = directive2.object.name.value; const d1FieldName = d1TypeName.charAt(0).toLowerCase() + d1TypeName.slice(1);