UNPKG

@aws-amplify/graphql-api-construct

Version:

AppSync GraphQL Api Construct using Amplify GraphQL Transformer.

238 lines (219 loc) 10.3 kB
import { AppSyncAuthConfiguration, AppSyncAuthConfigurationEntry, SynthParameters } from '@aws-amplify/graphql-transformer-interfaces'; import { CfnGraphQLApi } from 'aws-cdk-lib/aws-appsync'; import { isArray } from 'lodash'; import { IRole, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; import { AuthorizationModes, ApiKeyAuthorizationConfig, LambdaAuthorizationConfig, OIDCAuthorizationConfig, UserPoolAuthorizationConfig, } from '../types'; type AuthorizationConfigMode = | { type: 'AWS_IAM' } | (UserPoolAuthorizationConfig & { type: 'AMAZON_COGNITO_USER_POOLS' }) | (OIDCAuthorizationConfig & { type: 'OPENID_CONNECT' }) | (ApiKeyAuthorizationConfig & { type: 'API_KEY' }) | (LambdaAuthorizationConfig & { type: 'AWS_LAMBDA' }); /** * Validates authorization modes. * * Rules: * 1. Validates that deprecated settings ('iamConfig.authenticatedUserRole', 'iamConfig.unauthenticatedUserRole', * 'iamConfig.identityPoolId', 'iamConfig.allowListedRoles' and 'adminRoles') are mutually exclusive with new settings that * replaced them ('iamConfig.enableIamAuthorizationMode' and any of 'authorizationModes.identityPoolConfig') * 2. If deprecated identity pool settings are used ('iamConfig.authenticatedUserRole', 'iamConfig.unauthenticatedUserRole', * and 'iamConfig.identityPoolId') validate that all are provided. */ export const validateAuthorizationModes = (authorizationModes: AuthorizationModes): void => { const hasAnyDeprecatedIdentityPoolSetting = authorizationModes.iamConfig?.authenticatedUserRole || authorizationModes.iamConfig?.unauthenticatedUserRole || authorizationModes.iamConfig?.identityPoolId; const hasAllDeprecatedIdentityPoolSettings = authorizationModes.iamConfig?.authenticatedUserRole && authorizationModes.iamConfig?.unauthenticatedUserRole && authorizationModes.iamConfig?.identityPoolId; const hasDeprecatedIamSettings = authorizationModes.iamConfig?.authenticatedUserRole || authorizationModes.iamConfig?.unauthenticatedUserRole || authorizationModes.iamConfig?.identityPoolId || authorizationModes.iamConfig?.allowListedRoles || authorizationModes.adminRoles; const hasUnDeprecatedIamSettings = typeof authorizationModes.iamConfig?.enableIamAuthorizationMode !== 'undefined' || authorizationModes.identityPoolConfig; if (hasDeprecatedIamSettings && hasUnDeprecatedIamSettings) { throw new Error( 'Invalid authorization modes configuration provided. ' + "Deprecated IAM configuration cannot be used with identity pool configuration or when 'enableIamAuthorizationMode' is specified.", ); } if (hasAnyDeprecatedIdentityPoolSetting && !hasAllDeprecatedIdentityPoolSettings) { throw new Error( "'authorizationModes.iamConfig.authenticatedUserRole', 'authorizationModes.iamConfig.unauthenticatedUserRole' and" + " 'authorizationModes.iamConfig.identityPoolId' must be provided.", ); } }; /** * Converts a single auth mode config into the amplify-internal representation. * @param authMode the auth mode to convert into the Appsync CDK representation. */ const convertAuthModeToAuthProvider = (authMode: AuthorizationConfigMode): AppSyncAuthConfigurationEntry => { const authenticationType = authMode.type; switch (authMode.type) { case 'API_KEY': return { authenticationType, apiKeyConfig: { description: authMode.description, apiKeyExpirationDays: authMode.expires.toDays(), }, }; case 'AWS_IAM': return { authenticationType }; case 'AMAZON_COGNITO_USER_POOLS': return { authenticationType, userPoolConfig: { userPoolId: authMode.userPool.userPoolId, }, }; case 'OPENID_CONNECT': return { authenticationType, openIDConnectConfig: { name: authMode.oidcProviderName, issuerUrl: authMode.oidcIssuerUrl, clientId: authMode.clientId, iatTTL: authMode.tokenExpiryFromIssue.toMilliseconds(), authTTL: authMode.tokenExpiryFromAuth.toMilliseconds(), }, }; case 'AWS_LAMBDA': return { authenticationType, lambdaAuthorizerConfig: { lambdaArn: authMode.function.functionArn, lambdaFunction: authMode.function.functionName, ttlSeconds: authMode.ttl.toSeconds(), }, }; default: throw new Error(`Unexpected AuthMode type ${authenticationType} encountered.`); } }; /** * Given an appsync auth configuration, convert into appsync auth provider setup. * @param authModes the config to transform * @returns the appsync config object. */ const convertAuthConfigToAppSyncAuth = (authModes: AuthorizationModes): AppSyncAuthConfiguration => { // Convert auth modes into an array of appsync configs, and include the type so we can use that for switching and partitioning later. const authConfig = [ authModes.apiKeyConfig ? { type: 'API_KEY', ...authModes.apiKeyConfig } : null, authModes.lambdaConfig ? { type: 'AWS_LAMBDA', ...authModes.lambdaConfig } : null, authModes.oidcConfig ? { type: 'OPENID_CONNECT', ...authModes.oidcConfig } : null, authModes.userPoolConfig ? { type: 'AMAZON_COGNITO_USER_POOLS', ...authModes.userPoolConfig } : null, authModes.iamConfig || authModes.identityPoolConfig ? { type: 'AWS_IAM' } : null, ].filter((mode) => mode) as AuthorizationConfigMode[]; const authProviders = authConfig.map(convertAuthModeToAuthProvider); // Validate inputs make sense, needs at least one mode, and a default mode is required if there are multiple modes. if (authProviders.length === 0) { throw new Error('At least one auth config is required, but none were found.'); } if (authProviders.length > 1 && !authModes.defaultAuthorizationMode) { throw new Error('A defaultAuthorizationMode is required if multiple authorization modes are configured.'); } // Enable appsync to invoke a provided lambda authorizer function authModes.lambdaConfig?.function.addPermission('appsync-auth-invoke', { principal: new ServicePrincipal('appsync.amazonaws.com'), action: 'lambda:InvokeFunction', }); // In the case of a single mode, defaultAuthorizationMode is not required, just use the provided value. if (authProviders.length === 1) { return { defaultAuthentication: authProviders[0], additionalAuthenticationProviders: [], }; } // For multi-auth, partition into the defaultMode and non-default modes. return { defaultAuthentication: authProviders.filter((provider) => provider.authenticationType === authModes.defaultAuthorizationMode)[0], additionalAuthenticationProviders: authProviders.filter( (provider) => provider.authenticationType !== authModes.defaultAuthorizationMode, ), }; }; type AuthSynthParameters = Pick< SynthParameters, 'userPoolId' | 'authenticatedUserRoleName' | 'unauthenticatedUserRoleName' | 'identityPoolId' | 'adminRoles' | 'enableIamAccess' >; interface AuthConfig { /** * used mainly in the before step to pass the authConfig from the transformer core down to the directive */ authConfig?: AppSyncAuthConfiguration; /** * Params to include the transformer. */ authSynthParameters: AuthSynthParameters; } /** * Transforms additionalAuthenticationTypes for storage in CFN output */ export const getAdditionalAuthenticationTypes = (cfnGraphqlApi: CfnGraphQLApi): string | undefined => { if (!isArray(cfnGraphqlApi.additionalAuthenticationProviders)) { return undefined; } return (cfnGraphqlApi.additionalAuthenticationProviders as CfnGraphQLApi.AdditionalAuthenticationProviderProperty[]) .map( (additionalAuthenticationProvider: CfnGraphQLApi.AdditionalAuthenticationProviderProperty) => additionalAuthenticationProvider.authenticationType, ) .join(','); }; /** * Convert the list of auth modes into the necessary flags and params (effectively a reducer on the rule list) * @param authModes the list of auth modes configured on the API. * @returns the AuthConfig which the AuthTransformer needs as input. */ export const convertAuthorizationModesToTransformerAuthConfig = (authModes: AuthorizationModes): AuthConfig => ({ authConfig: convertAuthConfigToAppSyncAuth(authModes), authSynthParameters: getSynthParameters(authModes), }); /** * Merge iamConfig allowListedRoles with deprecated adminRoles property, converting to strings. * @param authModes the auth modes provided to the construct. * @returns the list of admin roles as strings to pass into the transformer */ const getAllowListedRoles = (authModes: AuthorizationModes): string[] => [...(authModes?.iamConfig?.allowListedRoles ?? []), ...(authModes.adminRoles ?? [])].map((roleOrRoleName: IRole | string) => { if (typeof roleOrRoleName === 'string' || roleOrRoleName instanceof String) { return roleOrRoleName as string; } return roleOrRoleName.roleName; }); /** * Transform the authorization config into the transformer synth parameters pertaining to auth. * @param authModes the auth modes provided to the construct. * @returns a record of params to be consumed by the transformer. */ const getSynthParameters = (authModes: AuthorizationModes): AuthSynthParameters => ({ adminRoles: getAllowListedRoles(authModes), identityPoolId: authModes.identityPoolConfig?.identityPoolId ?? authModes.iamConfig?.identityPoolId, enableIamAccess: authModes.iamConfig?.enableIamAuthorizationMode, ...(authModes.userPoolConfig ? { userPoolId: authModes.userPoolConfig.userPool.userPoolId } : {}), ...(authModes?.identityPoolConfig ? { authenticatedUserRoleName: authModes.identityPoolConfig.authenticatedUserRole.roleName, unauthenticatedUserRoleName: authModes.identityPoolConfig.unauthenticatedUserRole.roleName, } : {}), ...(authModes?.iamConfig && authModes?.iamConfig.authenticatedUserRole && authModes?.iamConfig.unauthenticatedUserRole ? { authenticatedUserRoleName: authModes.iamConfig.authenticatedUserRole?.roleName, unauthenticatedUserRoleName: authModes.iamConfig.unauthenticatedUserRole?.roleName, } : {}), });