Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: detect if org is a devhub at auth time #560

Merged
merged 3 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/org/authInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,11 @@ export class AuthInfo extends AsyncOptionalCreatable<AuthInfo.Options> {
}
}

authConfig.isDevHub = await this.determineIfDevHub(
ensureString(authConfig.instanceUrl),
ensureString(authConfig.accessToken)
);

// Update the auth fields WITH encryption
this.update(authConfig);
}
Expand Down Expand Up @@ -1065,6 +1070,30 @@ export class AuthInfo extends AsyncOptionalCreatable<AuthInfo.Options> {
}
throw new SfError(errorMsg);
}

/**
* Returns `true` if the org is a Dev Hub.
*
* Check access to the ScratchOrgInfo object to determine if the org is a dev hub.
*/
private async determineIfDevHub(instanceUrl: string, accessToken: string): Promise<boolean> {
peternhale marked this conversation as resolved.
Show resolved Hide resolved
// Make a REST call for the SratchOrgInfo obj directly. Normally this is done via a connection
// but we don't want to create circular dependencies or lots of snowflakes
// within this file to support it.
const apiVersion = 'v51.0'; // hardcoding to v51.0 just for this call is okay.
const instance = ensure(instanceUrl);
const baseUrl = new SfdcUrl(instance);
const scratchOrgInfoUrl = `${baseUrl}/services/data/${apiVersion}/query?q=SELECT%20Id%20FROM%20ScratchOrgInfo%20limit%201`;
const headers = Object.assign({ Authorization: `Bearer ${accessToken}` }, SFDX_HTTP_HEADERS);

try {
await new Transport().httpRequest({ url: scratchOrgInfoUrl, method: 'GET', headers });
peternhale marked this conversation as resolved.
Show resolved Hide resolved
return true;
} catch (err) {
/* Not a dev hub */
return false;
}
}
}

export namespace AuthInfo {
Expand Down
35 changes: 25 additions & 10 deletions test/unit/org/authInfoTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@ describe('AuthInfo', () => {
refreshToken: testMetadata.refreshToken,
clientId: testMetadata.clientId,
clientSecret: testMetadata.clientSecret,
isDevHub: false,
};
expect(authInfoUpdate.secondCall.args[0]).to.deep.equal(expectedAuthConfig);
});
Expand Down Expand Up @@ -603,6 +604,7 @@ describe('AuthInfo', () => {
orgId: authResponse.id.split('/')[0],
loginUrl: jwtConfig.loginUrl,
privateKey: jwtConfig.privateKey,
isDevHub: false,
};
expect(authInfoUpdate.firstCall.args[0]).to.deep.equal(expectedAuthConfig);
});
Expand Down Expand Up @@ -790,6 +792,7 @@ describe('AuthInfo', () => {
refreshToken: refreshTokenConfig.refreshToken,
clientId: testMetadata.defaultConnectedAppInfo.clientId,
clientSecret: testMetadata.defaultConnectedAppInfo.clientSecret,
isDevHub: false,
username,
};
expect(authInfoUpdate.firstCall.args[0]).to.deep.equal(expectedAuthConfig);
Expand Down Expand Up @@ -854,6 +857,7 @@ describe('AuthInfo', () => {
refreshToken: refreshTokenConfig.refreshToken,
clientId: testMetadata.defaultConnectedAppInfo.clientId,
clientSecret: testMetadata.defaultConnectedAppInfo.clientSecret,
isDevHub: false,
username,
};
expect(authInfoUpdate.firstCall.args[0]).to.deep.equal(expectedAuthConfig);
Expand Down Expand Up @@ -918,6 +922,7 @@ describe('AuthInfo', () => {
refreshToken: refreshTokenConfig.refreshToken,
clientId: refreshTokenConfig.clientId,
clientSecret: refreshTokenConfig.clientSecret,
isDevHub: false,
username,
};
expect(authInfoUpdate.firstCall.args[0]).to.deep.equal(expectedAuthConfig);
Expand Down Expand Up @@ -974,7 +979,9 @@ describe('AuthInfo', () => {
.onFirstCall()
.returns(Promise.resolve(userInfoResponseBody))
.onSecondCall()
.returns(Promise.resolve(userResponseBody));
.returns(Promise.resolve(userResponseBody))
.onThirdCall()
.throws();

// Create the refresh token AuthInfo instance
const authInfo = await AuthInfo.create({ oauth2Options: authCodeConfig });
Expand Down Expand Up @@ -1018,6 +1025,7 @@ describe('AuthInfo', () => {
orgId: authResponse.id.split('/')[0],
loginUrl: authCodeConfig.loginUrl,
refreshToken: authResponse.refresh_token,
isDevHub: false,
// These need to be passed in by the consumer. Since they are not, they will show up as undefined.
// In a non-test environment, the exchange will fail because no clientId is supplied.
clientId: undefined,
Expand Down Expand Up @@ -1277,6 +1285,7 @@ describe('AuthInfo', () => {
orgId: authResponse.id.split('/')[0],
loginUrl: refreshTokenConfig.loginUrl,
refreshToken: refreshTokenConfig.refreshToken,
isDevHub: false,
// clientId and clientSecret are now stored in the file, even the defaults.
// We just hard code the legacy values here to ensure old auth files will still work.
clientId: 'SalesforceDevelopmentExperience',
Expand Down Expand Up @@ -1348,18 +1357,24 @@ describe('AuthInfo', () => {

it('should path.resolve jwtkeyfilepath', async () => {
const pathSpy = $$.SANDBOX.spy(pathImport, 'resolve');
const context = {
update: () => {},
buildJwtConfig: () => {},
isTokenOptions: () => false,
getUsername: () => '',
privateKeyFile: 'authInfoTest/jwt/server.key',
options: {},
};

await AuthInfo.prototype['initAuthOptions'].call(context, context);
authInfoBuildJwtConfig.restore();
stubMethod($$.SANDBOX, AuthInfo.prototype, 'buildJwtConfig').resolves({
instanceUrl: '',
accessToken: '',
});
stubMethod($$.SANDBOX, AuthInfo.prototype, 'determineIfDevHub').resolves(false);

await AuthInfo.create({
username: 'cristiand391',
oauth2Options: {
clientId: '1234',
privateKeyFile: 'authInfoTest/jwt/server.key',
},
});

expect(pathSpy.calledOnce).to.be.true;
expect(pathSpy.args[0][0]).to.equal('authInfoTest/jwt/server.key');
});

it('should call the callback with OrgDataNotAvailableError when AuthInfo.init() fails', async () => {
Expand Down
2 changes: 2 additions & 0 deletions test/unit/org/orgTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,8 @@ describe('Org Tests', () => {
return Promise.resolve(responseBody);
});

stubMethod($$.SANDBOX, AuthInfo.prototype, 'determineIfDevHub').resolves(false);

for (const user of users) {
userAuthResponse = {
// eslint-disable-next-line camelcase
Expand Down
7 changes: 6 additions & 1 deletion test/unit/org/userTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ describe('User Tests', () => {
});

stubMethod($$.SANDBOX, AuthInfo.prototype, 'buildRefreshTokenConfig').callsFake(() => {
return {};
return {
instanceUrl: '',
accessToken: '',
};
});

stubMethod($$.SANDBOX, AuthInfo.prototype, 'determineIfDevHub').resolves(false);

refreshSpy = stubMethod($$.SANDBOX, Org.prototype, 'refreshAuth').resolves({});
});

Expand Down