diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc3566b1a..ca66c990f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This is the log of notable changes to EAS CLI and related packages. ### 🧹 Chores - Remove support for classic updates release channel in 50+. ([#2189](https://github.com/expo/eas-cli/pull/2189) by [@wschurman](https://github.com/wschurman)) +- Validate EAS Submit inputs better. ([#2198](https://github.com/expo/eas-cli/pull/2198) by [@szdziedzic](https://github.com/szdziedzic)) ## [7.0.0](https://github.com/expo/eas-cli/releases/tag/v7.0.0) - 2024-01-19 diff --git a/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts b/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts index d2dc5a6be1..cc67f6ba0f 100644 --- a/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts +++ b/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts @@ -111,6 +111,11 @@ export default class IosSubmitCommand { const envAppSpecificPassword = getenv.string('EXPO_APPLE_APP_SPECIFIC_PASSWORD', ''); if (envAppSpecificPassword) { + if (!/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/.test(envAppSpecificPassword)) { + throw new Error( + 'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format XXXX-XXXX-XXXX-XXXX, where X is a lowercase letter.' + ); + } return result({ sourceType: AppSpecificPasswordSourceType.userDefined, appSpecificPassword: envAppSpecificPassword, diff --git a/packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts b/packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts index e214002744..d0687fd330 100644 --- a/packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts +++ b/packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts @@ -78,6 +78,43 @@ describe(IosSubmitCommand, () => { jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]); }); + it('throws an error if using app specific password in invalid format', async () => { + const projectId = uuidv4(); + const graphqlClient = {} as any as ExpoGraphqlClient; + const analytics = instance(mock()); + jest + .mocked(getArchiveAsync) + .mockImplementation(jest.requireActual('../../ArchiveSource').getArchiveAsync); + + process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'ls -la'; + + const ctx = await createSubmissionContextAsync({ + platform: Platform.IOS, + projectDir: testProject.projectRoot, + archiveFlags: { + url: 'http://expo.dev/fake.ipa', + }, + profile: { + language: 'en-US', + appleId: 'test@example.com', + ascAppId: '12345678', + }, + nonInteractive: false, + actor: mockJester, + graphqlClient, + analytics, + exp: testProject.appJSON.expo, + projectId, + vcsClient, + }); + const command = new IosSubmitCommand(ctx); + await expect(command.runAsync()).rejects.toThrow( + 'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format XXXX-XXXX-XXXX-XXXX, where X is a lowercase letter.' + ); + + delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD; + }); + describe('non-interactive mode', () => { it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => { const projectId = uuidv4(); @@ -118,7 +155,7 @@ describe(IosSubmitCommand, () => { .mocked(getArchiveAsync) .mockImplementation(jest.requireActual('../../ArchiveSource').getArchiveAsync); - process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret'; + process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd'; const ctx = await createSubmissionContextAsync({ platform: Platform.IOS, @@ -147,7 +184,7 @@ describe(IosSubmitCommand, () => { archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.ipa' }, config: { appleIdUsername: 'test@example.com', - appleAppSpecificPassword: 'supersecret', + appleAppSpecificPassword: 'abcd-abcd-abcd-abcd', ascAppIdentifier: '12345678', }, }); @@ -182,7 +219,7 @@ describe(IosSubmitCommand, () => { return ctx; }); - process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret'; + process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd'; const ctx = await createSubmissionContextAsync({ platform: Platform.IOS, @@ -209,7 +246,7 @@ describe(IosSubmitCommand, () => { submittedBuildId: selectedBuild.id, config: { appleIdUsername: 'other-test@example.com', - appleAppSpecificPassword: 'supersecret', + appleAppSpecificPassword: 'abcd-abcd-abcd-abcd', ascAppIdentifier: '87654321', }, archiveSource: undefined, @@ -239,7 +276,7 @@ describe(IosSubmitCommand, () => { return ctx; }); - process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret'; + process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd'; const ctx = await createSubmissionContextAsync({ platform: Platform.IOS, @@ -266,7 +303,7 @@ describe(IosSubmitCommand, () => { submittedBuildId: selectedBuild.id, config: { appleIdUsername: 'test@example.com', - appleAppSpecificPassword: 'supersecret', + appleAppSpecificPassword: 'abcd-abcd-abcd-abcd', ascAppIdentifier: '12345678', }, archiveSource: undefined, @@ -301,7 +338,7 @@ describe(IosSubmitCommand, () => { return ctx; }); - process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret'; + process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd'; const ctx = await createSubmissionContextAsync({ platform: Platform.IOS, @@ -329,7 +366,7 @@ describe(IosSubmitCommand, () => { submittedBuildId: selectedBuild.id, config: { appleIdUsername: 'test@example.com', - appleAppSpecificPassword: 'supersecret', + appleAppSpecificPassword: 'abcd-abcd-abcd-abcd', ascAppIdentifier: '12345678', }, archiveSource: undefined, diff --git a/packages/eas-json/src/__tests__/submitProfiles-test.ts b/packages/eas-json/src/__tests__/submitProfiles-test.ts index cbde6bc1d3..c9a89d351c 100644 --- a/packages/eas-json/src/__tests__/submitProfiles-test.ts +++ b/packages/eas-json/src/__tests__/submitProfiles-test.ts @@ -107,10 +107,10 @@ test('ios config with all required values', async () => { ios: { appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'QWERTY', + appleTeamId: 'AB32CDE81F', ascApiKeyPath: './path-ABCD.p8', - ascApiKeyIssuerId: 'abc-123-def-456', - ascApiKeyId: 'ABCD', + ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', + ascApiKeyId: 'AB32CDE81F', }, }, }, @@ -121,11 +121,11 @@ test('ios config with all required values', async () => { expect(iosProfile).toEqual({ appleId: 'some@email.com', - appleTeamId: 'QWERTY', + appleTeamId: 'AB32CDE81F', ascAppId: '1223423523', ascApiKeyPath: './path-ABCD.p8', - ascApiKeyIssuerId: 'abc-123-def-456', - ascApiKeyId: 'ABCD', + ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', + ascApiKeyId: 'AB32CDE81F', language: 'en-US', }); }); @@ -137,7 +137,7 @@ test('ios config with ascApiKey fields set to env var', async () => { ios: { appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'QWERTY', + appleTeamId: 'AB32CDE81F', ascApiKeyPath: '$ASC_API_KEY_PATH', ascApiKeyIssuerId: '$ASC_API_KEY_ISSUER_ID', ascApiKeyId: '$ASC_API_KEY_ID', @@ -148,18 +148,18 @@ test('ios config with ascApiKey fields set to env var', async () => { try { process.env.ASC_API_KEY_PATH = './path-ABCD.p8'; - process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456'; - process.env.ASC_API_KEY_ID = 'ABCD'; + process.env.ASC_API_KEY_ISSUER_ID = 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a'; + process.env.ASC_API_KEY_ID = 'AB32CDE81F'; const accessor = EasJsonAccessor.fromProjectPath('/project'); const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); expect(iosProfile).toEqual({ appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'QWERTY', + appleTeamId: 'AB32CDE81F', ascApiKeyPath: './path-ABCD.p8', - ascApiKeyIssuerId: 'abc-123-def-456', - ascApiKeyId: 'ABCD', + ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', + ascApiKeyId: 'AB32CDE81F', language: 'en-US', }); } finally { @@ -176,16 +176,16 @@ test('valid profile extending other profile', async () => { ios: { appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'QWERTY', + appleTeamId: 'AB32CDE81F', }, }, extension: { extends: 'base', ios: { - appleTeamId: 'ABCDEF', + appleTeamId: 'AB32CDE81F', ascApiKeyPath: './path-ABCD.p8', - ascApiKeyIssuerId: 'abc-123-def-456', - ascApiKeyId: 'ABCD', + ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', + ascApiKeyId: 'AB32CDE81F', }, }, }, @@ -202,19 +202,134 @@ test('valid profile extending other profile', async () => { language: 'en-US', appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'QWERTY', + appleTeamId: 'AB32CDE81F', }); expect(extendedProfile).toEqual({ language: 'en-US', appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'ABCDEF', + appleTeamId: 'AB32CDE81F', ascApiKeyPath: './path-ABCD.p8', - ascApiKeyIssuerId: 'abc-123-def-456', - ascApiKeyId: 'ABCD', + ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', + ascApiKeyId: 'AB32CDE81F', }); }); +test('ios config with with invalid appleId', async () => { + await fs.writeJson('/project/eas.json', { + submit: { + release: { + ios: { + appleId: '| /bin/bash echo "hello"', + ascAppId: '1223423523', + appleTeamId: 'AB32CDE81F', + ascApiKeyPath: './path-ABCD.p8', + ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', + ascApiKeyId: 'AB32CDE81F', + }, + }, + }, + }); + + const accessor = EasJsonAccessor.fromProjectPath('/project'); + const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); + await expect(promise).rejects.toThrow( + 'Invalid Apple ID was specified. It should be a valid email address. Example: "name@example.com".' + ); +}); + +test('ios config with with invalid ascAppId', async () => { + await fs.writeJson('/project/eas.json', { + submit: { + release: { + ios: { + appleId: 'some@example.com', + ascAppId: 'othervalue', + appleTeamId: 'AB32CDE81F', + ascApiKeyPath: './path-ABCD.p8', + ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', + ascApiKeyId: 'AB32CDE81F', + }, + }, + }, + }); + + const accessor = EasJsonAccessor.fromProjectPath('/project'); + const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); + await expect(promise).rejects.toThrow( + 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist of 10 digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.' + ); +}); + +test('ios config with with invalid appleTeamId', async () => { + await fs.writeJson('/project/eas.json', { + submit: { + release: { + ios: { + appleId: 'some@example.com', + ascAppId: '1223423523', + appleTeamId: 'ls -la', + ascApiKeyPath: './path-ABCD.p8', + ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', + ascApiKeyId: 'AB32CDE81F', + }, + }, + }, + }); + + const accessor = EasJsonAccessor.fromProjectPath('/project'); + const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); + await expect(promise).rejects.toThrow( + 'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F".' + ); +}); + +test('ios config with with invalid ascApiKeyIssuerId', async () => { + await fs.writeJson('/project/eas.json', { + submit: { + release: { + ios: { + appleId: 'some@example.com', + ascAppId: '1223423523', + appleTeamId: 'AB32CDE81F', + ascApiKeyPath: './path-ABCD.p8', + ascApiKeyIssuerId: 'notanuuid', + ascApiKeyId: 'AB32CDE81F', + }, + }, + }, + }); + + const accessor = EasJsonAccessor.fromProjectPath('/project'); + const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); + await expect(promise).rejects.toThrow( + 'Invalid Apple App Store Connect API Key Issuer ID ("ascApiKeyIssuerId") was specified. It should be a valid UUID. Example: "b4d78f58-48c6-4f2c-96cb-94d8cd76970a". Learn more: https://expo.fyi/creating-asc-api-key.' + ); +}); + +test('ios config with with invalid ascApiKeyId', async () => { + await fs.writeJson('/project/eas.json', { + submit: { + release: { + ios: { + appleId: 'some@example.com', + ascAppId: '1223423523', + appleTeamId: 'AB32CDE81F', + ascApiKeyPath: './path-ABCD.p8', + ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', + ascApiKeyId: 'wrong value', + }, + }, + }, + }); + + const accessor = EasJsonAccessor.fromProjectPath('/project'); + const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); + await expect(promise).rejects.toThrow( + `Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.` + ); +}); + test('get profile names', async () => { await fs.writeJson('/project/eas.json', { submit: { diff --git a/packages/eas-json/src/schema.ts b/packages/eas-json/src/schema.ts index bf8d2585ac..d92a74e922 100644 --- a/packages/eas-json/src/schema.ts +++ b/packages/eas-json/src/schema.ts @@ -1,7 +1,7 @@ import Joi from 'joi'; import { BuildProfileSchema } from './build/schema'; -import { SubmitProfileSchema } from './submit/schema'; +import { UnresolvedSubmitProfileSchema } from './submit/schema'; import { AppVersionSource } from './types'; export const EasJsonSchema = Joi.object({ @@ -12,5 +12,5 @@ export const EasJsonSchema = Joi.object({ promptToConfigurePushNotifications: Joi.boolean(), }), build: Joi.object().pattern(Joi.string(), BuildProfileSchema), - submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema), + submit: Joi.object().pattern(Joi.string(), UnresolvedSubmitProfileSchema), }); diff --git a/packages/eas-json/src/submit/resolver.ts b/packages/eas-json/src/submit/resolver.ts index db74db5f71..e5c8657520 100644 --- a/packages/eas-json/src/submit/resolver.ts +++ b/packages/eas-json/src/submit/resolver.ts @@ -1,7 +1,7 @@ import { Platform } from '@expo/eas-build-job'; import envString from 'env-string'; -import { AndroidSubmitProfileSchema, IosSubmitProfileSchema } from './schema'; +import { AndroidSubmitProfileSchema, ResolvedIosSubmitProfileSchema } from './schema'; import { AndroidSubmitProfileFieldsToEvaluate, IosSubmitProfileFieldsToEvaluate, @@ -99,7 +99,7 @@ function mergeProfiles( export function getDefaultProfile(platform: T): SubmitProfile { const Schema = - platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema; + platform === Platform.ANDROID ? AndroidSubmitProfileSchema : ResolvedIosSubmitProfileSchema; return Schema.validate({}, { allowUnknown: false, abortEarly: false, convert: true }).value; } diff --git a/packages/eas-json/src/submit/schema.ts b/packages/eas-json/src/submit/schema.ts index 007eb8f2d7..b1a27448f7 100644 --- a/packages/eas-json/src/submit/schema.ts +++ b/packages/eas-json/src/submit/schema.ts @@ -19,7 +19,9 @@ export const AndroidSubmitProfileSchema = Joi.object({ }), }); -export const IosSubmitProfileSchema = Joi.object({ +// it is less strict submission schema allowing for magic syntax like "$ASC_API_KEY_PATH" +// to read value from environment variable later on +export const UnresolvedIosSubmitProfileSchema = Joi.object({ ascApiKeyPath: Joi.string(), ascApiKeyId: Joi.string(), ascApiKeyIssuerId: Joi.string(), @@ -34,8 +36,44 @@ export const IosSubmitProfileSchema = Joi.object({ metadataPath: Joi.string(), }); -export const SubmitProfileSchema = Joi.object({ +// more strict version after resolving all of the values +export const ResolvedIosSubmitProfileSchema = Joi.object({ + ascApiKeyPath: Joi.string(), + ascApiKeyId: Joi.string() + .regex(/^[\dA-Z]{10}$/) + .message( + 'Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.' + ), + ascApiKeyIssuerId: Joi.string() + .uuid() + .message( + 'Invalid Apple App Store Connect API Key Issuer ID ("ascApiKeyIssuerId") was specified. It should be a valid UUID. Example: "b4d78f58-48c6-4f2c-96cb-94d8cd76970a". Learn more: https://expo.fyi/creating-asc-api-key.' + ), + appleId: Joi.string() + .email() + .message( + 'Invalid Apple ID was specified. It should be a valid email address. Example: "name@example.com".' + ), + ascAppId: Joi.string() + .regex(/^\d{10}$/) + .message( + 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist of 10 digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.' + ), + appleTeamId: Joi.string() + .regex(/^[\dA-Z]{10}$/) + .message( + 'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F".' + ), + sku: Joi.string(), + language: Joi.string().default('en-US'), + companyName: Joi.string(), + appName: Joi.string(), + bundleIdentifier: Joi.string(), + metadataPath: Joi.string(), +}); + +export const UnresolvedSubmitProfileSchema = Joi.object({ extends: Joi.string(), android: AndroidSubmitProfileSchema, - ios: IosSubmitProfileSchema, + ios: UnresolvedIosSubmitProfileSchema, }); diff --git a/packages/eas-json/src/utils.ts b/packages/eas-json/src/utils.ts index d6869e1fd0..a44f102363 100644 --- a/packages/eas-json/src/utils.ts +++ b/packages/eas-json/src/utils.ts @@ -5,6 +5,7 @@ import { resolveBuildProfile } from './build/resolver'; import { BuildProfile } from './build/types'; import { MissingEasJsonError } from './errors'; import { resolveSubmitProfile } from './submit/resolver'; +import { AndroidSubmitProfileSchema, ResolvedIosSubmitProfileSchema } from './submit/schema'; import { SubmitProfile } from './submit/types'; import { EasJson } from './types'; @@ -102,6 +103,17 @@ export class EasJsonUtils { profileName?: string ): Promise> { const easJson = await accessor.readAsync(); - return resolveSubmitProfile({ easJson, platform, profileName }); + const profile = resolveSubmitProfile({ easJson, platform, profileName }); + const Schema = + platform === Platform.ANDROID ? AndroidSubmitProfileSchema : ResolvedIosSubmitProfileSchema; + const { value, error } = Schema.validate(profile, { + allowUnknown: false, + abortEarly: false, + convert: true, + }); + if (error) { + throw error; + } + return value; } }