From 03f7fb37ed5381ca59b51e45d1570c906c69694e Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 11:14:25 +0100 Subject: [PATCH 01/17] Calculate creators count when group role changes --- packages/account-portal | 2 +- packages/backend-core/src/index.ts | 1 + packages/backend-core/src/users/db.ts | 15 +++-- .../backend-core/src/users/test/utils.spec.ts | 67 +++++++++++++++++++ packages/backend-core/src/users/users.ts | 3 +- packages/backend-core/src/users/utils.ts | 35 +++++++++- packages/pro | 2 +- packages/server/src/middleware/authorized.ts | 2 +- .../shared-core/src/sdk/documents/users.ts | 2 +- 9 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 packages/backend-core/src/users/test/utils.spec.ts diff --git a/packages/account-portal b/packages/account-portal index 1bc0128714..b23fb3b179 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 1bc012871496ff55e376931b620075b565e34d09 +Subproject commit b23fb3b17961fb04badd9487913a683fcf26dbe6 diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 7bf26f3688..8946e37486 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -2,6 +2,7 @@ export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" +export * as usersUtils from "./users/utils" export * as roles from "./security/roles" export * as permissions from "./security/permissions" export * as accounts from "./accounts" diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 4d0d216603..136cb4b8ad 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -251,7 +251,8 @@ export class UserDB { } const change = dbUser ? 0 : 1 // no change if there is existing user - const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0 + const creatorsChange = + (await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0 return UserDB.quotas.addUsers(change, creatorsChange, async () => { await validateUniqueUser(email, tenantId) @@ -335,7 +336,7 @@ export class UserDB { } newUser.userGroups = groups || [] newUsers.push(newUser) - if (isCreator(newUser)) { + if (await isCreator(newUser)) { newCreators.push(newUser) } } @@ -432,12 +433,16 @@ export class UserDB { _deleted: true, })) const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) - const creatorsToDelete = usersToDelete.filter(isCreator) + + const creatorsEval = await Promise.all(usersToDelete.map(isCreator)) + const creatorsToDeleteCount = creatorsEval.filter( + creator => !!creator + ).length for (let user of usersToDelete) { await bulkDeleteProcessing(user) } - await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length) + await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount) // Build Response // index users by id @@ -486,7 +491,7 @@ export class UserDB { await db.remove(userId, dbUser._rev) - const creatorsToDelete = isCreator(dbUser) ? 1 : 0 + const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0 await UserDB.quotas.removeUsers(1, creatorsToDelete) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) diff --git a/packages/backend-core/src/users/test/utils.spec.ts b/packages/backend-core/src/users/test/utils.spec.ts new file mode 100644 index 0000000000..0fe27f57a6 --- /dev/null +++ b/packages/backend-core/src/users/test/utils.spec.ts @@ -0,0 +1,67 @@ +import { User, UserGroup } from "@budibase/types" +import { generator, structures } from "../../../tests" +import { DBTestConfiguration } from "../../../tests/extra" +import { getGlobalDB } from "../../context" +import { isCreator } from "../utils" + +const config = new DBTestConfiguration() + +describe("Users", () => { + it("User is a creator if it is configured as a global builder", async () => { + const user: User = structures.users.user({ builder: { global: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is configured as a global admin", async () => { + const user: User = structures.users.user({ admin: { global: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is configured with creator permission", async () => { + const user: User = structures.users.user({ builder: { creator: true } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it is a builder in some application", async () => { + const user: User = structures.users.user({ builder: { apps: ["app1"] } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it has CREATOR permission in some application", async () => { + const user: User = structures.users.user({ roles: { app1: "CREATOR" } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it has ADMIN permission in some application", async () => { + const user: User = structures.users.user({ roles: { app1: "ADMIN" } }) + expect(await isCreator(user)).toBe(true) + }) + + it("User is a creator if it remains to a group with ADMIN permissions", async () => { + const usersInGroup = 10 + const groupId = "gr_17abffe89e0b40268e755b952f101a59" + const group: UserGroup = { + ...structures.userGroups.userGroup(), + ...{ _id: groupId, roles: { app1: "ADMIN" } }, + } + const users: User[] = [] + for (const _ of Array.from({ length: usersInGroup })) { + const userId = `us_${generator.guid()}` + const user: User = structures.users.user({ + _id: userId, + userGroups: [groupId], + }) + users.push(user) + } + + await config.doInTenant(async () => { + const db = getGlobalDB() + await db.put(group) + for (let user of users) { + await db.put(user) + const creator = await isCreator(user) + expect(creator).toBe(true) + } + }) + }) +}) diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index cc2b4fc27f..638da4a5b1 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -309,7 +309,8 @@ export async function getCreatorCount() { let creators = 0 async function iterate(startPage?: string) { const page = await paginatedUsers({ bookmark: startPage }) - creators += page.data.filter(isCreator).length + const creatorsEval = await Promise.all(page.data.map(isCreator)) + creators += creatorsEval.filter(creator => !!creator).length if (page.hasNextPage) { await iterate(page.nextPage) } diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 0ef4b77998..348ad1532f 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,4 +1,4 @@ -import { CloudAccount } from "@budibase/types" +import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" import { getPlatformUser } from "./lookup" @@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors" import { getTenantId } from "../context" import { sdk } from "@budibase/shared-core" import { getAccountByTenantId } from "../accounts" +import { BUILTIN_ROLE_IDS } from "../security/roles" +import * as context from "../context" // extract from shared-core to make easily accessible from backend-core export const isBuilder = sdk.users.isBuilder export const isAdmin = sdk.users.isAdmin -export const isCreator = sdk.users.isCreator export const isGlobalBuilder = sdk.users.isGlobalBuilder export const isAdminOrBuilder = sdk.users.isAdminOrBuilder export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions +export async function isCreator(user?: User | ContextUser) { + const isCreatorByUserDefinition = sdk.users.isCreator(user) + if (!isCreatorByUserDefinition && user) { + return await isCreatorByGroupMembership(user) + } + return isCreatorByUserDefinition +} + +async function isCreatorByGroupMembership(user?: User | ContextUser) { + const userGroups = user?.userGroups || [] + if (userGroups.length > 0) { + const db = context.getGlobalDB() + const groups: UserGroup[] = [] + for (let groupId of userGroups) { + try { + const group = await db.get(groupId) + groups.push(group) + } catch (e: any) { + if (e.error !== "not_found") { + throw e + } + } + } + return groups.some(group => + Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN) + ) + } + return false +} + export async function validateUniqueUser(email: string, tenantId: string) { // check budibase users in other tenants if (env.MULTI_TENANCY) { diff --git a/packages/pro b/packages/pro index 9d80daaa5b..8c466d6ef2 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 9d80daaa5b79da68730d6c5f497f629c47a78ef8 +Subproject commit 8c466d6ef2a0c09b843ef63276793ab5af2e96f7 diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index cba765a887..fe7f6bb6fe 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -34,7 +34,7 @@ const checkAuthorized = async ( const isCreatorApi = permType === PermissionType.CREATOR const isBuilderApi = permType === PermissionType.BUILDER const isGlobalBuilder = users.isGlobalBuilder(ctx.user) - const isCreator = users.isCreator(ctx.user) + const isCreator = await users.isCreator(ctx.user) const isBuilder = appId ? users.isBuilder(ctx.user, appId) : users.hasBuilderPermissions(ctx.user) diff --git a/packages/shared-core/src/sdk/documents/users.ts b/packages/shared-core/src/sdk/documents/users.ts index 1aaf44ff7c..11e80dcf29 100644 --- a/packages/shared-core/src/sdk/documents/users.ts +++ b/packages/shared-core/src/sdk/documents/users.ts @@ -70,7 +70,7 @@ export function hasAppCreatorPermissions(user?: User | ContextUser): boolean { return _.flow( _.get("roles"), _.values, - _.find(x => x === "CREATOR"), + _.find(x => ["CREATOR", "ADMIN"].includes(x)), x => !!x )(user) } From ce0b1f8df3d9d11d3cf8c25daa1944aa257c5ded Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 11:22:16 +0100 Subject: [PATCH 02/17] Account portal submodule hash to master --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index b23fb3b179..1bc0128714 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit b23fb3b17961fb04badd9487913a683fcf26dbe6 +Subproject commit 1bc012871496ff55e376931b620075b565e34d09 From 64d3114c9f62f2b8f4c7f1382cce49ece836efd9 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 12:03:26 +0100 Subject: [PATCH 03/17] Refactor: usersUtils -> userUtils --- packages/backend-core/src/index.ts | 2 +- packages/pro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 8946e37486..8001017092 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -2,7 +2,7 @@ export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" -export * as usersUtils from "./users/utils" +export * as userUtils from "./users/utils" export * as roles from "./security/roles" export * as permissions from "./security/permissions" export * as accounts from "./accounts" diff --git a/packages/pro b/packages/pro index 8c466d6ef2..1857cbbd07 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 8c466d6ef2a0c09b843ef63276793ab5af2e96f7 +Subproject commit 1857cbbd071a855a56ed70877e55d5566c7f29e1 From 9befe6aa9d19098ccbb5314a1fa70d0f92ce8cc3 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 12:32:19 +0100 Subject: [PATCH 04/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 1857cbbd07..3d815d2629 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1857cbbd071a855a56ed70877e55d5566c7f29e1 +Subproject commit 3d815d262939c53092e5aeb0b033d6b481bb5583 From e47b19ef366b2df72ef5e0c72c82cbbe89dfd862 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 12:36:06 +0100 Subject: [PATCH 05/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 3d815d2629..e8511333e0 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 3d815d262939c53092e5aeb0b033d6b481bb5583 +Subproject commit e8511333e0ef84f89a4d5eb041534438744dc2fd From ede73efd81e03f49ee74c3df02541f6924769843 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 12:59:36 +0100 Subject: [PATCH 06/17] Upgrade pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index e8511333e0..35032a6aea 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit e8511333e0ef84f89a4d5eb041534438744dc2fd +Subproject commit 35032a6aeab96437c64b7892ca0ca585e11a6605 From a825aa191f44ac437de07cb4ff688b0a9d0e583c Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 16:37:25 +0100 Subject: [PATCH 07/17] Upgrade pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 35032a6aea..b91d3933e4 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 35032a6aeab96437c64b7892ca0ca585e11a6605 +Subproject commit b91d3933e453fb86b1ea66c8d92d44420fb6d2e7 From afcd9dc114806dc3aea006979c5fb3dd6582353f Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Thu, 18 Jan 2024 18:10:14 +0100 Subject: [PATCH 08/17] Upgrade pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index b91d3933e4..e131cce3d3 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit b91d3933e453fb86b1ea66c8d92d44420fb6d2e7 +Subproject commit e131cce3d36e33632d5af1e8d5a83b4fd9e8fe00 From 6fca47f3030bbfbe338c14c83eaf79ed0f436dc3 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Fri, 19 Jan 2024 10:26:54 +0100 Subject: [PATCH 09/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index e131cce3d3..26d25b6fe4 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit e131cce3d36e33632d5af1e8d5a83b4fd9e8fe00 +Subproject commit 26d25b6fe4e426abc924c03184138a2d836bb91e From c83a5219f6d7b21d0f0e12fa86f508115654d950 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Fri, 19 Jan 2024 13:12:17 +0100 Subject: [PATCH 10/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 26d25b6fe4..1d187da286 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 26d25b6fe4e426abc924c03184138a2d836bb91e +Subproject commit 1d187da286d2006d26d47ed6212c6026b500cf66 From 09b75c3924d653f3add28906ebaef1d8b827f30e Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Tue, 23 Jan 2024 13:21:54 +0100 Subject: [PATCH 11/17] Recalculate creators count on plan downgrade --- packages/pro | 2 +- packages/worker/src/api/controllers/global/self.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 1d187da286..38e7b34a70 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1d187da286d2006d26d47ed6212c6026b500cf66 +Subproject commit 38e7b34a708bcaf6abc677be54b9d74bd8d18ea5 diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index d14aecd1a7..2d7e83a2c3 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -91,6 +91,9 @@ export async function getSelf(ctx: any) { id: userId, } + // Adjust creators quotas (prevents wrong creators count if user has changed the plan) + await groups.adjustGroupCreatorsQuotas() + // get the main body of the user const user = await userSdk.db.getUser(userId) ctx.body = await groups.enrichUserRolesFromGroups(user) From 54903920df657067bdfa159ed166a150455d7b39 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 24 Jan 2024 08:59:31 +0100 Subject: [PATCH 12/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 38e7b34a70..8b30020974 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 38e7b34a708bcaf6abc677be54b9d74bd8d18ea5 +Subproject commit 8b30020974609cdfeb4ebd68e77fc5f8af38c0fb From 37cf41e19199b1792bc36bcd9da40e420500b1be Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 24 Jan 2024 09:06:27 +0100 Subject: [PATCH 13/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 8b30020974..d6227b9214 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 8b30020974609cdfeb4ebd68e77fc5f8af38c0fb +Subproject commit d6227b9214dcc92604c09560ac90f498ddb9726a From 118e1bf5a7f712c20f6e379e9090e4d23626bc7e Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 24 Jan 2024 09:09:06 +0100 Subject: [PATCH 14/17] Update account-portal submodule --- packages/account-portal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account-portal b/packages/account-portal index 1bc0128714..05c90ce551 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 1bc012871496ff55e376931b620075b565e34d09 +Subproject commit 05c90ce55144e260da6688335c16783eab79bf96 From d4387471d42a797238f1e25c2016f50c150359da Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 24 Jan 2024 09:35:58 +0100 Subject: [PATCH 15/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index d6227b9214..1ed918cc5a 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit d6227b9214dcc92604c09560ac90f498ddb9726a +Subproject commit 1ed918cc5a4c90de37d7521102c710066e462514 From d57db8c32d93b9906b920b298e7be9c383362173 Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 24 Jan 2024 10:13:01 +0100 Subject: [PATCH 16/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 1ed918cc5a..0e4c5f95bd 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 1ed918cc5a4c90de37d7521102c710066e462514 +Subproject commit 0e4c5f95bda6af126a5cddeef01b8cf551f236be From 6f9075d44cfd99977f95ada70512daf3e3dae27c Mon Sep 17 00:00:00 2001 From: jvcalderon Date: Wed, 24 Jan 2024 12:27:02 +0100 Subject: [PATCH 17/17] Solve a failing test --- packages/server/src/sdk/users/tests/utils.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/sdk/users/tests/utils.spec.ts b/packages/server/src/sdk/users/tests/utils.spec.ts index f7c9413ebd..86dc411caf 100644 --- a/packages/server/src/sdk/users/tests/utils.spec.ts +++ b/packages/server/src/sdk/users/tests/utils.spec.ts @@ -84,7 +84,7 @@ describe("syncGlobalUsers", () => { await syncGlobalUsers() const metadata = await rawUserMetadata() - expect(metadata).toHaveLength(3) + expect(metadata).toHaveLength(2) expect(metadata).toContainEqual( expect.objectContaining({ _id: db.generateUserMetadataID(user1._id!), @@ -121,7 +121,7 @@ describe("syncGlobalUsers", () => { await syncGlobalUsers() const metadata = await rawUserMetadata() - expect(metadata).toHaveLength(0) + expect(metadata).toHaveLength(1) //ADMIN user created in test bootstrap still in the application }) }) })