diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 7224d827e8..05abe70fe3 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -2,6 +2,7 @@ import { directCouchFind, DocumentType, generateAppUserID, + getGlobalIDFromUserMetadataID, getGlobalUserParams, getProdAppID, getUsersByAppParams, @@ -21,6 +22,7 @@ import { import { sdk } from "@budibase/shared-core" import { getGlobalDB } from "./context" import * as context from "./context" +import { user as userCache } from "./cache" type GetOpts = { cleanup?: boolean } @@ -42,8 +44,10 @@ function removeUserPassword(users: User | User[]) { // 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 isAdminOrBuilder = sdk.users.isAdminOrBuilder export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions +export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions export const bulkGetGlobalUsersById = async ( userIds: string[], @@ -77,6 +81,27 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { return (await db.bulkDocs(users)) as BulkDocsResponse } +export const grantAppBuilderAccess = async (userId: string, appId: string) => { + const prodAppId = getProdAppID(appId) + const db = getGlobalDB() + const user = (await db.get(userId)) as User + if (!user.builder) { + user.builder = {} + } + if (!user.builder.apps) { + user.builder.apps = [] + } + if (!user.builder.apps.includes(prodAppId)) { + user.builder.apps.push(prodAppId) + } + try { + await db.put(user) + await userCache.invalidateUser(userId) + } catch (err: any) { + throw new Error(`Unable to grant user access: ${err.message}`) + } +} + export async function getById(id: string, opts?: GetOpts): Promise { const db = context.getGlobalDB() let user = await db.get(id) diff --git a/packages/builder/src/stores/portal/menu.js b/packages/builder/src/stores/portal/menu.js index c66c98d00f..2c17ce0b36 100644 --- a/packages/builder/src/stores/portal/menu.js +++ b/packages/builder/src/stores/portal/menu.js @@ -2,8 +2,12 @@ import { derived } from "svelte/store" import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags" import { admin } from "./admin" import { auth } from "./auth" +import { sdk } from "@budibase/shared-core" export const menu = derived([admin, auth], ([$admin, $auth]) => { + const user = $auth?.user + const isAdmin = sdk.users.isAdmin(user) + const cloud = $admin?.cloud // Determine user sub pages let userSubPages = [ { @@ -24,19 +28,24 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => { title: "Apps", href: "/builder/portal/apps", }, - { + ] + if ( + sdk.users.hasBuilderPermissions(user) && + !sdk.users.hasAppBuilderPermissions(user) + ) { + menu.push({ title: "Users", href: "/builder/portal/users", subPages: userSubPages, - }, - { - title: "Plugins", - href: "/builder/portal/plugins", - }, - ] + }) + } + menu.push({ + title: "Plugins", + href: "/builder/portal/plugins", + }) // Add settings page for admins - if ($auth.isAdmin) { + if (isAdmin) { let settingsSubPages = [ { title: "Auth", @@ -59,7 +68,7 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => { href: "/builder/portal/settings/environment", }, ] - if (!$admin.cloud) { + if (!cloud) { settingsSubPages.push({ title: "Version", href: "/builder/portal/settings/version", @@ -84,38 +93,35 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => { href: "/builder/portal/account/usage", }, ] - if ($auth.isAdmin) { + if (isAdmin) { accountSubPages.push({ title: "Audit Logs", href: "/builder/portal/account/auditLogs", }) - if (!$admin.cloud) { + if (!cloud) { accountSubPages.push({ title: "System Logs", href: "/builder/portal/account/systemLogs", }) } } - if ($admin.cloud && $auth?.user?.accountPortalAccess) { + if (cloud && user?.accountPortalAccess) { accountSubPages.push({ title: "Upgrade", - href: $admin.accountPortalUrl + "/portal/upgrade", + href: $admin?.accountPortalUrl + "/portal/upgrade", }) - } else if (!$admin.cloud && $auth.isAdmin) { + } else if (!cloud && isAdmin) { accountSubPages.push({ title: "Upgrade", href: "/builder/portal/account/upgrade", }) } // add license check here - if ( - $auth?.user?.accountPortalAccess && - $auth.user.account.stripeCustomerId - ) { + if (user?.accountPortalAccess && user.account.stripeCustomerId) { accountSubPages.push({ title: "Billing", - href: $admin.accountPortalUrl + "/portal/billing", + href: $admin?.accountPortalUrl + "/portal/billing", }) } menu.push({ diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 6a0088d4dc..9538790827 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -50,12 +50,13 @@ import { MigrationType, PlanType, Screen, - SocketSession, UserCtx, + ContextUser, } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import sdk from "../../sdk" import { builderSocket } from "../../websockets" +import { grantAppBuilderAccess } from "@budibase/backend-core/src/users" // utility function, need to do away with this async function getLayouts() { @@ -178,32 +179,10 @@ export const addSampleData = async (ctx: UserCtx) => { } export async function fetch(ctx: UserCtx) { - const dev = ctx.query && ctx.query.status === AppStatus.DEV - const all = ctx.query && ctx.query.status === AppStatus.ALL - const apps = (await dbCore.getAllApps({ dev, all })) as App[] - - const appIds = apps - .filter((app: any) => app.status === "development") - .map((app: any) => app.appId) - - // get the locks for all the dev apps - if (dev || all) { - const locks = await getLocksById(appIds) - for (let app of apps) { - const lock = locks[app.appId] - if (lock) { - app.lockedBy = lock - } else { - // make sure its definitely not present - delete app.lockedBy - } - } - } - - // Enrich apps with all builder user sessions - const enrichedApps = await sdk.users.sessions.enrichApps(apps) - - ctx.body = await checkAppMetadata(enrichedApps) + ctx.body = await sdk.applications.fetch( + ctx.query.status as AppStatus, + ctx.user + ) } export async function fetchAppDefinition(ctx: UserCtx) { @@ -395,6 +374,10 @@ async function appPostCreate(ctx: UserCtx, app: App) { tenantId, appId: app.appId, }) + // they are an app builder, creating a new app, make sure they can access it + if (users.hasAppBuilderPermissions(ctx.user)) { + await users.grantAppBuilderAccess(ctx.user._id!, app.appId) + } await creationEvents(ctx.request, app) // app import & template creation if (ctx.request.body.useTemplate === "true") { diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index e08392c3a1..eb5cbc27ef 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -3,10 +3,10 @@ import { db as dbCore } from "@budibase/backend-core" type Optional = string | null -export const AppStatus = { - DEV: "development", - ALL: "all", - DEPLOYED: "published", +export enum AppStatus { + DEV = "development", + ALL = "all", + DEPLOYED = "published", } export const BudibaseInternalDB = { diff --git a/packages/server/src/middleware/authorized.ts b/packages/server/src/middleware/authorized.ts index 81e42604bc..dba5d47cb9 100644 --- a/packages/server/src/middleware/authorized.ts +++ b/packages/server/src/middleware/authorized.ts @@ -5,7 +5,7 @@ import { context, users, } from "@budibase/backend-core" -import { Role } from "@budibase/types" +import { Role, UserCtx } from "@budibase/types" import builderMiddleware from "./builder" import { isWebhookEndpoint } from "./utils" @@ -22,14 +22,19 @@ const csrf = auth.buildCsrfMiddleware() * - Otherwise the user must have the required role. */ const checkAuthorized = async ( - ctx: any, + ctx: UserCtx, resourceRoles: any, permType: any, permLevel: any ) => { const appId = context.getAppId() // check if this is a builder api and the user is not a builder - const isBuilder = users.isBuilder(ctx.user, appId) + let isBuilder + if (!appId) { + isBuilder = users.hasBuilderPermissions(ctx.user) + } else { + 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/sdk/app/applications/applications.ts b/packages/server/src/sdk/app/applications/applications.ts new file mode 100644 index 0000000000..73cacb8983 --- /dev/null +++ b/packages/server/src/sdk/app/applications/applications.ts @@ -0,0 +1,54 @@ +import { AppStatus } from "../../../db/utils" +import { App, ContextUser } from "@budibase/types" +import { getLocksById } from "../../../utilities/redis" +import { enrichApps } from "../../users/sessions" +import { checkAppMetadata } from "../../../automations/logging" +import { db as dbCore, users } from "@budibase/backend-core" + +export function filterAppList(user: ContextUser, apps: App[]) { + let appList: string[] = [] + if (users.hasAppBuilderPermissions(user)) { + appList = user.builder?.apps! + } else if (!users.isAdminOrBuilder(user)) { + appList = Object.keys(user.roles || {}) + } else { + return apps + } + const finalApps: App[] = [] + for (let app of apps) { + if (appList.includes(dbCore.getProdAppID(app.appId))) { + finalApps.push(app) + } + } + return finalApps +} + +export async function fetch(status: AppStatus, user: ContextUser) { + const dev = status === AppStatus.DEV + const all = status === AppStatus.ALL + let apps = (await dbCore.getAllApps({ dev, all })) as App[] + apps = filterAppList(user, apps) + + const appIds = apps + .filter((app: any) => app.status === "development") + .map((app: any) => app.appId) + + // get the locks for all the dev apps + if (dev || all) { + const locks = await getLocksById(appIds) + for (let app of apps) { + const lock = locks[app.appId] + if (lock) { + app.lockedBy = lock + } else { + // make sure its definitely not present + delete app.lockedBy + } + } + } + + // Enrich apps with all builder user sessions + const enrichedApps = await enrichApps(apps) + + return await checkAppMetadata(enrichedApps) +} diff --git a/packages/server/src/sdk/app/applications/index.ts b/packages/server/src/sdk/app/applications/index.ts index d917225e52..963d065ce2 100644 --- a/packages/server/src/sdk/app/applications/index.ts +++ b/packages/server/src/sdk/app/applications/index.ts @@ -1,7 +1,9 @@ import * as sync from "./sync" import * as utils from "./utils" +import * as applications from "./applications" export default { ...sync, ...utils, + ...applications, } diff --git a/packages/shared-core/src/sdk/documents/users.ts b/packages/shared-core/src/sdk/documents/users.ts index 931f651a0e..1a9314f731 100644 --- a/packages/shared-core/src/sdk/documents/users.ts +++ b/packages/shared-core/src/sdk/documents/users.ts @@ -3,6 +3,9 @@ import { getProdAppID } from "./applications" // checks if a user is specifically a builder, given an app ID export function isBuilder(user: User | ContextUser, appId?: string) { + if (!user) { + return false + } if (user.builder?.global) { return true } else if (appId && user.builder?.apps?.includes(getProdAppID(appId))) { @@ -15,6 +18,9 @@ export function isBuilder(user: User | ContextUser, appId?: string) { // 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) { + if (!user) { + return false + } return hasAdminPermissions(user) } @@ -22,12 +28,20 @@ export function isAdminOrBuilder(user: User | ContextUser, appId?: string) { return isBuilder(user, appId) || isAdmin(user) } +// check if they are a builder within an app (not necessarily a global builder) +export function hasAppBuilderPermissions(user?: User | ContextUser) { + if (!user) { + return false + } + return !user.builder?.global && user.builder?.apps?.length !== 0 +} + // 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 + return user.builder?.global || hasAppBuilderPermissions(user) } // checks if a user is capable of being an admin