google auth working

This commit is contained in:
Martin McKeaveney 2022-01-06 08:08:54 +00:00
parent a09fabc54b
commit c2d97b9449
13 changed files with 223 additions and 51 deletions

View File

@ -46,6 +46,7 @@ exports.strategyFactory = async function (
clientID: config.clientID, clientID: config.clientID,
clientSecret: config.clientSecret, clientSecret: config.clientSecret,
callbackURL: callbackUrl, callbackURL: callbackUrl,
store: true,
}, },
verify verify
) )

View File

@ -0,0 +1,44 @@
<script>
import { ActionButton } from "@budibase/bbui"
import GoogleLogo from "assets/google-logo.png"
import { store } from "builderStore"
import { auth } from "stores/portal"
export let preAuthStep
$: tenantId = $auth.tenantId
</script>
<ActionButton
on:click={async () => {
// TODO: can probably remove
const datasourceId = await preAuthStep()
window.open(
`/api/global/auth/${tenantId}/google2?datasourceId=${datasourceId}&appId=${$store.appId}`,
"_blank"
)
}}
>
<div class="inner">
<img src={GoogleLogo} alt="google icon" />
<p>Sign in with Google</p>
</div>
</ActionButton>
<style>
.inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--spacing-xs);
padding-bottom: var(--spacing-xs);
}
.inner img {
width: 18px;
margin: 3px 10px 3px 3px;
}
.inner p {
margin: 0;
}
</style>

View File

@ -32,6 +32,7 @@
plus: selected.plus, plus: selected.plus,
config, config,
schema: selected.datasource, schema: selected.datasource,
auth: selected.auth,
} }
} }

View File

@ -6,6 +6,7 @@
import { datasources, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { IntegrationNames } from "constants" import { IntegrationNames } from "constants"
import cloneDeep from "lodash/cloneDeepWith" import cloneDeep from "lodash/cloneDeepWith"
import GoogleButton from "../_components/GoogleButton.svelte"
export let integration export let integration
export let modal export let modal
@ -33,11 +34,14 @@
return datasource return datasource
} }
async function saveDatasource() { async function saveDatasource(fetchSchema) {
const datasource = prepareData() const datasource = prepareData()
try { try {
// Create datasource // Create datasource
const resp = await datasources.save(datasource, datasource.plus) const resp = await datasources.save(
datasource,
fetchSchema ?? datasource.plus
)
// update the tables incase data source plus // update the tables incase data source plus
await tables.fetch() await tables.fetch()
@ -48,7 +52,7 @@
name: resp.name, name: resp.name,
source: resp.source, source: resp.source,
}) })
return true return resp._id
} catch (err) { } catch (err) {
notifications.error(`Error saving datasource: ${err}`) notifications.error(`Error saving datasource: ${err}`)
return false return false
@ -71,8 +75,11 @@
>Connect your database to Budibase using the config below. >Connect your database to Budibase using the config below.
</Body> </Body>
</Layout> </Layout>
{#if config.auth?.type === "google"}
<GoogleButton preAuthStep={() => saveDatasource(false)} />
{:else}
<IntegrationConfigForm schema={config.schema} integration={config.config} /> <IntegrationConfigForm schema={config.schema} integration={config.config} />
{/if}
</ModalContent> </ModalContent>
<style> <style>

View File

@ -89,6 +89,7 @@
"download": "8.0.0", "download": "8.0.0",
"fix-path": "3.0.0", "fix-path": "3.0.0",
"fs-extra": "8.1.0", "fs-extra": "8.1.0",
"google-auth-library": "^7.11.0",
"google-spreadsheet": "^3.2.0", "google-spreadsheet": "^3.2.0",
"jimp": "0.16.1", "jimp": "0.16.1",
"joi": "17.2.1", "joi": "17.2.1",

View File

@ -75,6 +75,10 @@ exports.DataSourceOperation = {
DELETE_TABLE: "DELETE_TABLE", DELETE_TABLE: "DELETE_TABLE",
} }
exports.DatasourceAuthTypes = {
GOOGLE: "google",
}
exports.SortDirection = { exports.SortDirection = {
ASCENDING: "ASCENDING", ASCENDING: "ASCENDING",
DESCENDING: "DESCENDING", DESCENDING: "DESCENDING",

View File

@ -87,6 +87,8 @@ export interface ExtraQueryConfig {
export interface Integration { export interface Integration {
docs: string docs: string
plus?: boolean plus?: boolean
// TODO: use a proper type here
auth: { type: "google" }
description: string description: string
friendlyName: string friendlyName: string
datasource: {} datasource: {}

View File

@ -42,6 +42,8 @@ module.exports = {
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
// environment // environment
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
JEST_WORKER_ID: process.env.JEST_WORKER_ID, JEST_WORKER_ID: process.env.JEST_WORKER_ID,

View File

@ -5,21 +5,37 @@ import {
QueryTypes, QueryTypes,
} from "../definitions/datasource" } from "../definitions/datasource"
import { IntegrationBase } from "./base/IntegrationBase" import { IntegrationBase } from "./base/IntegrationBase"
import { OAuth2Client } from "google-auth-library"
import { GoogleSpreadsheet } from "google-spreadsheet" import { GoogleSpreadsheet } from "google-spreadsheet"
import { DatasourcePlus } from "./base/datasourcePlus" import { DatasourcePlus } from "./base/datasourcePlus"
import { Table } from "../definitions/common" import { Table } from "../definitions/common"
import { buildExternalTableId } from "./utils" import { buildExternalTableId } from "./utils"
import { DataSourceOperation, FieldTypes } from "../constants" import {
DataSourceOperation,
FieldTypes,
DatasourceAuthTypes,
} from "../constants"
import env from "../environment"
import CouchDB from "../db"
import { timeStamp } from "console"
module GoogleSheetsModule { module GoogleSheetsModule {
interface GoogleSheetsConfig { interface GoogleSheetsConfig {
spreadsheetId: string spreadsheetId: string
clientEmail: string auth: OAuthClientConfig
privateKey: string }
interface OAuthClientConfig {
appId: string
accessToken: string
refreshToken: string
} }
const SCHEMA: Integration = { const SCHEMA: Integration = {
plus: true, plus: true,
auth: {
type: "google",
},
docs: "https://developers.google.com/sheets/api/quickstart/nodejs", docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
description: description:
"Create and collaborate on online spreadsheets in real-time and from any device. ", "Create and collaborate on online spreadsheets in real-time and from any device. ",
@ -29,21 +45,13 @@ module GoogleSheetsModule {
type: DatasourceFieldTypes.STRING, type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
clientEmail: {
type: DatasourceFieldTypes.STRING,
required: true,
},
privateKey: {
type: DatasourceFieldTypes.LONGFORM,
required: true,
},
}, },
query: { query: {
create: { create: {
type: QueryTypes.FIELDS, type: QueryTypes.FIELDS,
fields: { fields: {
sheet: { sheet: {
type: "string", type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
row: { row: {
@ -56,7 +64,7 @@ module GoogleSheetsModule {
type: QueryTypes.FIELDS, type: QueryTypes.FIELDS,
fields: { fields: {
sheet: { sheet: {
type: "string", type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
}, },
@ -65,11 +73,11 @@ module GoogleSheetsModule {
type: QueryTypes.FIELDS, type: QueryTypes.FIELDS,
fields: { fields: {
sheet: { sheet: {
type: "string", type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
rowIndex: { rowIndex: {
type: "number", type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
row: { row: {
@ -82,11 +90,11 @@ module GoogleSheetsModule {
type: QueryTypes.FIELDS, type: QueryTypes.FIELDS,
fields: { fields: {
sheet: { sheet: {
type: "string", type: DatasourceFieldTypes.STRING,
required: true, required: true,
}, },
rowIndex: { rowIndex: {
type: "number", type: DatasourceFieldTypes.NUMBER,
required: true, required: true,
}, },
}, },
@ -107,12 +115,15 @@ module GoogleSheetsModule {
async connect() { async connect() {
try { try {
await this.client.useServiceAccountAuth({ // Initialise oAuth client
// env var values are copied from service account credentials generated by google // TODO: Move this to auth lib
// see "Authentication" section in docs for more info const oauthClient = new OAuth2Client({
client_email: this.config.clientEmail, clientId: env.GOOGLE_CLIENT_ID,
private_key: this.config.privateKey, clientSecret: env.GOOGLE_CLIENT_SECRET,
}) })
oauthClient.credentials.access_token = this.config.auth.accessToken
oauthClient.credentials.refresh_token = this.config.auth.refreshToken
this.client.useOAuth2Client(oauthClient)
await this.client.loadInfo() await this.client.loadInfo()
} catch (err) { } catch (err) {
console.error("Error connecting to google sheets", err) console.error("Error connecting to google sheets", err)
@ -178,7 +189,9 @@ module GoogleSheetsModule {
} }
if (json.endpoint.operation === DataSourceOperation.DELETE) { if (json.endpoint.operation === DataSourceOperation.DELETE) {
return await this.delete({}) return await this.delete({
// TODO: complete
})
} }
} }

View File

@ -450,7 +450,7 @@ module OracleModule {
}) })
return lastRow.rows return lastRow.rows
} else { } else {
return [{ [ operation.toLowerCase() ]: true }] return [{ [operation.toLowerCase()]: true }]
} }
} }
} }

View File

@ -952,9 +952,9 @@
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/auth@^1.0.30": "@budibase/auth@^1.0.30":
version "1.0.30" version "1.0.34"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.30.tgz#4ddc72518fc2d2a8e72a84acfd7d141d3b5e21b9" resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.34.tgz#f1daad174d494c5baae29ebfb2d0cf7a26d487bb"
integrity sha512-GbYH/SEqzOZPhxOa8gjy42Pl6b+aDZ/xHkcS4oBTzh1n+T45G53LF7WkQyVhxRZXrzh960VSI4QAWnA+ZeK9Sw== integrity sha512-RN5xZVqk4D4GIFoTrm6kv6vxIcAyDgoopsGMYj8dQRCYWb6pvWzCKyDw4o1THXiK8BBD8ctFUE/FhtLXRj8F4w==
dependencies: dependencies:
"@techpass/passport-openidconnect" "^0.3.0" "@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0" aws-sdk "^2.901.0"
@ -1024,10 +1024,10 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/bbui@^1.0.30": "@budibase/bbui@^1.0.34":
version "1.0.30" version "1.0.34"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.30.tgz#16b0a66055539709c71564a01253835e874b6ea3" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.34.tgz#3eba93984cd0075aecbf4ca8451b301e623c7afa"
integrity sha512-eYMXuzwZTMJMybbXLVoBz5rQIfVV86hLugog8TaZbkqFeMgPUgyuM8uHfw6wTQBdPazfgVbTeQK0UzkWMnwGOg== integrity sha512-9mVzIWx4dQ8LIy4ncJaJBVwuTx2LEMqZl1XRr/NAI3rsTv+HIULe+zCUJSCYM2MGoR3qKSU7geIzIuwnRa8k4Q==
dependencies: dependencies:
"@adobe/spectrum-css-workflow-icons" "^1.2.1" "@adobe/spectrum-css-workflow-icons" "^1.2.1"
"@spectrum-css/actionbutton" "^1.0.1" "@spectrum-css/actionbutton" "^1.0.1"
@ -1075,13 +1075,13 @@
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^1.0.30": "@budibase/client@^1.0.30":
version "1.0.30" version "1.0.34"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.30.tgz#a80e87d15722333a5bcfa96b6a14b024fe8574ee" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.34.tgz#2bdac2030eabf780659481fa751cd7fecf1b609f"
integrity sha512-Ac63c+uTj7tMbVU4J8T6oFFGiONd6JABaGG/k+W5XKidvytyMxYSeu/Z5xl6jP/sYb2e+Oevt1vv9xB0tlkARw== integrity sha512-WGEVjwmKYv+B+J6crbia62BNFCzgKHEggsNAlCN3+IB+w9jxS2oiOJYPpYQnsSM2l4j4/Zbu9W30OhenSiB8kQ==
dependencies: dependencies:
"@budibase/bbui" "^1.0.30" "@budibase/bbui" "^1.0.34"
"@budibase/standard-components" "^0.9.139" "@budibase/standard-components" "^0.9.139"
"@budibase/string-templates" "^1.0.30" "@budibase/string-templates" "^1.0.34"
regexparam "^1.3.0" regexparam "^1.3.0"
shortid "^2.2.15" shortid "^2.2.15"
svelte-spa-router "^3.0.5" svelte-spa-router "^3.0.5"
@ -1131,10 +1131,10 @@
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/string-templates@^1.0.30": "@budibase/string-templates@^1.0.30", "@budibase/string-templates@^1.0.34":
version "1.0.30" version "1.0.34"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.30.tgz#c7206687f4f3c1cecfd3334b89a36920edc2d86f" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.34.tgz#7e5f80a26db5d04063c0e78cdf37af88cff772c6"
integrity sha512-C5SyLNSsvfQLxAjoM77Pd03R8R9LFaoAu6CqgyJuO6JmUpAbP5JNBqi+PLmrxZEPJK0tykCHxdSLtaOfIaV+lQ== integrity sha512-HWqXxQBXqVwBgCh3ptczVFxWOLHVUhuTShXCafRDgKAVY3d+LJzKGwiD40/oR/nUqeNtBbHgYWu8Z4gMQQYZhA==
dependencies: dependencies:
"@budibase/handlebars-helpers" "^0.11.7" "@budibase/handlebars-helpers" "^0.11.7"
dayjs "^1.10.4" dayjs "^1.10.4"
@ -3244,9 +3244,9 @@ aws-sdk@^2.767.0:
xml2js "0.4.19" xml2js "0.4.19"
aws-sdk@^2.901.0: aws-sdk@^2.901.0:
version "2.1036.0" version "2.1050.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1036.0.tgz#47f0d0a38b5bfbd4a7a382a6756a6aa7af627aad" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1050.0.tgz#172e80839a3c1bed82e7c42db2fbe7d46fbb90ca"
integrity sha512-K0f4uXL32ZdoPmWiuSQEAC5ae5v7gNmhjzoEB7VonE5E8l2umWsoU0Ahm8WPr14LgsvtkeyBuqBjphbxLz6hIw== integrity sha512-xXY3wAZQyh/d6vk5oClyB3ViFENc1OOrsB/HUu/g+wnMzMLp+ea+OpZwRM5+g90mAiJ1eLOxje0Sgnbe7fP2oA==
dependencies: dependencies:
buffer "4.9.2" buffer "4.9.2"
events "1.1.1" events "1.1.1"
@ -6063,6 +6063,21 @@ google-auth-library@^6.1.3:
jws "^4.0.0" jws "^4.0.0"
lru-cache "^6.0.0" lru-cache "^6.0.0"
google-auth-library@^7.11.0:
version "7.11.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.11.0.tgz#b63699c65037310a424128a854ba7e736704cbdb"
integrity sha512-3S5jn2quRumvh9F/Ubf7GFrIq71HZ5a6vqosgdIu105kkk0WtSqc2jGCRqtWWOLRS8SX3AHACMOEDxhyWAQIcg==
dependencies:
arrify "^2.0.0"
base64-js "^1.3.0"
ecdsa-sig-formatter "^1.0.11"
fast-text-encoding "^1.0.0"
gaxios "^4.0.0"
gcp-metadata "^4.2.0"
gtoken "^5.0.4"
jws "^4.0.0"
lru-cache "^6.0.0"
google-auth-library@~0.10.0: google-auth-library@~0.10.0:
version "0.10.0" version "0.10.0"
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e"
@ -6642,9 +6657,9 @@ ioredis@^4.27.0:
standard-as-callback "^2.1.0" standard-as-callback "^2.1.0"
ioredis@^4.27.1: ioredis@^4.27.1:
version "4.28.1" version "4.28.2"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.1.tgz#c2a7038d6a187e020d7045e11d6a677e8b51f785" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.2.tgz#493ccd5d869fd0ec86c96498192718171f6c9203"
integrity sha512-7gcrUJEcPHWy+eEyq6wIZpXtfHt8crhbc5+z0sqrnHUkwBblXinygfamj+/jx83Qo+2LW3q87Nj2VsuH6BF2BA== integrity sha512-kQ+Iv7+c6HsDdPP2XUHaMv8DhnSeAeKEwMbaoqsXYbO+03dItXt7+5jGQDRyjdRUV2rFJbzg7P4Qt1iX2tqkOg==
dependencies: dependencies:
cluster-key-slot "^1.1.0" cluster-key-slot "^1.1.0"
debug "^4.3.1" debug "^4.3.1"

View File

@ -21,6 +21,7 @@ const {
isMultiTenant, isMultiTenant,
} = require("@budibase/auth/tenancy") } = require("@budibase/auth/tenancy")
const env = require("../../../environment") const env = require("../../../environment")
const CouchDB = require("../../../db")
const ssoCallbackUrl = async (config, type) => { const ssoCallbackUrl = async (config, type) => {
// incase there is a callback URL from before // incase there is a callback URL from before
@ -142,6 +143,80 @@ exports.logout = async ctx => {
ctx.body = { message: "User logged out." } ctx.body = { message: "User logged out." }
} }
/**
* The initial call that google authentication makes to take you to the google login screen.
* On a successful login, you will be redirected to the googleAuth callback route.
*/
exports.googlePreAuth2 = async (ctx, next) => {
const db = getGlobalDB()
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE,
workspace: ctx.query.workspace,
})
// let callbackUrl = await exports.googleCallbackUrl(config)
// TODO: Hardcoded - fix
let callbackUrl = "http://localhost:10000/api/global/auth/google2/callback"
const strategy = await google.strategyFactory(config, callbackUrl)
// TODO: fix
setCookie(
ctx,
{
appId: ctx.query.appId,
datasourceId: ctx.query.datasourceId,
},
"AuthCallback"
)
// TODO: prob update - shouldn't include the google sheets scopes here
return passport.authenticate(strategy, {
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"],
})(ctx, next)
}
exports.googleAuth2 = async (ctx, next) => {
const db = getGlobalDB()
const config = await authPkg.db.getScopedConfig(db, {
type: Configs.GOOGLE,
workspace: ctx.query.workspace,
})
// const callbackUrl = await exports.googleCallbackUrl(config)
const authStateCookie = getCookie(ctx, "AuthCallback")
// TODO: correct callback URL
let callbackUrl = "http://localhost:10000/api/global/auth/google2/callback"
const strategy = await google.strategyFactory(
config,
callbackUrl,
(accessToken, refreshToken, profile, done) => {
clearCookie(ctx, "AuthCallback")
done(null, { accessToken, refreshToken })
}
)
return passport.authenticate(
strategy,
{ successRedirect: "/", failureRedirect: "/error" },
async (err, tokens) => {
// update the DB for the datasource with all the user info
const db = new CouchDB(authStateCookie.appId)
const datasource = await db.get(authStateCookie.datasourceId)
datasource.config = {
auth: {
type: "google",
...tokens,
},
}
await db.put(datasource)
ctx.redirect(
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
)
}
)(ctx, next)
}
/** /**
* The initial call that google authentication makes to take you to the google login screen. * The initial call that google authentication makes to take you to the google login screen.
* On a successful login, you will be redirected to the googleAuth callback route. * On a successful login, you will be redirected to the googleAuth callback route.

View File

@ -63,8 +63,15 @@ router
updateTenant, updateTenant,
authController.googlePreAuth authController.googlePreAuth
) )
.get(
"/api/global/auth/:tenantId/google2",
updateTenant,
authController.googlePreAuth2
)
// single tenancy endpoint // single tenancy endpoint
.get("/api/global/auth/google/callback", authController.googleAuth) .get("/api/global/auth/google/callback", authController.googleAuth)
// TODO: Remove
.get("/api/global/auth/google2/callback", authController.googleAuth2)
// multi-tenancy endpoint // multi-tenancy endpoint
.get( .get(
"/api/global/auth/:tenantId/google/callback", "/api/global/auth/:tenantId/google/callback",