diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index 62f4e8820f..45ca675fa6 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -20,6 +20,7 @@ export enum ViewName { AUTOMATION_LOGS = "automation_logs", ACCOUNT_BY_EMAIL = "account_by_email", PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", + USER_BY_GROUP = "by_group_user", } export const DeprecatedViews = { diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index c337d26eaa..f0fff918fc 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -36,154 +36,91 @@ async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) { } } -export const createNewUserEmailView = async () => { - const db = getGlobalDB() +export async function createView(db: any, viewJs: string, viewName: string) { let designDoc try { - designDoc = await db.get(DESIGN_DB) + designDoc = (await db.get(DESIGN_DB)) as DesignDocument } catch (err) { // no design doc, make one designDoc = DesignDoc() } const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }`, + map: viewJs, } designDoc.views = { ...designDoc.views, - [ViewName.USER_BY_EMAIL]: view, + [viewName]: view, } await db.put(designDoc) } +export const createNewUserEmailView = async () => { + const db = getGlobalDB() + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` + await createView(db, viewJs, ViewName.USER_BY_EMAIL) +} + export const createAccountEmailView = async () => { + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` await doWithDB( StaticDatabases.PLATFORM_INFO.name, async (db: PouchDB.Database) => { - let designDoc - try { - designDoc = await db.get(DESIGN_DB) - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.ACCOUNT_BY_EMAIL]: view, - } - await db.put(designDoc) + await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) } ) } export const createUserAppView = async () => { const db = getGlobalDB() as PouchDB.Database - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { - for (let prodAppId of Object.keys(doc.roles)) { - let emitted = prodAppId + "${SEPARATOR}" + doc._id - emit(emitted, null) - } + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { + for (let prodAppId of Object.keys(doc.roles)) { + let emitted = prodAppId + "${SEPARATOR}" + doc._id + emit(emitted, null) } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.USER_BY_APP]: view, - } - await db.put(designDoc) + } + }` + await createView(db, viewJs, ViewName.USER_BY_APP) } export const createApiKeyView = async () => { const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - designDoc = DesignDoc() - } - const view = { - map: `function(doc) { - if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) { - emit(doc.apiKey, doc.userId) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.BY_API_KEY]: view, - } - await db.put(designDoc) + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) { + emit(doc.apiKey, doc.userId) + } + }` + await createView(db, viewJs, ViewName.BY_API_KEY) } export const createUserBuildersView = async () => { const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - map: `function(doc) { - if (doc.builder && doc.builder.global === true) { - emit(doc._id, doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.USER_BY_BUILDERS]: view, - } - await db.put(designDoc) + const viewJs = `function(doc) { + if (doc.builder && doc.builder.global === true) { + emit(doc._id, doc._id) + } + }` + await createView(db, viewJs, ViewName.USER_BY_BUILDERS) } export const createPlatformUserView = async () => { + const viewJs = `function(doc) { + if (doc.tenantId) { + emit(doc._id.toLowerCase(), doc._id) + } + }` await doWithDB( StaticDatabases.PLATFORM_INFO.name, async (db: PouchDB.Database) => { - let designDoc - try { - designDoc = await db.get(DESIGN_DB) - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc.tenantId) { - emit(doc._id.toLowerCase(), doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewName.PLATFORM_USERS_LOWERCASE]: view, - } - await db.put(designDoc) + await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) } ) } @@ -196,7 +133,7 @@ export const queryView = async ( viewName: ViewName, params: PouchDB.Query.Options, db: PouchDB.Database, - CreateFuncByName: any, + createFunc: any, opts?: QueryViewOptions ): Promise => { try { @@ -213,10 +150,9 @@ export const queryView = async ( } } catch (err: any) { if (err != null && err.name === "not_found") { - const createFunc = CreateFuncByName[viewName] await removeDeprecated(db, viewName) await createFunc() - return queryView(viewName, params, db, CreateFuncByName, opts) + return queryView(viewName, params, db, createFunc, opts) } else { throw err } @@ -228,7 +164,7 @@ export const queryPlatformView = async ( params: PouchDB.Query.Options, opts?: QueryViewOptions ): Promise => { - const CreateFuncByName = { + const CreateFuncByName: any = { [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, } @@ -236,7 +172,8 @@ export const queryPlatformView = async ( return doWithDB( StaticDatabases.PLATFORM_INFO.name, async (db: PouchDB.Database) => { - return queryView(viewName, params, db, CreateFuncByName, opts) + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) } ) } @@ -247,7 +184,7 @@ export const queryGlobalView = async ( db?: PouchDB.Database, opts?: QueryViewOptions ): Promise => { - const CreateFuncByName = { + const CreateFuncByName: any = { [ViewName.USER_BY_EMAIL]: createNewUserEmailView, [ViewName.BY_API_KEY]: createApiKeyView, [ViewName.USER_BY_BUILDERS]: createUserBuildersView, @@ -257,5 +194,6 @@ export const queryGlobalView = async ( if (!db) { db = getGlobalDB() as PouchDB.Database } - return queryView(viewName, params, db, CreateFuncByName, opts) + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) } diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts index d300873725..b4fd0d1469 100644 --- a/packages/backend-core/src/events/publishers/group.ts +++ b/packages/backend-core/src/events/publishers/group.ts @@ -40,9 +40,9 @@ export async function usersAdded(count: number, group: UserGroup) { await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) } -export async function usersDeleted(emails: string[], group: UserGroup) { +export async function usersDeleted(count: number, group: UserGroup) { const properties: GroupUsersDeletedEvent = { - count: emails.length, + count, groupId: group._id as string, } await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) diff --git a/packages/backend-core/src/featureFlags/index.js b/packages/backend-core/src/featureFlags/index.js index b328839fda..8a8162d0ba 100644 --- a/packages/backend-core/src/featureFlags/index.js +++ b/packages/backend-core/src/featureFlags/index.js @@ -53,7 +53,7 @@ exports.getTenantFeatureFlags = tenantId => { return flags } -exports.FeatureFlag = { +exports.TenantFeatureFlag = { LICENSING: "LICENSING", GOOGLE_SHEETS: "GOOGLE_SHEETS", USER_GROUPS: "USER_GROUPS", diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 2c234bd4b8..83b23b479d 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -18,6 +18,7 @@ import * as logging from "./logging" import pino from "./pino" import * as middleware from "./middleware" import plugins from "./plugin" +import encryption from "./security/encryption" // mimic the outer package exports import * as db from "./pkg/db" @@ -60,6 +61,7 @@ const core = { ...pino, ...errorClasses, middleware, + encryption, } export = core diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 983aebf676..33c9123b63 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -78,7 +78,7 @@ function isBuiltin(role) { */ exports.builtinRoleToNumber = id => { const builtins = exports.getBuiltinRoles() - const MAX = Object.values(BUILTIN_IDS).length + 1 + const MAX = Object.values(builtins).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { return MAX } @@ -94,6 +94,22 @@ exports.builtinRoleToNumber = id => { return count } +/** + * Converts any role to a number, but has to be async to get the roles from db. + */ +exports.roleToNumber = async id => { + if (exports.isBuiltin(id)) { + return exports.builtinRoleToNumber(id) + } + const hierarchy = await exports.getUserRoleHierarchy(id) + for (let role of hierarchy) { + if (isBuiltin(role.inherits)) { + return exports.builtinRoleToNumber(role.inherits) + 1 + } + } + return 0 +} + /** * Returns whichever builtin roleID is lower. */ @@ -172,7 +188,7 @@ async function getAllUserRoles(userRoleId) { * to determine if a user can access something that requires a specific role. * @param {string} userRoleId The user's role ID, this can be found in their access token. * @param {object} opts Various options, such as whether to only retrieve the IDs (default true). - * @returns {Promise} returns an ordered array of the roles, with the first being their + * @returns {Promise} returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => { diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index a100888212..ad5c6b5287 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -121,7 +121,7 @@ export const getTenantUser = async ( return response } -export const isUserInAppTenant = (appId: string, user: any) => { +export const isUserInAppTenant = (appId: string, user?: any) => { let userTenantId if (user) { userTenantId = user.tenantId || DEFAULT_TENANT_ID diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 0793eeb1d9..44f04749c9 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -6,7 +6,24 @@ import { } from "./db/utils" import { queryGlobalView } from "./db/views" import { UNICODE_MAX } from "./db/constants" -import { User } from "@budibase/types" +import { BulkDocsResponse, User } from "@budibase/types" +import { getGlobalDB } from "./context" +import PouchDB from "pouchdb" + +export const bulkGetGlobalUsersById = async (userIds: string[]) => { + const db = getGlobalDB() as PouchDB.Database + return ( + await db.allDocs({ + keys: userIds, + include_docs: true, + }) + ).rows.map(row => row.doc) as User[] +} + +export const bulkUpdateGlobalUsers = async (users: User[]) => { + const db = getGlobalDB() as PouchDB.Database + return (await db.bulkDocs(users)) as BulkDocsResponse +} /** * Given an email address this will use a view to search through diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte index 28cb2b2a4e..1607876b46 100644 --- a/packages/bbui/src/Form/Core/PickerDropdown.svelte +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -9,13 +9,13 @@ import StatusLight from "../../StatusLight/StatusLight.svelte" import Detail from "../../Typography/Detail.svelte" import Search from "./Search.svelte" + import IconAvatar from "../../Icon/IconAvatar.svelte" export let primaryLabel = "" export let primaryValue = null export let id = null export let placeholder = "Choose an option or type" export let disabled = false - export let updateOnChange = true export let error = null export let secondaryOptions = [] export let primaryOptions = [] @@ -204,19 +204,11 @@ })} > {#if primaryOptions[title].getIcon(option)} -
-
- -
-
+ {:else if getPrimaryOptionColour(option, idx)} {/if} - {primaryOptions[title].getLabel(option)} - +
+ +
+ + diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte index c9e4e397e2..40d3c5541c 100644 --- a/packages/bbui/src/List/ListItem.svelte +++ b/packages/bbui/src/List/ListItem.svelte @@ -1,11 +1,12 @@ - +
Headers @@ -61,7 +61,7 @@
- +
Authentication @@ -73,7 +73,7 @@ - +
Variables diff --git a/packages/builder/src/components/common/DashCard.svelte b/packages/builder/src/components/common/DashCard.svelte index d5d9d2ff37..40c7133c42 100644 --- a/packages/builder/src/components/common/DashCard.svelte +++ b/packages/builder/src/components/common/DashCard.svelte @@ -30,13 +30,14 @@ background: var(--spectrum-alias-background-color-primary); border-radius: var(--border-radius-s); overflow: hidden; - min-height: 150px; + min-height: 170px; } .dash-card-header { padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400); border-bottom: 1px solid var(--spectrum-global-color-gray-300); display: flex; justify-content: space-between; + transition: background-color 130ms ease-out; } .dash-card-body { padding: var(--spacing-xl) calc(var(--spacing-xl) * 2); diff --git a/packages/builder/src/components/common/RoleSelect.svelte b/packages/builder/src/components/common/RoleSelect.svelte index a3f75fd4eb..aa39e5cb60 100644 --- a/packages/builder/src/components/common/RoleSelect.svelte +++ b/packages/builder/src/components/common/RoleSelect.svelte @@ -1,13 +1,23 @@ +
- +
- +
{#if userId !== $auth.user._id} @@ -301,13 +274,14 @@
addGroup(e.detail)} + on:deselect={e => removeGroup(e.detail)} + iconComponent={GroupIcon} + extractIconProps={item => ({ group: item, size: "S" })} />
@@ -322,7 +296,10 @@ on:click={() => $goto(`../groups/${group._id}`)} > { + removeGroup(group._id) + e.stopPropagation() + }} hoverable size="S" name="Close" @@ -330,7 +307,7 @@ {/each} {:else} - + {/if} @@ -339,27 +316,28 @@ Apps - {#if allAppList.length} - {#each allAppList as app} + {#if privileged} + + This user's role grants admin access to all apps + + {:else if availableApps.length} + {#each availableApps as app} $goto(`../../overview/${app.devId}`)} >
- - {getRoleLabel(getHighestRole(app.roles))} + + {getRoleLabel(app.role)}
{/each} {:else} - + {/if}
@@ -367,13 +345,10 @@ {/if} - + - +