diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 2ab8c550ccf..4e93e8d9ee9 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -54,30 +54,46 @@ function getPackageJsonFields(): { VERSION: string SERVICE_NAME: string } { - function findFileInAncestors( - fileName: string, - currentDir: string - ): string | null { - const filePath = `${currentDir}/${fileName}` - if (existsSync(filePath)) { - return filePath - } + function getParentFile(file: string) { + function findFileInAncestors( + fileName: string, + currentDir: string + ): string | null { + const filePath = `${currentDir}/${fileName}` + if (existsSync(filePath)) { + return filePath + } + + const parentDir = `${currentDir}/..` + if (parentDir === currentDir) { + // reached root directory + return null + } - const parentDir = `${currentDir}/..` - if (parentDir === currentDir) { - // reached root directory - return null + return findFileInAncestors(fileName, parentDir) } - return findFileInAncestors(fileName, parentDir) + const packageJsonFile = findFileInAncestors(file, process.cwd()) + const content = readFileSync(packageJsonFile!, "utf-8") + const parsedContent = JSON.parse(content) + return parsedContent + } + + let localVersion: string | undefined + if (isDev() && !isTest()) { + try { + const lerna = getParentFile("lerna.json") + localVersion = lerna.version + } catch { + // + } } try { - const packageJsonFile = findFileInAncestors("package.json", process.cwd()) - const content = readFileSync(packageJsonFile!, "utf-8") - const parsedContent = JSON.parse(content) + const parsedContent = getParentFile("package.json") return { - VERSION: process.env.BUDIBASE_VERSION || parsedContent.version, + VERSION: + localVersion || process.env.BUDIBASE_VERSION || parsedContent.version, SERVICE_NAME: parsedContent.name, } } catch { diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index fa2d114d7dd..fad5f7cb741 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -1,3 +1,4 @@ +import semver from "semver" import { BuiltinPermissionID, PermissionLevel } from "./permissions" import { prefixRoleID, @@ -7,7 +8,13 @@ import { doWithDB, } from "../db" 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 { RoleColor } from "@budibase/shared-core" @@ -23,14 +30,6 @@ const BUILTIN_IDS = { 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 = { // original version, with a UUID based ID UUID: undefined, @@ -319,7 +318,7 @@ export async function getAllRoles(appId?: string): Promise { } return internal(appDB) } - async function internal(db: any) { + async function internal(db: Database | undefined) { let roles: RoleDoc[] = [] if (db) { const body = await db.allDocs( @@ -334,8 +333,26 @@ export async function getAllRoles(appId?: string): Promise { } 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) - for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) { + for (let builtinRoleId of externalBuiltinRoles) { const builtinRole = builtinRoles[builtinRoleId] const dbBuiltin = roles.filter( dbRole => @@ -366,6 +383,18 @@ export async function getAllRoles(appId?: string): Promise { } } +async function shouldIncludePowerRole(db: Database) { + const app = await db.tryGet(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 { userHierarchies: { [key: string]: string[] } constructor() { diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 830acc55bf8..59f67540fec 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -208,9 +208,8 @@ export async function fetchAppDefinition( export async function fetchAppPackage( ctx: UserCtx ) { - const db = context.getAppDB() const appId = context.getAppId() - let application = await db.get(DocumentType.APP_METADATA) + const application = await sdk.applications.metadata.get() const layouts = await getLayouts() let screens = await getScreens() const license = await licensing.cache.getCachedLicense() @@ -272,6 +271,7 @@ async function performAppCreate(ctx: UserCtx) { path: ctx.request.body.file?.path, } } + const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null const appId = generateDevAppID(generateAppID(tenantId)) @@ -279,7 +279,7 @@ async function performAppCreate(ctx: UserCtx) { const instance = await createInstance(appId, instanceConfig) const db = context.getAppDB() - let newApplication: App = { + const newApplication: App = { _id: DocumentType.APP_METADATA, _rev: undefined, appId, @@ -310,12 +310,18 @@ async function performAppCreate(ctx: UserCtx) { disableUserMetadata: 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. // Fetch and migrate some metadata from the existing app. - try { - const existing: App = await db.get(DocumentType.APP_METADATA) + if (existing) { const keys: (keyof App)[] = [ "_rev", "navigation", @@ -323,6 +329,7 @@ async function performAppCreate(ctx: UserCtx) { "customTheme", "icon", "snippets", + "creationVersion", ] keys.forEach(key => { if (existing[key]) { @@ -340,14 +347,10 @@ async function performAppCreate(ctx: UserCtx) { } // Migrate navigation settings and screens if required - if (existing) { - const navigation = await migrateAppNavigation() - if (navigation) { - newApplication.navigation = navigation - } + const navigation = await migrateAppNavigation() + if (navigation) { + newApplication.navigation = navigation } - } catch (err) { - // Nothing to do } const response = await db.put(newApplication, { force: true }) @@ -489,8 +492,7 @@ export async function update( export async function updateClient(ctx: UserCtx) { // Get current app version - const db = context.getAppDB() - const application = await db.get(DocumentType.APP_METADATA) + const application = await sdk.applications.metadata.get() const currentVersion = application.version let manifest @@ -518,8 +520,7 @@ export async function updateClient(ctx: UserCtx) { export async function revertClient(ctx: UserCtx) { // Check app can be reverted - const db = context.getAppDB() - const application = await db.get(DocumentType.APP_METADATA) + const application = await sdk.applications.metadata.get() if (!application.revertableVersion) { ctx.throw(400, "There is no version to revert to") } @@ -577,7 +578,7 @@ async function destroyApp(ctx: UserCtx) { const db = dbCore.getDB(devAppId) // standard app deletion flow - const app = await db.get(DocumentType.APP_METADATA) + const app = await sdk.applications.metadata.get() const result = await db.destroy() await quotas.removeApp() await events.app.deleted(app) @@ -728,7 +729,7 @@ export async function updateAppPackage( ) { return context.doInAppContext(appId, async () => { const db = context.getAppDB() - const application = await db.get(DocumentType.APP_METADATA) + const application = await sdk.applications.metadata.get() const newAppPackage: App = { ...application, ...appPackage } if (appPackage._rev !== application._rev) { @@ -754,7 +755,7 @@ export async function setRevertableVersion( return } const db = context.getAppDB() - const app = await db.get(DocumentType.APP_METADATA) + const app = await sdk.applications.metadata.get() app.revertableVersion = ctx.request.body.revertableVersion await db.put(app) @@ -763,7 +764,7 @@ export async function setRevertableVersion( async function migrateAppNavigation() { 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 screens: Screen[] = await getScreens() diff --git a/packages/server/src/sdk/app/applications/index.ts b/packages/server/src/sdk/app/applications/index.ts index 04ed3b29195..f315e848969 100644 --- a/packages/server/src/sdk/app/applications/index.ts +++ b/packages/server/src/sdk/app/applications/index.ts @@ -2,10 +2,12 @@ import * as sync from "./sync" import * as utils from "./utils" import * as applications from "./applications" import * as imports from "./import" +import * as metadata from "./metadata" export default { ...sync, ...utils, ...applications, ...imports, + metadata, } diff --git a/packages/server/src/sdk/app/applications/metadata.ts b/packages/server/src/sdk/app/applications/metadata.ts new file mode 100644 index 00000000000..bbda55c085b --- /dev/null +++ b/packages/server/src/sdk/app/applications/metadata.ts @@ -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(DocumentType.APP_METADATA) + return application +} + +export async function tryGet() { + const db = context.getAppDB() + const application = await db.tryGet(DocumentType.APP_METADATA) + return application +} diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts index 69bf3489e1d..06fca8307c0 100644 --- a/packages/types/src/documents/app/app.ts +++ b/packages/types/src/documents/app/app.ts @@ -27,6 +27,7 @@ export interface App extends Document { usedPlugins?: Plugin[] upgradableVersion?: string snippets?: Snippet[] + creationVersion?: string } export interface AppInstance { diff --git a/packages/worker/src/api/routes/global/tests/roles.spec.ts b/packages/worker/src/api/routes/global/tests/roles.spec.ts index 11de06328e4..28977d85215 100644 --- a/packages/worker/src/api/routes/global/tests/roles.spec.ts +++ b/packages/worker/src/api/routes/global/tests/roles.spec.ts @@ -1,6 +1,6 @@ import { structures, TestConfiguration } from "../../../../tests" 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", () => { const core = jest.requireActual("@budibase/backend-core") @@ -30,6 +30,14 @@ async function addAppMetadata() { }) } +async function updateAppMetadata(update: Partial>) { + const app = await appDb.get("app_metadata") + await appDb.put({ + ...app, + ...update, + }) +} + describe("/api/global/roles", () => { 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.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", () => {