Merge pull request #13139 from Budibase/feat/BUDI-8046
Allow SCIM and internal users at the same time
This commit is contained in:
commit
600d593bdd
|
@ -1,22 +1,41 @@
|
|||
<script>
|
||||
import Icon from "./Icon.svelte"
|
||||
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import { fade } from "svelte/transition"
|
||||
|
||||
export let icon
|
||||
export let background
|
||||
export let color
|
||||
export let size = "M"
|
||||
export let tooltip
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="icon size--{size}"
|
||||
style="background: {background || `transparent`};"
|
||||
class:filled={!!background}
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
on:focus={() => (showTooltip = true)}
|
||||
on:blur={() => (showTooltip = false)}
|
||||
on:click={() => (showTooltip = false)}
|
||||
>
|
||||
<Icon name={icon} color={background ? "white" : color} />
|
||||
{#if tooltip && showTooltip}
|
||||
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||
<Tooltip textWrapping direction="right" text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: 0 0 28px;
|
||||
|
@ -32,6 +51,15 @@
|
|||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.icon.size--XS {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
}
|
||||
.icon.size--XS :global(.spectrum-Icon) {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
.icon.size--S {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
@ -58,4 +86,14 @@
|
|||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: calc(50% + 8px);
|
||||
bottom: calc(-50% + 6px);
|
||||
/* transform: translateY(-50%); */
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -470,6 +470,7 @@
|
|||
--table-border: 1px solid var(--spectrum-alias-border-color-mid);
|
||||
--cell-padding: var(--spectrum-global-dimension-size-250);
|
||||
overflow: auto;
|
||||
display: contents;
|
||||
}
|
||||
.wrapper--quiet {
|
||||
--table-bg: var(--spectrum-alias-background-color-transparent);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
import { IconAvatar } from "@budibase/bbui"
|
||||
|
||||
export let text = ""
|
||||
export let iconSize = "S"
|
||||
</script>
|
||||
|
||||
<div class="scim-banner">
|
||||
<IconAvatar
|
||||
icon="Sync"
|
||||
size={iconSize}
|
||||
background={"var(--spectrum-global-color-gray-500)"}
|
||||
tooltip="Synced from your AD"
|
||||
/>
|
||||
{text}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scim-banner {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<div class="scim-banner">
|
||||
<Icon name="Info" size="S" />
|
||||
Users are synced from your AD
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scim-banner {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
|
@ -13,7 +13,7 @@
|
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { Breadcrumb, Breadcrumbs } from "components/portal/page"
|
||||
import { roles } from "stores/builder"
|
||||
import { apps, auth, features, groups } from "stores/portal"
|
||||
import { apps, auth, groups } from "stores/portal"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
|
||||
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
|
||||
|
@ -47,9 +47,9 @@
|
|||
let loaded = false
|
||||
let editModal, deleteModal
|
||||
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
$: isScimGroup = group?.scimInfo?.isSync
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || isScimGroup
|
||||
$: groupApps = $apps
|
||||
.filter(app =>
|
||||
groups.actions
|
||||
|
@ -119,23 +119,27 @@
|
|||
<div class="header">
|
||||
<GroupIcon {group} size="L" />
|
||||
<Heading>{group?.name}</Heading>
|
||||
{#if !readonly}
|
||||
<ActionMenu align="right">
|
||||
<span slot="control">
|
||||
<Icon hoverable name="More" />
|
||||
</span>
|
||||
<MenuItem icon="Refresh" on:click={() => editModal.show()}>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={() => deleteModal.show()}>
|
||||
<ActionMenu align="right">
|
||||
<span slot="control">
|
||||
<Icon hoverable name="More" />
|
||||
</span>
|
||||
<MenuItem icon="Refresh" on:click={() => editModal.show()}>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<div title={isScimGroup && "Group synced from your AD"}>
|
||||
<MenuItem
|
||||
icon="Delete"
|
||||
on:click={() => deleteModal.show()}
|
||||
disabled={isScimGroup}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
{/if}
|
||||
</div>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
<GroupUsers {groupId} />
|
||||
<GroupUsers {groupId} {readonly} />
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
export let group
|
||||
export let saveGroup
|
||||
|
||||
let readonlyTitle = group?.scimInfo?.isSync
|
||||
|
||||
let nameError
|
||||
</script>
|
||||
|
||||
|
@ -26,7 +28,12 @@
|
|||
title={group?._rev ? "Edit group" : "Create group"}
|
||||
confirmText="Save"
|
||||
>
|
||||
<Input bind:value={group.name} label="Name" error={nameError} />
|
||||
<Input
|
||||
bind:value={group.name}
|
||||
label="Name"
|
||||
error={nameError}
|
||||
disabled={readonlyTitle}
|
||||
/>
|
||||
<div class="modal-format">
|
||||
<div class="modal-inner">
|
||||
<Body size="XS">Icon</Body>
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
import { Button, Popover, notifications } from "@budibase/bbui"
|
||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { auth, groups, users } from "stores/portal"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { groups, users } from "stores/portal"
|
||||
|
||||
export let groupId
|
||||
export let onUsersUpdated
|
||||
|
@ -14,7 +13,6 @@
|
|||
let prevSearch = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
|
||||
$: readonly = !sdk.users.isAdmin($auth.user)
|
||||
$: page = $pageInfo.page
|
||||
$: searchUsers(page, searchTerm)
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
|
@ -43,7 +41,7 @@
|
|||
</script>
|
||||
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button disabled={readonly} on:click={popover.show()} cta>Add user</Button>
|
||||
<Button on:click={popover.show()} cta>Add user</Button>
|
||||
</div>
|
||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||
<UserGroupPicker
|
||||
|
|
|
@ -5,4 +5,12 @@
|
|||
export let size = "M"
|
||||
</script>
|
||||
|
||||
<IconAvatar icon={group?.icon} background={group?.color} {size} />
|
||||
<div class="icon-group">
|
||||
<IconAvatar icon={group?.icon} background={group?.color} {size} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.icon-group {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import GroupIcon from "./GroupIcon.svelte"
|
||||
import ActiveDirectoryInfo from "../../_components/ActiveDirectoryInfo.svelte"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
|
@ -14,13 +15,16 @@
|
|||
{:else}
|
||||
<div class="text">-</div>
|
||||
{/if}
|
||||
{#if row.scimInfo?.isSync}
|
||||
<ActiveDirectoryInfo iconSize="XS" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
gap: var(--spacing-m);
|
||||
max-width: var(--max-cell-width);
|
||||
flex: 1 1 auto;
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { API } from "api"
|
||||
import { auth, features, groups } from "stores/portal"
|
||||
import { groups } from "stores/portal"
|
||||
import { setContext } from "svelte"
|
||||
import ScimBanner from "../../_components/SCIMBanner.svelte"
|
||||
|
||||
import RemoveUserTableRenderer from "../_components/RemoveUserTableRenderer.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import ActiveDirectoryInfo from "../../_components/ActiveDirectoryInfo.svelte"
|
||||
|
||||
export let groupId
|
||||
export let readonly
|
||||
|
||||
let emailSearch
|
||||
let fetchGroupUsers
|
||||
|
@ -49,9 +50,6 @@
|
|||
},
|
||||
]
|
||||
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
|
||||
const removeUser = async id => {
|
||||
await groups.actions.removeUser(groupId, id)
|
||||
fetchGroupUsers.refresh()
|
||||
|
@ -63,10 +61,10 @@
|
|||
</script>
|
||||
|
||||
<div class="header">
|
||||
{#if !scimEnabled}
|
||||
{#if !readonly}
|
||||
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
|
||||
{:else}
|
||||
<ScimBanner />
|
||||
<ActiveDirectoryInfo text="Users synced from your AD" />
|
||||
{/if}
|
||||
|
||||
<div class="controls-right">
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
Search,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { groups, auth, licensing, admin, features } from "stores/portal"
|
||||
import { groups, auth, licensing, admin } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
@ -21,7 +21,6 @@
|
|||
import UsersTableRenderer from "./_components/UsersTableRenderer.svelte"
|
||||
import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
const DefaultGroup = {
|
||||
|
@ -110,14 +109,10 @@
|
|||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
{#if $licensing.groupsEnabled}
|
||||
{#if !$features.isScimEnabled}
|
||||
<!--Show the group create button-->
|
||||
<Button disabled={readonly} cta on:click={showCreateGroupModal}>
|
||||
Add group
|
||||
</Button>
|
||||
{:else}
|
||||
<ScimBanner />
|
||||
{/if}
|
||||
<!--Show the group create button-->
|
||||
<Button disabled={readonly} cta on:click={showCreateGroupModal}>
|
||||
Add group
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
primary
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
Table,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { users, auth, groups, apps, licensing, features } from "stores/portal"
|
||||
import { users, auth, groups, apps, licensing } from "stores/portal"
|
||||
import { roles } from "stores/builder"
|
||||
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
|
@ -30,8 +30,8 @@
|
|||
import GroupNameTableRenderer from "../groups/_components/GroupNameTableRenderer.svelte"
|
||||
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
|
||||
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
|
||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import ActiveDirectoryInfo from "../_components/ActiveDirectoryInfo.svelte"
|
||||
|
||||
export let userId
|
||||
|
||||
|
@ -87,12 +87,13 @@
|
|||
let user
|
||||
let loaded = false
|
||||
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
||||
$: isSSO = !!user?.provider
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || user?.scimInfo?.isSync
|
||||
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||
$: nameLabel = getNameLabel(user)
|
||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
|
||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||
$: userGroups = $groups.filter(x => {
|
||||
return x.users?.find(y => {
|
||||
|
@ -274,8 +275,8 @@
|
|||
<Layout noPadding gap="S">
|
||||
<div class="details-title">
|
||||
<Heading size="S">Details</Heading>
|
||||
{#if scimEnabled}
|
||||
<ScimBanner />
|
||||
{#if user?.scimInfo?.isSync}
|
||||
<ActiveDirectoryInfo text="User synced from your AD" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="fields">
|
||||
|
@ -321,11 +322,11 @@
|
|||
<Layout gap="S" noPadding>
|
||||
<div class="tableTitle">
|
||||
<Heading size="S">Groups</Heading>
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button disabled={readonly} on:click={popover.show()} secondary>
|
||||
Add to group
|
||||
</Button>
|
||||
</div>
|
||||
{#if internalGroups?.length}
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button on:click={popover.show()} secondary>Add to group</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||
<UserGroupPicker
|
||||
labelKey="name"
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
$: reached = licensing.usersLimitReached(userCount)
|
||||
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
||||
function removeInput(idx) {
|
||||
userData = userData.filter((e, i) => i !== idx)
|
||||
}
|
||||
|
@ -133,12 +135,12 @@
|
|||
{/if}
|
||||
</Layout>
|
||||
|
||||
{#if $licensing.groupsEnabled}
|
||||
{#if $licensing.groupsEnabled && internalGroups?.length}
|
||||
<Multiselect
|
||||
bind:value={userGroups}
|
||||
placeholder="No groups"
|
||||
label="Groups"
|
||||
options={$groups}
|
||||
options={internalGroups}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
import ActiveDirectoryInfo from "../../_components/ActiveDirectoryInfo.svelte"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
</script>
|
||||
|
||||
{value}
|
||||
{#if row.scimInfo?.isSync}
|
||||
<ActiveDirectoryInfo iconSize="XS" />
|
||||
{/if}
|
|
@ -34,6 +34,8 @@
|
|||
label: `${option.label} - ${option.subtitle}`,
|
||||
}))
|
||||
|
||||
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
|
||||
|
||||
const validEmails = userEmails => {
|
||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||
notifications.error(
|
||||
|
@ -106,12 +108,12 @@
|
|||
{/if}
|
||||
<RadioGroup bind:value={usersRole} options={roleOptions} />
|
||||
|
||||
{#if $licensing.groupsEnabled}
|
||||
{#if $licensing.groupsEnabled && internalGroups?.length}
|
||||
<Multiselect
|
||||
bind:value={userGroups}
|
||||
placeholder="No groups"
|
||||
label="Groups"
|
||||
options={$groups}
|
||||
options={internalGroups}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
|
||||
const groupContext = getContext("groups")
|
||||
|
||||
|
@ -12,12 +13,11 @@
|
|||
e.stopPropagation()
|
||||
groupContext.removeGroup(value)
|
||||
}
|
||||
|
||||
$: disabled = !sdk.users.isAdmin($auth.user) || row?.scimInfo?.isSync
|
||||
$: tooltip = row?.scimInfo?.isSync && "This group is managed via your AD"
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
disabled={!sdk.users.isAdmin($auth.user)}
|
||||
size="S"
|
||||
on:click={onClick}
|
||||
<ActionButton {disabled} size="S" on:click={onClick} {tooltip}
|
||||
>Remove</ActionButton
|
||||
>
|
||||
Remove
|
||||
</ActionButton>
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
auth,
|
||||
licensing,
|
||||
organisation,
|
||||
features,
|
||||
admin,
|
||||
} from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
|
@ -28,6 +27,7 @@
|
|||
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
|
||||
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
|
||||
import RoleTableRenderer from "./_components/RoleTableRenderer.svelte"
|
||||
import EmailTableRenderer from "./_components/EmailTableRenderer.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||
|
@ -37,7 +37,6 @@
|
|||
import { Constants, Utils, fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { OnboardingType } from "constants"
|
||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
const fetch = fetchData({
|
||||
|
@ -63,6 +62,7 @@
|
|||
let selectedRows = []
|
||||
let bulkSaveResponse
|
||||
let customRenderers = [
|
||||
{ column: "email", component: EmailTableRenderer },
|
||||
{ column: "userGroups", component: GroupsTableRenderer },
|
||||
{ column: "apps", component: AppsTableRenderer },
|
||||
{ column: "role", component: RoleTableRenderer },
|
||||
|
@ -73,7 +73,7 @@
|
|||
let parsedInvites = []
|
||||
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || $features.isScimEnabled
|
||||
$: readonly = !sdk.users.isAdmin($auth.user)
|
||||
$: debouncedUpdateFetch(searchEmail)
|
||||
$: schema = {
|
||||
email: {
|
||||
|
@ -247,19 +247,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
const deleteRows = async () => {
|
||||
const deleteUsers = async () => {
|
||||
try {
|
||||
let ids = selectedRows.map(user => user._id)
|
||||
if (ids.includes(get(auth).user._id)) {
|
||||
notifications.error("You cannot delete yourself")
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedRows.some(u => u.scimInfo?.isSync)) {
|
||||
notifications.error("You cannot remove users created via your AD")
|
||||
return
|
||||
}
|
||||
|
||||
await users.bulkDelete(ids)
|
||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
||||
selectedRows = []
|
||||
await fetch.refresh()
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting rows")
|
||||
notifications.error("Error deleting users")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -320,8 +326,6 @@
|
|||
Import
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<ScimBanner />
|
||||
{/if}
|
||||
<div class="controls-right">
|
||||
<Search bind:value={searchEmail} placeholder="Search" />
|
||||
|
@ -330,7 +334,7 @@
|
|||
item="user"
|
||||
on:updaterows
|
||||
{selectedRows}
|
||||
{deleteRows}
|
||||
deleteRows={deleteUsers}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -346,6 +350,7 @@
|
|||
{customRenderers}
|
||||
loading={!$fetch.loaded || !groupsLoaded}
|
||||
/>
|
||||
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$fetch.pageNumber + 1}
|
||||
|
@ -355,6 +360,7 @@
|
|||
goToNextPage={fetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
schema={pendingSchema}
|
||||
data={parsedInvites}
|
||||
|
@ -402,6 +408,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
|
|
|
@ -35,7 +35,8 @@ export function createGroupsStore() {
|
|||
get: getGroup,
|
||||
|
||||
save: async group => {
|
||||
const response = await API.saveGroup(group)
|
||||
const { _scimInfo, ...dataToSave } = group
|
||||
const response = await API.saveGroup(dataToSave)
|
||||
group._id = response._id
|
||||
group._rev = response._rev
|
||||
updateStore(group)
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 80a4d8ff998895fc298ee510158a82ce7daebc67
|
||||
Subproject commit 4e66a0f7042652763c238b10367310b168905f87
|
|
@ -23,6 +23,6 @@
|
|||
"typescript": "5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"scim-patch": "^0.7.0"
|
||||
"scim-patch": "^0.8.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,25 +10,20 @@ import {
|
|||
import { TestConfiguration } from "../../../../tests"
|
||||
import { events } from "@budibase/backend-core"
|
||||
|
||||
// this test can 409 - retries reduce issues with this
|
||||
jest.retryTimes(2, { logErrorsBeforeRetry: true })
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("scim", () => {
|
||||
beforeAll(async () => {
|
||||
tk.freeze(mocks.date.MOCK_DATE)
|
||||
mocks.licenses.useScimIntegration()
|
||||
|
||||
await config.setSCIMConfig(true)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
async function setup() {
|
||||
jest.resetAllMocks()
|
||||
tk.freeze(mocks.date.MOCK_DATE)
|
||||
mocks.licenses.useScimIntegration()
|
||||
mocks.licenses.useGroups()
|
||||
|
||||
await config.setSCIMConfig(true)
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(setup)
|
||||
beforeEach(setup)
|
||||
|
||||
const config = new TestConfiguration()
|
||||
|
||||
|
@ -367,13 +362,77 @@ describe("scim", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("creating an existing user name returns a conflict", async () => {
|
||||
const body = structures.scim.createUserRequest()
|
||||
it("creating an external user that conflicts an internal one syncs the existing user", async () => {
|
||||
const { body: internalUser } = await config.api.users.saveUser(
|
||||
structures.users.user()
|
||||
)
|
||||
|
||||
await postScimUser({ body })
|
||||
const scimUserData = {
|
||||
externalId: structures.uuid(),
|
||||
email: internalUser.email,
|
||||
firstName: structures.generator.first(),
|
||||
lastName: structures.generator.last(),
|
||||
username: structures.generator.name(),
|
||||
}
|
||||
const scimUserRequest = structures.scim.createUserRequest(scimUserData)
|
||||
|
||||
const res = await postScimUser({ body }, { expect: 409 })
|
||||
expect((res as any).message).toBe("Email already in use")
|
||||
const res = await postScimUser(
|
||||
{ body: scimUserRequest },
|
||||
{ expect: 200 }
|
||||
)
|
||||
|
||||
const expectedScimUser: ScimUserResponse = {
|
||||
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
id: internalUser._id!,
|
||||
externalId: scimUserRequest.externalId,
|
||||
meta: {
|
||||
resourceType: "User",
|
||||
// @ts-ignore
|
||||
created: mocks.date.MOCK_DATE.toISOString(),
|
||||
// @ts-ignore
|
||||
lastModified: mocks.date.MOCK_DATE.toISOString(),
|
||||
},
|
||||
userName: scimUserData.username,
|
||||
name: {
|
||||
formatted: `${scimUserData.firstName} ${scimUserData.lastName}`,
|
||||
familyName: scimUserData.lastName,
|
||||
givenName: scimUserData.firstName,
|
||||
},
|
||||
active: true,
|
||||
emails: [
|
||||
{
|
||||
value: internalUser.email,
|
||||
type: "work",
|
||||
primary: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
expect(res).toEqual(expectedScimUser)
|
||||
})
|
||||
|
||||
it("a user cannot be SCIM synchronised with another SCIM user", async () => {
|
||||
const { body: internalUser } = await config.api.users.saveUser(
|
||||
structures.users.user()
|
||||
)
|
||||
|
||||
await postScimUser(
|
||||
{
|
||||
body: structures.scim.createUserRequest({
|
||||
email: internalUser.email,
|
||||
}),
|
||||
},
|
||||
{ expect: 200 }
|
||||
)
|
||||
|
||||
await postScimUser(
|
||||
{
|
||||
body: structures.scim.createUserRequest({
|
||||
email: internalUser.email,
|
||||
}),
|
||||
},
|
||||
{ expect: 409 }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -656,7 +715,6 @@ describe("scim", () => {
|
|||
})
|
||||
|
||||
it("can fetch groups even if internal groups exist", async () => {
|
||||
mocks.licenses.useGroups()
|
||||
await config.api.groups.saveGroup(structures.userGroups.userGroup())
|
||||
await config.api.groups.saveGroup(structures.userGroups.userGroup())
|
||||
|
||||
|
@ -722,6 +780,43 @@ describe("scim", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("creating an external group that conflicts an internal one syncs the existing group", async () => {
|
||||
const groupToSave = structures.userGroups.userGroup()
|
||||
const { body: internalGroup } = await config.api.groups.saveGroup(
|
||||
groupToSave
|
||||
)
|
||||
|
||||
const scimGroupData = {
|
||||
externalId: structures.uuid(),
|
||||
displayName: groupToSave.name,
|
||||
}
|
||||
|
||||
const res = await postScimGroup(
|
||||
{ body: structures.scim.createGroupRequest(scimGroupData) },
|
||||
{ expect: 200 }
|
||||
)
|
||||
|
||||
expect(res).toEqual(
|
||||
expect.objectContaining({
|
||||
id: internalGroup._id!,
|
||||
externalId: scimGroupData.externalId,
|
||||
displayName: scimGroupData.displayName,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("a group cannot be SCIM synchronised with another SCIM group", async () => {
|
||||
const groupToSave = structures.userGroups.userGroup()
|
||||
await config.api.groups.saveGroup(groupToSave)
|
||||
|
||||
const createGroupRequest = structures.scim.createGroupRequest({
|
||||
displayName: groupToSave.name,
|
||||
})
|
||||
await postScimGroup({ body: createGroupRequest }, { expect: 200 })
|
||||
|
||||
await postScimGroup({ body: createGroupRequest }, { expect: 409 })
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /api/global/scim/v2/groups/:id", () => {
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -5666,6 +5666,13 @@
|
|||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^20.4.5":
|
||||
version "20.11.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.25.tgz#0f50d62f274e54dd7a49f7704cc16bfbcccaf49f"
|
||||
integrity sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/nodemailer@^6.4.4":
|
||||
version "6.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.14.tgz#5c81a5e856db7f8ede80013e6dbad7c5fb2283e2"
|
||||
|
@ -19491,11 +19498,12 @@ schema-utils@^3.1.1, schema-utils@^3.1.2:
|
|||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
scim-patch@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/scim-patch/-/scim-patch-0.7.0.tgz#3f6d94256c07be415a74a49c0ff48dc91e4e0219"
|
||||
integrity sha512-wXKcsZl+aLfE0yId7MjiOd91v8as6dEYLFvm1gGu3yJxSPhl1Fl3vWiNN4V3D68UKpqO/umK5rwWc8wGpBaOHw==
|
||||
scim-patch@^0.8.1:
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/scim-patch/-/scim-patch-0.8.1.tgz#611bfdb5538f6d8b97aba0ab0f8bd01055b70c1c"
|
||||
integrity sha512-JRYTA+mJZ8Z5DJGO7kkFc0lGCDs100rNs7iN77mld7gQajTp1R1xjUzMfZTOMAkBDA75GSdbMmfdfMqMJCn/Yg==
|
||||
dependencies:
|
||||
"@types/node" "^20.4.5"
|
||||
fast-deep-equal "3.1.3"
|
||||
scim2-parse-filter "0.2.8"
|
||||
|
||||
|
|
Loading…
Reference in New Issue