From 59452a166429d275dd199c5ae5bbd287c415051a Mon Sep 17 00:00:00 2001 From: Josue Ruiz Date: Wed, 2 Oct 2019 18:17:42 -0700 Subject: [PATCH] test(graphql-transformers-e2e-tests): adding subscriptions with auth e2e (#2400) * test(graphql-transformers-e2e-tests): adding subscriptions with auth e2e adding e2e subscriptions with auth depedent on pr #2368 * refactor(graphql-transformers-e2e-tests): refactored changes based on pr * test(amplify-util-mock): added mock e2e subscription tests --- packages/amplify-util-mock/package.json | 1 + .../subscriptions-with-auth.e2e.test.ts | 397 ++++++++ .../package.json | 1 + .../SubscriptionsWithAuthTest.e2e.test.ts | 914 ++++++++++++++++++ yarn.lock | 12 +- 5 files changed, 1323 insertions(+), 2 deletions(-) create mode 100644 packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts create mode 100644 packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts diff --git a/packages/amplify-util-mock/package.json b/packages/amplify-util-mock/package.json index e37b643cce6..6f93460c785 100755 --- a/packages/amplify-util-mock/package.json +++ b/packages/amplify-util-mock/package.json @@ -40,6 +40,7 @@ "graphql-transformer-common": "3.26.0", "graphql-transformer-core": "5.12.0", "graphql-versioned-transformer": "3.22.0", + "isomorphic-fetch": "^2.2.1", "jest": "^24.8.0", "jest-environment-node": "^24.8.0", "jest-junit": "^8.0.0", diff --git a/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts b/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts new file mode 100644 index 00000000000..bc17df42c4f --- /dev/null +++ b/packages/amplify-util-mock/src/__e2e__/subscriptions-with-auth.e2e.test.ts @@ -0,0 +1,397 @@ +import ModelAuthTransformer from 'graphql-auth-transformer'; +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer'; +import GraphQLTransform from 'graphql-transformer-core'; +import { deploy, launchDDBLocal, terminateDDB } from './utils/index'; +import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync'; +import { signUpAddToGroupAndGetJwtToken } from './utils/cognito-utils'; +import { AWS } from '@aws-amplify/core'; +import gql from 'graphql-tag'; +import 'isomorphic-fetch'; + +// To overcome of the way of how AmplifyJS picks up currentUserCredentials +const anyAWS = (AWS as any); + +if (anyAWS && anyAWS.config && anyAWS.config.credentials) { + delete anyAWS.config.credentials; +} + +// to deal with bug in cognito-identity-js +(global as any).fetch = require('node-fetch'); +// to deal with subscriptions in node env +(global as any).WebSocket = require('ws'); + +jest.setTimeout(2000000); + +let GRAPHQL_ENDPOINT = undefined; +let ddbEmulator = null; +let dbPath = null; +let server; +const AWS_REGION = 'my-local-2' + +let GRAPHQL_CLIENT_1: AWSAppSyncClient = undefined; + +let GRAPHQL_CLIENT_2: AWSAppSyncClient = undefined; + +let GRAPHQL_CLIENT_3: AWSAppSyncClient = undefined; + +const USER_POOL_ID = 'fake_user_pool'; + +const USERNAME1 = 'user1@domain.com'; +const USERNAME2 = 'user2@domain.com'; +const USERNAME3 = 'user3@domain.com'; + +const INSTRUCTOR_GROUP_NAME = 'Instructor'; + +/** + * Interface Inputs + */ +interface CreateStudentInput { + id?: string, + name?: string, + email?: string, + ssn?: string, +} + +interface UpdateStudentInput { + id: string, + name?: string, + email?: string, + ssn?: string, +} + +interface CreatePostInput { + id?: string, + title: string, + postOwner: string, +} + +interface DeleteTypeInput { + id: string, +} + + +beforeAll(async () => { + const validSchema = ` + type Student @model + @auth(rules: [ + {allow: owner} + {allow: groups, groups: ["Instructor"]} + ]) { + id: String, + name: String, + email: AWSEmail, + ssn: String @auth(rules: [{allow: owner}]) + } + + type Post @model + @auth(rules: [ + {allow: owner, ownerField: "postOwner"} + ]) + { + id: ID! + title: String + postOwner: String + } + `; + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS' + }, + additionalAuthenticationProviders: [], + } + }), + ] + }); + + try { + const out = transformer.transform(validSchema); + + let ddbClient; + ({ dbPath, emulator: ddbEmulator, client: ddbClient } = await launchDDBLocal()); + + const result = await deploy(out, ddbClient); + server = result.simulator; + + GRAPHQL_ENDPOINT = server.url + '/graphql'; + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy(); + // Configure Amplify, create users, and sign in. + const idToken1 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [ + INSTRUCTOR_GROUP_NAME + ]); + GRAPHQL_CLIENT_1 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools' + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken1, + }}) + const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [ + INSTRUCTOR_GROUP_NAME, + ]); + GRAPHQL_CLIENT_2 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools' + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken2, + }}) + const idToken3 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []); + GRAPHQL_CLIENT_3 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools' + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken3, + }}) + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise(res => setTimeout(() => res(), 5000)); + } catch (e) { + console.error(e); + expect(true).toEqual(false); + } +}) + +afterAll(async () => { + try { + if (server) { + await server.stop(); + } + await terminateDDB(ddbEmulator, dbPath); + } catch (e) { + console.error(e); + throw e; + } +}) + +/** + * Tests + */ +test('Test that only authorized members are allowed to view subscriptions', async done => { + // subscribe to create students as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql` + subscription OnCreateStudent { + onCreateStudent { + id + name + email + ssn + owner + } + }`}) + console.log(observer) + let subscription = observer.subscribe( (event: any) => { + console.log('subscription event: ', event) + const student = event.data.onCreateStudent; + subscription.unsubscribe() + expect(student.name).toEqual('student1') + expect(student.email).toEqual('student1@domain.com') + expect(student.ssn).toBeNull() + done(); + }); + + await new Promise((res) => setTimeout(() => res(), 1000)) + + createStudent(GRAPHQL_CLIENT_1, + { + name: "student1", + email: "student1@domain.com", + ssn: "AAA-01-SSSS", + }) +}) + +test('Test that a user not in the group is not allowed to view the subscription', async done => { + // suscribe to create students as user 3 + const observer = GRAPHQL_CLIENT_3.subscribe({ query: gql` + subscription OnCreateStudent { + onCreateStudent { + id + name + email + ssn + owner + } + }`}) + observer.subscribe({ + error: (err: any) => { + console.log(err.graphQLErrors[0]) + expect(err.graphQLErrors[0].message).toEqual('Unauthorized') + done() + } + }); + await new Promise((res) => setTimeout(() => res(), 1000)) + + createStudent(GRAPHQL_CLIENT_1, + { + name: "student2", + email: "student2@domain.com", + ssn: "BBB-00-SNSN" + }) +}) + +test('Test a subscription on update', async done => { + // susbcribe to update students as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql` + subscription OnUpdateStudent { + onUpdateStudent { + id + name + email + ssn + owner + } + }` }) + let subscription = observer.subscribe( (event: any) => { + const student = event.data.onUpdateStudent; + subscription.unsubscribe() + expect(student.id).toEqual(student3ID) + expect(student.name).toEqual('student3') + expect(student.email).toEqual('emailChanged@domain.com') + expect(student.ssn).toBeNull() + done(); + }); + + const student3 = await createStudent(GRAPHQL_CLIENT_1, + { + name: "student3", + email: "changeThisEmail@domain.com", + ssn: "CCC-01-SNSN" + }) + expect(student3.data.createStudent).toBeDefined() + const student3ID = student3.data.createStudent.id + expect(student3.data.createStudent.name).toEqual('student3') + expect(student3.data.createStudent.email).toEqual('changeThisEmail@domain.com') + expect(student3.data.createStudent.ssn).toBeNull() + + updateStudent(GRAPHQL_CLIENT_1, + { + id: student3ID, + email: 'emailChanged@domain.com' + }) +}) + + +test('Test a subscription on delete', async done => { + // subscribe to onDelete as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql ` + subscription OnDeleteStudent { + onDeleteStudent { + id + name + email + ssn + owner + } + }`}) + let subscription = observer.subscribe( (event: any) => { + const student = event.data.onDeleteStudent; + subscription.unsubscribe() + expect(student.id).toEqual(student4ID) + expect(student.name).toEqual('student4') + expect(student.email).toEqual('plsDelete@domain.com') + expect(student.ssn).toBeNull() + done(); + }); + + const student4 = await createStudent(GRAPHQL_CLIENT_1, + { + name: "student4", + email: "plsDelete@domain.com", + ssn: "DDD-02-SNSN" + }) + expect(student4).toBeDefined() + const student4ID = student4.data.createStudent.id + expect(student4.data.createStudent.email).toEqual('plsDelete@domain.com') + expect(student4.data.createStudent.ssn).toBeNull() + + await deleteStudent(GRAPHQL_CLIENT_1, { id: student4ID }) +}) + +// ownerField Tests +test('Test subscription onCreatePost with ownerField', async done => { + const observer = GRAPHQL_CLIENT_1.subscribe({ query: gql` + subscription OnCreatePost { + onCreatePost(postOwner: "${USERNAME1}") { + id + title + postOwner + } + }`}) + let subscription = observer.subscribe( (event: any) => { + const post = event.data.onCreatePost; + subscription.unsubscribe() + expect(post.title).toEqual('someTitle') + expect(post.postOwner).toEqual(USERNAME1) + done(); + }); + await new Promise((res) => setTimeout(() => res(), 1000)) + + createPost(GRAPHQL_CLIENT_1, { + title: "someTitle", + postOwner: USERNAME1 + }) +}) + +// mutations +async function createStudent(client: AWSAppSyncClient, input: CreateStudentInput) { + const request = gql`mutation CreateStudent($input: CreateStudentInput!) { + createStudent(input: $input) { + id + name + email + ssn + owner + } + } + `; + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function updateStudent(client: AWSAppSyncClient, input: UpdateStudentInput) { + const request = gql`mutation UpdateStudent($input: UpdateStudentInput!) { + updateStudent(input: $input) { + id + name + email + ssn + owner + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function deleteStudent(client: AWSAppSyncClient, input: DeleteTypeInput) { + const request = gql`mutation DeleteStudent($input: DeleteStudentInput!) { + deleteStudent(input: $input) { + id + name + email + ssn + owner + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function createPost(client: AWSAppSyncClient, input: CreatePostInput) { + const request = gql`mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + postOwner + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} \ No newline at end of file diff --git a/packages/graphql-transformers-e2e-tests/package.json b/packages/graphql-transformers-e2e-tests/package.json index f4b24ded7a8..e3fffb0cefa 100644 --- a/packages/graphql-transformers-e2e-tests/package.json +++ b/packages/graphql-transformers-e2e-tests/package.json @@ -41,6 +41,7 @@ "graphql-key-transformer": "1.19.0", "graphql-tag": "^2.10.1", "graphql-versioned-transformer": "3.22.0", + "isomorphic-fetch": "^2.2.1", "jest": "^23.1.0", "jest-junit": "^8.0.0", "node-fetch": "^2.6.0", diff --git a/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts b/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts new file mode 100644 index 00000000000..66797c540bb --- /dev/null +++ b/packages/graphql-transformers-e2e-tests/src/__tests__/SubscriptionsWithAuthTest.e2e.test.ts @@ -0,0 +1,914 @@ +import { ResourceConstants } from 'graphql-transformer-common' +import GraphQLTransform from 'graphql-transformer-core' +import DynamoDBModelTransformer from 'graphql-dynamodb-transformer' +import ModelAuthTransformer from 'graphql-auth-transformer' +import * as fs from 'fs' +import { CloudFormationClient } from '../CloudFormationClient' +import { Output } from 'aws-sdk/clients/cloudformation' +import * as S3 from 'aws-sdk/clients/s3' +import { CreateBucketRequest } from 'aws-sdk/clients/s3' +import * as CognitoClient from 'aws-sdk/clients/cognitoidentityserviceprovider' +import AWSAppSyncClient, { AUTH_TYPE } from 'aws-appsync'; +import { AWS } from '@aws-amplify/core'; +import { Auth } from 'aws-amplify'; +import gql from 'graphql-tag'; +import { S3Client } from '../S3Client'; +import { deploy } from '../deployNestedStacks' +import * as moment from 'moment'; +import emptyBucket from '../emptyBucket'; +import { IAM as cfnIAM, Cognito as cfnCognito } from 'cloudform-types'; +import { + createUserPool, createUserPoolClient, deleteUserPool, + signupAndAuthenticateUser, createGroup, addUserToGroup, configureAmplify +} from '../cognitoUtils'; +import 'isomorphic-fetch'; + +// To overcome of the way of how AmplifyJS picks up currentUserCredentials +const anyAWS = (AWS as any); + +if (anyAWS && anyAWS.config && anyAWS.config.credentials) { + delete anyAWS.config.credentials; +} + + +// to deal with bug in cognito-identity-js +(global as any).fetch = require("node-fetch"); +// to deal with subscriptions in node env +(global as any).WebSocket = require('ws'); + +jest.setTimeout(2000000); + +const AWS_REGION = 'us-west-2' +const cf = new CloudFormationClient(AWS_REGION) + +const BUILD_TIMESTAMP = moment().format('YYYYMMDDHHmmss') +const STACK_NAME = `SubscriptionAuthTests-${BUILD_TIMESTAMP}` +const BUCKET_NAME = `subscription-auth-tests-bucket-${BUILD_TIMESTAMP}` +const LOCAL_BUILD_ROOT = '/tmp/subscription_auth_tests/' +const DEPLOYMENT_ROOT_KEY = 'deployments' +const AUTH_ROLE_NAME = `${STACK_NAME}-authRole`; +const UNAUTH_ROLE_NAME = `${STACK_NAME}-unauthRole`; +const IDENTITY_POOL_NAME = `SubscriptionAuthModelAuthTransformerTest_${BUILD_TIMESTAMP}_identity_pool`; +const USER_POOL_CLIENTWEB_NAME = `subs_auth_${BUILD_TIMESTAMP}_clientweb`; +const USER_POOL_CLIENT_NAME = `subs_auth_${BUILD_TIMESTAMP}_client`; + +let GRAPHQL_ENDPOINT = undefined; + +/** + * Client 1 is logged in and is a member of the Admin group. + */ +let GRAPHQL_CLIENT_1: AWSAppSyncClient = undefined; + +/** + * Client 2 is logged in and is a member of the Devs group. + */ +let GRAPHQL_CLIENT_2: AWSAppSyncClient = undefined; + +/** + * Client 3 is logged in and has no group memberships. + */ +let GRAPHQL_CLIENT_3: AWSAppSyncClient = undefined; + +/** + * Auth IAM Client + */ +let GRAPHQL_IAM_AUTH_CLIENT: AWSAppSyncClient = undefined; + +/** + * API Key Client + */ +let GRAPHQL_APIKEY_CLIENT: AWSAppSyncClient = undefined; + +let USER_POOL_ID = undefined; + +const USERNAME1 = 'user1@test.com' +const USERNAME2 = 'user2@test.com' +const USERNAME3 = 'user3@test.com' +const TMP_PASSWORD = 'Password123!' +const REAL_PASSWORD = 'Password1234!' + +const INSTRUCTOR_GROUP_NAME = 'Instructor'; + +const cognitoClient = new CognitoClient({ apiVersion: '2016-04-19', region: AWS_REGION }) +const customS3Client = new S3Client(AWS_REGION) +const awsS3Client = new S3({ region: AWS_REGION }) + +// interface inputs +interface CreateStudentInput { + id?: string, + name?: string, + email?: string, + ssn?: string, +} + +interface UpdateStudentInput { + id: string, + name?: string, + email?: string, + ssn?: string, +} + +interface CreatePostInput { + id?: string, + title: string, + postOwner: string, +} + +interface CreateTodoInput { + id?: string, + name?: string, + description?: string, +} + +interface UpdateTodoInput { + id: string, + name?: string, + description?: string, +} + +interface DeleteTypeInput { + id: string, +} + + +function outputValueSelector(key: string) { + return (outputs: Output[]) => { + const output = outputs.find((o: Output) => o.OutputKey === key) + return output ? output.OutputValue : null + } +} + +async function createBucket(name: string) { + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + } + awsS3Client.createBucket(params, (err, data) => err ? rej(err) : res(data)) + }) +} + +async function deleteBucket(name: string) { + return new Promise((res, rej) => { + const params: CreateBucketRequest = { + Bucket: name, + } + awsS3Client.deleteBucket(params, (err, data) => err ? rej(err) : res(data)) + }) +} + +beforeAll(async () => { + // Create a stack for the post model with auth enabled. + if (!fs.existsSync(LOCAL_BUILD_ROOT)) { + fs.mkdirSync(LOCAL_BUILD_ROOT); + } + await createBucket(BUCKET_NAME) + const validSchema = ` + # Owners may update their owned records. + # Instructors may create Student records. + # Any authenticated user may view Student names & emails. + # Only Owners can see the ssn + + type Student @model + @auth(rules: [ + {allow: owner} + {allow: groups, groups: ["Instructor"]} + ]) { + id: String, + name: String, + email: AWSEmail, + ssn: String @auth(rules: [{allow: owner}]) + } + + type Post @model + @auth(rules: [ + {allow: owner, ownerField: "postOwner"} + ]) + { + id: ID! + title: String + postOwner: String + } + + type Todo @model @auth(rules: [ + { allow: public } + ]){ + id: ID! + name: String @auth(rules: [ + { allow: private, provider: iam } + ]) + description: String + }` + + const transformer = new GraphQLTransform({ + transformers: [ + new DynamoDBModelTransformer(), + new ModelAuthTransformer({ + authConfig: { + defaultAuthentication: { + authenticationType: 'AMAZON_COGNITO_USER_POOLS' + }, + additionalAuthenticationProviders: [ + { + authenticationType: 'API_KEY', + apiKeyConfig: { + description: 'E2E Test API Key', + apiKeyExpirationDays: 300 + } + }, + { + authenticationType: 'AWS_IAM' + }, + ], + } + }), + ] + }) + const userPoolResponse = await createUserPool(cognitoClient, `UserPool${STACK_NAME}`); + USER_POOL_ID = userPoolResponse.UserPool.Id; + const userPoolClientResponse = await createUserPoolClient(cognitoClient, USER_POOL_ID, `UserPool${STACK_NAME}`); + const userPoolClientId = userPoolClientResponse.UserPoolClient.ClientId; + try { + // Clean the bucket + const out = transformer.transform(validSchema) + + const authRole = new cfnIAM.Role({ + RoleName: AUTH_ROLE_NAME, + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Sid: '', + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com' + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated' + } + } + } + ] + } + }); + + const unauthRole = new cfnIAM.Role({ + RoleName: UNAUTH_ROLE_NAME, + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Sid: '', + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com' + }, + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'unauthenticated' + } + } + } + ] + }, + Policies: [ + new cfnIAM.Role.Policy({ + PolicyName: 'appsync-unauthrole-policy', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: [ + 'appsync:GraphQL' + ], + Resource: [{ + 'Fn::Join': [ + '', + [ + 'arn:aws:appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { + 'Fn::GetAtt': [ + 'GraphQLAPI', + 'ApiId' + ] + }, + '/*', + ], + ], + }], + }], + }, + }), + ] + }); + + const identityPool = new cfnCognito.IdentityPool({ + IdentityPoolName: IDENTITY_POOL_NAME, + CognitoIdentityProviders: [ + { + ClientId: { + Ref: 'UserPoolClient' + }, + ProviderName: { + 'Fn::Sub': [ + 'cognito-idp.${region}.amazonaws.com/${client}', + { + 'region': { + Ref: 'AWS::Region' + }, + 'client': USER_POOL_ID + } + ] + } + } as unknown, + { + ClientId: { + Ref: 'UserPoolClientWeb' + }, + ProviderName: { + 'Fn::Sub': [ + 'cognito-idp.${region}.amazonaws.com/${client}', + { + 'region': { + Ref: 'AWS::Region' + }, + 'client': USER_POOL_ID + } + ] + } + } as unknown, + ], + AllowUnauthenticatedIdentities: true + }); + + const identityPoolRoleMap = new cfnCognito.IdentityPoolRoleAttachment({ + IdentityPoolId: { Ref: 'IdentityPool' } as unknown as string, + Roles: { + 'unauthenticated': { 'Fn::GetAtt': [ 'UnauthRole', 'Arn' ] }, + 'authenticated': { 'Fn::GetAtt': [ 'AuthRole', 'Arn' ] } + } + }); + + const userPoolClientWeb = new cfnCognito.UserPoolClient({ + ClientName: USER_POOL_CLIENTWEB_NAME, + RefreshTokenValidity: 30, + UserPoolId: USER_POOL_ID + }); + + const userPoolClient = new cfnCognito.UserPoolClient({ + ClientName: USER_POOL_CLIENT_NAME, + GenerateSecret: true, + RefreshTokenValidity: 30, + UserPoolId: USER_POOL_ID + }); + + out.rootStack.Resources.IdentityPool = identityPool; + out.rootStack.Resources.IdentityPoolRoleMap = identityPoolRoleMap; + out.rootStack.Resources.UserPoolClientWeb = userPoolClientWeb; + out.rootStack.Resources.UserPoolClient = userPoolClient; + out.rootStack.Outputs.IdentityPoolId = { Value: { Ref: 'IdentityPool' } }; + out.rootStack.Outputs.IdentityPoolName = { Value: { 'Fn::GetAtt': [ 'IdentityPool', 'Name' ] } }; + + out.rootStack.Resources.AuthRole = authRole; + out.rootStack.Outputs.AuthRoleArn = { Value: { 'Fn::GetAtt': [ 'AuthRole', 'Arn' ] } }; + out.rootStack.Resources.UnauthRole = unauthRole; + out.rootStack.Outputs.UnauthRoleArn = { Value: { 'Fn::GetAtt': [ 'UnauthRole', 'Arn' ] } }; + + // Since we're doing the policy here we've to remove the transformer generated artifacts from + // the generated stack. + delete out.rootStack.Resources[ResourceConstants.RESOURCES.UnauthRolePolicy]; + delete out.rootStack.Parameters.unauthRoleName; + delete out.rootStack.Resources[ResourceConstants.RESOURCES.AuthRolePolicy]; + delete out.rootStack.Parameters.authRoleName; + + for (const key of Object.keys(out.rootStack.Resources)) { + if (out.rootStack.Resources[key].Properties && + out.rootStack.Resources[key].Properties.Parameters && + out.rootStack.Resources[key].Properties.Parameters.unauthRoleName) { + delete out.rootStack.Resources[key].Properties.Parameters.unauthRoleName; + } + + if (out.rootStack.Resources[key].Properties && + out.rootStack.Resources[key].Properties.Parameters && + out.rootStack.Resources[key].Properties.Parameters.authRoleName) { + delete out.rootStack.Resources[key].Properties.Parameters.authRoleName; + } + } + + for (const stackKey of Object.keys(out.stacks)) { + const stack = out.stacks[stackKey]; + + for (const key of Object.keys(stack.Resources)) { + if (stack.Parameters && + stack.Parameters.unauthRoleName) { + delete stack.Parameters.unauthRoleName; + } + if (stack.Parameters && + stack.Parameters.authRoleName) { + delete stack.Parameters.authRoleName; + } + if (stack.Resources[key].Properties && + stack.Resources[key].Properties.Parameters && + stack.Resources[key].Properties.Parameters.unauthRoleName) { + delete stack.Resources[key].Properties.Parameters.unauthRoleName; + } + if (stack.Resources[key].Properties && + stack.Resources[key].Properties.Parameters && + stack.Resources[key].Properties.Parameters.authRoleName) { + delete stack.Resources[key].Properties.Parameters.authRoleName; + } + } + } + + const params = { + CreateAPIKey: '1', + AuthCognitoUserPoolId: USER_POOL_ID + } + + const finishedStack = await deploy( + customS3Client, cf, STACK_NAME, out, params, + LOCAL_BUILD_ROOT, BUCKET_NAME, DEPLOYMENT_ROOT_KEY, BUILD_TIMESTAMP + ) + expect(finishedStack).toBeDefined() + const getApiEndpoint = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIEndpointOutput) + const getApiKey = outputValueSelector(ResourceConstants.OUTPUTS.GraphQLAPIApiKeyOutput); + GRAPHQL_ENDPOINT = getApiEndpoint(finishedStack.Outputs) + console.log(`Using graphql url: ${GRAPHQL_ENDPOINT}`); + + + const apiKey = getApiKey(finishedStack.Outputs); + console.log(`API KEY: ${apiKey}`); + expect(apiKey).toBeTruthy(); + + const getIdentityPoolId = outputValueSelector('IdentityPoolId') + const identityPoolId = getIdentityPoolId(finishedStack.Outputs); + expect(identityPoolId).toBeTruthy(); + console.log(`Identity Pool Id: ${identityPoolId}`); + + + console.log(`User pool Id: ${USER_POOL_ID}`); + console.log(`User pool ClientId: ${userPoolClientId}`); + + // Verify we have all the details + expect(GRAPHQL_ENDPOINT).toBeTruthy() + expect(USER_POOL_ID).toBeTruthy() + expect(userPoolClientId).toBeTruthy() + + // Configure Amplify, create users, and sign in. + configureAmplify(USER_POOL_ID, userPoolClientId, identityPoolId); + + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) + await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) + await createGroup(USER_POOL_ID, INSTRUCTOR_GROUP_NAME) + await addUserToGroup(INSTRUCTOR_GROUP_NAME, USERNAME1, USER_POOL_ID) + await addUserToGroup(INSTRUCTOR_GROUP_NAME, USERNAME2, USER_POOL_ID) + + const authResAfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME1, TMP_PASSWORD, REAL_PASSWORD) + const idToken = authResAfterGroup.getIdToken().getJwtToken() + GRAPHQL_CLIENT_1 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools' + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken, + }}) + + const authRes2AfterGroup: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME2, TMP_PASSWORD, REAL_PASSWORD) + const idToken2 = authRes2AfterGroup.getIdToken().getJwtToken() + GRAPHQL_CLIENT_2 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools' + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken2, + }}) + + const authRes3: any = await signupAndAuthenticateUser(USER_POOL_ID, USERNAME3, TMP_PASSWORD, REAL_PASSWORD) + const idToken3 = authRes3.getIdToken().getJwtToken() + GRAPHQL_CLIENT_3 = new AWSAppSyncClient({url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'userPools' + }, + auth: { + type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, + jwtToken: idToken3, + }}) + + await Auth.signIn(USERNAME1, REAL_PASSWORD); + const authCredentials = await Auth.currentUserCredentials(); + GRAPHQL_IAM_AUTH_CLIENT = new AWSAppSyncClient({ url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'iam' + }, + auth: { + type: AUTH_TYPE.AWS_IAM, + credentials: Auth.essentialCredentials(authCredentials) + } + }) + + GRAPHQL_APIKEY_CLIENT = new AWSAppSyncClient({ url: GRAPHQL_ENDPOINT, region: AWS_REGION, + disableOffline: true, + offlineConfig: { + keyPrefix: 'apikey' + }, + auth: { + type: AUTH_TYPE.API_KEY, + apiKey: apiKey, + } + }) + + // Wait for any propagation to avoid random + // "The security token included in the request is invalid" errors + await new Promise((res) => setTimeout(() => res(), 5000)) + } catch (e) { + console.error(e) + expect(true).toEqual(false) + } +}); + +afterAll(async () => { + try { + console.log('Deleting stack ' + STACK_NAME) + await cf.deleteStack(STACK_NAME) + await deleteUserPool(cognitoClient, USER_POOL_ID) + await cf.waitForStack(STACK_NAME) + console.log('Successfully deleted stack ' + STACK_NAME) + } catch (e) { + if (e.code === 'ValidationError' && e.message === `Stack with id ${STACK_NAME} does not exist`) { + // The stack was deleted. This is good. + expect(true).toEqual(true) + console.log('Successfully deleted stack ' + STACK_NAME) + } else { + console.error(e) + throw e; + } + } + try { + await emptyBucket(BUCKET_NAME); + } catch (e) { + console.error(`Failed to empty S3 bucket: ${e}`) + } +}) + +/** + * Tests + */ + +// tests using cognito +test('Test that only authorized members are allowed to view subscriptions', async done => { + // subscribe to create students as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql` + subscription OnCreateStudent { + onCreateStudent { + id + name + email + ssn + owner + } + }`}) + let subscription = observer.subscribe( (event: any) => { + console.log('subscription event: ', event) + const student = event.data.onCreateStudent; + subscription.unsubscribe() + expect(student.name).toEqual('student1') + expect(student.email).toEqual('student1@domain.com') + expect(student.ssn).toBeNull() + done(); + }); + + await new Promise((res) => setTimeout(() => res(), 1000)) + + createStudent(GRAPHQL_CLIENT_1, + { + name: "student1", + email: "student1@domain.com", + ssn: "AAA-01-SSSS", + }) +}) + +test('Test that an user not in the group is not allowed to view the subscription', async done => { + // suscribe to create students as user 3 + // const observer = onCreateStudent(GRAPHQL_CLIENT_3) + const observer = GRAPHQL_CLIENT_3.subscribe({ query: gql` + subscription OnCreateStudent { + onCreateStudent { + id + name + email + ssn + owner + } + }`}) + observer.subscribe({ + error: (err: any) => { + console.log(err.graphQLErrors[0]) + expect(err.graphQLErrors[0].message).toEqual('Not Authorized to access onCreateStudent on type Subscription') + expect(err.graphQLErrors[0].errorType).toEqual('Unauthorized') + done() + } + }); + await new Promise((res) => setTimeout(() => res(), 1000)) + + createStudent(GRAPHQL_CLIENT_1, + { + name: "student2", + email: "student2@domain.com", + ssn: "BBB-00-SNSN" + }) +}) + +test('Test a subscription on update', async done => { + // susbcribe to update students as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql` + subscription OnUpdateStudent { + onUpdateStudent { + id + name + email + ssn + owner + } + }` }) + let subscription = observer.subscribe( (event: any) => { + const student = event.data.onUpdateStudent; + subscription.unsubscribe() + expect(student.id).toEqual(student3ID) + expect(student.name).toEqual('student3') + expect(student.email).toEqual('emailChanged@domain.com') + expect(student.ssn).toBeNull() + done(); + }); + + const student3 = await createStudent(GRAPHQL_CLIENT_1, + { + name: "student3", + email: "changeThisEmail@domain.com", + ssn: "CCC-01-SNSN" + }) + expect(student3.data.createStudent).toBeDefined() + const student3ID = student3.data.createStudent.id + expect(student3.data.createStudent.name).toEqual('student3') + expect(student3.data.createStudent.email).toEqual('changeThisEmail@domain.com') + expect(student3.data.createStudent.ssn).toBeNull() + + updateStudent(GRAPHQL_CLIENT_1, + { + id: student3ID, + email: 'emailChanged@domain.com' + }) +}) + + +test('Test a subscription on delete', async done => { + // subscribe to onDelete as user 2 + const observer = GRAPHQL_CLIENT_2.subscribe({ query: gql ` + subscription OnDeleteStudent { + onDeleteStudent { + id + name + email + ssn + owner + } + }`}) + let subscription = observer.subscribe( (event: any) => { + const student = event.data.onDeleteStudent; + subscription.unsubscribe() + expect(student.id).toEqual(student4ID) + expect(student.name).toEqual('student4') + expect(student.email).toEqual('plsDelete@domain.com') + expect(student.ssn).toBeNull() + done(); + }); + + const student4 = await createStudent(GRAPHQL_CLIENT_1, + { + name: "student4", + email: "plsDelete@domain.com", + ssn: "DDD-02-SNSN" + }) + expect(student4).toBeDefined() + const student4ID = student4.data.createStudent.id + expect(student4.data.createStudent.email).toEqual('plsDelete@domain.com') + expect(student4.data.createStudent.ssn).toBeNull() + + await deleteStudent(GRAPHQL_CLIENT_1, { id: student4ID }) +}) + +// ownerField Tests +test('Test subscription onCreatePost with ownerField', async done => { + const observer = GRAPHQL_CLIENT_1.subscribe({ query: gql` + subscription OnCreatePost { + onCreatePost(postOwner: "${USERNAME1}") { + id + title + postOwner + } + }`}) + let subscription = observer.subscribe( (event: any) => { + const post = event.data.onCreatePost; + subscription.unsubscribe() + expect(post.title).toEqual('someTitle') + expect(post.postOwner).toEqual(USERNAME1) + done(); + }); + await new Promise((res) => setTimeout(() => res(), 1000)) + + createPost(GRAPHQL_CLIENT_1, { + title: "someTitle", + postOwner: USERNAME1 + }) +}) + +// iam tests +test('test that subcsription with apiKey', async done => { + const observer = GRAPHQL_APIKEY_CLIENT.subscribe({ query: gql` + subscription OnCreateTodo { + onCreateTodo { + id + description + name + } + }`}) + + let subscription = observer.subscribe( (event: any) => { + const post = event.data.onCreateTodo; + subscription.unsubscribe() + expect(post.description).toEqual('someDescription') + expect(post.name).toBeNull() + done(); + }); + await new Promise((res) => setTimeout(() => res(), 1000)) + + createTodo(GRAPHQL_IAM_AUTH_CLIENT, { + description: "someDescription", + name: "todo1" + }) +}) + +test('test that subscription with apiKey onUpdate', async done => { + const observer = GRAPHQL_APIKEY_CLIENT.subscribe({ query: gql` + subscription OnUpdateTodo { + onUpdateTodo { + id + description + name + } + }`}) + let subscription = observer.subscribe( (event: any) => { + const todo = event.data.onUpdateTodo; + subscription.unsubscribe() + expect(todo.id).toEqual(todo2ID) + expect(todo.description).toEqual('todo2newDesc') + expect(todo.name).toBeNull() + done(); + }); + + const todo2 = await createTodo(GRAPHQL_IAM_AUTH_CLIENT, { + description: "updateTodoDesc", + name: "todo2" + }) + expect(todo2.data.createTodo.id).toBeDefined() + const todo2ID = todo2.data.createTodo.id; + expect(todo2.data.createTodo.description).toEqual("updateTodoDesc") + expect(todo2.data.createTodo.name).toEqual("todo2") + + // update the description on todo + updateTodo(GRAPHQL_IAM_AUTH_CLIENT, { + id: todo2ID, + description: "todo2newDesc" + }) +}) + +test('test that subscription with apiKey onDelete', async done => { + const observer = GRAPHQL_APIKEY_CLIENT.subscribe({ query: gql` + subscription OnDeleteTodo { + onDeleteTodo { + id + description + name + } + }`}) + let subscription = observer.subscribe( (event: any) => { + const todo = event.data.onDeleteTodo; + subscription.unsubscribe() + expect(todo.id).toEqual(todo3ID) + expect(todo.description).toEqual('deleteTodoDesc') + expect(todo.name).toBeNull() + done(); + }); + + const todo3 = await createTodo(GRAPHQL_IAM_AUTH_CLIENT, { + description: "deleteTodoDesc", + name: "todo3" + }) + expect(todo3.data.createTodo.id).toBeDefined() + const todo3ID = todo3.data.createTodo.id; + expect(todo3.data.createTodo.description).toEqual("deleteTodoDesc") + expect(todo3.data.createTodo.name).toEqual("todo3") + + // delete todo3 + deleteTodo(GRAPHQL_IAM_AUTH_CLIENT, { + id: todo3ID, + }) +}) + + +// mutations +async function createStudent(client: AWSAppSyncClient, input: CreateStudentInput) { + const request = gql`mutation CreateStudent($input: CreateStudentInput!) { + createStudent(input: $input) { + id + name + email + ssn + owner + } + } + `; + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function updateStudent(client: AWSAppSyncClient, input: UpdateStudentInput) { + const request = gql`mutation UpdateStudent($input: UpdateStudentInput!) { + updateStudent(input: $input) { + id + name + email + ssn + owner + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function deleteStudent(client: AWSAppSyncClient, input: DeleteTypeInput) { + const request = gql`mutation DeleteStudent($input: DeleteStudentInput!) { + deleteStudent(input: $input) { + id + name + email + ssn + owner + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function createPost(client: AWSAppSyncClient, input: CreatePostInput) { + const request = gql`mutation CreatePost($input: CreatePostInput!) { + createPost(input: $input) { + id + title + postOwner + } + }` + return await client.mutate({ mutation: request, variables: { input }}) +} + +async function createTodo(client: AWSAppSyncClient, input: CreateTodoInput) { + const request = gql`mutation CreateTodo($input: CreateTodoInput!) { + createTodo(input: $input) { + id + description + name + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function updateTodo(client: AWSAppSyncClient, input: UpdateTodoInput) { + const request = gql`mutation UpdateTodo($input: UpdateTodoInput!) { + updateTodo(input: $input) { + id + description + name + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} + +async function deleteTodo(client: AWSAppSyncClient, input: DeleteTypeInput) { + const request = gql`mutation DeleteTodo($input: DeleteTodoInput!) { + deleteTodo(input: $input) { + id + description + name + } + }` + return await client.mutate({ mutation: request, variables: { input }}); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7658115a2cb..4bd7ff8be83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10872,6 +10872,14 @@ isobject@^4.0.0: resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== +isomorphic-fetch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -14184,7 +14192,7 @@ node-fetch@2.6.0, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-fetch@^1.7.2: +node-fetch@^1.0.1, node-fetch@^1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== @@ -20214,7 +20222,7 @@ whatwg-fetch@2.0.4: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== -whatwg-fetch@3.0.0: +whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==