Merge branch 'master' into cheeks-lab-day-binding-eval

This commit is contained in:
Martin McKeaveney 2024-03-12 13:27:03 +00:00 committed by GitHub
commit 30b7797421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 159 additions and 46 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.21.6", "version": "2.21.8",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -49,7 +49,8 @@
$: group = $groups.find(x => x._id === groupId) $: group = $groups.find(x => x._id === groupId)
$: isScimGroup = group?.scimInfo?.isSync $: isScimGroup = group?.scimInfo?.isSync
$: readonly = !sdk.users.isAdmin($auth.user) || isScimGroup $: isAdmin = sdk.users.isAdmin($auth.user)
$: readonly = !isAdmin || isScimGroup
$: groupApps = $apps $: groupApps = $apps
.filter(app => .filter(app =>
groups.actions groups.actions
@ -123,14 +124,18 @@
<span slot="control"> <span slot="control">
<Icon hoverable name="More" /> <Icon hoverable name="More" />
</span> </span>
<MenuItem icon="Refresh" on:click={() => editModal.show()}> <MenuItem
icon="Refresh"
on:click={() => editModal.show()}
disabled={!isAdmin}
>
Edit Edit
</MenuItem> </MenuItem>
<div title={isScimGroup && "Group synced from your AD"}> <div title={isScimGroup && "Group synced from your AD"}>
<MenuItem <MenuItem
icon="Delete" icon="Delete"
on:click={() => deleteModal.show()} on:click={() => deleteModal.show()}
disabled={isScimGroup} disabled={readonly}
> >
Delete Delete
</MenuItem> </MenuItem>
@ -139,7 +144,7 @@
</div> </div>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<GroupUsers {groupId} {readonly} /> <GroupUsers {groupId} {readonly} {isScimGroup} />
</Layout> </Layout>
<Layout noPadding gap="S"> <Layout noPadding gap="S">

View File

@ -13,6 +13,7 @@
export let groupId export let groupId
export let readonly export let readonly
export let isScimGroup
let emailSearch let emailSearch
let fetchGroupUsers let fetchGroupUsers
@ -61,10 +62,10 @@
</script> </script>
<div class="header"> <div class="header">
{#if !readonly} {#if isScimGroup}
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
{:else}
<ActiveDirectoryInfo text="Users synced from your AD" /> <ActiveDirectoryInfo text="Users synced from your AD" />
{:else if !readonly}
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
{/if} {/if}
<div class="controls-right"> <div class="controls-right">

View File

@ -39,9 +39,10 @@
name: { name: {
width: "1fr", width: "1fr",
}, },
...(readonly ...(!isAdmin
? {} ? {}
: { : // Add
{
_id: { _id: {
displayName: "", displayName: "",
width: "auto", width: "auto",
@ -90,7 +91,9 @@
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync) $: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
$: isSSO = !!user?.provider $: isSSO = !!user?.provider
$: readonly = !sdk.users.isAdmin($auth.user) || user?.scimInfo?.isSync $: isAdmin = sdk.users.isAdmin($auth.user)
$: isScim = user?.scimInfo?.isSync
$: readonly = !isAdmin || isScim
$: privileged = sdk.users.isAdminOrGlobalBuilder(user) $: privileged = sdk.users.isAdminOrGlobalBuilder(user)
$: nameLabel = getNameLabel(user) $: nameLabel = getNameLabel(user)
$: filteredGroups = getFilteredGroups(internalGroups, searchTerm) $: filteredGroups = getFilteredGroups(internalGroups, searchTerm)
@ -322,23 +325,23 @@
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
<div class="tableTitle"> <div class="tableTitle">
<Heading size="S">Groups</Heading> <Heading size="S">Groups</Heading>
{#if internalGroups?.length} {#if internalGroups?.length && isAdmin}
<div bind:this={popoverAnchor}> <div bind:this={popoverAnchor}>
<Button on:click={popover.show()} secondary>Add to group</Button> <Button on:click={popover.show()} secondary>Add to group</Button>
</div> </div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
labelKey="name"
bind:searchTerm
list={filteredGroups}
selected={user.userGroups}
on:select={e => addGroup(e.detail)}
on:deselect={e => removeGroup(e.detail)}
iconComponent={GroupIcon}
extractIconProps={item => ({ group: item, size: "S" })}
/>
</Popover>
{/if} {/if}
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
labelKey="name"
bind:searchTerm
list={filteredGroups}
selected={user.userGroups}
on:select={e => addGroup(e.detail)}
on:deselect={e => removeGroup(e.detail)}
iconComponent={GroupIcon}
extractIconProps={item => ({ group: item, size: "S" })}
/>
</Popover>
</div> </div>
<Table <Table
schema={groupSchema} schema={groupSchema}

View File

@ -35,7 +35,9 @@ export function createGroupsStore() {
get: getGroup, get: getGroup,
save: async group => { save: async group => {
const { _scimInfo, ...dataToSave } = group const { ...dataToSave } = group
delete dataToSave.scimInfo
delete dataToSave.userGroups
const response = await API.saveGroup(dataToSave) const response = await API.saveGroup(dataToSave)
group._id = response._id group._id = response._id
group._rev = response._rev group._rev = response._rev

@ -1 +1 @@
Subproject commit 4e66a0f7042652763c238b10367310b168905f87 Subproject commit c4c98ae70f2e936009250893898ecf11f4ddf2c3

View File

@ -104,17 +104,79 @@ describe("/api/global/groups", () => {
expect(events.group.permissionsEdited).not.toBeCalled() expect(events.group.permissionsEdited).not.toBeCalled()
}) })
describe("destroy", () => { describe("scim", () => {
it("should be able to delete a basic group", async () => { async function createScimGroup() {
const group = structures.groups.UserGroup() mocks.licenses.useScimIntegration()
let oldGroup = await config.api.groups.saveGroup(group) await config.setSCIMConfig(true)
await config.api.groups.deleteGroup(
oldGroup.body._id,
oldGroup.body._rev
)
expect(events.group.deleted).toBeCalledTimes(1) const scimGroup = await config.api.scimGroupsAPI.post({
body: structures.scim.createGroupRequest({
displayName: generator.word(),
}),
})
const { body: group } = await config.api.groups.find(scimGroup.id)
expect(group).toBeDefined()
return group
}
it("update will not allow sending SCIM fields", async () => {
const group = await createScimGroup()
const updatedGroup: UserGroup = {
...group,
name: generator.word(),
}
await config.api.groups.saveGroup(updatedGroup, {
expect: {
message: 'Invalid body - "scimInfo" is not allowed',
status: 400,
},
})
expect(events.group.updated).not.toBeCalled()
}) })
it("update will not amend the SCIM fields", async () => {
const group: UserGroup = await createScimGroup()
const updatedGroup: UserGroup = {
...group,
name: generator.word(),
scimInfo: undefined,
}
await config.api.groups.saveGroup(updatedGroup, {
expect: 200,
})
expect(events.group.updated).toBeCalledTimes(1)
expect(
(
await config.api.groups.find(group._id!, {
expect: 200,
})
).body
).toEqual(
expect.objectContaining({
...group,
name: updatedGroup.name,
scimInfo: group.scimInfo,
_rev: expect.any(String),
})
)
})
})
})
describe("destroy", () => {
it("should be able to delete a basic group", async () => {
const group = structures.groups.UserGroup()
let oldGroup = await config.api.groups.saveGroup(group)
await config.api.groups.deleteGroup(oldGroup.body._id, oldGroup.body._rev)
expect(events.group.deleted).toBeCalledTimes(1)
}) })
}) })
@ -147,7 +209,7 @@ describe("/api/global/groups", () => {
await Promise.all( await Promise.all(
Array.from({ length: 30 }).map(async (_, i) => { Array.from({ length: 30 }).map(async (_, i) => {
const email = `user${i}@example.com` const email = `user${i}+${generator.guid()}@example.com`
const user = await config.api.users.saveUser({ const user = await config.api.users.saveUser({
...structures.users.user(), ...structures.users.user(),
email, email,
@ -257,12 +319,16 @@ describe("/api/global/groups", () => {
}) })
}) })
it("update should return 200", async () => { it("update should return forbidden", async () => {
await config.withUser(builder, async () => { await config.withUser(builder, async () => {
await config.api.groups.updateGroupUsers(group._id!, { await config.api.groups.updateGroupUsers(
add: [builder._id!], group._id!,
remove: [], {
}) add: [builder._id!],
remove: [],
},
{ expect: 403 }
)
}) })
}) })
}) })

View File

@ -2,6 +2,7 @@ import tk from "timekeeper"
import _ from "lodash" import _ from "lodash"
import { generator, mocks, structures } from "@budibase/backend-core/tests" import { generator, mocks, structures } from "@budibase/backend-core/tests"
import { import {
CloudAccount,
ScimCreateUserRequest, ScimCreateUserRequest,
ScimGroupResponse, ScimGroupResponse,
ScimUpdateRequest, ScimUpdateRequest,
@ -604,6 +605,25 @@ describe("scim", () => {
expect(events.user.deleted).toBeCalledTimes(1) expect(events.user.deleted).toBeCalledTimes(1)
}) })
it("an account holder cannot be removed even when synched", async () => {
const account: CloudAccount = {
...structures.accounts.account(),
budibaseUserId: user.id,
email: user.emails![0].value,
}
mocks.accounts.getAccount.mockResolvedValue(account)
await deleteScimUser(user.id, {
expect: {
message: "Account holder cannot be deleted",
status: 400,
error: { code: "http" },
},
})
await config.api.scimUsersAPI.find(user.id, { expect: 200 })
})
}) })
}) })

View File

@ -7,7 +7,10 @@ export class GroupsAPI extends TestAPI {
super(config) super(config)
} }
saveGroup = (group: UserGroup, { expect } = { expect: 200 }) => { saveGroup = (
group: UserGroup,
{ expect }: { expect: number | object } = { expect: 200 }
) => {
return this.request return this.request
.post(`/api/global/groups`) .post(`/api/global/groups`)
.send(group) .send(group)
@ -44,14 +47,15 @@ export class GroupsAPI extends TestAPI {
updateGroupUsers = ( updateGroupUsers = (
id: string, id: string,
body: { add: string[]; remove: string[] } body: { add: string[]; remove: string[] },
{ expect } = { expect: 200 }
) => { ) => {
return this.request return this.request
.post(`/api/global/groups/${id}/users`) .post(`/api/global/groups/${id}/users`)
.send(body) .send(body)
.set(this.config.defaultHeaders()) .set(this.config.defaultHeaders())
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(expect)
} }
fetch = ({ expect } = { expect: 200 }) => { fetch = ({ expect } = { expect: 200 }) => {
@ -61,4 +65,12 @@ export class GroupsAPI extends TestAPI {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(expect) .expect(expect)
} }
find = (id: string, { expect } = { expect: 200 }) => {
return this.request
.get(`/api/global/groups/${id}`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(expect)
}
} }

View File

@ -1,13 +1,17 @@
import TestConfiguration from "../../TestConfiguration" import TestConfiguration from "../../TestConfiguration"
import { TestAPI } from "../base" import { TestAPI } from "../base"
const defaultConfig = { const defaultConfig: RequestSettings = {
expect: 200, expect: 200,
setHeaders: true, setHeaders: true,
skipContentTypeCheck: false, skipContentTypeCheck: false,
} }
export type RequestSettings = typeof defaultConfig export type RequestSettings = {
expect: number | object
setHeaders: boolean
skipContentTypeCheck: boolean
}
export abstract class ScimTestAPI extends TestAPI { export abstract class ScimTestAPI extends TestAPI {
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {