From 39746e0bf0db3633723ee2df60551ec3b25469df Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 18 Jul 2023 16:57:48 +0100 Subject: [PATCH] Main body of work to handle the new approach of per app builders support. --- packages/backend-core/src/constants/db.ts | 2 - packages/backend-core/src/db/views.ts | 11 ---- .../backend-core/src/events/identification.ts | 5 +- .../src/middleware/builderOnly.ts | 12 ++-- .../src/middleware/builderOrAdmin.ts | 15 +++-- packages/backend-core/src/users.ts | 62 ++++++++++++++++++- .../server/src/api/controllers/application.ts | 4 +- packages/server/src/middleware/authorized.ts | 11 +++- packages/server/src/middleware/currentapp.ts | 14 ++--- .../functions/backfill/global/users.ts | 10 ++- packages/server/src/utilities/global.ts | 6 +- packages/types/src/documents/global/user.ts | 1 - packages/worker/src/sdk/users/events.ts | 20 +++--- packages/worker/src/sdk/users/users.ts | 4 +- 14 files changed, 115 insertions(+), 62 deletions(-) diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index be49b9f261..34dab6c088 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -14,8 +14,6 @@ export enum ViewName { USER_BY_APP = "by_app", USER_BY_EMAIL = "by_email2", BY_API_KEY = "by_api_key", - /** @deprecated - could be deleted */ - USER_BY_BUILDERS = "by_builders", LINK = "by_link", ROUTING = "screen_routes", AUTOMATION_LOGS = "automation_logs", diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index fddb1ab34b..7f5ef29a0a 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -105,16 +105,6 @@ export const createApiKeyView = async () => { await createView(db, viewJs, ViewName.BY_API_KEY) } -export const createUserBuildersView = async () => { - const db = getGlobalDB() - const viewJs = `function(doc) { - if (doc.builder && doc.builder.global === true) { - emit(doc._id, doc._id) - } - }` - await createView(db, viewJs, ViewName.USER_BY_BUILDERS) -} - export interface QueryViewOptions { arrayResponse?: boolean } @@ -223,7 +213,6 @@ export const queryPlatformView = async ( const CreateFuncByName: any = { [ViewName.USER_BY_EMAIL]: createNewUserEmailView, [ViewName.BY_API_KEY]: createApiKeyView, - [ViewName.USER_BY_BUILDERS]: createUserBuildersView, [ViewName.USER_BY_APP]: createUserAppView, } diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 5eb11d1354..52fcb2431f 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -21,6 +21,7 @@ import { processors } from "./processors" import { newid } from "../utils" import * as installation from "../installation" import * as configs from "../configs" +import * as users from "../users" import { withCache, TTL, CacheKey } from "../cache/generic" /** @@ -164,8 +165,8 @@ const identifyUser = async ( const id = user._id as string const tenantId = await getEventTenantId(user.tenantId) const type = IdentityType.USER - let builder = user.builder?.global || false - let admin = user.admin?.global || false + let builder = users.hasBuilderPermissions(user) + let admin = users.hasAdminPermissions(user) let providerType if (isSSOUser(user)) { providerType = user.providerType diff --git a/packages/backend-core/src/middleware/builderOnly.ts b/packages/backend-core/src/middleware/builderOnly.ts index a00fd63a22..744321252e 100644 --- a/packages/backend-core/src/middleware/builderOnly.ts +++ b/packages/backend-core/src/middleware/builderOnly.ts @@ -1,10 +1,10 @@ -import { BBContext } from "@budibase/types" +import { UserCtx } from "@budibase/types" +import { isBuilder } from "../users" +import { getAppId } from "../context" -export default async (ctx: BBContext, next: any) => { - if ( - !ctx.internal && - (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) - ) { +export default async (ctx: UserCtx, next: any) => { + const appId = getAppId() + if (!ctx.internal && !isBuilder(ctx.user, appId)) { ctx.throw(403, "Builder user only endpoint.") } return next() diff --git a/packages/backend-core/src/middleware/builderOrAdmin.ts b/packages/backend-core/src/middleware/builderOrAdmin.ts index 26bb3a1bda..2ba5bfe1e2 100644 --- a/packages/backend-core/src/middleware/builderOrAdmin.ts +++ b/packages/backend-core/src/middleware/builderOrAdmin.ts @@ -1,12 +1,11 @@ -import { BBContext } from "@budibase/types" +import { UserCtx } from "@budibase/types" +import { isBuilder, isAdmin } from "../users" +import { getAppId } from "../context" -export default async (ctx: BBContext, next: any) => { - if ( - !ctx.internal && - (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) && - (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) - ) { - ctx.throw(403, "Builder user only endpoint.") +export default async (ctx: UserCtx, next: any) => { + const appId = getAppId() + if (!ctx.internal && !isBuilder(ctx.user, appId) && !isAdmin(ctx.user)) { + ctx.throw(403, "Admin/Builder user only endpoint.") } return next() } diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index b49058f546..2e6ede3cf3 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -12,7 +12,12 @@ import { UNICODE_MAX, ViewName, } from "./db" -import { BulkDocsResponse, SearchUsersRequest, User } from "@budibase/types" +import { + BulkDocsResponse, + SearchUsersRequest, + User, + ContextUser, +} from "@budibase/types" import { getGlobalDB } from "./context" import * as context from "./context" @@ -178,7 +183,7 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { * Performs a starts with search on the global email view. */ export const searchGlobalUsersByEmail = async ( - email: string, + email: string | unknown, opts: any, getOpts?: GetOpts ) => { @@ -248,3 +253,56 @@ export async function getUserCount() { }) return response.total_rows } + +// checks if a user is specifically a builder, given an app ID +export function isBuilder(user: User | ContextUser, appId?: string) { + if (user.builder?.global) { + return true + } else if (appId && user.builder?.apps?.includes(appId)) { + return true + } + return false +} + +// alias for hasAdminPermission, currently do the same thing +// in future whether someone has admin permissions and whether they are +// an admin for a specific resource could be separated +export function isAdmin(user: User | ContextUser) { + return hasAdminPermissions(user) +} + +// checks if a user is capable of building any app +export function hasBuilderPermissions(user?: User | ContextUser) { + if (!user) { + return false + } + return user.builder?.global || user.builder?.apps?.length !== 0 +} + +// checks if a user is capable of being an admin +export function hasAdminPermissions(user?: User | ContextUser) { + if (!user) { + return false + } + return user.admin?.global +} + +// used to remove the builder/admin permissions, for processing the +// user as an app user (they may have some specific role/group +export function removePortalUserPermissions(user: User | ContextUser) { + delete user.admin + delete user.builder + return user +} + +export function cleanseUserObject(user: User | ContextUser, base?: User) { + delete user.admin + delete user.builder + delete user.roles + if (base) { + user.admin = base.admin + user.builder = base.builder + user.roles = base.roles + } + return user +} diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 418ccf637e..6a0088d4dc 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -30,6 +30,7 @@ import { objectStore, roles, tenancy, + users, } from "@budibase/backend-core" import { USERS_TABLE_SCHEMA } from "../../constants" import { @@ -222,6 +223,7 @@ export async function fetchAppDefinition(ctx: UserCtx) { export async function fetchAppPackage(ctx: UserCtx) { const db = context.getAppDB() + const appId = context.getAppId() let application = await db.get(DocumentType.APP_METADATA) const layouts = await getLayouts() let screens = await getScreens() @@ -233,7 +235,7 @@ export async function fetchAppPackage(ctx: UserCtx) { ) // Only filter screens if the user is not a builder - if (!(ctx.user.builder && ctx.user.builder.global)) { + if (!users.isBuilder(ctx.user, appId)) { const userRoleId = getUserRoleId(ctx) const accessController = new roles.AccessController() screens = await accessController.checkScreensAccess(screens, userRoleId) diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 6268163fb2..81e42604bc 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -1,4 +1,10 @@ -import { roles, permissions, auth, context } from "@budibase/backend-core" +import { + roles, + permissions, + auth, + context, + users, +} from "@budibase/backend-core" import { Role } from "@budibase/types" import builderMiddleware from "./builder" import { isWebhookEndpoint } from "./utils" @@ -21,8 +27,9 @@ const checkAuthorized = async ( permType: any, permLevel: any ) => { + const appId = context.getAppId() // check if this is a builder api and the user is not a builder - const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global + const isBuilder = users.isBuilder(ctx.user, appId) const isBuilderApi = permType === permissions.PermissionType.BUILDER if (isBuilderApi && !isBuilder) { return ctx.throw(403, "Not Authorized") diff --git a/packages/server/src/middleware/currentapp.ts b/packages/server/src/middleware/currentapp.ts index 6879a103bc..10ee13a67a 100644 --- a/packages/server/src/middleware/currentapp.ts +++ b/packages/server/src/middleware/currentapp.ts @@ -4,12 +4,14 @@ import { roles, tenancy, context, + users, } from "@budibase/backend-core" import { generateUserMetadataID, isDevAppID } from "../db/utils" import { getCachedSelf } from "../utilities/global" import env from "../environment" import { isWebhookEndpoint } from "./utils" -import { UserCtx } from "@budibase/types" +import { UserCtx, ContextUser } from "@budibase/types" +import { removePortalUserPermissions } from "@budibase/backend-core/src/users" export default async (ctx: UserCtx, next: any) => { // try to get the appID from the request @@ -42,8 +44,7 @@ export default async (ctx: UserCtx, next: any) => { roleId = globalUser.roleId || roleId // Allow builders to specify their role via a header - const isBuilder = - globalUser && globalUser.builder && globalUser.builder.global + const isBuilder = users.isBuilder(globalUser, appId) const isDevApp = appId && isDevAppID(appId) const roleHeader = ctx.request && @@ -56,8 +57,7 @@ export default async (ctx: UserCtx, next: any) => { roleId = roleHeader // Delete admin and builder flags so that the specified role is honoured - delete ctx.user.builder - delete ctx.user.admin + ctx.user = users.removePortalUserPermissions(ctx.user) as ContextUser } } catch (error) { // Swallow error and do nothing @@ -81,9 +81,7 @@ export default async (ctx: UserCtx, next: any) => { !tenancy.isUserInAppTenant(requestAppId, ctx.user) ) { // don't error, simply remove the users rights (they are a public user) - delete ctx.user.builder - delete ctx.user.admin - delete ctx.user.roles + ctx.user = users.cleanseUserObject(ctx.user) as ContextUser ctx.isAuthenticated = false roleId = roles.BUILTIN_ROLE_IDS.PUBLIC skipCookie = true diff --git a/packages/server/src/migrations/functions/backfill/global/users.ts b/packages/server/src/migrations/functions/backfill/global/users.ts index 05c5f8f56e..9f536a97a5 100644 --- a/packages/server/src/migrations/functions/backfill/global/users.ts +++ b/packages/server/src/migrations/functions/backfill/global/users.ts @@ -1,4 +1,8 @@ -import { events, db as dbUtils } from "@budibase/backend-core" +import { + events, + db as dbUtils, + users as usersCore, +} from "@budibase/backend-core" import { User, CloudAccount } from "@budibase/types" import { DEFAULT_TIMESTAMP } from ".." @@ -30,11 +34,11 @@ export const backfill = async ( await events.identification.identifyUser(user, account, timestamp) await events.user.created(user, timestamp) - if (user.admin?.global) { + if (usersCore.hasAdminPermissions(user)) { await events.user.permissionAdminAssigned(user, timestamp) } - if (user.builder?.global) { + if (usersCore.hasBuilderPermissions(user)) { await events.user.permissionBuilderAssigned(user, timestamp) } diff --git a/packages/server/src/utilities/global.ts b/packages/server/src/utilities/global.ts index c9869b4920..63e33dafee 100644 --- a/packages/server/src/utilities/global.ts +++ b/packages/server/src/utilities/global.ts @@ -5,6 +5,7 @@ import { cache, tenancy, context, + users, } from "@budibase/backend-core" import env from "../environment" import { groups } from "@budibase/pro" @@ -22,8 +23,7 @@ export function updateAppRole( } // if in an multi-tenancy environment make sure roles are never updated if (env.MULTI_TENANCY && appId && !tenancy.isUserInAppTenant(appId, user)) { - delete user.builder - delete user.admin + user = users.removePortalUserPermissions(user) user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC return user } @@ -32,7 +32,7 @@ export function updateAppRole( user.roleId = user.roles[dbCore.getProdAppID(appId)] } // if a role wasn't found then either set as admin (builder) or public (everyone else) - if (!user.roleId && user.builder && user.builder.global) { + if (!user.roleId && users.isBuilder(user, appId)) { user.roleId = roles.BUILTIN_ROLE_IDS.ADMIN } else if (!user.roleId && !user?.userGroups?.length) { user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index cae3c0e6e2..e9e3ccc662 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -47,7 +47,6 @@ export interface User extends Document { } admin?: { global: boolean - apps?: string[] } password?: string status?: UserStatus diff --git a/packages/worker/src/sdk/users/events.ts b/packages/worker/src/sdk/users/events.ts index 7d86182a3c..d8af13a82f 100644 --- a/packages/worker/src/sdk/users/events.ts +++ b/packages/worker/src/sdk/users/events.ts @@ -1,15 +1,18 @@ import env from "../../environment" -import { events, accounts, tenancy } from "@budibase/backend-core" +import { events, accounts, tenancy, users } from "@budibase/backend-core" import { User, UserRoles, CloudAccount } from "@budibase/types" +const hasBuilderPerm = users.hasBuilderPermissions +const hasAdminPerm = users.hasAdminPermissions + export const handleDeleteEvents = async (user: any) => { await events.user.deleted(user) - if (isBuilder(user)) { + if (hasBuilderPerm(user)) { await events.user.permissionBuilderRemoved(user) } - if (isAdmin(user)) { + if (hasAdminPerm(user)) { await events.user.permissionAdminRemoved(user) } } @@ -103,23 +106,20 @@ export const handleSaveEvents = async ( await handleAppRoleEvents(user, existingUser) } -const isBuilder = (user: any) => user.builder && user.builder.global -const isAdmin = (user: any) => user.admin && user.admin.global - export const isAddingBuilder = (user: any, existingUser: any) => { - return isAddingPermission(user, existingUser, isBuilder) + return isAddingPermission(user, existingUser, hasBuilderPerm) } export const isRemovingBuilder = (user: any, existingUser: any) => { - return isRemovingPermission(user, existingUser, isBuilder) + return isRemovingPermission(user, existingUser, hasBuilderPerm) } const isAddingAdmin = (user: any, existingUser: any) => { - return isAddingPermission(user, existingUser, isAdmin) + return isAddingPermission(user, existingUser, hasAdminPerm) } const isRemovingAdmin = (user: any, existingUser: any) => { - return isRemovingPermission(user, existingUser, isAdmin) + return isRemovingPermission(user, existingUser, hasAdminPerm) } const isOnboardingComplete = (user: any, existingUser: any) => { diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index c3cb8500cb..cfa68932d2 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -252,9 +252,7 @@ export const save = async ( let builtUser = await buildUser(user, opts, tenantId, dbUser) // don't allow a user to update its own roles/perms if (opts.currentUserId && opts.currentUserId === dbUser?._id) { - builtUser.builder = dbUser.builder - builtUser.admin = dbUser.admin - builtUser.roles = dbUser.roles + builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User } if (!dbUser && roles?.length) {