Frontend work to support logging in as an app builder - also making sure when a new app is created that the user is assigned app access to it.

This commit is contained in:
mike12345567 2023-07-20 18:34:12 +01:00
parent b84b8dd988
commit 3abe5d4cb2
8 changed files with 143 additions and 54 deletions

View File

@ -2,6 +2,7 @@ import {
directCouchFind, directCouchFind,
DocumentType, DocumentType,
generateAppUserID, generateAppUserID,
getGlobalIDFromUserMetadataID,
getGlobalUserParams, getGlobalUserParams,
getProdAppID, getProdAppID,
getUsersByAppParams, getUsersByAppParams,
@ -21,6 +22,7 @@ import {
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { getGlobalDB } from "./context" import { getGlobalDB } from "./context"
import * as context from "./context" import * as context from "./context"
import { user as userCache } from "./cache"
type GetOpts = { cleanup?: boolean } type GetOpts = { cleanup?: boolean }
@ -42,8 +44,10 @@ function removeUserPassword(users: User | User[]) {
// extract from shared-core to make easily accessible from backend-core // extract from shared-core to make easily accessible from backend-core
export const isBuilder = sdk.users.isBuilder export const isBuilder = sdk.users.isBuilder
export const isAdmin = sdk.users.isAdmin export const isAdmin = sdk.users.isAdmin
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
export const hasAdminPermissions = sdk.users.hasAdminPermissions export const hasAdminPermissions = sdk.users.hasAdminPermissions
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
export const bulkGetGlobalUsersById = async ( export const bulkGetGlobalUsersById = async (
userIds: string[], userIds: string[],
@ -77,6 +81,27 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
return (await db.bulkDocs(users)) as BulkDocsResponse 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<User> { export async function getById(id: string, opts?: GetOpts): Promise<User> {
const db = context.getGlobalDB() const db = context.getGlobalDB()
let user = await db.get<User>(id) let user = await db.get<User>(id)

View File

@ -2,8 +2,12 @@ import { derived } from "svelte/store"
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags" import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
import { admin } from "./admin" import { admin } from "./admin"
import { auth } from "./auth" import { auth } from "./auth"
import { sdk } from "@budibase/shared-core"
export const menu = derived([admin, auth], ([$admin, $auth]) => { 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 // Determine user sub pages
let userSubPages = [ let userSubPages = [
{ {
@ -24,19 +28,24 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
title: "Apps", title: "Apps",
href: "/builder/portal/apps", href: "/builder/portal/apps",
}, },
{ ]
if (
sdk.users.hasBuilderPermissions(user) &&
!sdk.users.hasAppBuilderPermissions(user)
) {
menu.push({
title: "Users", title: "Users",
href: "/builder/portal/users", href: "/builder/portal/users",
subPages: userSubPages, subPages: userSubPages,
}, })
{ }
title: "Plugins", menu.push({
href: "/builder/portal/plugins", title: "Plugins",
}, href: "/builder/portal/plugins",
] })
// Add settings page for admins // Add settings page for admins
if ($auth.isAdmin) { if (isAdmin) {
let settingsSubPages = [ let settingsSubPages = [
{ {
title: "Auth", title: "Auth",
@ -59,7 +68,7 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
href: "/builder/portal/settings/environment", href: "/builder/portal/settings/environment",
}, },
] ]
if (!$admin.cloud) { if (!cloud) {
settingsSubPages.push({ settingsSubPages.push({
title: "Version", title: "Version",
href: "/builder/portal/settings/version", href: "/builder/portal/settings/version",
@ -84,38 +93,35 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
href: "/builder/portal/account/usage", href: "/builder/portal/account/usage",
}, },
] ]
if ($auth.isAdmin) { if (isAdmin) {
accountSubPages.push({ accountSubPages.push({
title: "Audit Logs", title: "Audit Logs",
href: "/builder/portal/account/auditLogs", href: "/builder/portal/account/auditLogs",
}) })
if (!$admin.cloud) { if (!cloud) {
accountSubPages.push({ accountSubPages.push({
title: "System Logs", title: "System Logs",
href: "/builder/portal/account/systemLogs", href: "/builder/portal/account/systemLogs",
}) })
} }
} }
if ($admin.cloud && $auth?.user?.accountPortalAccess) { if (cloud && user?.accountPortalAccess) {
accountSubPages.push({ accountSubPages.push({
title: "Upgrade", title: "Upgrade",
href: $admin.accountPortalUrl + "/portal/upgrade", href: $admin?.accountPortalUrl + "/portal/upgrade",
}) })
} else if (!$admin.cloud && $auth.isAdmin) { } else if (!cloud && isAdmin) {
accountSubPages.push({ accountSubPages.push({
title: "Upgrade", title: "Upgrade",
href: "/builder/portal/account/upgrade", href: "/builder/portal/account/upgrade",
}) })
} }
// add license check here // add license check here
if ( if (user?.accountPortalAccess && user.account.stripeCustomerId) {
$auth?.user?.accountPortalAccess &&
$auth.user.account.stripeCustomerId
) {
accountSubPages.push({ accountSubPages.push({
title: "Billing", title: "Billing",
href: $admin.accountPortalUrl + "/portal/billing", href: $admin?.accountPortalUrl + "/portal/billing",
}) })
} }
menu.push({ menu.push({

View File

@ -50,12 +50,13 @@ import {
MigrationType, MigrationType,
PlanType, PlanType,
Screen, Screen,
SocketSession,
UserCtx, UserCtx,
ContextUser,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { grantAppBuilderAccess } from "@budibase/backend-core/src/users"
// utility function, need to do away with this // utility function, need to do away with this
async function getLayouts() { async function getLayouts() {
@ -178,32 +179,10 @@ export const addSampleData = async (ctx: UserCtx) => {
} }
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
const dev = ctx.query && ctx.query.status === AppStatus.DEV ctx.body = await sdk.applications.fetch(
const all = ctx.query && ctx.query.status === AppStatus.ALL ctx.query.status as AppStatus,
const apps = (await dbCore.getAllApps({ dev, all })) as App[] ctx.user
)
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)
} }
export async function fetchAppDefinition(ctx: UserCtx) { export async function fetchAppDefinition(ctx: UserCtx) {
@ -395,6 +374,10 @@ async function appPostCreate(ctx: UserCtx, app: App) {
tenantId, tenantId,
appId: app.appId, 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) await creationEvents(ctx.request, app)
// app import & template creation // app import & template creation
if (ctx.request.body.useTemplate === "true") { if (ctx.request.body.useTemplate === "true") {

View File

@ -3,10 +3,10 @@ import { db as dbCore } from "@budibase/backend-core"
type Optional = string | null type Optional = string | null
export const AppStatus = { export enum AppStatus {
DEV: "development", DEV = "development",
ALL: "all", ALL = "all",
DEPLOYED: "published", DEPLOYED = "published",
} }
export const BudibaseInternalDB = { export const BudibaseInternalDB = {

View File

@ -5,7 +5,7 @@ import {
context, context,
users, users,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { Role } from "@budibase/types" import { Role, UserCtx } from "@budibase/types"
import builderMiddleware from "./builder" import builderMiddleware from "./builder"
import { isWebhookEndpoint } from "./utils" import { isWebhookEndpoint } from "./utils"
@ -22,14 +22,19 @@ const csrf = auth.buildCsrfMiddleware()
* - Otherwise the user must have the required role. * - Otherwise the user must have the required role.
*/ */
const checkAuthorized = async ( const checkAuthorized = async (
ctx: any, ctx: UserCtx,
resourceRoles: any, resourceRoles: any,
permType: any, permType: any,
permLevel: any permLevel: any
) => { ) => {
const appId = context.getAppId() const appId = context.getAppId()
// check if this is a builder api and the user is not a builder // 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 const isBuilderApi = permType === permissions.PermissionType.BUILDER
if (isBuilderApi && !isBuilder) { if (isBuilderApi && !isBuilder) {
return ctx.throw(403, "Not Authorized") return ctx.throw(403, "Not Authorized")

View File

@ -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)
}

View File

@ -1,7 +1,9 @@
import * as sync from "./sync" import * as sync from "./sync"
import * as utils from "./utils" import * as utils from "./utils"
import * as applications from "./applications"
export default { export default {
...sync, ...sync,
...utils, ...utils,
...applications,
} }

View File

@ -3,6 +3,9 @@ import { getProdAppID } from "./applications"
// checks if a user is specifically a builder, given an app ID // checks if a user is specifically a builder, given an app ID
export function isBuilder(user: User | ContextUser, appId?: string) { export function isBuilder(user: User | ContextUser, appId?: string) {
if (!user) {
return false
}
if (user.builder?.global) { if (user.builder?.global) {
return true return true
} else if (appId && user.builder?.apps?.includes(getProdAppID(appId))) { } 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 // in future whether someone has admin permissions and whether they are
// an admin for a specific resource could be separated // an admin for a specific resource could be separated
export function isAdmin(user: User | ContextUser) { export function isAdmin(user: User | ContextUser) {
if (!user) {
return false
}
return hasAdminPermissions(user) return hasAdminPermissions(user)
} }
@ -22,12 +28,20 @@ export function isAdminOrBuilder(user: User | ContextUser, appId?: string) {
return isBuilder(user, appId) || isAdmin(user) 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 // checks if a user is capable of building any app
export function hasBuilderPermissions(user?: User | ContextUser) { export function hasBuilderPermissions(user?: User | ContextUser) {
if (!user) { if (!user) {
return false 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 // checks if a user is capable of being an admin