Skip to content

Commit

Permalink
Revert to user auth.
Browse files Browse the repository at this point in the history
  • Loading branch information
erickoledadevrel committed Mar 6, 2024
1 parent 0b6999d commit bd770dd
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 57 deletions.
69 changes: 69 additions & 0 deletions bigquery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# BigQuery Pack

This BigQuery Pack can't be published to the Coda gallery due to Google's policies around sensitive scopes. Although it's possible to request Google Cloud scopes during the OAuth flow, Google won't approve those scopes for a public project. Therefore to use this Pack you must deploy your own copy of it with your own OAuth credentials.

## Setup

### Pack

1. Clone this repository.

```sh
git clone https://github.com/erickoledadevrel/packs.git
```

1. Install the dependencies.

```sh
npm install
```

1. Register an API token.

```sh
npx coda register
```

1. Create the Pack.

```sh
npx coda create bigquery/pack.ts --name "BigQuery"
```

1. Upload the Pack.

```
npx coda upload bigquery/pack.ts
```

### OAuth

1. Open the Pack in the Pack Studio and navigate to the **Settings** tab.
1. Click **Add OAuth credentials**.
1. Copy the value shown for **Redirect URL**.
1. In a new tab, open the [Google Cloud Console](https://console.cloud.google.com/).
1. Create or select a project to use with the Pack.
1. Navigate to **APIs & Services > OAuth consent screen**.
1. Select the **User Type** value **Internal**, and click **Create**.

If you aren't part of a Google Workspace organization you must select **External**. You should keep the project in **Testing** mode, and add yourself as a test user.
1. Enter a value for all required fields and click **Save and continue**.
1. Click **Add or remove scopes**, and under **Manually add scopes** enter the following:
```
profile
https://www.googleapis.com/auth/bigquery.readonly
https://www.googleapis.com/auth/cloudplatformprojects.readonly
```
1. Click **Add to table**, then **Update**, and finally **Save and continue**.
1. Navigate to the **Credentials** tab.
1. Click **+ Create Credentials > OAuth client ID**.
1. For **Application type** select **Web application**.
1. Under **Authorized redirect URIs** click **+ Add URI**.
1. Paste the URL you copied earlier from the Pack Studio.
1. Click the **Create** button.
1. Copy the **Client ID** and **Client secret**.
1. Back in the Pack Studio OAuth credentials dialog, paste the client ID and secret into the corresponding text boxes.
1. Click **Save**.
44 changes: 0 additions & 44 deletions bigquery/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as coda from "@codahq/packs-sdk";
import * as rs from "jsrsasign";
import { onError } from "./helpers";
const ServiceAccountKey = require("./service-account.json");

const QueryPageSize = 100;
const QueryTimeoutMs = 30 * 1000;
Expand All @@ -14,9 +12,6 @@ export class BigQueryApi {
constructor(context: coda.ExecutionContext, projectId: string) {
this.context = context;
this.projectId = projectId;
this.accessTokenPromise = this.getAccessToken(context, [
"https://www.googleapis.com/auth/bigquery.readonly",
]);
}

async runQuery(query: string, dryRun = false, parameters?) {
Expand Down Expand Up @@ -55,49 +50,10 @@ export class BigQueryApi {
return response.body;
}

private async getAccessToken(context: coda.ExecutionContext, scopes: string[]) {
// Extract data from the service account key JSON.
let {private_key_id, client_email, private_key, token_uri} = ServiceAccountKey;

// Construct the JWT header and payload.
let algorithm = "RS256";
let issued_at = Date.now() / 1000;
let expires_at = issued_at + 3600;
let header = JSON.stringify({
alg: algorithm,
typ: "JWT",
kid: private_key_id,
});
let payload = JSON.stringify({
iss: client_email,
aud: token_uri,
exp: expires_at,
iat: issued_at,
sub: client_email,
scope: scopes.join(" "),
});

// Generate the signed JWT.
let jwt = rs.jws.JWS.sign(algorithm, header, payload, rs.KEYUTIL.getKey(private_key));

// Use the JWT to fetch an ID token from the token endpoint.
let response = await context.fetcher.fetch({
method: "POST",
url: token_uri,
form: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
}
});
let data = response.body;
return data.access_token;
}

private async makeRequest(request: coda.FetchRequest) {
if (request.url?.startsWith("/")) {
request.url = coda.joinUrl("https://www.googleapis.com/bigquery/v2/projects", this.projectId, request.url);
}
request.headers["Authorization"] = "Bearer " + await this.accessTokenPromise;
try {
return await this.context.fetcher.fetch(request);
} catch (error) {
Expand Down
10 changes: 6 additions & 4 deletions bigquery/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as coda from "@codahq/packs-sdk";
const crypto = require("crypto");
import * as crypto from "crypto";
import { ObjectTypeKey } from "./schemas";
import { BaseQueryObjectSchema } from "./schemas";

export function getProjectId(context: coda.ExecutionContext) {
return context.endpoint.split("#").pop();
}

export function formatObjectValue(obj, schema) {
let result: Record<string, any> = {};
let fieldValues = obj.f;
Expand Down Expand Up @@ -90,11 +94,9 @@ export function randomId() {
}

export function onError(error: Error) {
console.log("onError");
console.error(error)
if (coda.StatusCodeError.isStatusCodeError(error) && error.statusCode != 401 && error.body?.error?.message) {
console.log(JSON.stringify(error.body));
let message = error.body.error.message;
console.log(message);
if (message.includes("CloudRegion")) {
console.log("match");
message = "Invalid project ID, or access hasn't been granted.";
Expand Down
71 changes: 62 additions & 9 deletions bigquery/pack.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,67 @@
import * as coda from "@codahq/packs-sdk";
import { randomId, getSchema, formatObjectValue, getHash, maybeParseJsonList } from "./helpers";
import { randomId, getSchema, formatObjectValue, getHash, maybeParseJsonList, getProjectId } from "./helpers";
import { BigQueryApi } from "./api";
import { BaseQueryRowSchema, RowIdKey, RowIndexKey } from "./schemas";

export const pack = coda.newPack();

const ListProjectsPageSize = 100;
const OneDaySecs = 24 * 60 * 60;

pack.addNetworkDomain("googleapis.com");

pack.setUserAuthentication({
type: coda.AuthenticationType.OAuth2,
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
scopes: [
"profile",
"https://www.googleapis.com/auth/bigquery.readonly",
"https://www.googleapis.com/auth/cloudplatformprojects.readonly",
],
additionalParams: {
access_type: "offline",
prompt: "consent",
},
postSetup: [
{
type: coda.PostSetupType.SetEndpoint,
name: "SelectProject",
description: "Select the Google Cloud Console project to bill the queries to.",
getOptions: async function (context) {
let url = coda.withQueryParams("https://cloudresourcemanager.googleapis.com/v1/projects", {
filter: "lifecycleState:ACTIVE",
pageSize: ListProjectsPageSize,
});
let response = await context.fetcher.fetch({
method: "GET",
url: url,
});
let data = response.body;
return data.projects.map(project => {
return {
display: `${project.name} (${project.projectId})`,
value: `https://www.googleapis.com/#${project.projectId}`,
};
});
},
},
],
getConnectionName: async function (context) {
if (!context.endpoint) {
return "Incomplete";
}
let projectId = getProjectId(context);
let response = await context.fetcher.fetch({
method: "GET",
url: "https://www.googleapis.com/oauth2/v1/userinfo",
cacheTtlSecs: OneDaySecs,
});
let user = response.body;
return `${projectId} (${user.name})`;
},
});

pack.addDynamicSyncTable({
name: "Query",
description: "Run a query and load the results into a table.",
Expand All @@ -23,7 +78,8 @@ pack.addDynamicSyncTable({
return "Query results";
},
getSchema: async function (context, _, args) {
let { projectId, query, parameters } = args;
let { query, parameters } = args;
let projectId = getProjectId(context);
parameters = maybeParseJsonList(parameters);
if (!query) {
return BaseQueryRowSchema;
Expand All @@ -39,11 +95,6 @@ pack.addDynamicSyncTable({
name: "SyncQuery",
description: "Syncs the data.",
parameters: [
coda.makeParameter({
type: coda.ParameterType.String,
name: "projectId",
description: "The ID of the Google Cloud project to bill the queries to.",
}),
coda.makeParameter({
type: coda.ParameterType.String,
name: "query",
Expand All @@ -55,7 +106,8 @@ pack.addDynamicSyncTable({
description: "Which columns, if their values are combined, will form a unique ID for the row.",
optional: true,
autocomplete: async function (context, _, args) {
let { projectId, query } = args;
let { query } = args;
let projectId = getProjectId(context);
if (!query) {
return [];
}
Expand All @@ -72,7 +124,8 @@ pack.addDynamicSyncTable({
}),
],
execute: async function (args, context) {
let [projectId, query, uniqueColumns, parameters] = args;
let [query, uniqueColumns, parameters] = args;
let projectId = getProjectId(context);
parameters = maybeParseJsonList(parameters);

let jobId = context.sync.continuation?.jobId as string;
Expand Down

0 comments on commit bd770dd

Please sign in to comment.