diff --git a/lerna.json b/lerna.json index f7db674343..e3f12c1ad0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.10.12-alpha.26", + "version": "2.10.16-alpha.7", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 83f8298f54..f918dcc352 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -18,7 +18,7 @@ export enum ViewName { ROUTING = "screen_routes", AUTOMATION_LOGS = "automation_logs", ACCOUNT_BY_EMAIL = "account_by_email", - PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", + PLATFORM_USERS_LOWERCASE = "platform_users_lowercase_2", USER_BY_GROUP = "user_by_group", APP_BACKUP_BY_TRIGGER = "by_trigger", } diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 7f5ef29a0a..f0980ad217 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -190,6 +190,10 @@ export const createPlatformUserView = async () => { if (doc.tenantId) { emit(doc._id.toLowerCase(), doc._id) } + + if (doc.ssoId) { + emit(doc.ssoId, doc._id) + } }` await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE) } diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index c65a7e0ec4..6f030afb7c 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -5,6 +5,7 @@ import { PlatformUser, PlatformUserByEmail, PlatformUserById, + PlatformUserBySsoId, User, } from "@budibase/types" @@ -45,6 +46,20 @@ function newUserEmailDoc( } } +function newUserSsoIdDoc( + ssoId: string, + email: string, + userId: string, + tenantId: string +): PlatformUserBySsoId { + return { + _id: ssoId, + userId, + email, + tenantId, + } +} + /** * Add a new user id or email doc if it doesn't exist. */ @@ -64,11 +79,24 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { } } -export async function addUser(tenantId: string, userId: string, email: string) { - await Promise.all([ +export async function addUser( + tenantId: string, + userId: string, + email: string, + ssoId?: string +) { + const promises = [ addUserDoc(userId, () => newUserIdDoc(userId, tenantId)), addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)), - ]) + ] + + if (ssoId) { + promises.push( + addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId)) + ) + } + + await Promise.all(promises) } // DELETE diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index 13083534b1..539bbaef27 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -1,8 +1,9 @@ -import { PermissionType, PermissionLevel } from "@budibase/types" -export { PermissionType, PermissionLevel } from "@budibase/types" +import { PermissionLevel, PermissionType } from "@budibase/types" import flatten from "lodash/flatten" import cloneDeep from "lodash/fp/cloneDeep" +export { PermissionType, PermissionLevel } from "@budibase/types" + export type RoleHierarchy = { permissionId: string }[] @@ -78,6 +79,7 @@ export const BUILTIN_PERMISSIONS = { permissions: [ new Permission(PermissionType.QUERY, PermissionLevel.READ), new Permission(PermissionType.TABLE, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, WRITE: { @@ -88,6 +90,7 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, POWER: { @@ -99,6 +102,7 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, ADMIN: { @@ -111,6 +115,7 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ), + new Permission(PermissionType.APP, PermissionLevel.READ), ], }, } diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index e87df2e9c9..24279e6b5c 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -215,21 +215,23 @@ async function getAllUserRoles(userRoleId?: string): Promise { return roles } +export async function getUserRoleIdHierarchy( + userRoleId?: string +): Promise { + const roles = await getUserRoleHierarchy(userRoleId) + return roles.map(role => role._id!) +} + /** * Returns an ordered array of the user's inherited role IDs, this can be used * 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. */ -export async function getUserRoleHierarchy( - userRoleId?: string, - opts = { idOnly: true } -) { +export async function getUserRoleHierarchy(userRoleId?: string) { // special case, if they don't have a role then they are a public user - const roles = await getAllUserRoles(userRoleId) - return opts.idOnly ? roles.map(role => role._id) : roles + return getAllUserRoles(userRoleId) } // this function checks that the provided permissions are in an array format @@ -249,6 +251,11 @@ export function checkForRoleResourceArray( return rolePerms } +export async function getAllRoleIds(appId?: string) { + const roles = await getAllRoles(appId) + return roles.map(role => role._id) +} + /** * Given an app ID this will retrieve all of the roles that are currently within that app. * @return {Promise} An array of the role objects that were found. @@ -332,9 +339,7 @@ export class AccessController { } let roleIds = userRoleId ? this.userHierarchies[userRoleId] : null if (!roleIds && userRoleId) { - roleIds = (await getUserRoleHierarchy(userRoleId, { - idOnly: true, - })) as string[] + roleIds = await getUserRoleIdHierarchy(userRoleId) this.userHierarchies[userRoleId] = roleIds } diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index c288540f35..1d02bebc32 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -278,7 +278,12 @@ export class UserDB { builtUser._rev = response.rev await eventHelpers.handleSaveEvents(builtUser, dbUser) - await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) + await platform.users.addUser( + tenantId, + builtUser._id!, + builtUser.email, + builtUser.ssoId + ) await cache.user.invalidateUser(response.id) await Promise.all(groupPromises) diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 67e4411ea3..515f94db1e 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -1,4 +1,4 @@ -import { generator, uuid, quotas } from "." +import { generator, quotas, uuid } from "." import { generateGlobalUserID } from "../../../../src/docIds" import { Account, @@ -6,10 +6,11 @@ import { AccountSSOProviderType, AuthType, CloudAccount, - Hosting, - SSOAccount, CreateAccount, CreatePassswordAccount, + CreateVerifiableSSOAccount, + Hosting, + SSOAccount, } from "@budibase/types" import sample from "lodash/sample" @@ -68,6 +69,23 @@ export function ssoAccount(account: Account = cloudAccount()): SSOAccount { } } +export function verifiableSsoAccount( + account: Account = cloudAccount() +): SSOAccount { + return { + ...account, + authType: AuthType.SSO, + oauth2: { + accessToken: generator.string(), + refreshToken: generator.string(), + }, + pictureUrl: generator.url(), + provider: AccountSSOProvider.MICROSOFT, + providerType: AccountSSOProviderType.MICROSOFT, + thirdPartyProfile: { id: "abc123" }, + } +} + export const cloudCreateAccount: CreatePassswordAccount = { email: "cloud@budibase.com", tenantId: "cloud", @@ -91,6 +109,19 @@ export const cloudSSOCreateAccount: CreateAccount = { profession: "Software Engineer", } +export const cloudVerifiableSSOCreateAccount: CreateVerifiableSSOAccount = { + email: "cloud-sso@budibase.com", + tenantId: "cloud-sso", + hosting: Hosting.CLOUD, + authType: AuthType.SSO, + tenantName: "cloudsso", + name: "Budi Armstrong", + size: "10+", + profession: "Software Engineer", + provider: AccountSSOProvider.MICROSOFT, + thirdPartyProfile: { id: "abc123" }, +} + export const selfCreateAccount: CreatePassswordAccount = { email: "self@budibase.com", tenantId: "self", diff --git a/packages/builder/README.md b/packages/builder/README.md index c1dd8b7871..101d483185 100644 --- a/packages/builder/README.md +++ b/packages/builder/README.md @@ -1,3 +1,5 @@ +test + ## Get Started `yarn install` diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index bbefe65fc8..b17bd99e10 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -33,6 +33,8 @@ const generateTableBlock = datasource => { showTitleButton: true, titleButtonText: "Create row", titleButtonClickBehaviour: "new", + sidePanelSaveLabel: "Save", + sidePanelDeleteLabel: "Delete", }) .instanceName(`${datasource.label} - Table block`) return tableBlock diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index fc0965e274..5f1c8044d9 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -33,7 +33,7 @@ import { getBindings } from "components/backend/DataTable/formula" import JSONSchemaModal from "./JSONSchemaModal.svelte" import { ValidColumnNameRegex } from "@budibase/shared-core" - import { FieldSubtype, FieldType } from "@budibase/types" + import { FieldType } from "@budibase/types" import RelationshipSelector from "components/common/RelationshipSelector.svelte" const AUTO_TYPE = "auto" @@ -43,11 +43,7 @@ const NUMBER_TYPE = FIELDS.NUMBER.type const JSON_TYPE = FIELDS.JSON.type const DATE_TYPE = FIELDS.DATETIME.type - const BB_REFERENCE_TYPE = FieldType.BB_REFERENCE - const BB_USER_REFERENCE_TYPE = composeType( - BB_REFERENCE_TYPE, - FieldSubtype.USER - ) + const USER_REFRENCE_TYPE = FIELDS.BB_REFERENCE_USER.compositeType const dispatch = createEventDispatcher() const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"] @@ -84,33 +80,6 @@ let relationshipOpts1 = Object.values(PrettyRelationshipDefinitions) let relationshipOpts2 = Object.values(PrettyRelationshipDefinitions) - const bbRefTypeMapping = {} - - function composeType(fieldType, subtype) { - return `${fieldType}_${subtype}` - } - - // Handling fields with subtypes - fieldDefinitions = Object.entries(fieldDefinitions).reduce( - (p, [key, field]) => { - if (field.type === BB_REFERENCE_TYPE) { - const composedType = composeType(field.type, field.subtype) - p[key] = { - ...field, - type: composedType, - } - bbRefTypeMapping[composedType] = { - type: field.type, - subtype: field.subtype, - } - } else { - p[key] = field - } - return p - }, - {} - ) - $: if (primaryDisplay) { editableColumn.constraints.presence = { allowEmpty: false } } @@ -170,12 +139,8 @@ $tables.selected.primaryDisplay == null || $tables.selected.primaryDisplay === editableColumn.name - const mapped = Object.entries(bbRefTypeMapping).find( - ([_, v]) => v.type === field.type && v.subtype === field.subtype - ) - if (mapped) { - editableColumn.type = mapped[0] - delete editableColumn.subtype + if (editableColumn.type === FieldType.BB_REFERENCE) { + editableColumn.type = `${editableColumn.type}_${editableColumn.subtype}` } // Here we are setting the relationship values based on the editableColumn // This part of the code is used when viewing an existing field hence the check @@ -212,8 +177,6 @@ $: initialiseField(field, savingColumn) - $: isBBReference = !!bbRefTypeMapping[editableColumn.type] - $: checkConstraints(editableColumn) $: required = !!editableColumn?.constraints?.presence || primaryDisplay $: uneditable = @@ -286,11 +249,12 @@ let saveColumn = cloneDeep(editableColumn) - if (bbRefTypeMapping[saveColumn.type]) { - saveColumn = { - ...saveColumn, - ...bbRefTypeMapping[saveColumn.type], - } + // Handle types on composite types + const definition = fieldDefinitions[saveColumn.type.toUpperCase()] + if (definition && saveColumn.type === definition.compositeType) { + saveColumn.type = definition.type + saveColumn.subtype = definition.subtype + delete saveColumn.compositeType } if (saveColumn.type === AUTO_TYPE) { @@ -373,7 +337,7 @@ editableColumn.relationshipType = RelationshipType.MANY_TO_MANY } else if (editableColumn.type === FORMULA_TYPE) { editableColumn.formulaType = "dynamic" - } else if (editableColumn.type === BB_USER_REFERENCE_TYPE) { + } else if (editableColumn.type === USER_REFRENCE_TYPE) { editableColumn.relationshipType = RelationshipType.ONE_TO_MANY } } @@ -431,14 +395,12 @@ FIELDS.BOOLEAN, FIELDS.FORMULA, FIELDS.BIGINT, + FIELDS.BB_REFERENCE_USER, ] // no-sql or a spreadsheet if (!external || table.sql) { fields = [...fields, FIELDS.LINK, FIELDS.ARRAY] } - if (fieldDefinitions.USER) { - fields.push(fieldDefinitions.USER) - } return fields } } @@ -447,8 +409,9 @@ if (!fieldToCheck) { return } + // most types need this, just make sure its always present - if (fieldToCheck && !fieldToCheck.constraints) { + if (!fieldToCheck.constraints) { fieldToCheck.constraints = {} } // some string types may have been built by server, may not always have constraints @@ -528,7 +491,7 @@ on:change={handleTypeChange} options={allowedTypes} getOptionLabel={field => field.name} - getOptionValue={field => field.type} + getOptionValue={field => field.compositeType || field.type} getOptionIcon={field => field.icon} isOptionEnabled={option => { if (option.type == AUTO_TYPE) { @@ -694,7 +657,7 @@ - {:else if isBBReference} + {:else if editableColumn.type === USER_REFRENCE_TYPE} diff --git a/packages/builder/src/components/integration/QueryViewer.svelte b/packages/builder/src/components/integration/QueryViewer.svelte index 6e7ee52268..59a3289731 100644 --- a/packages/builder/src/components/integration/QueryViewer.svelte +++ b/packages/builder/src/components/integration/QueryViewer.svelte @@ -337,11 +337,12 @@ padding: 8px 10px 8px 16px; display: flex; border-bottom: 2px solid transparent; - transition: border-bottom 130ms ease-out; + transition: border-bottom 130ms ease-out, background 130ms ease-out; } .header.scrolling { border-bottom: var(--border-light); + background: var(--background); } .body { diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index 047152eeed..8b76207822 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -120,10 +120,11 @@ export const FIELDS = { presence: false, }, }, - USER: { + BB_REFERENCE_USER: { name: "User", type: "bb_reference", subtype: "user", + compositeType: "bb_reference_user", // Used for working with the subtype on CreateEditColumn as is it was a primary type icon: "User", }, } diff --git a/packages/builder/src/pages/builder/portal/apps/_layout.svelte b/packages/builder/src/pages/builder/portal/apps/_layout.svelte index c4a0bfd913..4067856bfa 100644 --- a/packages/builder/src/pages/builder/portal/apps/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/apps/_layout.svelte @@ -46,7 +46,9 @@ {#if loaded}
- + {#if $apps.length > 0} + + {/if}
{/if} diff --git a/packages/client/src/components/app/Layout.svelte b/packages/client/src/components/app/Layout.svelte index e557874edb..e482e6b336 100644 --- a/packages/client/src/components/app/Layout.svelte +++ b/packages/client/src/components/app/Layout.svelte @@ -4,15 +4,14 @@ import { Heading, Icon, clickOutside } from "@budibase/bbui" import { FieldTypes } from "constants" import active from "svelte-spa-router/active" - import { RoleUtils } from "@budibase/frontend-core" const sdk = getContext("sdk") const { routeStore, + roleStore, styleable, linkable, builderStore, - currentRole, sidePanelStore, } = sdk const component = getContext("component") @@ -61,7 +60,7 @@ }) setContext("layout", store) - $: validLinks = getValidLinks(links, $currentRole) + $: validLinks = getValidLinks(links, $roleStore) $: typeClass = NavigationClasses[navigation] || NavigationClasses.None $: navWidthClass = WidthClasses[navWidth || width] || WidthClasses.Large $: pageWidthClass = WidthClasses[pageWidth || width] || WidthClasses.Large @@ -99,14 +98,12 @@ } } - const getValidLinks = (allLinks, role) => { + const getValidLinks = (allLinks, userRoleHierarchy) => { // Strip links missing required info let validLinks = (allLinks || []).filter(link => link.text && link.url) - // Filter to only links allowed by the current role - const priority = RoleUtils.getRolePriority(role) return validLinks.filter(link => { - return !link.roleId || RoleUtils.getRolePriority(link.roleId) <= priority + return userRoleHierarchy?.find(roleId => roleId === link.roleId) }) } diff --git a/packages/client/src/components/app/Section.svelte b/packages/client/src/components/app/Section.svelte index 11f0f2978f..b86c5cc352 100644 --- a/packages/client/src/components/app/Section.svelte +++ b/packages/client/src/components/app/Section.svelte @@ -47,28 +47,29 @@