From 248fa68aa84653ada84c120973b903ca674ec33a Mon Sep 17 00:00:00 2001 From: Rory Shanks <6383578+rorylshanks@users.noreply.github.com> Date: Tue, 23 Jan 2024 21:21:15 +0100 Subject: [PATCH] Add google workspace (#13) * Added ability to use google workspace * Added ability to use google workspace * Fixed tests --- .gitignore | 4 +- docker-compose.yaml | 2 + docs/idp/googleworkspace.md | 2 + docs/idp/msgraph.md | 0 example-config.yaml | 2 + lib/authz.js | 2 +- lib/idp_adapters/googleworkspace.js | 69 ++++++++++ lib/idp_adapters/localfile.js | 14 ++ lib/idp_adapters/localtest.js | 10 -- lib/idp_adapters/none.js | 8 ++ package-lock.json | 190 ++++++++++++++++++++++++++++ package.json | 1 + test/e2e/configs/veriflow.yaml | 3 +- 13 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 docs/idp/googleworkspace.md create mode 100644 docs/idp/msgraph.md create mode 100644 lib/idp_adapters/googleworkspace.js create mode 100644 lib/idp_adapters/localfile.js delete mode 100644 lib/idp_adapters/localtest.js create mode 100644 lib/idp_adapters/none.js diff --git a/.gitignore b/.gitignore index 005f178..1ed5afe 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,6 @@ caddy.json output.json token-auth.json -request_header_map.json \ No newline at end of file +request_header_map.json + +gcp.json \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 4f5dbf2..5794501 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,6 +9,8 @@ services: context: "." dockerfile: Dockerfile.dev network_mode: host + depends_on: + - redis volumes: - $PWD:/appdata - /secrets:/secrets diff --git a/docs/idp/googleworkspace.md b/docs/idp/googleworkspace.md new file mode 100644 index 0000000..c99e06f --- /dev/null +++ b/docs/idp/googleworkspace.md @@ -0,0 +1,2 @@ +idp_service_account_json_file +idp_service_account_subject \ No newline at end of file diff --git a/docs/idp/msgraph.md b/docs/idp/msgraph.md new file mode 100644 index 0000000..e69de29 diff --git a/example-config.yaml b/example-config.yaml index 32fef9d..5fd6679 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -34,6 +34,8 @@ policy: claims_headers: X-Pomerium-Claim-Email: mail X-Pomerium-Jwt-Assertion: jwt + allowed_domains: + - test.com allowed_groups: - All Users cors_allow_preflight: true diff --git a/lib/authz.js b/lib/authz.js index 5e2c306..f64691c 100644 --- a/lib/authz.js +++ b/lib/authz.js @@ -37,7 +37,7 @@ async function update() { var duration = (endDate - startDate) / 1000 log.info(`Updated users from IDP in ${duration} seconds`) } catch (error) { - log.error(error) + log.error({error, details: error.message}) } } diff --git a/lib/idp_adapters/googleworkspace.js b/lib/idp_adapters/googleworkspace.js new file mode 100644 index 0000000..24b4886 --- /dev/null +++ b/lib/idp_adapters/googleworkspace.js @@ -0,0 +1,69 @@ +import axios from 'axios'; +import fs from 'fs'; +import log from '../../util/logging.js'; +import {GoogleAuth} from 'google-auth-library'; +import { getConfig } from '../../util/config.js'; + +async function getAccessToken() { + const config = getConfig() + const auth = new GoogleAuth({ + keyFile: config.idp_service_account_json_file, + scopes: [ + 'https://www.googleapis.com/auth/admin.directory.user.readonly', + 'https://www.googleapis.com/auth/admin.directory.group.readonly' + ], + subject: config.idp_service_account_subject, + clientOptions : { + subject : config.idp_service_account_subject + } + }); + + const client = await auth.getClient(); + client.subject = config.idp_service_account_subject; + return client +} + +async function getUsers(client) { + const config = getConfig() + const response = await client.request({url: `https://admin.googleapis.com/admin/directory/v1/users?domain=${config.idp_tenant_id}&maxResults=500`}); + log.info(`Found ${response.data.users.length} users in domain`) + return response.data.users; +} + +async function getUserGroups(client, userEmail) { + const response = await client.request({ url : `https://admin.googleapis.com/admin/directory/v1/groups?userKey=${userEmail}`}); + return response.data.groups; +} + +async function getUsersAndGroups() { + const client = await getAccessToken(); + const users = await getUsers(client); + + let userData = {}; + for (const user of users) { + log.info(`Requesting groups for user ${user.primaryEmail}`) + const groups = await getUserGroups(client, user.primaryEmail); + userData[user.primaryEmail] = { + displayName: user.name.fullName, + givenName: user.name.givenName, + preferredLanguage: user.language || 'en', + surname: user.name.familyName, + userPrincipalName: user.primaryEmail, + mail: user.primaryEmail, + id: user.id, + groups: groups ? groups.map(group => group.name) : [] + }; + } + + return userData; +} + +async function runUpdate() { + log.debug("Starting update of users and groups from Google Workspace"); + const userData = await getUsersAndGroups(); + fs.writeFileSync("output.json", JSON.stringify(userData, null, 2)); + log.debug("Finished update of users and groups from Google Workspace"); + return userData; +} + +export default { runUpdate }; diff --git a/lib/idp_adapters/localfile.js b/lib/idp_adapters/localfile.js new file mode 100644 index 0000000..ec0974a --- /dev/null +++ b/lib/idp_adapters/localfile.js @@ -0,0 +1,14 @@ +import fs from 'fs/promises'; +import { getConfig } from '../../util/config.js'; +import log from '../../util/logging.js' + +async function runUpdate() { + const currentConfig = getConfig() + let localFile = currentConfig.idp_provider_localfile_location + let fileContents = await fs.readFile(localFile) + var result = JSON.parse(fileContents) + log.debug(result) + return result +} + +export default { runUpdate }; \ No newline at end of file diff --git a/lib/idp_adapters/localtest.js b/lib/idp_adapters/localtest.js deleted file mode 100644 index fc4ea9b..0000000 --- a/lib/idp_adapters/localtest.js +++ /dev/null @@ -1,10 +0,0 @@ -import fs from 'fs'; - - -async function runUpdate() { - var result = JSON.parse(fs.readFileSync("output.json")) - console.log(result) - return result -} - -export default { runUpdate }; \ No newline at end of file diff --git a/lib/idp_adapters/none.js b/lib/idp_adapters/none.js new file mode 100644 index 0000000..46003ea --- /dev/null +++ b/lib/idp_adapters/none.js @@ -0,0 +1,8 @@ +import log from '../util/logging.js' + +async function runUpdate() { + log.debug("Running idp_update for none") + return {} +} + +export default { runUpdate }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9104357..4b1bf80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "express": "^4.18.2", "express-session": "^1.17.3", "form-data": "^4.0.0", + "google-auth-library": "^9.4.2", "isomorphic-fetch": "^3.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", @@ -153,6 +154,38 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -268,6 +301,14 @@ } ] }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -752,6 +793,11 @@ "node": ">= 0.6" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-redact": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", @@ -892,6 +938,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz", + "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/generic-pool": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", @@ -925,6 +997,72 @@ "node": ">= 6" } }, + "node_modules/google-auth-library": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.2.tgz", + "integrity": "sha512-rTLO4gjhqqo3WvYKL5IdtlCvRqeQ4hxUx/p4lObobY2xotFW3bCQC+Qf1N51CYOfiqfMecdMwW9RIo7dFWYjqw==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz", + "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -993,6 +1131,39 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", + "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1114,6 +1285,17 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -1159,6 +1341,14 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", diff --git a/package.json b/package.json index f70b97a..242e41e 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "express": "^4.18.2", "express-session": "^1.17.3", "form-data": "^4.0.0", + "google-auth-library": "^9.4.2", "isomorphic-fetch": "^3.0.0", "js-yaml": "^4.1.0", "jsonwebtoken": "^9.0.2", diff --git a/test/e2e/configs/veriflow.yaml b/test/e2e/configs/veriflow.yaml index e8e3331..c5b665e 100644 --- a/test/e2e/configs/veriflow.yaml +++ b/test/e2e/configs/veriflow.yaml @@ -7,7 +7,8 @@ redis_host: localhost redis_port: 6379 idp_client_id: 0c4860a4-ae2b-4f49-97d7-b581252a7166 idp_client_secret: supersecret -idp_provider: localtest +idp_provider: localfile +idp_provider_localfile_location: output.json idp_provider_scope: openid email profile idp_provider_user_id_claim: email idp_provider_url: http://localhost:5556/dex