Skip to content
This repository has been archived by the owner on Jun 25, 2024. It is now read-only.

Add support for token-based authentication #446

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 3 additions & 7 deletions src/getStepStartStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ function validateInvocationConfig(
const { instance } = context;
const { config } = instance;

if (!config.serviceAccountKeyFile) {
if (!config.serviceAccountKeyFile && !config.accessToken) {
throw new IntegrationValidationError(
'Missing a required integration config value {serviceAccountKeyFile}',
'Missing a required integration config value {serviceAccountKeyFile} or {accessToken}',
);
}
}
Expand Down Expand Up @@ -200,18 +200,14 @@ export default async function getStepStartStates(
projectId: config.projectId,
configureOrganizationProjects: config.configureOrganizationProjects,
organizationId: config.organizationId,
serviceAccountKeyEmail: config.serviceAccountKeyConfig.client_email,
serviceAccountKeyProjectId: config.serviceAccountKeyConfig.project_id,
folderId: config.folderId,
},
'Starting integration with config',
);

logger.publishEvent({
name: 'integration_config',
description: `Starting Google Cloud integration with service account (email=${
config.serviceAccountKeyConfig.client_email
}, configureOrganizationProjects=${!!config.configureOrganizationProjects})`,
description: `Starting Google Cloud integration (project=${config.projectId}, configureOrganizationProjects=${!!config.configureOrganizationProjects})`,
});

const masterOrgInstance = isMasterOrganizationInstance(config);
Expand Down
78 changes: 62 additions & 16 deletions src/google-cloud/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IntegrationConfig } from '../types';
import { google } from 'googleapis';
import { CredentialBody, BaseExternalAccountClient } from 'google-auth-library';
import { CredentialBody, BaseExternalAccountClient, OAuth2Client, UserRefreshClient } from 'google-auth-library';
import { GaxiosResponse } from 'gaxios';
import {
IntegrationProviderAuthorizationError,
Expand Down Expand Up @@ -54,37 +54,83 @@ export async function iterateApi<T>(
}

export class Client {
readonly projectId: string;
sourceProjectId: string;
readonly projectId?: string;
readonly organizationId?: string;
readonly folderId?: string;

private credentials: CredentialBody;
private serviceAccountKeyConfig?: CredentialBody;
private accessTokenConfig?: string;

private auth: BaseExternalAccountClient;
private readonly onRetry?: (err: any) => void;

constructor({ config, projectId, organizationId, onRetry }: ClientOptions) {
this.projectId =
projectId ||
config.projectId ||
config.serviceAccountKeyConfig.project_id;
config.serviceAccountKeyConfig?.project_id;
this.organizationId = organizationId || config.organizationId;
this.credentials = {
client_email: config.serviceAccountKeyConfig.client_email,
private_key: config.serviceAccountKeyConfig.private_key,
};
if (config.serviceAccountKeyConfig) {
this.serviceAccountKeyConfig = {
client_email: config.serviceAccountKeyConfig.client_email,
private_key: config.serviceAccountKeyConfig.private_key,
};
this.sourceProjectId = config.serviceAccountKeyConfig.project_id;
} else if (config.accessToken) {
this.accessTokenConfig = config.accessToken;
} else {
throw new Error(`Invalid credentials`);
}
this.folderId = config.folderId;
this.onRetry = onRetry;
}

private async getClient(): Promise<BaseExternalAccountClient> {
const auth = new google.auth.GoogleAuth({
credentials: this.credentials,
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});

const client = (await auth.getClient()) as BaseExternalAccountClient;
await client.getAccessToken();
return client;
if (this.serviceAccountKeyConfig) {
const auth = new google.auth.GoogleAuth({
credentials: this.serviceAccountKeyConfig,
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});

const client = (await auth.getClient()) as BaseExternalAccountClient;
await client.getAccessToken();
return client;
} else if (this.accessTokenConfig) {
const oAuth2Client = new OAuth2Client();
const token = this.accessTokenConfig;
oAuth2Client.credentials = {
access_token: token,
}

/*
* Users of the google-cloud Client sometimes require knowledge of the
* project ID that the service account belongs to. For example, the service
* usage step uses this to avoid scanning excess services when they are not
* enabled in the main project. With a service account key, the project ID
* is embedded in the JSON itself. With an access token, we must interrogate
* Google Cloud APIs to determine the project ID. Specifically we call:
* - oauth2.googleapis.com/tokeninfo
* - iam.googleapis.com/v1/projects/-/serviceAccounts/{serviceAccount}
* - cloudresourcemanager.googleapis.com/v1/projects/{projectId}
*/
const tokenInfo = await oAuth2Client.getTokenInfo(token);
const iam = google.iam('v1');
const crm = google.cloudresourcemanager('v1');

const sa = await iam.projects.serviceAccounts.get({ access_token: token, name: `projects/-/serviceAccounts/${tokenInfo.azp}` });
const project = await crm.projects.get({ access_token: token, projectId: sa.data.projectId || undefined });
if (!project.data.projectNumber) {
throw new Error(
`Could not find project number for service account ${tokenInfo.email}`,
);
}
this.sourceProjectId = project.data.projectNumber;

return oAuth2Client as UserRefreshClient;
} else {
throw new Error(`Invalid credentials`);
}
}

async getAuthenticatedServiceClient(): Promise<BaseExternalAccountClient> {
Expand Down
2 changes: 1 addition & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async function validateInvocationInvalidConfigTest({
expect(err instanceof IntegrationValidationError).toBe(true);
expect(err.message).toEqual(
expectedErrorMessage ||
'Missing a required integration config value {serviceAccountKeyFile}',
'Missing a required integration config value {serviceAccountKeyFile} or {accessToken}',
);
failed = true;
}
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export const invocationConfig: IntegrationInvocationConfig<IntegrationConfig> =
type: 'string',
mask: true,
},
accessToken: {
type: 'string',
mask: true,
},
projectId: {
type: 'string',
},
Expand Down
19 changes: 9 additions & 10 deletions src/steps/enablement.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StepStartState } from '@jupiterone/integration-sdk-core';
import { ServiceUsageName } from '../google-cloud/types';
import { IntegrationConfig } from '../types';
import { collectEnabledServicesForProject } from './service-usage/client';
import { collectEnabledServicesForProject, ServiceUsageClient } from './service-usage/client';

export interface EnabledServiceData {
// Enabled APIs in the Google Cloud Project that the Service Account used to authenticate with resides.
Expand Down Expand Up @@ -32,12 +32,18 @@ export interface EnabledServiceData {
export async function getEnabledServiceNames(
config: IntegrationConfig,
): Promise<EnabledServiceData> {

// look up main project ID from authenticated client
const client = new ServiceUsageClient({ config });
await client.getAuthenticatedServiceClient();
const mainProjectId = client.sourceProjectId;

const targetProjectId = config.projectId;
const mainProjectId = config.serviceAccountKeyConfig.project_id;
const enabledServiceData: EnabledServiceData = {};

const mainProjectEnabledServices = await getMainProjectEnabledServices(
const mainProjectEnabledServices = await collectEnabledServicesForProject(
config,
mainProjectId,
);
enabledServiceData.mainProjectEnabledServices = mainProjectEnabledServices;

Expand Down Expand Up @@ -71,13 +77,6 @@ export async function getEnabledServiceNames(
return enabledServiceData;
}

export async function getMainProjectEnabledServices(config: IntegrationConfig) {
return await collectEnabledServicesForProject(
config,
config.serviceAccountKeyConfig.project_id,
);
}

export function createStepStartState(
enabledServiceNames: string[],
primaryServiceName: ServiceUsageName,
Expand Down
2 changes: 1 addition & 1 deletion src/steps/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function fetchStorageBuckets(

const bucketEntity = createCloudStorageBucketEntity({
data: bucket,
projectId: config.serviceAccountKeyConfig.project_id,
projectId: bucket.projectNumber || client.sourceProjectId,
bucketPolicy,
publicAccessPreventionPolicy,
});
Expand Down
5 changes: 3 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export type IntegrationStepContext =
IntegrationStepExecutionContext<IntegrationConfig>;

export interface SerializedIntegrationConfig extends IntegrationInstanceConfig {
serviceAccountKeyFile: string;
serviceAccountKeyFile?: string;
accessToken?: string;
organizationId?: string;
/**
* The project ID that this integration should target for ingestion. This
Expand All @@ -26,7 +27,7 @@ export interface SerializedIntegrationConfig extends IntegrationInstanceConfig {
}

export interface IntegrationConfig extends SerializedIntegrationConfig {
serviceAccountKeyConfig: ParsedServiceAccountKeyFile;
serviceAccountKeyConfig?: ParsedServiceAccountKeyFile;
// HACK - used to prevent binding step ingestion for large accounts. Think twice before using.
markBindingStepsAsPartial?: boolean;
}
6 changes: 5 additions & 1 deletion src/utils/integrationConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import { IntegrationConfig, SerializedIntegrationConfig } from '../types';
import { parseServiceAccountKeyFile } from './parseServiceAccountKeyFile';

/**
* The incoming Google Cloud config includes a `serviceAccountKeyFile` property
* The incoming Google Cloud config may include a `serviceAccountKeyFile` property
* that we need to deserialize. We will override the value of the
* `IntegrationExecutionContext` `config` with the return value of this function,
* so that the deserialized config can be used throughout all of the steps.
*/
export function deserializeIntegrationConfig(
serializedIntegrationConfig: SerializedIntegrationConfig,
): IntegrationConfig {
if (!serializedIntegrationConfig.serviceAccountKeyFile) {
return serializedIntegrationConfig;
}

const parsedServiceAccountKeyFile = parseServiceAccountKeyFile(
serializedIntegrationConfig.serviceAccountKeyFile,
);
Expand Down
2 changes: 1 addition & 1 deletion src/utils/maybeDefaultProjectIdOnEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ export function maybeDefaultProjectIdOnEntity(context, entity: Entity): Entity {
projectId:
entity.projectId ??
context.instance.config.projectId ??
context.instance.config.serviceAccountKeyConfig.project_id,
context.instance.config.serviceAccountKeyConfig?.project_id,
};
}