Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide power role for v3 apps #14813

Merged
merged 11 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions packages/backend-core/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
51 changes: 40 additions & 11 deletions packages/backend-core/src/security/roles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import semver from "semver"
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import {
prefixRoleID,
Expand All @@ -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"

Expand All @@ -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,
Expand Down Expand Up @@ -319,7 +318,7 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
}
return internal(appDB)
}
async function internal(db: any) {
async function internal(db: Database | undefined) {
let roles: RoleDoc[] = []
if (db) {
const body = await db.allDocs(
Expand All @@ -334,8 +333,26 @@ export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
}
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 =>
Expand Down Expand Up @@ -366,6 +383,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be tempted to actually put this on the root Document type and call it _creationVersion, anticipating that we will likely introduce this idea onto other documents going forward.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, then in DatabaseImpl.post we know when a doc is new because it has no ID, we could slap _creationVersion onto it there. Though it is a wide-reaching change that likely has risk.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about this - I don't think we need this on every document, since every DB will have an app_metadata (it won't load without this/is corrupt) - in theory noting it here is enough since its a static document that is always easy to retrieve.

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() {
Expand Down
41 changes: 21 additions & 20 deletions packages/server/src/api/controllers/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,8 @@ export async function fetchAppDefinition(
export async function fetchAppPackage(
ctx: UserCtx<void, FetchAppPackageResponse>
) {
const db = context.getAppDB()
const appId = context.getAppId()
let application = await db.get<App>(DocumentType.APP_METADATA)
const application = await sdk.applications.metadata.get()
const layouts = await getLayouts()
let screens = await getScreens()
const license = await licensing.cache.getCachedLicense()
Expand Down Expand Up @@ -272,14 +271,15 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
path: ctx.request.body.file?.path,
}
}

const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
const appId = generateDevAppID(generateAppID(tenantId))

return await context.doInAppContext(appId, async () => {
const instance = await createInstance(appId, instanceConfig)
const db = context.getAppDB()

let newApplication: App = {
const newApplication: App = {
_id: DocumentType.APP_METADATA,
_rev: undefined,
appId,
Expand Down Expand Up @@ -310,19 +310,26 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
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",
"theme",
"customTheme",
"icon",
"snippets",
"creationVersion",
]
keys.forEach(key => {
if (existing[key]) {
Expand All @@ -340,14 +347,10 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
}

// 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 })
Expand Down Expand Up @@ -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<App>(DocumentType.APP_METADATA)
const application = await sdk.applications.metadata.get()
const currentVersion = application.version

let manifest
Expand Down Expand Up @@ -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<App>(DocumentType.APP_METADATA)
const application = await sdk.applications.metadata.get()
if (!application.revertableVersion) {
ctx.throw(400, "There is no version to revert to")
}
Expand Down Expand Up @@ -577,7 +578,7 @@ async function destroyApp(ctx: UserCtx) {

const db = dbCore.getDB(devAppId)
// 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()
await quotas.removeApp()
await events.app.deleted(app)
Expand Down Expand Up @@ -728,7 +729,7 @@ export async function updateAppPackage(
) {
return context.doInAppContext(appId, async () => {
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 }
if (appPackage._rev !== application._rev) {
Expand All @@ -754,7 +755,7 @@ export async function setRevertableVersion(
return
}
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
await db.put(app)

Expand All @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/sdk/app/applications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
18 changes: 18 additions & 0 deletions packages/server/src/sdk/app/applications/metadata.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/types/src/documents/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface App extends Document {
usedPlugins?: Plugin[]
upgradableVersion?: string
snippets?: Snippet[]
creationVersion?: string
}

export interface AppInstance {
Expand Down
Loading
Loading