From 9aa708588a38b8adbc9a228b9b8c55f4b222658e Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 4 Nov 2021 14:53:03 +0000 Subject: [PATCH] Adding the sync call from the worker for creation, updating and deletion of users. Making sure that production and development apps are always up to date with user metadata. --- hosting/docker-compose.yaml | 1 + .../templates/worker-service-deployment.yaml | 2 + packages/server/src/api/controllers/user.js | 64 ++++++++++++++++--- packages/server/src/middleware/authorized.js | 3 +- packages/server/src/utilities/global.js | 1 + packages/server/src/utilities/index.js | 10 ++- packages/worker/scripts/dev/manage.js | 1 + .../src/api/controllers/global/users.js | 8 ++- packages/worker/src/environment.js | 8 +++ packages/worker/src/utilities/appService.js | 23 +++++++ 10 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 packages/worker/src/utilities/appService.js diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 66b24f4e49..c94d1520a1 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -45,6 +45,7 @@ services: MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_URL: http://minio-service:9000 + APPS_URL: http://app-service:4002 COUCH_DB_USERNAME: ${COUCH_DB_USER} COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984 diff --git a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml index d7d8702567..6cded8545f 100644 --- a/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml +++ b/hosting/kubernetes/budibase/templates/worker-service-deployment.yaml @@ -113,6 +113,8 @@ spec: value: {{ .Values.globals.smtp.port | quote }} - name: SMTP_FROM_ADDRESS value: {{ .Values.globals.smtp.from | quote }} + - name: APPS_URL + value: http://app-service:{{ .Values.services.apps.port }} image: budibase/worker imagePullPolicy: Always name: bbworker diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 0362d1298b..5faf821349 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -8,8 +8,13 @@ const { getGlobalUsers, getRawGlobalUser } = require("../../utilities/global") const { getFullUser } = require("../../utilities/users") const { isEqual } = require("lodash") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") -const { getDevelopmentAppID } = require("@budibase/auth/db") +const { + getDevelopmentAppID, + getAllApps, + isDevAppID, +} = require("@budibase/auth/db") const { doesDatabaseExist } = require("../../utilities") +const { UserStatus } = require("@budibase/auth/constants") async function rawMetadata(db) { return ( @@ -21,7 +26,7 @@ async function rawMetadata(db) { ).rows.map(row => row.doc) } -async function combineMetadataAndUser(user, metadata) { +function combineMetadataAndUser(user, metadata) { // skip users with no access if (user.roleId === BUILTIN_ROLE_IDS.PUBLIC) { return null @@ -67,22 +72,61 @@ exports.syncGlobalUsers = async appId => { } exports.syncUser = async function (ctx) { - const user = await getRawGlobalUser(ctx.params.id) + let deleting = false, + user + const userId = ctx.params.id + try { + user = await getRawGlobalUser(userId) + } catch (err) { + user = {} + deleting = true + } const roles = user.roles + // remove props which aren't useful to metadata + delete user.password + delete user.forceResetPassword delete user.roles - for (let [prodAppId, roleId] of Object.entries(roles)) { - if (roleId === BUILTIN_ROLE_IDS.PUBLIC) { - continue - } + // run through all production appIDs in the users roles + let prodAppIds + // if they are a builder then get all production app IDs + if ((user.builder && user.builder.global) || deleting) { + prodAppIds = (await getAllApps(CouchDB, { idsOnly: true })).filter( + id => !isDevAppID(id) + ) + } else { + prodAppIds = Object.entries(roles) + .filter(entry => entry[1] !== BUILTIN_ROLE_IDS.PUBLIC) + .map(([appId]) => appId) + } + for (let prodAppId of prodAppIds) { const devAppId = getDevelopmentAppID(prodAppId) for (let appId of [prodAppId, devAppId]) { if (!(await doesDatabaseExist(appId))) { continue } const db = new CouchDB(appId) - const userId = generateUserMetadataID(user._id) - const metadata = await db.get(userId) - const combined = combineMetadataAndUser(user, metadata) + const metadataId = generateUserMetadataID(userId) + let metadata + try { + metadata = await db.get(metadataId) + } catch (err) { + if (deleting) { + continue + } + metadata = { + tableId: InternalTables.USER_METADATA, + } + } + let combined + if (deleting) { + combined = { + ...metadata, + status: UserStatus.INACTIVE, + metadata: BUILTIN_ROLE_IDS.PUBLIC, + } + } else { + combined = combineMetadataAndUser(user, metadata) + } await db.put(combined) } } diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js index bd064f7e66..a30e034be2 100644 --- a/packages/server/src/middleware/authorized.js +++ b/packages/server/src/middleware/authorized.js @@ -18,7 +18,8 @@ module.exports = (permType, permLevel = null) => async (ctx, next) => { // webhooks don't need authentication, each webhook unique - if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) { + // also internal requests (between services) don't need authorized + if (WEBHOOK_ENDPOINTS.test(ctx.request.url) || ctx.internal) { return next() } diff --git a/packages/server/src/utilities/global.js b/packages/server/src/utilities/global.js index eb64aa7f77..6527aa0601 100644 --- a/packages/server/src/utilities/global.js +++ b/packages/server/src/utilities/global.js @@ -77,6 +77,7 @@ exports.getGlobalUsers = async (appId = null, users = null) => { .filter(user => user != null) .map(user => { delete user.password + delete user.forceResetPassword return user }) if (!appId) { diff --git a/packages/server/src/utilities/index.js b/packages/server/src/utilities/index.js index d67c8f1da0..266ee09f9f 100644 --- a/packages/server/src/utilities/index.js +++ b/packages/server/src/utilities/index.js @@ -136,7 +136,11 @@ exports.stringToReadStream = string => { } exports.doesDatabaseExist = async dbName => { - const db = new CouchDB(dbName, { skip_setup: true }) - const info = await db.info() - return info && !info.error + try { + const db = new CouchDB(dbName, { skip_setup: true }) + const info = await db.info() + return info && !info.error + } catch (err) { + return false + } } diff --git a/packages/worker/scripts/dev/manage.js b/packages/worker/scripts/dev/manage.js index e0b8c3586a..179167883f 100644 --- a/packages/worker/scripts/dev/manage.js +++ b/packages/worker/scripts/dev/manage.js @@ -25,6 +25,7 @@ async function init() { ACCOUNT_PORTAL_URL: "http://localhost:10001", ACCOUNT_PORTAL_API_KEY: "budibase", PLATFORM_URL: "http://localhost:10000", + APPS_URL: "http://localhost:4001", } let envFile = "" Object.keys(envFileJson).forEach(key => { diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index ed70d6122e..42166faad7 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -19,6 +19,7 @@ const { } = require("@budibase/auth/tenancy") const { removeUserFromInfoDB } = require("@budibase/auth/deprovision") const env = require("../../../environment") +const { syncUserInApps } = require("../../../utilities/appService") async function allUsers() { const db = getGlobalDB() @@ -32,7 +33,10 @@ async function allUsers() { exports.save = async ctx => { try { - ctx.body = await saveUser(ctx.request.body, getTenantId()) + const user = await saveUser(ctx.request.body, getTenantId()) + // let server know to sync user + await syncUserInApps(user._id) + ctx.body = user } catch (err) { ctx.throw(err.status || 400, err) } @@ -129,6 +133,8 @@ exports.destroy = async ctx => { await db.remove(dbUser._id, dbUser._rev) await userCache.invalidateUser(dbUser._id) await invalidateSessions(dbUser._id) + // let server know to sync user + await syncUserInApps(dbUser._id) ctx.body = { message: `User ${ctx.params.id} deleted.`, } diff --git a/packages/worker/src/environment.js b/packages/worker/src/environment.js index a1fab84112..91f06ea46d 100644 --- a/packages/worker/src/environment.js +++ b/packages/worker/src/environment.js @@ -42,6 +42,7 @@ module.exports = { SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, PLATFORM_URL: process.env.PLATFORM_URL, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, + APPS_URL: process.env.APPS_URL, _set(key, value) { process.env[key] = value module.exports[key] = value @@ -53,6 +54,13 @@ module.exports = { }, } +// if some var haven't been set, define them +if (!module.exports.APPS_URL) { + module.exports.APPS_URL = isDev() + ? "http://localhost:4001" + : "http://app-service:4002" +} + // clean up any environment variable edge cases for (let [key, value] of Object.entries(module.exports)) { // handle the edge case of "0" to disable an environment variable diff --git a/packages/worker/src/utilities/appService.js b/packages/worker/src/utilities/appService.js new file mode 100644 index 0000000000..a7be05b227 --- /dev/null +++ b/packages/worker/src/utilities/appService.js @@ -0,0 +1,23 @@ +const fetch = require("node-fetch") +const { Headers } = require("@budibase/auth/constants") +const { getTenantId, isTenantIdSet } = require("@budibase/auth/tenancy") +const { checkSlashesInUrl } = require("../utilities") +const env = require("../environment") + +exports.syncUserInApps = async userId => { + const request = { headers: {} } + request.headers[Headers.API_KEY] = env.INTERNAL_API_KEY + if (isTenantIdSet()) { + request.headers[Headers.TENANT_ID] = getTenantId() + } + request.headers["Content-Type"] = "application/json" + request.body = JSON.stringify({}) + request.method = "POST" + const response = await fetch( + checkSlashesInUrl(env.APPS_URL + `/api/users/sync/${userId}`), + request + ) + if (response.status !== 200) { + throw "Unable to sync user." + } +}