From 1511a19f31aca16b1dc41b300e0c7193d516e6c3 Mon Sep 17 00:00:00 2001 From: fflorent Date: Mon, 13 Jan 2025 21:06:56 +0100 Subject: [PATCH] Implement PUT + PATCH /Groups/{id} --- app/server/lib/scim/v2/ScimGroupController.ts | 19 +++ app/server/lib/scim/v2/ScimUserController.ts | 4 +- test/server/lib/Scim.ts | 124 ++++++++++++++++-- 3 files changed, 133 insertions(+), 14 deletions(-) diff --git a/app/server/lib/scim/v2/ScimGroupController.ts b/app/server/lib/scim/v2/ScimGroupController.ts index 6af71d69ed..4f31d8a00f 100644 --- a/app/server/lib/scim/v2/ScimGroupController.ts +++ b/app/server/lib/scim/v2/ScimGroupController.ts @@ -59,6 +59,22 @@ class ScimGroupController extends BaseController { return toSCIMMYGroup(group); }); } + + /** + * Overwrites a group with the passed data. + * + * @param resource The SCIMMY group resource performing the operation + * @param data The data to overwrite the group with + * @param context The request context + */ + public async overwriteGroup(resource: any, data: any, context: RequestContext) { + return this.runAndHandleErrors(context, async () => { + const id = this.getIdFromResource(resource); + const groupDescriptor = toGroupDescriptor(data); + const group = await this.dbManager.overwriteGroup(id, groupDescriptor); + return toSCIMMYGroup(group); + }); + } } export const getScimGroupConfig = ( @@ -74,6 +90,9 @@ export const getScimGroupConfig = ( return await controller.getGroups(resource, context); }, ingress: async (resource: any, data: any, context: RequestContext) => { + if (resource.id) { + return await controller.overwriteGroup(resource, data, context); + } return await controller.createGroup(data, context); }, }; diff --git a/app/server/lib/scim/v2/ScimUserController.ts b/app/server/lib/scim/v2/ScimUserController.ts index b10aa93eaa..b9ef5570d5 100644 --- a/app/server/lib/scim/v2/ScimUserController.ts +++ b/app/server/lib/scim/v2/ScimUserController.ts @@ -63,10 +63,10 @@ class ScimUserController extends BaseController { } /** - * Overrides a user with the passed data. + * Overwrite a user with the passed data. * * @param resource The SCIMMY user resource performing the operation - * @param data The data to override the user with + * @param data The data to overwrite the user with * @param context The request context */ public async overwriteUser(resource: any, data: any, context: RequestContext) { diff --git a/test/server/lib/Scim.ts b/test/server/lib/Scim.ts index f4ece382b1..b4cc155039 100644 --- a/test/server/lib/Scim.ts +++ b/test/server/lib/Scim.ts @@ -193,6 +193,8 @@ describe('Scim', () => { sandbox.stub(getDbManager(), 'getGroupsWithMembersByType').throws(error); sandbox.stub(getDbManager(), 'getGroupsWithMembers').throws(error); sandbox.stub(getDbManager(), 'createGroup').throws(error); + sandbox.stub(getDbManager(), 'overwriteGroup').throws(error); + sandbox.stub(getDbManager(), 'deleteGroup').throws(error); const res = await makeCallWith('chimpy'); assert.deepEqual(res.data, { @@ -485,7 +487,6 @@ describe('Scim', () => { }); }); - it('should deduce the name from the displayEmail when not provided', async function () { const res = await axios.put(scimUrl(`/Users/${userToUpdateId}`), { schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], @@ -643,6 +644,15 @@ describe('Scim', () => { return await withGroupNames([groupName], (groupNames) => cb(groupNames[0])); } + function getUserMember(user: keyof UserConfigByName) { + return { value: String(userIdByName[user]), display: capitalize(user), type: 'User' }; + } + + function getUserMemberWithRef(user: keyof UserConfigByName) { + return { ...getUserMember(user), $ref: `/api/scim/v2/Users/${userIdByName[user]}` }; + } + + describe('GET /Groups/{id}', function () { it(`should return a "${Group.RESOURCE_USERS_TYPE}" group for chimpy`, async function () { await withGroupName('test-get-group-by-id', async (groupName) => { @@ -743,13 +753,13 @@ describe('Scim', () => { schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], id: String(group1.id), displayName: group1Name, - members: [{ value: '1', display: 'Chimpy', $ref: '/api/scim/v2/Users/1', type: 'User' }], + members: [getUserMemberWithRef('chimpy')], meta: { resourceType: 'Group', location: `/api/scim/v2/Groups/${group1.id}` } }, { schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], id: String(group2.id), displayName: group2Name, - members: [{ value: '2', display: 'Kiwi', $ref: '/api/scim/v2/Users/2', type: 'User' }], + members: [getUserMemberWithRef('kiwi')], meta: { resourceType: 'Group', location: `/api/scim/v2/Groups/${group2.id}` } } ]); @@ -761,14 +771,14 @@ describe('Scim', () => { }); describe('POST /Groups', function () { - it(`should create a new ${Group.RESOURCE_USERS_TYPE} group`, async function () { + it(`should create a new group of type "${Group.RESOURCE_USERS_TYPE}`, async function () { await withGroupName('test-group', async (groupName) => { const res = await axios.post(scimUrl('/Groups'), { schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], displayName: groupName, members: [ - { value: String(userIdByName['chimpy']), display: 'Chimpy', type: 'User' }, - { value: String(userIdByName['kiwi']), display: 'Kiwi', type: 'User' }, + getUserMember('chimpy'), + getUserMember('kiwi'), ] }, chimpy); assert.equal(res.status, 201); @@ -778,8 +788,8 @@ describe('Scim', () => { id: String(newGroupId), displayName: groupName, members: [ - { value: '1', display: 'Chimpy', $ref: '/api/scim/v2/Users/1', type: 'User' }, - { value: '2', display: 'Kiwi', $ref: '/api/scim/v2/Users/2', type: 'User' }, + getUserMemberWithRef('chimpy'), + getUserMemberWithRef('kiwi'), ], meta: { resourceType: 'Group', location: `/api/scim/v2/Groups/${newGroupId}` } }); @@ -858,8 +868,7 @@ describe('Scim', () => { schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], displayName: 'test-group', members: [ - { value: String(userIdByName['chimpy']), display: 'Chimpy', - $ref: `/api/scim/v2/Users/${String(userIdByName['chimpy'])}`, type: 'User' }, + getUserMember('chimpy'), { value: '1000', display: 'Non-Existing User', type: 'User' }, ] }, chimpy); @@ -893,13 +902,104 @@ describe('Scim', () => { // We need to differ the moment we call userIdByName['chimpy'] (set during a "before()" hook) get members() { return [ - { value: String(userIdByName['chimpy']), display: 'Chimpy', type: 'User' }, - { value: String(userIdByName['kiwi']), display: 'Kiwi', type: 'User' }, + getUserMember('chimpy'), + getUserMember('kiwi'), ]; } }); }); + describe('PUT /Groups/{id}', function () { + beforeEach(async function () { + await withGroupName('test-group', async (groupName) => { + await getDbManager().createGroup({ + name: groupName, + type: Group.RESOURCE_USERS_TYPE, + memberUsers: [userIdByName['chimpy']!] + }); + }); + }); + + it('should update an existing group', async function () { + const newGroupName = 'Updated Group Name'; + const res = await axios.put(scimUrl('/Groups/1'), { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + displayName: newGroupName, + members: [ + getUserMember('kiwi'), + ] + }, chimpy); + assert.equal(res.status, 200); + assert.deepEqual(res.data, { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + id: '1', + displayName: newGroupName, + members: [ + getUserMemberWithRef('kiwi'), + ], + meta: { resourceType: 'Group', location: '/api/scim/v2/Groups/1' } + }); + }); + + it('should updating a group with members omitted', async function () { + const newGroupName = 'Updated Group Name'; + const res = await axios.put(scimUrl('/Groups/1'), { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + displayName: newGroupName, + }, chimpy); + assert.equal(res.status, 200); + assert.deepEqual(res.data, { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + id: '1', + displayName: newGroupName, + members: [], + meta: { resourceType: 'Group', location: '/api/scim/v2/Groups/1' } + }); + }); + + it('should return 404 when the group is not found', async function () { + const res = await axios.put(scimUrl('/Groups/1000'), { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + displayName: 'New Group Name', + members: [ + getUserMember('kiwi'), + ] + }, chimpy); + assert.deepEqual(res.data, { + schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], + status: '404', + detail: 'Group with id 1000 not found' + }); + assert.equal(res.status, 404); + }); + + it('should return 400 when the group id is malformed', async function () { + const res = await axios.put(scimUrl('/Groups/not-an-id'), { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + displayName: 'New Group Name', + members: [ + getUserMember('kiwi'), + ] + }, chimpy); + assert.deepEqual(res.data, { + schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], + status: '400', + detail: 'Invalid passed group ID', + scimType: 'invalidValue' + }); + assert.equal(res.status, 400); + }); + + checkCommonErrors('put', '/Groups/1', { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], + displayName: 'New Group Name', + // We need to differ the moment we call userIdByName['kiwi'] (set during a "before()" hook) + get members() { + return [ getUserMember('kiwi') ]; + } + }); + }); + }); describe('POST /Bulk', function () {