Skip to content

Commit

Permalink
implement SetupInternalProvisioningProfile (#344)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsokal authored Apr 22, 2021
1 parent 61e505f commit e1de210
Show file tree
Hide file tree
Showing 14 changed files with 522 additions and 95 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- Implement offline distribution certificate validation when running a build in non-interactive mode. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))
- Add support for building internal distribution apps for Apple Enterprise Teams. ([#344](https://github.com/expo/eas-cli/pull/344) by [@dsokal](https://github.com/dsokal))

### 🐛 Bug fixes

- Display descriptive error message when API for EAS Build changes. ([#359](https://github.com/expo/eas-cli/pull/359) by [@wkozyra95](https://github.com/wkozyra95))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function createCtxMock(mockOverride: Record<string, any> = {}): Context {
newIos: getNewIosApiMockWithoutCredentials(),
android: getAndroidApiMockWithoutCredentials(),
appStore: getAppstoreMock(),
bestEffortAppStoreAuthenticateAsync: jest.fn(),
ensureAppleCtx: jest.fn(),
ensureProjectContext: jest.fn(),
user: {
Expand Down
69 changes: 13 additions & 56 deletions packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { Platform } from '@expo/eas-build-job';
import { CredentialsSource, IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';

import { IosAppBuildCredentialsFragment } from '../../graphql/generated';
import Log from '../../log';
import { CredentialsManager } from '../CredentialsManager';
import { Context } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import type { IosCredentials } from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import {
getBuildCredentialsAsync,
resolveDistributionType,
} from './actions/new/BuildCredentialsUtils';
import { AppLookupParams } from './api/GraphqlClient';
import { isAdHocProfile } from './utils/provisioningProfile';

Expand All @@ -32,11 +26,10 @@ export default class IosCredentialsProvider {
public async getCredentialsAsync(
src: CredentialsSource.LOCAL | CredentialsSource.REMOTE
): Promise<IosCredentials> {
switch (src) {
case CredentialsSource.LOCAL:
return await this.getLocalAsync();
case CredentialsSource.REMOTE:
return await this.getRemoteAsync();
if (src === CredentialsSource.LOCAL) {
return await this.getLocalAsync();
} else {
return await this.getRemoteAsync();
}
}

Expand Down Expand Up @@ -70,54 +63,18 @@ export default class IosCredentialsProvider {
}

private async getRemoteAsync(): Promise<IosCredentials> {
if (this.options.skipCredentialsCheck) {
Log.log('Skipping credentials check');
} else {
await new CredentialsManager(this.ctx).runActionAsync(
new SetupBuildCredentials({
app: this.options.app,
distribution: this.options.distribution,
enterpriseProvisioning: this.options.enterpriseProvisioning,
})
);
}

const buildCredentials = await this.fetchRemoteAsync();
if (
!buildCredentials?.distributionCertificate?.certificateP12 ||
!buildCredentials.distributionCertificate?.certificatePassword
) {
if (this.options.skipCredentialsCheck) {
throw new Error(
'Distribution certificate is missing and credentials check was skipped. Run without --skip-credentials-check to set it up.'
);
} else {
throw new Error('Distribution certificate is missing');
}
}
if (!buildCredentials.provisioningProfile?.provisioningProfile) {
if (this.options.skipCredentialsCheck) {
throw new Error(
'Provisioning profile is missing and credentials check was skipped. Run without --skip-credentials-check to set it up.'
);
} else {
throw new Error('Provisioning profile is missing');
}
}
const manager = new CredentialsManager(this.ctx);
const { provisioningProfile, distributionCertificate } = await new SetupBuildCredentials({
app: this.options.app,
distribution: this.options.distribution,
enterpriseProvisioning: this.options.enterpriseProvisioning,
}).runAsync(manager, this.ctx);
return {
provisioningProfile: buildCredentials.provisioningProfile.provisioningProfile,
provisioningProfile,
distributionCertificate: {
certP12: buildCredentials.distributionCertificate.certificateP12,
certPassword: buildCredentials.distributionCertificate.certificatePassword,
certP12: distributionCertificate.certificateP12,
certPassword: distributionCertificate.certificatePassword,
},
};
}

private async fetchRemoteAsync(): Promise<IosAppBuildCredentialsFragment | null> {
const distributionType = resolveDistributionType(
this.options.distribution,
this.options.enterpriseProvisioning
);
return await getBuildCredentialsAsync(this.ctx, this.options.app, distributionType);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CredentialsSource } from '@expo/eas-json';
import { vol } from 'memfs';

import { IosAppBuildCredentialsFragment } from '../../../graphql/generated';
import { getAppstoreMock } from '../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../__tests__/fixtures-context';
import { testIosAppCredentialsWithBuildCredentialsQueryResult } from '../../__tests__/fixtures-ios';
Expand All @@ -9,13 +10,32 @@ import IosCredentialsProvider from '../IosCredentialsProvider';
import { getAppLookupParamsFromContext } from '../actions/new/BuildCredentialsUtils';

jest.mock('fs');
jest.mock('../validators/validateProvisioningProfile', () => ({
validateProvisioningProfileAsync: async (
_ctx: any,
_app: any,
buildCredentials: Partial<IosAppBuildCredentialsFragment> | null
): Promise<boolean> => {
return !!(
buildCredentials &&
buildCredentials.distributionCertificate &&
buildCredentials.provisioningProfile
);
},
}));

const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
beforeAll(() => {
console.log = jest.fn();
console.error = jest.fn();
console.warn = jest.fn();
});
afterAll(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
});

beforeEach(() => {
Expand All @@ -27,7 +47,7 @@ describe(IosCredentialsProvider, () => {
describe('remote credentials', () => {
it('throws an error is credentials do not exist', async () => {
const ctx = createCtxMock({
nonInteractive: false,
nonInteractive: true,
appStore: getAppstoreMock(),
projectDir: '/app',
newIos: {
Expand All @@ -46,22 +66,28 @@ describe(IosCredentialsProvider, () => {
projectName: appLookupParams.projectName,
},
distribution: 'store',
skipCredentialsCheck: true,
});

await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError();
await expect(provider.getCredentialsAsync(CredentialsSource.REMOTE)).rejects.toThrowError(
/Credentials are not set up/
);
});

it('returns credentials if they exist', async () => {
const ctx = createCtxMock({
nonInteractive: false,
nonInteractive: true,
appStore: getAppstoreMock(),
projectDir: '/app',
newIos: {
...getNewIosApiMockWithoutCredentials(),
getIosAppCredentialsWithBuildCredentialsAsync: jest.fn(
() => testIosAppCredentialsWithBuildCredentialsQueryResult
),
getDistributionCertificateForAppAsync: jest.fn(
() =>
testIosAppCredentialsWithBuildCredentialsQueryResult.iosAppBuildCredentialsArray[0]
.distributionCertificate
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx);
Expand All @@ -75,7 +101,6 @@ describe(IosCredentialsProvider, () => {
projectName: appLookupParams.projectName,
},
distribution: 'store',
skipCredentialsCheck: true,
});

const buildCredentials =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';

import {
IosDistributionType as GraphQLIosDistributionType,
Expand All @@ -26,10 +27,19 @@ interface Options {
skipCredentialsCheck?: boolean;
}

export class SetupBuildCredentials implements Action {
interface IosAppBuildCredentials {
iosDistributionType: GraphQLIosDistributionType;
provisioningProfile: string;
distributionCertificate: {
certificateP12: string;
certificatePassword: string;
};
}

export class SetupBuildCredentials implements Action<IosAppBuildCredentials> {
constructor(private options: Options) {}

async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
async runAsync(manager: CredentialsManager, ctx: Context): Promise<IosAppBuildCredentials> {
const { app } = this.options;

await ctx.bestEffortAppStoreAuthenticateAsync();
Expand All @@ -46,12 +56,21 @@ export class SetupBuildCredentials implements Action {
}
try {
const buildCredentials = await this.setupBuildCredentials(ctx);

const appInfo = `@${app.account.name}/${app.projectName} (${app.bundleIdentifier})`;
displayProjectCredentials(app, buildCredentials);
Log.newLine();
Log.log(chalk.green(`All credentials are ready to build ${appInfo}`));
Log.newLine();
return {
iosDistributionType: buildCredentials.iosDistributionType,
provisioningProfile: nullthrows(buildCredentials.provisioningProfile?.provisioningProfile),
distributionCertificate: {
certificateP12: nullthrows(buildCredentials.distributionCertificate?.certificateP12),
certificatePassword: nullthrows(
buildCredentials.distributionCertificate?.certificatePassword
),
},
};
} catch (error) {
Log.error('Failed to setup credentials.');
throw error;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IosDistributionType, IosEnterpriseProvisioning } from '@expo/eas-json';
import nullthrows from 'nullthrows';

import {
Expand All @@ -17,6 +16,17 @@ import { Context } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';

export async function getAllBuildCredentialsAsync(
ctx: Context,
app: AppLookupParams
): Promise<IosAppBuildCredentialsFragment[]> {
const appCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(app, {});
if (!appCredentials) {
return [];
}
return appCredentials.iosAppBuildCredentialsArray;
}

export async function getBuildCredentialsAsync(
ctx: Context,
app: AppLookupParams,
Expand Down Expand Up @@ -94,20 +104,3 @@ export function getAppLookupParamsFromContext(ctx: Context): AppLookupParams {

return { account, projectName, bundleIdentifier };
}

export function resolveDistributionType(
distribution: IosDistributionType,
enterpriseProvisioning?: IosEnterpriseProvisioning
): GraphQLIosDistributionType {
if (distribution === 'internal') {
if (enterpriseProvisioning === 'adhoc') {
return GraphQLIosDistributionType.AdHoc;
} else if (enterpriseProvisioning === 'universal') {
return GraphQLIosDistributionType.Enterprise;
} else {
return GraphQLIosDistributionType.AdHoc;
}
} else {
return GraphQLIosDistributionType.AppStore;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ export class SetupDistributionCertificate {
if (!currentCertificate) {
return false;
}

const now = new Date();
if (
now < new Date(currentCertificate.validityNotBefore) ||
now > new Date(currentCertificate.validityNotAfter)
) {
return false;
}

if (!ctx.appStore.authCtx) {
Log.warn(
"Skipping Distribution Certificate validation on Apple Servers because we aren't authenticated."
);
return true;
}

const validCertSerialNumbers = (await this.getValidDistCertsAsync(ctx)).map(
i => i.serialNumber
);
Expand Down
Loading

0 comments on commit e1de210

Please sign in to comment.