From 7aeacde147b5def65911464c9968124c68ede1ab Mon Sep 17 00:00:00 2001 From: Ivan Artemiev <29709626+iartemiev@users.noreply.github.com> Date: Wed, 21 Jul 2021 14:44:14 -0400 Subject: [PATCH] feat(@aws-amplify/datastore): support lambda authorizers --- packages/api-graphql/src/GraphQLAPI.ts | 1 + .../__tests__/authStrategies.test.ts | 45 + packages/datastore/__tests__/helpers.ts | 8 +- .../datastore/__tests__/subscription.test.ts | 1182 ++++------------- .../authModeStrategies/multiAuthStrategy.ts | 52 +- packages/datastore/src/datastore/datastore.ts | 6 + .../datastore/src/sync/processors/mutation.ts | 15 +- .../src/sync/processors/subscription.ts | 10 +- .../datastore/src/sync/processors/sync.ts | 7 + packages/datastore/src/sync/utils.ts | 26 + packages/datastore/src/types.ts | 8 + .../Providers/AWSAppSyncRealTimeProvider.ts | 2 +- 12 files changed, 447 insertions(+), 915 deletions(-) diff --git a/packages/api-graphql/src/GraphQLAPI.ts b/packages/api-graphql/src/GraphQLAPI.ts index 0119f7c2c83..216710d5a55 100644 --- a/packages/api-graphql/src/GraphQLAPI.ts +++ b/packages/api-graphql/src/GraphQLAPI.ts @@ -186,6 +186,7 @@ export class GraphQLAPIClass { headers = { Authorization: additionalHeaders.Authorization, }; + break; default: headers = { Authorization: null, diff --git a/packages/datastore/__tests__/authStrategies.test.ts b/packages/datastore/__tests__/authStrategies.test.ts index 54173f30c3a..c61a5f1db90 100644 --- a/packages/datastore/__tests__/authStrategies.test.ts +++ b/packages/datastore/__tests__/authStrategies.test.ts @@ -10,6 +10,11 @@ import { NAMESPACES } from '../src/util'; describe('Auth Strategies', () => { describe('multiAuthStrategy', () => { const rules = { + function: { + provider: ModelAttributeAuthProvider.FUNCTION, + allow: ModelAttributeAuthAllow.CUSTOM, + operations: ['create', 'update', 'delete', 'read'], + }, owner: { provider: ModelAttributeAuthProvider.USER_POOLS, ownerField: 'owner', @@ -68,6 +73,21 @@ describe('Auth Strategies', () => { }, }; + test('function', async () => { + const authRules = [rules.function]; + await testMultiAuthStrategy({ + authRules, + hasAuthenticatedUser: true, + result: ['AWS_LAMBDA'], + }); + + await testMultiAuthStrategy({ + authRules, + hasAuthenticatedUser: false, + result: ['AWS_LAMBDA'], + }); + }); + test('owner', async () => { const authRules = [rules.owner]; await testMultiAuthStrategy({ @@ -353,6 +373,31 @@ describe('Auth Strategies', () => { }); }); + test('function/owner/public IAM/API key', async () => { + const authRules = [ + rules.function, + rules.owner, + rules.publicIAM, + rules.publicAPIKeyExplicit, + ]; + await testMultiAuthStrategy({ + authRules, + hasAuthenticatedUser: true, + result: [ + 'AWS_LAMBDA', + 'AMAZON_COGNITO_USER_POOLS', + 'AWS_IAM', + 'API_KEY', + ], + }); + + await testMultiAuthStrategy({ + authRules, + hasAuthenticatedUser: false, + result: ['AWS_LAMBDA', 'AWS_IAM', 'API_KEY'], + }); + }); + test('duplicates', async () => { const authRules = [ rules.owner, diff --git a/packages/datastore/__tests__/helpers.ts b/packages/datastore/__tests__/helpers.ts index 01ce1f08701..5fcfe24f138 100644 --- a/packages/datastore/__tests__/helpers.ts +++ b/packages/datastore/__tests__/helpers.ts @@ -1,4 +1,10 @@ -import { ModelInit, MutableModel, Schema, InternalSchema } from '../src/types'; +import { + ModelInit, + MutableModel, + Schema, + InternalSchema, + SchemaModel, +} from '../src/types'; export declare class Model { public readonly id: string; diff --git a/packages/datastore/__tests__/subscription.test.ts b/packages/datastore/__tests__/subscription.test.ts index 7d3ed51f4d0..3f5b3531c2e 100644 --- a/packages/datastore/__tests__/subscription.test.ts +++ b/packages/datastore/__tests__/subscription.test.ts @@ -7,67 +7,17 @@ import { SchemaModel } from '../src/types'; describe('sync engine subscription module', () => { test('owner authorization', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], }, - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: true, @@ -81,74 +31,24 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); test('owner authorization with only read operation', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['read'], }, - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: true, @@ -162,74 +62,24 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); test('owner authorization without read operation', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete'], }, - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: false, @@ -243,81 +93,31 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); test('owner authorization with public subscription', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { - type: 'model', - properties: { - subscriptions: { - level: 'public', - }, - }, - }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete'], + }, + ]; + + const modelProperties = { + subscriptions: { + level: 'public', }, }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + + const model = generateModelWithAuth(authRules, modelProperties); + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: false, @@ -331,77 +131,33 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); test('owner authorization with custom owner (explicit)', () => { - const model: SchemaModel = { - name: 'Post', - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - customOwner: { - name: 'customOwner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'customOwner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], }, - syncable: true, - pluralName: 'Posts', - attributes: [ - { - type: 'model', - properties: {}, - }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'customOwner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', + ]; + const model = generateModelWithAuth(authRules); + + // add custom owner field + model.fields.customOwner = { + name: 'customOwner', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], }; + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: true, @@ -415,79 +171,29 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); test('owner authorization with auth different than default auth mode', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'iam', - allow: 'private', - operations: ['create', 'update', 'delete', 'read'], - }, - { - provider: 'userPools', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'iam', + allow: 'private', + operations: ['create', 'update', 'delete', 'read'], }, - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], + }, + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AWS_IAM', isOwner: false, @@ -499,75 +205,25 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, // default auth mode - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AWS_IAM ) ).toEqual(authInfo); }); test('groups authorization', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'groups', - groups: ['mygroup'], - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'groups', + groups: ['mygroup'], + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], }, - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: false, @@ -579,75 +235,29 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); test('groups authorization with groupClaim (array as string)', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'groups', - groups: ['mygroup'], - groupClaim: 'custom:groups', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'groups', + groups: ['mygroup'], + groupClaim: 'custom:groups', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', + ...accessTokenPayload, 'custom:groups': '["mygroup"]', - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', }; const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', @@ -667,68 +277,22 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('groups authorization with groupClaim (string)', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'groups', - groups: ['mygroup'], - groupClaim: 'custom:group', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'groups', + groups: ['mygroup'], + groupClaim: 'custom:group', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', + ...accessTokenPayload, 'custom:group': '"mygroup"', - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', }; const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', @@ -748,68 +312,22 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('groups authorization with groupClaim (plain string)', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'userPools', - ownerField: 'owner', - allow: 'groups', - groups: ['mygroup'], - groupClaim: 'custom:group', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'userPools', + ownerField: 'owner', + allow: 'groups', + groups: ['mygroup'], + groupClaim: 'custom:group', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', + ...accessTokenPayload, 'custom:group': 'mygroup', - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', }; const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', @@ -829,49 +347,15 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('public iam authorization for unauth user', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'iam', - allow: 'public', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'iam', + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AWS_IAM', isOwner: false, @@ -890,49 +374,15 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('private iam authorization for unauth user', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'iam', - allow: 'private', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'iam', + allow: 'private', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AWS_IAM', isOwner: false, @@ -951,49 +401,15 @@ describe('sync engine subscription module', () => { ).toEqual(null); }); test('private iam authorization for auth user', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'iam', - allow: 'private', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'iam', + allow: 'private', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AWS_IAM', isOwner: false, @@ -1012,49 +428,15 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('public apiKey authorization without credentials', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'apiKey', - allow: 'public', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'apiKey', + allow: 'public', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'API_KEY', isOwner: false, @@ -1073,51 +455,17 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('OIDC owner authorization', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'oidc', - ownerField: 'sub', - allow: 'owner', - identityClaim: 'sub', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'oidc', + ownerField: 'sub', + allow: 'owner', + identityClaim: 'sub', + operations: ['create', 'update', 'delete', 'read'], }, - }; + ]; + const model = generateModelWithAuth(authRules); + const oidcTokenPayload = { sub: 'user1', email_verified: true, @@ -1151,74 +499,24 @@ describe('sync engine subscription module', () => { ).toEqual(authInfo); }); test('User Pools & OIDC owner authorization with Cognito token', () => { - const model: SchemaModel = { - syncable: true, - name: 'Post', - pluralName: 'Posts', - attributes: [ - { type: 'model', properties: {} }, - { - type: 'auth', - properties: { - rules: [ - { - provider: 'oidc', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'sub', - operations: ['create', 'update', 'delete', 'read'], - }, - { - provider: 'userPools', - ownerField: 'owner', - allow: 'owner', - identityClaim: 'cognito:username', - operations: ['create', 'update', 'delete', 'read'], - }, - ], - }, - }, - ], - fields: { - id: { - name: 'id', - isArray: false, - type: 'ID', - isRequired: true, - attributes: [], - }, - title: { - name: 'title', - isArray: false, - type: 'String', - isRequired: true, - attributes: [], - }, - owner: { - name: 'owner', - isArray: false, - type: 'String', - isRequired: false, - attributes: [], - }, + const authRules = [ + { + provider: 'oidc', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'sub', + operations: ['create', 'update', 'delete', 'read'], }, - }; - const tokenPayload = { - sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', - 'cognito:groups': ['mygroup'], - email_verified: true, - iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', - phone_number_verified: false, - 'cognito:username': 'user1', - aud: '6l99pm4b729dn8c7bj7d3t1lnc', - event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', - token_use: 'id', - auth_time: 1578541322, - phone_number: '+12068220398', - exp: 1578544922, - iat: 1578541322, - email: 'user1@user.com', - }; + { + provider: 'userPools', + ownerField: 'owner', + allow: 'owner', + identityClaim: 'cognito:username', + operations: ['create', 'update', 'delete', 'read'], + }, + ]; + const model = generateModelWithAuth(authRules); + const authInfo = { authMode: 'AMAZON_COGNITO_USER_POOLS', isOwner: true, @@ -1232,10 +530,98 @@ describe('sync engine subscription module', () => { model, USER_CREDENTIALS.auth, GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS, - tokenPayload, + accessTokenPayload, undefined, // No OIDC token GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS ) ).toEqual(authInfo); }); + + test('Function default auth mode', () => { + const authRules = [ + { + provider: 'custom', + allow: 'function', + operations: ['create', 'update', 'delete', 'read'], + }, + ]; + const model = generateModelWithAuth(authRules); + + const authInfo = { + authMode: 'AWS_LAMBDA', + isOwner: false, + }; + + expect( + // @ts-ignore + SubscriptionProcessor.prototype.getAuthorizationInfo( + model, + USER_CREDENTIALS.none, + GRAPHQL_AUTH_MODE.AWS_LAMBDA, + undefined, // No Cognito token + undefined, // No OIDC token + GRAPHQL_AUTH_MODE.AWS_LAMBDA + ) + ).toEqual(authInfo); + }); }); + +const accessTokenPayload = { + sub: 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', + 'cognito:groups': ['mygroup'], + email_verified: true, + iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_XXXXXXXX', + phone_number_verified: false, + 'cognito:username': 'user1', + aud: '6l99pm4b729dn8c7bj7d3t1lnc', + event_id: 'b4c25daa-0c03-4617-aab8-e5c74403536b', + token_use: 'id', + auth_time: 1578541322, + phone_number: '+12068220398', + exp: 1578544922, + iat: 1578541322, + email: 'user1@user.com', +}; + +export function generateModelWithAuth( + authRules, + modelProperties = {} +): SchemaModel { + return { + syncable: true, + name: 'Post', + pluralName: 'Posts', + attributes: [ + { type: 'model', properties: modelProperties }, + { + type: 'auth', + properties: { + rules: [...authRules], + }, + }, + ], + fields: { + id: { + name: 'id', + isArray: false, + type: 'ID', + isRequired: true, + attributes: [], + }, + title: { + name: 'title', + isArray: false, + type: 'String', + isRequired: true, + attributes: [], + }, + owner: { + name: 'owner', + isArray: false, + type: 'String', + isRequired: false, + attributes: [], + }, + }, + }; +} diff --git a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts index 394826ab8ae..0d601daa161 100644 --- a/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts +++ b/packages/datastore/src/authModeStrategies/multiAuthStrategy.ts @@ -4,6 +4,7 @@ import { AuthModeStrategy, ModelAttributeAuthProperty, ModelAttributeAuthProvider, + ModelAttributeAuthAllow, } from '../types'; function getProviderFromRule( @@ -21,8 +22,20 @@ function getProviderFromRule( } function sortAuthRulesWithPriority(rules: ModelAttributeAuthProperty[]) { - const allowSortPriority = ['owner', 'groups', 'private', 'public']; - const providerSortPriority = ['userPools', 'oidc', 'iam', 'apiKey']; + const allowSortPriority = [ + ModelAttributeAuthAllow.CUSTOM, + ModelAttributeAuthAllow.OWNER, + ModelAttributeAuthAllow.GROUPS, + ModelAttributeAuthAllow.PRIVATE, + ModelAttributeAuthAllow.PUBLIC, + ]; + const providerSortPriority = [ + ModelAttributeAuthProvider.FUNCTION, + ModelAttributeAuthProvider.USER_POOLS, + ModelAttributeAuthProvider.OIDC, + ModelAttributeAuthProvider.IAM, + ModelAttributeAuthProvider.API_KEY, + ]; return [...rules].sort( (a: ModelAttributeAuthProperty, b: ModelAttributeAuthProperty) => { @@ -51,35 +64,50 @@ function getAuthRules({ rules.forEach(rule => { switch (rule.allow) { - case 'groups': - case 'owner': { + case ModelAttributeAuthAllow.CUSTOM: + // custom with no provider -> function + if ( + !rule.provider || + rule.provider === ModelAttributeAuthProvider.FUNCTION + ) { + authModes.add(GRAPHQL_AUTH_MODE.AWS_LAMBDA); + } + break; + case ModelAttributeAuthAllow.GROUPS: + case ModelAttributeAuthAllow.OWNER: { // We shouldn't attempt User Pool or OIDC if there isn't an authenticated user if (currentUser) { - if (rule.provider === 'userPools') { + if (rule.provider === ModelAttributeAuthProvider.USER_POOLS) { authModes.add(GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS); - } else if (rule.provider === 'oidc') { + } else if (rule.provider === ModelAttributeAuthProvider.OIDC) { authModes.add(GRAPHQL_AUTH_MODE.OPENID_CONNECT); } } break; } - case 'private': { + case ModelAttributeAuthAllow.PRIVATE: { // We shouldn't attempt private if there isn't an authenticated user if (currentUser) { // private with no provider means userPools - if (!rule.provider || rule.provider === 'userPools') { + if ( + !rule.provider || + rule.provider === ModelAttributeAuthProvider.USER_POOLS + ) { authModes.add(GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS); - } else if (rule.provider === 'iam') { + } else if (rule.provider === ModelAttributeAuthProvider.IAM) { authModes.add(GRAPHQL_AUTH_MODE.AWS_IAM); } } break; } - case 'public': { - if (rule.provider === 'iam') { + case ModelAttributeAuthAllow.PUBLIC: { + if (rule.provider === ModelAttributeAuthProvider.IAM) { authModes.add(GRAPHQL_AUTH_MODE.AWS_IAM); - } else if (!rule.provider || rule.provider === 'apiKey') { + } else if ( + !rule.provider || + rule.provider === ModelAttributeAuthProvider.API_KEY + ) { // public with no provider means apiKey authModes.add(GRAPHQL_AUTH_MODE.API_KEY); } diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index efe4ae2f958..94e21b58205 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -617,6 +617,7 @@ class DataStore { ModelPredicate > = new WeakMap>(); private sessionId: string; + private getAuthToken: Promise; getModuleName() { return 'DataStore'; @@ -1110,6 +1111,7 @@ class DataStore { syncPageSize: configSyncPageSize, fullSyncInterval: configFullSyncInterval, syncExpressions: configSyncExpressions, + authProviders: configAuthProviders, ...configFromAmplify } = config; @@ -1135,6 +1137,10 @@ class DataStore { break; } + // store on config object, so that Sync, Subscription, and Mutation processors can have access + this.amplifyConfig.authProviders = + (configDataStore && configDataStore.authProviders) || configAuthProviders; + this.syncExpressions = (configDataStore && configDataStore.syncExpressions) || this.syncExpressions || diff --git a/packages/datastore/src/sync/processors/mutation.ts b/packages/datastore/src/sync/processors/mutation.ts index bd17eaeb35e..ccff049a746 100644 --- a/packages/datastore/src/sync/processors/mutation.ts +++ b/packages/datastore/src/sync/processors/mutation.ts @@ -31,6 +31,7 @@ import { createMutationInstanceFromModelOperation, getModelAuthModes, TransformerMutationType, + getTokenForCustomAuth, } from '../utils'; const MAX_ATTEMPTS = 10; @@ -263,7 +264,13 @@ class MutationProcessor { data, condition ); - const tryWith = { query, variables, authMode }; + + const authToken = await getTokenForCustomAuth( + authMode, + this.amplifyConfig + ); + + const tryWith = { query, variables, authMode, authToken }; let attempt = 0; const opType = this.opTypeFromTransformerOperation(operation); @@ -331,12 +338,18 @@ class MutationProcessor { 'GET' ); + const authToken = await getTokenForCustomAuth( + authMode, + this.amplifyConfig + ); + const serverData = < GraphQLResult> >await API.graphql({ query, variables: { id: variables.input.id }, authMode, + authToken, }); return [serverData, opName, modelDefinition]; diff --git a/packages/datastore/src/sync/processors/subscription.ts b/packages/datastore/src/sync/processors/subscription.ts index 6d89655e08a..2c6ebbd1db8 100644 --- a/packages/datastore/src/sync/processors/subscription.ts +++ b/packages/datastore/src/sync/processors/subscription.ts @@ -19,6 +19,7 @@ import { getModelAuthModes, getUserGroupsFromToken, TransformerMutationType, + getTokenForCustomAuth, } from '../utils'; import { ModelPredicateCreator } from '../../predicates'; import { validatePredicate } from '../../util'; @@ -338,7 +339,7 @@ class SubscriptionProcessor { }; // Retry failed subscriptions with next auth mode (if available) - const authModeRetry = operation => { + const authModeRetry = async operation => { const { opType: transformerMutationType, opName, @@ -357,6 +358,11 @@ class SubscriptionProcessor { readAuthModes[operationAuthModeAttempts[operation]] ); + const authToken = await getTokenForCustomAuth( + authMode, + this.amplifyConfig + ); + const variables = {}; if (isOwner) { @@ -380,7 +386,7 @@ class SubscriptionProcessor { Observable<{ value: GraphQLResult>; }> - >(API.graphql({ query, variables, ...{ authMode } })); + >(API.graphql({ query, variables, ...{ authMode }, authToken })); let subscriptionReadyCallback: () => void; subscriptions[modelDefinition.name][ diff --git a/packages/datastore/src/sync/processors/sync.ts b/packages/datastore/src/sync/processors/sync.ts index b7b573d8c36..983ebead3ab 100644 --- a/packages/datastore/src/sync/processors/sync.ts +++ b/packages/datastore/src/sync/processors/sync.ts @@ -15,6 +15,7 @@ import { getClientSideAuthError, getForbiddenError, predicateToGraphQLFilter, + getTokenForCustomAuth, } from '../utils'; import { jitteredExponentialRetry, @@ -201,10 +202,16 @@ class SyncProcessor { return await jitteredExponentialRetry( async (query, variables) => { try { + const authToken = await getTokenForCustomAuth( + authMode, + this.amplifyConfig + ); + return await API.graphql({ query, variables, authMode, + authToken, }); } catch (error) { // Catch client-side (GraphQLAuthError) & 401/403 errors here so that we don't continue to retry diff --git a/packages/datastore/src/sync/utils.ts b/packages/datastore/src/sync/utils.ts index 9f9b238d7e8..36c45722905 100644 --- a/packages/datastore/src/sync/utils.ts +++ b/packages/datastore/src/sync/utils.ts @@ -569,3 +569,29 @@ export function getClientSideAuthError(error) { ); return clientSideError || null; } + +export async function getTokenForCustomAuth( + authMode: GRAPHQL_AUTH_MODE, + amplifyConfig: Record = {} +): Promise { + if (authMode === GRAPHQL_AUTH_MODE.AWS_LAMBDA) { + const { + authProviders: { functionAuthProvider } = { functionAuthProvider: null }, + } = amplifyConfig; + if (functionAuthProvider && typeof functionAuthProvider === 'function') { + try { + const { token } = await functionAuthProvider(); + return token; + } catch (error) { + throw new Error( + `Error retrieving token from \`functionAuthProvider\`: ${error}` + ); + } + } else { + // TODO: add docs link once available + throw new Error( + `You must provide a \`functionAuthProvider\` function to \`DataStore.configure\` when using ${GRAPHQL_AUTH_MODE.AWS_LAMBDA}` + ); + } + } +} diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index 6223d83986c..9feb1a265be 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -140,6 +140,7 @@ export type ModelAttributeAuthProperty = { }; export enum ModelAttributeAuthAllow { + CUSTOM = 'custom', OWNER = 'owner', GROUPS = 'groups', PRIVATE = 'private', @@ -147,6 +148,7 @@ export enum ModelAttributeAuthAllow { } export enum ModelAttributeAuthProvider { + FUNCTION = 'function', USER_POOLS = 'userPools', OIDC = 'oidc', IAM = 'iam', @@ -616,6 +618,7 @@ export type DataStoreConfig = { syncPageSize?: number; fullSyncInterval?: number; syncExpressions?: SyncExpression[]; + authProviders?: AuthProviders; }; authModeStrategyType?: AuthModeStrategyType; conflictHandler?: ConflictHandler; // default : retry until client wins up to x times @@ -624,6 +627,11 @@ export type DataStoreConfig = { syncPageSize?: number; fullSyncInterval?: number; syncExpressions?: SyncExpression[]; + authProviders?: AuthProviders; +}; + +export type AuthProviders = { + functionAuthProvider: Promise; }; export enum AuthModeStrategyType { diff --git a/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider.ts b/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider.ts index 14314c2ef27..85d06d4fe52 100644 --- a/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider.ts +++ b/packages/pubsub/src/Providers/AWSAppSyncRealTimeProvider.ts @@ -166,7 +166,7 @@ export class AWSAppSyncRealTimeProvider extends AbstractPubSubProvider { _topics: string[] | string, options?: ProvidertOptions ): Observable { - const { appSyncGraphqlEndpoint, additionalHeaders } = options; + const { appSyncGraphqlEndpoint } = options; return new Observable(observer => { if (!appSyncGraphqlEndpoint) {