Skip to content

Commit fe8f1b3

Browse files
Revert "Revert "[eas-json] validate EAS Submit inputs better"" (#2202)
* Revert "Revert "[eas-json] validate EAS Submit inputs better"" * make validation constraints more loose where necessary * update CHANGELOG.md * fix message * Apply suggestions from code review Co-authored-by: Stanisław Chmiela <[email protected]> * apply suggested changes --------- Co-authored-by: Stanisław Chmiela <[email protected]>
1 parent 3a1e529 commit fe8f1b3

File tree

8 files changed

+247
-36
lines changed

8 files changed

+247
-36
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ This is the log of notable changes to EAS CLI and related packages.
1212

1313
### 🧹 Chores
1414

15+
- Add better validation for EAS Submit inputs. ([#2202](https://github.com/expo/eas-cli/pull/2202) by [@szdziedzic](https://github.com/szdziedzic))
16+
1517
## [7.1.1](https://github.com/expo/eas-cli/releases/tag/v7.1.1) - 2024-01-26
1618

1719
### 🐛 Bug fixes

Diff for: packages/eas-cli/src/submit/ios/IosSubmitCommand.ts

+5
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ export default class IosSubmitCommand {
111111
const envAppSpecificPassword = getenv.string('EXPO_APPLE_APP_SPECIFIC_PASSWORD', '');
112112

113113
if (envAppSpecificPassword) {
114+
if (!/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/.test(envAppSpecificPassword)) {
115+
throw new Error(
116+
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format xxxx-xxxx-xxxx-xxxx, where x is a lowercase letter.'
117+
);
118+
}
114119
return result({
115120
sourceType: AppSpecificPasswordSourceType.userDefined,
116121
appSpecificPassword: envAppSpecificPassword,

Diff for: packages/eas-cli/src/submit/ios/__tests__/IosSubmitCommand-test.ts

+45-8
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,43 @@ describe(IosSubmitCommand, () => {
7878
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]);
7979
});
8080

81+
it('throws an error if using app specific password in invalid format', async () => {
82+
const projectId = uuidv4();
83+
const graphqlClient = {} as any as ExpoGraphqlClient;
84+
const analytics = instance(mock<Analytics>());
85+
jest
86+
.mocked(getArchiveAsync)
87+
.mockImplementation(jest.requireActual('../../ArchiveSource').getArchiveAsync);
88+
89+
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'ls -la';
90+
91+
const ctx = await createSubmissionContextAsync({
92+
platform: Platform.IOS,
93+
projectDir: testProject.projectRoot,
94+
archiveFlags: {
95+
url: 'http://expo.dev/fake.ipa',
96+
},
97+
profile: {
98+
language: 'en-US',
99+
appleId: '[email protected]',
100+
ascAppId: '12345678',
101+
},
102+
nonInteractive: false,
103+
actor: mockJester,
104+
graphqlClient,
105+
analytics,
106+
exp: testProject.appJSON.expo,
107+
projectId,
108+
vcsClient,
109+
});
110+
const command = new IosSubmitCommand(ctx);
111+
await expect(command.runAsync()).rejects.toThrow(
112+
'EXPO_APPLE_APP_SPECIFIC_PASSWORD must be in the format xxxx-xxxx-xxxx-xxxx, where x is a lowercase letter.'
113+
);
114+
115+
delete process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD;
116+
});
117+
81118
describe('non-interactive mode', () => {
82119
it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => {
83120
const projectId = uuidv4();
@@ -118,7 +155,7 @@ describe(IosSubmitCommand, () => {
118155
.mocked(getArchiveAsync)
119156
.mockImplementation(jest.requireActual('../../ArchiveSource').getArchiveAsync);
120157

121-
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
158+
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';
122159

123160
const ctx = await createSubmissionContextAsync({
124161
platform: Platform.IOS,
@@ -147,7 +184,7 @@ describe(IosSubmitCommand, () => {
147184
archiveSource: { type: SubmissionArchiveSourceType.Url, url: 'http://expo.dev/fake.ipa' },
148185
config: {
149186
appleIdUsername: '[email protected]',
150-
appleAppSpecificPassword: 'supersecret',
187+
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
151188
ascAppIdentifier: '12345678',
152189
},
153190
});
@@ -182,7 +219,7 @@ describe(IosSubmitCommand, () => {
182219
return ctx;
183220
});
184221

185-
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
222+
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';
186223

187224
const ctx = await createSubmissionContextAsync({
188225
platform: Platform.IOS,
@@ -209,7 +246,7 @@ describe(IosSubmitCommand, () => {
209246
submittedBuildId: selectedBuild.id,
210247
config: {
211248
appleIdUsername: '[email protected]',
212-
appleAppSpecificPassword: 'supersecret',
249+
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
213250
ascAppIdentifier: '87654321',
214251
},
215252
archiveSource: undefined,
@@ -239,7 +276,7 @@ describe(IosSubmitCommand, () => {
239276
return ctx;
240277
});
241278

242-
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
279+
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';
243280

244281
const ctx = await createSubmissionContextAsync({
245282
platform: Platform.IOS,
@@ -266,7 +303,7 @@ describe(IosSubmitCommand, () => {
266303
submittedBuildId: selectedBuild.id,
267304
config: {
268305
appleIdUsername: '[email protected]',
269-
appleAppSpecificPassword: 'supersecret',
306+
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
270307
ascAppIdentifier: '12345678',
271308
},
272309
archiveSource: undefined,
@@ -301,7 +338,7 @@ describe(IosSubmitCommand, () => {
301338
return ctx;
302339
});
303340

304-
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'supersecret';
341+
process.env.EXPO_APPLE_APP_SPECIFIC_PASSWORD = 'abcd-abcd-abcd-abcd';
305342

306343
const ctx = await createSubmissionContextAsync({
307344
platform: Platform.IOS,
@@ -329,7 +366,7 @@ describe(IosSubmitCommand, () => {
329366
submittedBuildId: selectedBuild.id,
330367
config: {
331368
appleIdUsername: '[email protected]',
332-
appleAppSpecificPassword: 'supersecret',
369+
appleAppSpecificPassword: 'abcd-abcd-abcd-abcd',
333370
ascAppIdentifier: '12345678',
334371
},
335372
archiveSource: undefined,

Diff for: packages/eas-json/src/__tests__/submitProfiles-test.ts

+135-20
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ test('ios config with all required values', async () => {
107107
ios: {
108108
appleId: '[email protected]',
109109
ascAppId: '1223423523',
110-
appleTeamId: 'QWERTY',
110+
appleTeamId: 'AB32CZE81F',
111111
ascApiKeyPath: './path-ABCD.p8',
112-
ascApiKeyIssuerId: 'abc-123-def-456',
113-
ascApiKeyId: 'ABCD',
112+
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
113+
ascApiKeyId: 'AB32CZE81F',
114114
},
115115
},
116116
},
@@ -121,11 +121,11 @@ test('ios config with all required values', async () => {
121121

122122
expect(iosProfile).toEqual({
123123
appleId: '[email protected]',
124-
appleTeamId: 'QWERTY',
124+
appleTeamId: 'AB32CZE81F',
125125
ascAppId: '1223423523',
126126
ascApiKeyPath: './path-ABCD.p8',
127-
ascApiKeyIssuerId: 'abc-123-def-456',
128-
ascApiKeyId: 'ABCD',
127+
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
128+
ascApiKeyId: 'AB32CZE81F',
129129
language: 'en-US',
130130
});
131131
});
@@ -137,7 +137,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
137137
ios: {
138138
appleId: '[email protected]',
139139
ascAppId: '1223423523',
140-
appleTeamId: 'QWERTY',
140+
appleTeamId: 'AB32CZE81F',
141141
ascApiKeyPath: '$ASC_API_KEY_PATH',
142142
ascApiKeyIssuerId: '$ASC_API_KEY_ISSUER_ID',
143143
ascApiKeyId: '$ASC_API_KEY_ID',
@@ -148,18 +148,18 @@ test('ios config with ascApiKey fields set to env var', async () => {
148148

149149
try {
150150
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
151-
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
152-
process.env.ASC_API_KEY_ID = 'ABCD';
151+
process.env.ASC_API_KEY_ISSUER_ID = 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a';
152+
process.env.ASC_API_KEY_ID = 'AB32CZE81F';
153153
const accessor = EasJsonAccessor.fromProjectPath('/project');
154154
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
155155

156156
expect(iosProfile).toEqual({
157157
appleId: '[email protected]',
158158
ascAppId: '1223423523',
159-
appleTeamId: 'QWERTY',
159+
appleTeamId: 'AB32CZE81F',
160160
ascApiKeyPath: './path-ABCD.p8',
161-
ascApiKeyIssuerId: 'abc-123-def-456',
162-
ascApiKeyId: 'ABCD',
161+
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
162+
ascApiKeyId: 'AB32CZE81F',
163163
language: 'en-US',
164164
});
165165
} finally {
@@ -176,16 +176,16 @@ test('valid profile extending other profile', async () => {
176176
ios: {
177177
appleId: '[email protected]',
178178
ascAppId: '1223423523',
179-
appleTeamId: 'QWERTY',
179+
appleTeamId: 'AB32CZE81F',
180180
},
181181
},
182182
extension: {
183183
extends: 'base',
184184
ios: {
185-
appleTeamId: 'ABCDEF',
185+
appleTeamId: 'AB32CZE81F',
186186
ascApiKeyPath: './path-ABCD.p8',
187-
ascApiKeyIssuerId: 'abc-123-def-456',
188-
ascApiKeyId: 'ABCD',
187+
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
188+
ascApiKeyId: 'AB32CZE81F',
189189
},
190190
},
191191
},
@@ -202,19 +202,134 @@ test('valid profile extending other profile', async () => {
202202
language: 'en-US',
203203
appleId: '[email protected]',
204204
ascAppId: '1223423523',
205-
appleTeamId: 'QWERTY',
205+
appleTeamId: 'AB32CZE81F',
206206
});
207207
expect(extendedProfile).toEqual({
208208
language: 'en-US',
209209
appleId: '[email protected]',
210210
ascAppId: '1223423523',
211-
appleTeamId: 'ABCDEF',
211+
appleTeamId: 'AB32CZE81F',
212212
ascApiKeyPath: './path-ABCD.p8',
213-
ascApiKeyIssuerId: 'abc-123-def-456',
214-
ascApiKeyId: 'ABCD',
213+
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
214+
ascApiKeyId: 'AB32CZE81F',
215215
});
216216
});
217217

218+
test('ios config with with invalid appleId', async () => {
219+
await fs.writeJson('/project/eas.json', {
220+
submit: {
221+
release: {
222+
ios: {
223+
appleId: '| /bin/bash echo "hello"',
224+
ascAppId: '1223423523',
225+
appleTeamId: 'AB32CZE81F',
226+
ascApiKeyPath: './path-ABCD.p8',
227+
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
228+
ascApiKeyId: 'AB32CZE81F',
229+
},
230+
},
231+
},
232+
});
233+
234+
const accessor = EasJsonAccessor.fromProjectPath('/project');
235+
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
236+
await expect(promise).rejects.toThrow(
237+
'Invalid Apple ID was specified. It should be a valid email address. Example: "[email protected]".'
238+
);
239+
});
240+
241+
test('ios config with with invalid ascAppId', async () => {
242+
await fs.writeJson('/project/eas.json', {
243+
submit: {
244+
release: {
245+
ios: {
246+
appleId: '[email protected]',
247+
ascAppId: 'othervalue',
248+
appleTeamId: 'AB32CZE81F',
249+
ascApiKeyPath: './path-ABCD.p8',
250+
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
251+
ascApiKeyId: 'AB32CZE81F',
252+
},
253+
},
254+
},
255+
});
256+
257+
const accessor = EasJsonAccessor.fromProjectPath('/project');
258+
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
259+
await expect(promise).rejects.toThrow(
260+
'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.'
261+
);
262+
});
263+
264+
test('ios config with with invalid appleTeamId', async () => {
265+
await fs.writeJson('/project/eas.json', {
266+
submit: {
267+
release: {
268+
ios: {
269+
appleId: '[email protected]',
270+
ascAppId: '1223423523',
271+
appleTeamId: 'ls -la',
272+
ascApiKeyPath: './path-ABCD.p8',
273+
ascApiKeyIssuerId: '2af70a7a-2ac5-44d4-924e-ae97a7ca9333',
274+
ascApiKeyId: 'AB32CZE81F',
275+
},
276+
},
277+
},
278+
});
279+
280+
const accessor = EasJsonAccessor.fromProjectPath('/project');
281+
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
282+
await expect(promise).rejects.toThrow(
283+
'Invalid Apple Team ID was specified. It should consist of 10 uppercase letters or digits. Example: "AB32CZE81F".'
284+
);
285+
});
286+
287+
test('ios config with with invalid ascApiKeyIssuerId', async () => {
288+
await fs.writeJson('/project/eas.json', {
289+
submit: {
290+
release: {
291+
ios: {
292+
appleId: '[email protected]',
293+
ascAppId: '1223423523',
294+
appleTeamId: 'AB32CZE81F',
295+
ascApiKeyPath: './path-ABCD.p8',
296+
ascApiKeyIssuerId: 'notanuuid',
297+
ascApiKeyId: 'AB32CZE81F',
298+
},
299+
},
300+
},
301+
});
302+
303+
const accessor = EasJsonAccessor.fromProjectPath('/project');
304+
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
305+
await expect(promise).rejects.toThrow(
306+
'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.'
307+
);
308+
});
309+
310+
test('ios config with with invalid ascApiKeyId', async () => {
311+
await fs.writeJson('/project/eas.json', {
312+
submit: {
313+
release: {
314+
ios: {
315+
appleId: '[email protected]',
316+
ascAppId: '1223423523',
317+
appleTeamId: 'AB32CZE81F',
318+
ascApiKeyPath: './path-ABCD.p8',
319+
ascApiKeyIssuerId: 'b4d78f58-48c6-4f2c-96cb-94d8cd76970a',
320+
ascApiKeyId: 'wrong value',
321+
},
322+
},
323+
},
324+
});
325+
326+
const accessor = EasJsonAccessor.fromProjectPath('/project');
327+
const promise = EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
328+
await expect(promise).rejects.toThrow(
329+
`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.`
330+
);
331+
});
332+
218333
test('get profile names', async () => {
219334
await fs.writeJson('/project/eas.json', {
220335
submit: {

Diff for: packages/eas-json/src/schema.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Joi from 'joi';
22

33
import { BuildProfileSchema } from './build/schema';
4-
import { SubmitProfileSchema } from './submit/schema';
4+
import { UnresolvedSubmitProfileSchema } from './submit/schema';
55
import { AppVersionSource } from './types';
66

77
export const EasJsonSchema = Joi.object({
@@ -12,5 +12,5 @@ export const EasJsonSchema = Joi.object({
1212
promptToConfigurePushNotifications: Joi.boolean(),
1313
}),
1414
build: Joi.object().pattern(Joi.string(), BuildProfileSchema),
15-
submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema),
15+
submit: Joi.object().pattern(Joi.string(), UnresolvedSubmitProfileSchema),
1616
});

Diff for: packages/eas-json/src/submit/resolver.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Platform } from '@expo/eas-build-job';
22
import envString from 'env-string';
33

4-
import { AndroidSubmitProfileSchema, IosSubmitProfileSchema } from './schema';
4+
import { AndroidSubmitProfileSchema, ResolvedIosSubmitProfileSchema } from './schema';
55
import {
66
AndroidSubmitProfileFieldsToEvaluate,
77
IosSubmitProfileFieldsToEvaluate,
@@ -99,7 +99,7 @@ function mergeProfiles<T extends Platform>(
9999

100100
export function getDefaultProfile<T extends Platform>(platform: T): SubmitProfile<T> {
101101
const Schema =
102-
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : IosSubmitProfileSchema;
102+
platform === Platform.ANDROID ? AndroidSubmitProfileSchema : ResolvedIosSubmitProfileSchema;
103103
return Schema.validate({}, { allowUnknown: false, abortEarly: false, convert: true }).value;
104104
}
105105

0 commit comments

Comments
 (0)