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:
parent
b84b8dd988
commit
3abe5d4cb2
|
@ -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)
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import * as sync from "./sync"
|
||||
import * as utils from "./utils"
|
||||
import * as applications from "./applications"
|
||||
|
||||
export default {
|
||||
...sync,
|
||||
...utils,
|
||||
...applications,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue