From 07f1648bb4e1c4e8d7b195d3c7c7f7c0733b52e8 Mon Sep 17 00:00:00 2001 From: Szymon Dziedzic Date: Mon, 29 Jan 2024 12:25:57 +0100 Subject: [PATCH 1/6] Revert "Revert "[eas-json] validate EAS Submit inputs better"" --- .../src/submit/ios/IosSubmitCommand.ts | 5 + .../ios/__tests__/IosSubmitCommand-test.ts | 53 +++++- .../src/__tests__/submitProfiles-test.ts | 155 +++++++++++++++--- packages/eas-json/src/schema.ts | 4 +- packages/eas-json/src/submit/resolver.ts | 4 +- packages/eas-json/src/submit/schema.ts | 44 ++++- packages/eas-json/src/utils.ts | 14 +- 7 files changed, 243 insertions(+), 36 deletions(-) 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; } } From 731cfe47b5a7c3f9ddedd9cfc7aa7fbdad453c8a Mon Sep 17 00:00:00 2001 From: Szymon Dziedzic Date: Mon, 29 Jan 2024 12:39:43 +0100 Subject: [PATCH 2/6] make validation constraints more loose where necessary --- .../src/__tests__/submitProfiles-test.ts | 2 +- packages/eas-json/src/submit/schema.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/eas-json/src/__tests__/submitProfiles-test.ts b/packages/eas-json/src/__tests__/submitProfiles-test.ts index c9a89d351c..4403052ce4 100644 --- a/packages/eas-json/src/__tests__/submitProfiles-test.ts +++ b/packages/eas-json/src/__tests__/submitProfiles-test.ts @@ -257,7 +257,7 @@ test('ios config with with invalid ascAppId', async () => { 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.' + 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist only of digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.' ); }); diff --git a/packages/eas-json/src/submit/schema.ts b/packages/eas-json/src/submit/schema.ts index b1a27448f7..71db16494f 100644 --- a/packages/eas-json/src/submit/schema.ts +++ b/packages/eas-json/src/submit/schema.ts @@ -40,12 +40,13 @@ export const UnresolvedIosSubmitProfileSchema = Joi.object({ export const ResolvedIosSubmitProfileSchema = Joi.object({ ascApiKeyPath: Joi.string(), ascApiKeyId: Joi.string() - .regex(/^[\dA-Z]{10}$/) + .regex(/^[\dA-Z]+$/) .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.' - ), + ) + .max(30), // I didn't find any docs about it, but all of the ones I've seen are 10 characters long so 30 characters limit should be enough ascApiKeyIssuerId: Joi.string() - .uuid() + .uuid() // All of the issuer IDs I've seen are UUIDs, but again, I didn't find any docs about it .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.' ), @@ -55,12 +56,13 @@ export const ResolvedIosSubmitProfileSchema = Joi.object({ 'Invalid Apple ID was specified. It should be a valid email address. Example: "name@example.com".' ), ascAppId: Joi.string() - .regex(/^\d{10}$/) + .regex(/^\d+$/) .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.' - ), + 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist only of digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.' + ) + .max(30), // I didn't find any docs about it, but the longest app ID I've seen is 10 digits long so 30 characters limit should be enough appleTeamId: Joi.string() - .regex(/^[\dA-Z]{10}$/) + .regex(/^[\dA-Z]{10}$/) // Apple says that it always has to be 10 characters long https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/ .message( 'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F".' ), From 40fd1509fbf29d5dc94cb9685ac377eebbf7e5dd Mon Sep 17 00:00:00 2001 From: Szymon Dziedzic Date: Mon, 29 Jan 2024 11:41:49 +0000 Subject: [PATCH 3/6] update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 965d4e8362..fd451ed8b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This is the log of notable changes to EAS CLI and related packages. ### 🧹 Chores +- Add better validation for EAS Submit inputs. ([#2202](https://github.com/expo/eas-cli/pull/2202) by [@szdziedzic](https://github.com/szdziedzic)) + ## [7.1.1](https://github.com/expo/eas-cli/releases/tag/v7.1.1) - 2024-01-26 ### 🐛 Bug fixes From e463c60d37e1b08cb33d3cf7c440dae935c6512a Mon Sep 17 00:00:00 2001 From: Szymon Dziedzic Date: Mon, 29 Jan 2024 12:49:13 +0100 Subject: [PATCH 4/6] fix message --- packages/eas-json/src/__tests__/submitProfiles-test.ts | 2 +- packages/eas-json/src/submit/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eas-json/src/__tests__/submitProfiles-test.ts b/packages/eas-json/src/__tests__/submitProfiles-test.ts index 4403052ce4..7d598fa748 100644 --- a/packages/eas-json/src/__tests__/submitProfiles-test.ts +++ b/packages/eas-json/src/__tests__/submitProfiles-test.ts @@ -326,7 +326,7 @@ test('ios config with with invalid ascApiKeyId', async () => { 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.` + `Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.` ); }); diff --git a/packages/eas-json/src/submit/schema.ts b/packages/eas-json/src/submit/schema.ts index 71db16494f..9a7e9db007 100644 --- a/packages/eas-json/src/submit/schema.ts +++ b/packages/eas-json/src/submit/schema.ts @@ -42,7 +42,7 @@ export const ResolvedIosSubmitProfileSchema = Joi.object({ ascApiKeyId: Joi.string() .regex(/^[\dA-Z]+$/) .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.' + 'Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.' ) .max(30), // I didn't find any docs about it, but all of the ones I've seen are 10 characters long so 30 characters limit should be enough ascApiKeyIssuerId: Joi.string() From ec4c41b505bd437e0398b99f2566abb585753bf1 Mon Sep 17 00:00:00 2001 From: Szymon Dziedzic Date: Mon, 29 Jan 2024 14:05:53 +0100 Subject: [PATCH 5/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanisław Chmiela --- packages/eas-json/src/__tests__/submitProfiles-test.ts | 2 +- packages/eas-json/src/submit/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eas-json/src/__tests__/submitProfiles-test.ts b/packages/eas-json/src/__tests__/submitProfiles-test.ts index 7d598fa748..fafa00254a 100644 --- a/packages/eas-json/src/__tests__/submitProfiles-test.ts +++ b/packages/eas-json/src/__tests__/submitProfiles-test.ts @@ -257,7 +257,7 @@ test('ios config with with invalid ascAppId', async () => { 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 only of digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.' + 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist only of digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.' ); }); diff --git a/packages/eas-json/src/submit/schema.ts b/packages/eas-json/src/submit/schema.ts index 9a7e9db007..2b3d1fc5ae 100644 --- a/packages/eas-json/src/submit/schema.ts +++ b/packages/eas-json/src/submit/schema.ts @@ -58,7 +58,7 @@ export const ResolvedIosSubmitProfileSchema = Joi.object({ ascAppId: Joi.string() .regex(/^\d+$/) .message( - 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist only of digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.md.' + 'Invalid Apple App Store Connect App ID ("ascAppId") was specified. It should consist only of digits. Example: "1234567891". Learn more: https://expo.fyi/asc-app-id.' ) .max(30), // I didn't find any docs about it, but the longest app ID I've seen is 10 digits long so 30 characters limit should be enough appleTeamId: Joi.string() From 2852a454ad26b4cca9ef5311b3728633a7543a1c Mon Sep 17 00:00:00 2001 From: Szymon Dziedzic Date: Tue, 30 Jan 2024 11:08:44 +0100 Subject: [PATCH 6/6] apply suggested changes --- .../src/submit/ios/IosSubmitCommand.ts | 2 +- .../ios/__tests__/IosSubmitCommand-test.ts | 2 +- .../src/__tests__/submitProfiles-test.ts | 50 +++++++++---------- packages/eas-json/src/submit/schema.ts | 6 +-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts b/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts index cc67f6ba0f..530420d5e5 100644 --- a/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts +++ b/packages/eas-cli/src/submit/ios/IosSubmitCommand.ts @@ -113,7 +113,7 @@ export default class IosSubmitCommand { 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.' + 'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format xxxx-xxxx-xxxx-xxxx, where x is a lowercase letter.' ); } return result({ 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 d0687fd330..aa6b87fdac 100644 --- a/packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts +++ b/packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts @@ -109,7 +109,7 @@ describe(IosSubmitCommand, () => { }); 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.' + '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; diff --git a/packages/eas-json/src/__tests__/submitProfiles-test.ts b/packages/eas-json/src/__tests__/submitProfiles-test.ts index fafa00254a..69c9b05d0b 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: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }, }, }, @@ -121,11 +121,11 @@ test('ios config with all required values', async () => { expect(iosProfile).toEqual({ appleId: 'some@email.com', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascAppId: '1223423523', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', 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: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: '$ASC_API_KEY_PATH', ascApiKeyIssuerId: '$ASC_API_KEY_ISSUER_ID', ascApiKeyId: '$ASC_API_KEY_ID', @@ -149,17 +149,17 @@ 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 = 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a'; - process.env.ASC_API_KEY_ID = 'AB32CDE81F'; + process.env.ASC_API_KEY_ID = 'AB32CZE81F'; const accessor = EasJsonAccessor.fromProjectPath('/project'); const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release'); expect(iosProfile).toEqual({ appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', language: 'en-US', }); } finally { @@ -176,16 +176,16 @@ test('valid profile extending other profile', async () => { ios: { appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', }, }, extension: { extends: 'base', ios: { - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }, }, }, @@ -202,16 +202,16 @@ test('valid profile extending other profile', async () => { language: 'en-US', appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', }); expect(extendedProfile).toEqual({ language: 'en-US', appleId: 'some@email.com', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }); }); @@ -222,10 +222,10 @@ test('ios config with with invalid appleId', async () => { ios: { appleId: '| /bin/bash echo "hello"', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }, }, }, @@ -245,10 +245,10 @@ test('ios config with with invalid ascAppId', async () => { ios: { appleId: 'some@example.com', ascAppId: 'othervalue', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }, }, }, @@ -271,7 +271,7 @@ test('ios config with with invalid appleTeamId', async () => { appleTeamId: 'ls -la', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }, }, }, @@ -280,7 +280,7 @@ test('ios config with with invalid appleTeamId', async () => { 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".' + 'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CZE81F".' ); }); @@ -291,10 +291,10 @@ test('ios config with with invalid ascApiKeyIssuerId', async () => { ios: { appleId: 'some@example.com', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: 'notanuuid', - ascApiKeyId: 'AB32CDE81F', + ascApiKeyId: 'AB32CZE81F', }, }, }, @@ -303,7 +303,7 @@ test('ios config with with invalid ascApiKeyIssuerId', async () => { 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.' + 'Invalid Apple App Store Connect API Key Issuer ID ("ascApiKeyIssuerId") was specified. It should be a valid UUID. Example: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". Learn more: https://expo.fyi/creating-asc-api-key.' ); }); @@ -314,7 +314,7 @@ test('ios config with with invalid ascApiKeyId', async () => { ios: { appleId: 'some@example.com', ascAppId: '1223423523', - appleTeamId: 'AB32CDE81F', + appleTeamId: 'AB32CZE81F', ascApiKeyPath: './path-ABCD.p8', ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a', ascApiKeyId: 'wrong value', @@ -326,7 +326,7 @@ test('ios config with with invalid ascApiKeyId', async () => { 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 uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.` + `Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of uppercase letters or digits. Example: "AB32CZE81F". Learn more: https://expo.fyi/creating-asc-api-key.` ); }); diff --git a/packages/eas-json/src/submit/schema.ts b/packages/eas-json/src/submit/schema.ts index 2b3d1fc5ae..a27af46a05 100644 --- a/packages/eas-json/src/submit/schema.ts +++ b/packages/eas-json/src/submit/schema.ts @@ -42,13 +42,13 @@ export const ResolvedIosSubmitProfileSchema = Joi.object({ ascApiKeyId: Joi.string() .regex(/^[\dA-Z]+$/) .message( - 'Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of uppercase letters or digits. Example: "AB32CDE81F". Learn more: https://expo.fyi/creating-asc-api-key.' + 'Invalid Apple App Store Connect API Key ID ("ascApiKeyId") was specified. It should consist of uppercase letters or digits. Example: "AB32CZE81F". Learn more: https://expo.fyi/creating-asc-api-key.' ) .max(30), // I didn't find any docs about it, but all of the ones I've seen are 10 characters long so 30 characters limit should be enough ascApiKeyIssuerId: Joi.string() .uuid() // All of the issuer IDs I've seen are UUIDs, but again, I didn't find any docs about it .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.' + 'Invalid Apple App Store Connect API Key Issuer ID ("ascApiKeyIssuerId") was specified. It should be a valid UUID. Example: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". Learn more: https://expo.fyi/creating-asc-api-key.' ), appleId: Joi.string() .email() @@ -64,7 +64,7 @@ export const ResolvedIosSubmitProfileSchema = Joi.object({ appleTeamId: Joi.string() .regex(/^[\dA-Z]{10}$/) // Apple says that it always has to be 10 characters long https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/ .message( - 'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CDE81F".' + 'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CZE81F".' ), sku: Joi.string(), language: Joi.string().default('en-US'),