Merge branch 'master' into feature/role-multi-inheritance

This commit is contained in:
Michael Drury 2024-10-17 17:01:27 +01:00 committed by GitHub
commit aaf5debc52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 171 additions and 49 deletions

View File

@ -54,30 +54,46 @@ function getPackageJsonFields(): {
VERSION: string VERSION: string
SERVICE_NAME: string SERVICE_NAME: string
} { } {
function findFileInAncestors( function getParentFile(file: string) {
fileName: string, function findFileInAncestors(
currentDir: string fileName: string,
): string | null { currentDir: string
const filePath = `${currentDir}/${fileName}` ): string | null {
if (existsSync(filePath)) { const filePath = `${currentDir}/${fileName}`
return filePath if (existsSync(filePath)) {
return filePath
}
const parentDir = `${currentDir}/..`
if (parentDir === currentDir) {
// reached root directory
return null
}
return findFileInAncestors(fileName, parentDir)
} }
const parentDir = `${currentDir}/..` const packageJsonFile = findFileInAncestors(file, process.cwd())
if (parentDir === currentDir) { const content = readFileSync(packageJsonFile!, "utf-8")
// reached root directory const parsedContent = JSON.parse(content)
return null return parsedContent
} }
return findFileInAncestors(fileName, parentDir) let localVersion: string | undefined
if (isDev() && !isTest()) {
try {
const lerna = getParentFile("lerna.json")
localVersion = lerna.version
} catch {
//
}
} }
try { try {
const packageJsonFile = findFileInAncestors("package.json", process.cwd()) const parsedContent = getParentFile("package.json")
const content = readFileSync(packageJsonFile!, "utf-8")
const parsedContent = JSON.parse(content)
return { return {
VERSION: process.env.BUDIBASE_VERSION || parsedContent.version, VERSION:
localVersion || process.env.BUDIBASE_VERSION || parsedContent.version,
SERVICE_NAME: parsedContent.name, SERVICE_NAME: parsedContent.name,
} }
} catch { } catch {

View File

@ -1,3 +1,4 @@
import semver from "semver"
import { BuiltinPermissionID, PermissionLevel } from "./permissions" import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import { import {
prefixRoleID, prefixRoleID,
@ -7,7 +8,13 @@ import {
doWithDB, doWithDB,
} from "../db" } from "../db"
import { getAppDB } from "../context" import { getAppDB } from "../context"
import { Screen, Role as RoleDoc, RoleUIMetadata } from "@budibase/types" import {
Screen,
Role as RoleDoc,
RoleUIMetadata,
Database,
App,
} from "@budibase/types"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"
import { RoleColor, helpers } from "@budibase/shared-core" import { RoleColor, helpers } from "@budibase/shared-core"
import { uniqBy } from "lodash" import { uniqBy } from "lodash"
@ -24,14 +31,6 @@ const BUILTIN_IDS = {
BUILDER: "BUILDER", BUILDER: "BUILDER",
} }
// exclude internal roles like builder
const EXTERNAL_BUILTIN_ROLE_IDS = [
BUILTIN_IDS.ADMIN,
BUILTIN_IDS.POWER,
BUILTIN_IDS.BASIC,
BUILTIN_IDS.PUBLIC,
]
export const RoleIDVersion = { export const RoleIDVersion = {
// original version, with a UUID based ID // original version, with a UUID based ID
UUID: undefined, UUID: undefined,
@ -432,7 +431,7 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
} }
return internal(appDB) return internal(appDB)
} }
async function internal(db: any) { async function internal(db: Database | undefined) {
let roles: RoleDoc[] = [] let roles: RoleDoc[] = []
if (db) { if (db) {
const body = await db.allDocs( const body = await db.allDocs(
@ -447,8 +446,26 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
} }
const builtinRoles = getBuiltinRoles() const builtinRoles = getBuiltinRoles()
// exclude internal roles like builder
let externalBuiltinRoles = []
if (!db || (await shouldIncludePowerRole(db))) {
externalBuiltinRoles = [
BUILTIN_IDS.ADMIN,
BUILTIN_IDS.POWER,
BUILTIN_IDS.BASIC,
BUILTIN_IDS.PUBLIC,
]
} else {
externalBuiltinRoles = [
BUILTIN_IDS.ADMIN,
BUILTIN_IDS.BASIC,
BUILTIN_IDS.PUBLIC,
]
}
// need to combine builtin with any DB record of them (for sake of permissions) // need to combine builtin with any DB record of them (for sake of permissions)
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) { for (let builtinRoleId of externalBuiltinRoles) {
const builtinRole = builtinRoles[builtinRoleId] const builtinRole = builtinRoles[builtinRoleId]
const dbBuiltin = roles.filter( const dbBuiltin = roles.filter(
dbRole => dbRole =>
@ -479,6 +496,18 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
} }
} }
async function shouldIncludePowerRole(db: Database) {
const app = await db.tryGet<App>(DocumentType.APP_METADATA)
const creationVersion = app?.creationVersion
if (!creationVersion || !semver.valid(creationVersion)) {
// Old apps don't have creationVersion, so we should include it for backward compatibility
return true
}
const isGreaterThan3x = semver.gte(creationVersion, "3.0.0")
return !isGreaterThan3x
}
export class AccessController { export class AccessController {
userHierarchies: { [key: string]: string[] } userHierarchies: { [key: string]: string[] }
constructor() { constructor() {

View File

@ -208,9 +208,8 @@ export async function fetchAppDefinition(
export async function fetchAppPackage( export async function fetchAppPackage(
ctx: UserCtx<void, FetchAppPackageResponse> ctx: UserCtx<void, FetchAppPackageResponse>
) { ) {
const db = context.getAppDB()
const appId = context.getAppId() const appId = context.getAppId()
let application = await db.get<App>(DocumentType.APP_METADATA) const application = await sdk.applications.metadata.get()
const layouts = await getLayouts() const layouts = await getLayouts()
let screens = await getScreens() let screens = await getScreens()
const license = await licensing.cache.getCachedLicense() const license = await licensing.cache.getCachedLicense()
@ -272,6 +271,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
path: ctx.request.body.file?.path, path: ctx.request.body.file?.path,
} }
} }
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
const appId = generateDevAppID(generateAppID(tenantId)) const appId = generateDevAppID(generateAppID(tenantId))
@ -279,7 +279,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
const instance = await createInstance(appId, instanceConfig) const instance = await createInstance(appId, instanceConfig)
const db = context.getAppDB() const db = context.getAppDB()
let newApplication: App = { const newApplication: App = {
_id: DocumentType.APP_METADATA, _id: DocumentType.APP_METADATA,
_rev: undefined, _rev: undefined,
appId, appId,
@ -310,12 +310,18 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
disableUserMetadata: true, disableUserMetadata: true,
skeletonLoader: true, skeletonLoader: true,
}, },
creationVersion: undefined,
} }
const isImport = !!instanceConfig.file
if (!isImport) {
newApplication.creationVersion = envCore.VERSION
}
const existing = await sdk.applications.metadata.tryGet()
// If we used a template or imported an app there will be an existing doc. // If we used a template or imported an app there will be an existing doc.
// Fetch and migrate some metadata from the existing app. // Fetch and migrate some metadata from the existing app.
try { if (existing) {
const existing: App = await db.get(DocumentType.APP_METADATA)
const keys: (keyof App)[] = [ const keys: (keyof App)[] = [
"_rev", "_rev",
"navigation", "navigation",
@ -323,6 +329,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
"customTheme", "customTheme",
"icon", "icon",
"snippets", "snippets",
"creationVersion",
] ]
keys.forEach(key => { keys.forEach(key => {
if (existing[key]) { if (existing[key]) {
@ -340,14 +347,10 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
} }
// Migrate navigation settings and screens if required // Migrate navigation settings and screens if required
if (existing) { const navigation = await migrateAppNavigation()
const navigation = await migrateAppNavigation() if (navigation) {
if (navigation) { newApplication.navigation = navigation
newApplication.navigation = navigation
}
} }
} catch (err) {
// Nothing to do
} }
const response = await db.put(newApplication, { force: true }) const response = await db.put(newApplication, { force: true })
@ -489,8 +492,7 @@ export async function update(
export async function updateClient(ctx: UserCtx) { export async function updateClient(ctx: UserCtx) {
// Get current app version // Get current app version
const db = context.getAppDB() const application = await sdk.applications.metadata.get()
const application = await db.get<App>(DocumentType.APP_METADATA)
const currentVersion = application.version const currentVersion = application.version
let manifest let manifest
@ -518,8 +520,7 @@ export async function updateClient(ctx: UserCtx) {
export async function revertClient(ctx: UserCtx) { export async function revertClient(ctx: UserCtx) {
// Check app can be reverted // Check app can be reverted
const db = context.getAppDB() const application = await sdk.applications.metadata.get()
const application = await db.get<App>(DocumentType.APP_METADATA)
if (!application.revertableVersion) { if (!application.revertableVersion) {
ctx.throw(400, "There is no version to revert to") ctx.throw(400, "There is no version to revert to")
} }
@ -577,7 +578,7 @@ async function destroyApp(ctx: UserCtx) {
const db = dbCore.getDB(devAppId) const db = dbCore.getDB(devAppId)
// standard app deletion flow // standard app deletion flow
const app = await db.get<App>(DocumentType.APP_METADATA) const app = await sdk.applications.metadata.get()
const result = await db.destroy() const result = await db.destroy()
await quotas.removeApp() await quotas.removeApp()
await events.app.deleted(app) await events.app.deleted(app)
@ -728,7 +729,7 @@ export async function updateAppPackage(
) { ) {
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
const db = context.getAppDB() const db = context.getAppDB()
const application = await db.get<App>(DocumentType.APP_METADATA) const application = await sdk.applications.metadata.get()
const newAppPackage: App = { ...application, ...appPackage } const newAppPackage: App = { ...application, ...appPackage }
if (appPackage._rev !== application._rev) { if (appPackage._rev !== application._rev) {
@ -754,7 +755,7 @@ export async function setRevertableVersion(
return return
} }
const db = context.getAppDB() const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA) const app = await sdk.applications.metadata.get()
app.revertableVersion = ctx.request.body.revertableVersion app.revertableVersion = ctx.request.body.revertableVersion
await db.put(app) await db.put(app)
@ -763,7 +764,7 @@ export async function setRevertableVersion(
async function migrateAppNavigation() { async function migrateAppNavigation() {
const db = context.getAppDB() const db = context.getAppDB()
const existing: App = await db.get(DocumentType.APP_METADATA) const existing = await sdk.applications.metadata.get()
const layouts: Layout[] = await getLayouts() const layouts: Layout[] = await getLayouts()
const screens: Screen[] = await getScreens() const screens: Screen[] = await getScreens()

View File

@ -2,10 +2,12 @@ import * as sync from "./sync"
import * as utils from "./utils" import * as utils from "./utils"
import * as applications from "./applications" import * as applications from "./applications"
import * as imports from "./import" import * as imports from "./import"
import * as metadata from "./metadata"
export default { export default {
...sync, ...sync,
...utils, ...utils,
...applications, ...applications,
...imports, ...imports,
metadata,
} }

View File

@ -0,0 +1,18 @@
import { context, DocumentType } from "@budibase/backend-core"
import { App } from "@budibase/types"
/**
* @deprecated the plan is to get everything using `tryGet` instead, then rename
* `tryGet` to `get`.
*/
export async function get() {
const db = context.getAppDB()
const application = await db.get<App>(DocumentType.APP_METADATA)
return application
}
export async function tryGet() {
const db = context.getAppDB()
const application = await db.tryGet<App>(DocumentType.APP_METADATA)
return application
}

View File

@ -27,6 +27,7 @@ export interface App extends Document {
usedPlugins?: Plugin[] usedPlugins?: Plugin[]
upgradableVersion?: string upgradableVersion?: string
snippets?: Snippet[] snippets?: Snippet[]
creationVersion?: string
} }
export interface AppInstance { export interface AppInstance {

View File

@ -1,6 +1,6 @@
import { structures, TestConfiguration } from "../../../../tests" import { structures, TestConfiguration } from "../../../../tests"
import { context, db, permissions, roles } from "@budibase/backend-core" import { context, db, permissions, roles } from "@budibase/backend-core"
import { Database } from "@budibase/types" import { App, Database } from "@budibase/types"
jest.mock("@budibase/backend-core", () => { jest.mock("@budibase/backend-core", () => {
const core = jest.requireActual("@budibase/backend-core") const core = jest.requireActual("@budibase/backend-core")
@ -30,6 +30,14 @@ async function addAppMetadata() {
}) })
} }
async function updateAppMetadata(update: Partial<Omit<App, "_id" | "_rev">>) {
const app = await appDb.get("app_metadata")
await appDb.put({
...app,
...update,
})
}
describe("/api/global/roles", () => { describe("/api/global/roles", () => {
const config = new TestConfiguration() const config = new TestConfiguration()
@ -69,6 +77,53 @@ describe("/api/global/roles", () => {
expect(res.body[appId].roles.length).toEqual(5) expect(res.body[appId].roles.length).toEqual(5)
expect(res.body[appId].roles.map((r: any) => r._id)).toContain(ROLE_NAME) expect(res.body[appId].roles.map((r: any) => r._id)).toContain(ROLE_NAME)
}) })
it.each(["3.0.0", "3.0.1", "3.1.0", "3.0.0+2146.b125a7c"])(
"exclude POWER roles after v3 (%s)",
async creationVersion => {
await updateAppMetadata({ creationVersion })
const res = await config.api.roles.get()
expect(res.body).toBeDefined()
expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
ROLE_NAME,
roles.BUILTIN_ROLE_IDS.ADMIN,
roles.BUILTIN_ROLE_IDS.BASIC,
roles.BUILTIN_ROLE_IDS.PUBLIC,
])
}
)
it.each(["2.9.0", "1.0.0", "0.0.0", "2.32.17+2146.b125a7c"])(
"include POWER roles before v3 (%s)",
async creationVersion => {
await updateAppMetadata({ creationVersion })
const res = await config.api.roles.get()
expect(res.body).toBeDefined()
expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
ROLE_NAME,
roles.BUILTIN_ROLE_IDS.ADMIN,
roles.BUILTIN_ROLE_IDS.POWER,
roles.BUILTIN_ROLE_IDS.BASIC,
roles.BUILTIN_ROLE_IDS.PUBLIC,
])
}
)
it.each(["invalid", ""])(
"include POWER roles when the version is corrupted (%s)",
async creationVersion => {
await updateAppMetadata({ creationVersion })
const res = await config.api.roles.get()
expect(res.body[appId].roles.map((r: any) => r._id)).toEqual([
ROLE_NAME,
roles.BUILTIN_ROLE_IDS.ADMIN,
roles.BUILTIN_ROLE_IDS.POWER,
roles.BUILTIN_ROLE_IDS.BASIC,
roles.BUILTIN_ROLE_IDS.PUBLIC,
])
}
)
}) })
describe("GET api/global/roles/:appId", () => { describe("GET api/global/roles/:appId", () => {