Skip to content

Commit

Permalink
fix(graphql-auth-transformer): allow auth progation to recursive types (
Browse files Browse the repository at this point in the history
aws-amplify#4788)

allow auth progation to recursive types - once auth has been added to a non model type it will not
re-add auth to said type

re aws-amplify#4631
  • Loading branch information
SwaySway authored Jul 14, 2020
1 parent 2303b08 commit 6402304
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 20 deletions.
40 changes: 21 additions & 19 deletions packages/graphql-auth-transformer/src/ModelAuthTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
import { Expression, print, raw, iff, forEach, set, ref, list, compoundExpression, newline, comment, not } from 'graphql-mapping-template';
import { ModelDirectiveConfiguration, ModelDirectiveOperationType, ModelSubscriptionLevel } from './ModelDirectiveConfiguration';

import { OWNER_AUTH_STRATEGY, GROUPS_AUTH_STRATEGY, DEFAULT_OWNER_FIELD } from './constants';
import { OWNER_AUTH_STRATEGY, GROUPS_AUTH_STRATEGY, DEFAULT_OWNER_FIELD, AUTH_NON_MODEL_TYPES } from './constants';

/**
* Implements the ModelAuthTransformer.
Expand Down Expand Up @@ -265,6 +265,9 @@ export class ModelAuthTransformer extends Transformer {
ctx.mergeOutputs(template.Outputs);
ctx.mergeConditions(template.Conditions);
this.updateAPIAuthentication(ctx);
if (!ctx.metadata.has(AUTH_NON_MODEL_TYPES)) {
ctx.metadata.set(AUTH_NON_MODEL_TYPES, new Set<string>());
}
};

public after = (ctx: TransformerContext): void => {
Expand Down Expand Up @@ -521,38 +524,37 @@ Static group authorization should perform as expected.`,
};

private propagateAuthDirectivesToNestedTypes(type: ObjectTypeDefinitionNode, rules: AuthRule[], ctx: TransformerContext) {
const seenNonModelTypes: Set<string> = ctx.metadata.get(AUTH_NON_MODEL_TYPES);

const nonModelTypePredicate = (fieldType: TypeDefinitionNode): TypeDefinitionNode | undefined => {
if (fieldType) {
if (fieldType.kind !== 'ObjectTypeDefinition') {
return undefined;
}

const typeModel = fieldType.directives.find(dir => dir.name.value === 'model');
return typeModel !== undefined ? undefined : fieldType;
}

return fieldType;
};

const nonModelFieldTypes = type.fields.map(f => ctx.getType(getBaseType(f.type)) as TypeDefinitionNode).filter(nonModelTypePredicate);

for (const nonModelFieldType of nonModelFieldTypes) {
const directives = this.getDirectivesForRules(rules, false);

// Add the directives to the Type node itself
if (directives.length > 0) {
this.extendTypeWithDirectives(ctx, nonModelFieldType.name.value, directives);
}

const hasIAM = directives.filter(directive => directive.name.value === 'aws_iam') || this.configuredAuthProviders.default === 'iam';
if (!seenNonModelTypes.has(nonModelFieldType.name.value)) {
// add to the set of seen non model types
seenNonModelTypes.add(nonModelFieldType.name.value);
const directives = this.getDirectivesForRules(rules, false);
// Add the directives to the Type node itself
if (directives.length > 0) {
this.extendTypeWithDirectives(ctx, nonModelFieldType.name.value, directives);
}
const hasIAM = directives.filter(directive => directive.name.value === 'aws_iam') || this.configuredAuthProviders.default === 'iam';
if (hasIAM) {
this.unauthPolicyResources.add(`${nonModelFieldType.name.value}/null`);
this.authPolicyResources.add(`${nonModelFieldType.name.value}/null`);
}

if (hasIAM) {
this.unauthPolicyResources.add(`${nonModelFieldType.name.value}/null`);
this.authPolicyResources.add(`${nonModelFieldType.name.value}/null`);
// Recursively process the nested types if there is any
this.propagateAuthDirectivesToNestedTypes(<ObjectTypeDefinitionNode>nonModelFieldType, rules, ctx);
}

// Recursively process the nested types if there is any
this.propagateAuthDirectivesToNestedTypes(<ObjectTypeDefinitionNode>nonModelFieldType, rules, ctx);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ObjectTypeDefinitionNode, parse, DocumentNode, Kind } from 'graphql';
import { ObjectTypeDefinitionNode, FieldDefinitionNode, parse, DocumentNode, Kind } from 'graphql';
import { GraphQLTransform } from 'graphql-transformer-core';
import { DynamoDBModelTransformer } from 'graphql-dynamodb-transformer';
import { ModelConnectionTransformer } from 'graphql-connection-transformer';
Expand Down Expand Up @@ -66,6 +66,7 @@ const multiAuthDirective =
const ownerAuthDirective = '@auth(rules: [{allow: owner}])';
const ownerWithIAMAuthDirective = '@auth(rules: [{allow: owner, provider: iam }])';
const ownerRestrictedPublicAuthDirective = '@auth(rules: [{allow: owner},{allow: public, operations: [read]}])';
const ownerRestrictedIAMPrivateAuthDirective = '@auth(rules: [{allow: owner},{allow: private, operations: [read], provider: iam }])';
const groupsAuthDirective = '@auth(rules: [{allow: groups}])';
const groupsWithApiKeyAuthDirective = '@auth(rules: [{allow: groups}, {allow: public, operations: [read]}])';
const groupsWithProviderAuthDirective = '@auth(rules: [{allow: groups, provider: iam }])';
Expand Down Expand Up @@ -140,6 +141,21 @@ const getSchemaWithNonModelField = (authDirective: string) => {
}`;
};

const getSchemaWithRecursiveNonModelField = (authDirective: string) => {
return `
type Post @model ${authDirective} {
id: ID!
title: String!
tags: [Tag]
}
type Tag {
id: ID
tags: [Tag]
}
`;
};

const getTransformer = (authConfig: AppSyncAuthConfiguration) =>
new GraphQLTransform({
transformers: [new DynamoDBModelTransformer(), new ModelConnectionTransformer(), new ModelAuthTransformer({ authConfig })],
Expand Down Expand Up @@ -168,6 +184,21 @@ const expectTwo = (fieldOrType, directiveNames) => {
expect(fieldOrType.directives.find(d => d.name.value === directiveNames[1])).toBeDefined();
};

const expectMultiple = (fieldOrType: ObjectTypeDefinitionNode | FieldDefinitionNode, directiveNames: string[]) => {
expect(directiveNames).toBeDefined();
expect(directiveNames).toHaveLength(directiveNames.length);
expect(fieldOrType.directives.length).toEqual(directiveNames.length);
directiveNames.forEach(directiveName => {
expect(fieldOrType.directives).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: expect.objectContaining({ value: directiveName }),
}),
]),
);
});
};

const getField = (type, name) => type.fields.find(f => f.name.value === name);

describe('Validation tests', () => {
Expand Down Expand Up @@ -461,6 +492,19 @@ describe('Type directive transformation tests', () => {
}
});

test(`Recursive types without @model`, () => {
const schema = getSchemaWithRecursiveNonModelField(ownerRestrictedIAMPrivateAuthDirective);
const transformer = getTransformer(withAuthModes(userPoolsDefaultConfig, ['AWS_IAM']));

const out = transformer.transform(schema);
const schemaDoc = parse(out.schema);

const tagType = getObjectType(schemaDoc, 'Tag');
const expectedDirectiveNames = [userPoolsDirectiveName, iamDirectiveName];

expectMultiple(tagType, expectedDirectiveNames);
});

test(`Nested types without @model getting directives applied (cognito default, iam additional)`, () => {
const schema = getSchemaWithNonModelField(privateAndPrivateIAMDirective);
const transformer = getTransformer(withAuthModes(userPoolsDefaultConfig, ['AWS_IAM']));
Expand Down
1 change: 1 addition & 0 deletions packages/graphql-auth-transformer/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export const DEFAULT_GROUP_CLAIM = 'cognito:groups';
export const ON_CREATE_FIELD = 'onCreate';
export const ON_UPDATE_FIELD = 'onUpdate';
export const ON_DELETE_FIELD = 'onDelete';
export const AUTH_NON_MODEL_TYPES = 'authNonModelTypes';

0 comments on commit 6402304

Please sign in to comment.