SCIM: Implement DELETE

This commit is contained in:
fflorent 2024-09-05 21:27:27 +02:00
parent 6f49f83b66
commit 41718cb0da
2 changed files with 91 additions and 14 deletions

View File

@ -1,11 +1,12 @@
import * as express from 'express'; import * as express from 'express';
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager'; import { HomeDBManager, Scope } from 'app/gen-server/lib/homedb/HomeDBManager';
import SCIMMY from "scimmy"; import SCIMMY from "scimmy";
import SCIMMYRouters from "scimmy-routers"; import SCIMMYRouters from "scimmy-routers";
import { RequestWithLogin } from '../../Authorizer'; import { RequestWithLogin } from '../../Authorizer';
import { InstallAdmin } from '../../InstallAdmin'; import { InstallAdmin } from '../../InstallAdmin';
import { toSCIMMYUser, toUserProfile } from './ScimUserUtils'; import { toSCIMMYUser, toUserProfile } from './ScimUserUtils';
import { ApiError } from 'app/common/ApiError'; import { ApiError } from 'app/common/ApiError';
import { parseInt } from 'lodash';
const WHITELISTED_PATHS_FOR_NON_ADMINS = [ "/Me", "/Schemas", "/ResourceTypes", "/ServiceProviderConfig" ]; const WHITELISTED_PATHS_FOR_NON_ADMINS = [ "/Me", "/Schemas", "/ResourceTypes", "/ServiceProviderConfig" ];
@ -20,7 +21,8 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
SCIMMY.Resources.declare(SCIMMY.Resources.User, { SCIMMY.Resources.declare(SCIMMY.Resources.User, {
egress: async (resource: any) => { egress: async (resource: any) => {
const { id, filter } = resource; const { filter } = resource;
const id = parseInt(resource.id, 10);
if (id) { if (id) {
const user = await dbManager.getUser(id); const user = await dbManager.getUser(id);
if (!user) { if (!user) {
@ -33,7 +35,7 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
}, },
ingress: async (resource: any, data: any) => { ingress: async (resource: any, data: any) => {
try { try {
const { id } = resource; const id = parseInt(resource.id, 10);
if (id) { if (id) {
const updatedUser = await dbManager.overrideUser(id, toUserProfile(data)); const updatedUser = await dbManager.overrideUser(id, toUserProfile(data));
return toSCIMMYUser(updatedUser); return toSCIMMYUser(updatedUser);
@ -55,9 +57,13 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
throw new SCIMMY.Types.Error(ex.status, null!, ex.message); throw new SCIMMY.Types.Error(ex.status, null!, ex.message);
} }
// FIXME: Remove this part and find another way to detect a constraint error.
if (ex.code?.startsWith('SQLITE')) { if (ex.code?.startsWith('SQLITE')) {
switch (ex.code) { switch (ex.code) {
case 'SQLITE_CONSTRAINT': case 'SQLITE_CONSTRAINT':
// Return a 409 error if a conflict is detected (e.g. email already exists)
// "uniqueness" is an error code expected by the SCIM RFC for this case.
// FIXME: the emails are unique in the database, but this is not enforced in the schema.
throw new SCIMMY.Types.Error(409, 'uniqueness', ex.message); throw new SCIMMY.Types.Error(409, 'uniqueness', ex.message);
default: default:
throw new SCIMMY.Types.Error(500, 'serverError', ex.message); throw new SCIMMY.Types.Error(500, 'serverError', ex.message);
@ -67,8 +73,19 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
throw ex; throw ex;
} }
}, },
degress: () => { degress: async (resource: any) => {
return null; const id = parseInt(resource.id, 10);
const fakeScope: Scope = { userId: id }; // FIXME: deleteUser should probably better not requiring a scope.
try {
await dbManager.deleteUser(fakeScope, id);
} catch (ex) {
console.error('Error deleting user', ex);
if (ex instanceof ApiError) {
throw new SCIMMY.Types.Error(ex.status, null!, ex.message);
}
throw new SCIMMY.Types.Error(500, 'serverError', ex.message);
}
} }
}); });

View File

@ -44,7 +44,7 @@ describe('Scim', () => {
homeUrl = await server.start(); homeUrl = await server.start();
const userNames = Object.keys(USER_CONFIG_BY_NAME) as Array<keyof UserConfigByName>; const userNames = Object.keys(USER_CONFIG_BY_NAME) as Array<keyof UserConfigByName>;
for (const user of userNames) { for (const user of userNames) {
userIdByName[user] = await getUserId(user); userIdByName[user] = await getOrCreateUserId(user);
} }
}); });
@ -77,10 +77,16 @@ describe('Scim', () => {
}; };
} }
async function getUserId(user: string) { async function getOrCreateUserId(user: string) {
return (await server.dbManager.getUserByLogin(user + '@getgrist.com'))!.id; return (await server.dbManager.getUserByLogin(user + '@getgrist.com'))!.id;
} }
async function cleanupUser(userId: number) {
if (await server.dbManager.getUser(userId)) {
await server.dbManager.deleteUser({ userId: userId }, userId);
}
}
function checkEndpointNotAccessibleForNonAdminUsers( function checkEndpointNotAccessibleForNonAdminUsers(
method: 'get' | 'post' | 'put' | 'patch' | 'delete', method: 'get' | 'post' | 'put' | 'patch' | 'delete',
path: string path: string
@ -199,7 +205,7 @@ describe('Scim', () => {
it('should create a new user', async function () { it('should create a new user', async function () {
const res = await axios.post(scimUrl('/Users'), toSCIMUserWithoutId('newuser1'), chimpy); const res = await axios.post(scimUrl('/Users'), toSCIMUserWithoutId('newuser1'), chimpy);
assert.equal(res.status, 201); assert.equal(res.status, 201);
const newUserId = await getUserId('newuser1'); const newUserId = await getOrCreateUserId('newuser1');
assert.deepEqual(res.data, toSCIMUserWithId('newuser1', newUserId)); assert.deepEqual(res.data, toSCIMUserWithId('newuser1', newUserId));
}); });
@ -247,10 +253,10 @@ describe('Scim', () => {
const userToUpdateEmailLocalPart = 'user-to-update'; const userToUpdateEmailLocalPart = 'user-to-update';
beforeEach(async function () { beforeEach(async function () {
userToUpdateId = await getUserId(userToUpdateEmailLocalPart); userToUpdateId = await getOrCreateUserId(userToUpdateEmailLocalPart);
}); });
afterEach(async function () { afterEach(async function () {
await server.dbManager.deleteUser({ userId: userToUpdateId }, userToUpdateId); await cleanupUser(userToUpdateId);
}); });
it('should update an existing user', async function () { it('should update an existing user', async function () {
@ -344,15 +350,69 @@ describe('Scim', () => {
}); });
describe('PATCH /Users/{id}', function () { describe('PATCH /Users/{id}', function () {
it('should update the user of id=1', async function () { let userToPatchId: number;
throw new Error("This is a reminder :)"); const userToPatchEmailLocalPart = 'user-to-patch';
beforeEach(async function () {
userToPatchId = await getOrCreateUserId(userToPatchEmailLocalPart);
}); });
afterEach(async function () {
await cleanupUser(userToPatchId);
});
it('should replace values of an existing user', async function () {
const newName = 'User to Patch new Name';
const res = await axios.patch(scimUrl(`/Users/${userToPatchId}`), {
schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
Operations: [{
op: "replace",
path: "displayName",
value: newName,
}, {
op: "replace",
path: "locale",
value: 'fr'
}],
}, chimpy);
assert.equal(res.status, 200);
const refreshedUser = await axios.get(scimUrl(`/Users/${userToPatchId}`), chimpy);
assert.deepEqual(refreshedUser.data, {
...toSCIMUserWithId(userToPatchEmailLocalPart, userToPatchId),
displayName: newName,
name: { formatted: newName },
locale: 'fr',
preferredLanguage: 'fr',
});
});
checkEndpointNotAccessibleForNonAdminUsers('patch', '/Users/1'); checkEndpointNotAccessibleForNonAdminUsers('patch', '/Users/1');
}); });
describe('DELETE /Users/{id}', function () { describe('DELETE /Users/{id}', function () {
it('should delete the user of id=1', async function () { let userToDeleteId: number;
throw new Error("This is a reminder :)"); const userToDeleteEmailLocalPart = 'user-to-delete';
beforeEach(async function () {
userToDeleteId = await getOrCreateUserId(userToDeleteEmailLocalPart);
});
afterEach(async function () {
await cleanupUser(userToDeleteId);
});
it('should delete some user', async function () {
const res = await axios.delete(scimUrl(`/Users/${userToDeleteId}`), chimpy);
assert.equal(res.status, 204);
const refreshedUser = await axios.get(scimUrl(`/Users/${userToDeleteId}`), chimpy);
assert.equal(refreshedUser.status, 404);
});
it('should return 404 when the user is not found', async function () {
const res = await axios.delete(scimUrl('/Users/1000'), chimpy);
assert.equal(res.status, 404);
assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '404',
detail: 'user not found'
});
}); });
checkEndpointNotAccessibleForNonAdminUsers('delete', '/Users/1'); checkEndpointNotAccessibleForNonAdminUsers('delete', '/Users/1');
}); });