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,
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<User> {
const db = context.getGlobalDB()
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 { 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({

View File

@ -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") {

View File

@ -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 = {

View File

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

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 utils from "./utils"
import * as applications from "./applications"
export default {
...sync,
...utils,
...applications,
}

View File

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