More tests and cleanups

This commit is contained in:
fflorent 2024-09-06 17:02:14 +02:00
parent 41718cb0da
commit 9c81ddbba9
2 changed files with 329 additions and 86 deletions

View File

@ -10,20 +10,36 @@ import { parseInt } from 'lodash';
const WHITELISTED_PATHS_FOR_NON_ADMINS = [ "/Me", "/Schemas", "/ResourceTypes", "/ServiceProviderConfig" ]; const WHITELISTED_PATHS_FOR_NON_ADMINS = [ "/Me", "/Schemas", "/ResourceTypes", "/ServiceProviderConfig" ];
async function isAuthorizedAction(mreq: RequestWithLogin, installAdmin: InstallAdmin): Promise<boolean> { interface RequestContext {
const isAdmin = await installAdmin.isAdminReq(mreq); path: string;
const isScimUser = Boolean(process.env.GRIST_SCIM_EMAIL && mreq.user?.loginEmail === process.env.GRIST_SCIM_EMAIL); isAdmin: boolean;
return isAdmin || isScimUser || WHITELISTED_PATHS_FOR_NON_ADMINS.includes(mreq.path); isScimUser: boolean;
}
function checkAccess(context: RequestContext) {
const {isAdmin, isScimUser, path } = context;
if (!isAdmin && !isScimUser && !WHITELISTED_PATHS_FOR_NON_ADMINS.includes(path)) {
throw new SCIMMY.Types.Error(403, null!, 'You are not authorized to access this resource');
}
}
async function checkEmailIsUnique(dbManager: HomeDBManager, email: string, id?: number) {
const existingUser = await dbManager.getExistingUserByLogin(email);
if (existingUser !== undefined && existingUser.id !== id) {
throw new SCIMMY.Types.Error(409, 'uniqueness', 'An existing user with the passed email exist.');
}
} }
const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin) => { const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin) => {
const v2 = express.Router(); const v2 = express.Router();
SCIMMY.Resources.declare(SCIMMY.Resources.User, { SCIMMY.Resources.declare(SCIMMY.Resources.User, {
egress: async (resource: any) => { egress: async (resource: any, context: RequestContext) => {
checkAccess(context);
const { filter } = resource; const { filter } = resource;
const id = parseInt(resource.id, 10); const id = parseInt(resource.id, 10);
if (id) { if (!isNaN(id)) {
const user = await dbManager.getUser(id); const user = await dbManager.getUser(id);
if (!user) { if (!user) {
throw new SCIMMY.Types.Error(404, null!, `User with ID ${id} not found`); throw new SCIMMY.Types.Error(404, null!, `User with ID ${id} not found`);
@ -33,18 +49,18 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
const scimmyUsers = (await dbManager.getUsers()).map(user => toSCIMMYUser(user)); const scimmyUsers = (await dbManager.getUsers()).map(user => toSCIMMYUser(user));
return filter ? filter.match(scimmyUsers) : scimmyUsers; return filter ? filter.match(scimmyUsers) : scimmyUsers;
}, },
ingress: async (resource: any, data: any) => { ingress: async (resource: any, data: any, context: RequestContext) => {
checkAccess(context);
try { try {
const id = parseInt(resource.id, 10); const id = parseInt(resource.id, 10);
if (id) { if (!isNaN(id)) {
await checkEmailIsUnique(dbManager, data.userName, id);
const updatedUser = await dbManager.overrideUser(id, toUserProfile(data)); const updatedUser = await dbManager.overrideUser(id, toUserProfile(data));
return toSCIMMYUser(updatedUser); return toSCIMMYUser(updatedUser);
} }
await checkEmailIsUnique(dbManager, data.userName);
const userProfileToInsert = toUserProfile(data); const userProfileToInsert = toUserProfile(data);
const maybeExistingUser = await dbManager.getExistingUserByLogin(userProfileToInsert.email);
if (maybeExistingUser !== undefined) {
throw new SCIMMY.Types.Error(409, 'uniqueness', 'An existing user with the passed email exist.');
}
const newUser = await dbManager.getUserByLoginWithRetry(userProfileToInsert.email, { const newUser = await dbManager.getUserByLoginWithRetry(userProfileToInsert.email, {
profile: userProfileToInsert profile: userProfileToInsert
}); });
@ -57,29 +73,20 @@ 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')) {
switch (ex.code) {
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);
default:
throw new SCIMMY.Types.Error(500, 'serverError', ex.message);
}
}
throw ex; throw ex;
} }
}, },
degress: async (resource: any) => { degress: async (resource: any, context: RequestContext) => {
checkAccess(context);
const id = parseInt(resource.id, 10); const id = parseInt(resource.id, 10);
if (isNaN(id)) {
throw new SCIMMY.Types.Error(400, null!, 'Invalid ID');
}
const fakeScope: Scope = { userId: id }; // FIXME: deleteUser should probably better not requiring a scope. const fakeScope: Scope = { userId: id }; // FIXME: deleteUser should probably better not requiring a scope.
try { try {
await dbManager.deleteUser(fakeScope, id); await dbManager.deleteUser(fakeScope, id);
} catch (ex) { } catch (ex) {
console.error('Error deleting user', ex);
if (ex instanceof ApiError) { if (ex instanceof ApiError) {
throw new SCIMMY.Types.Error(ex.status, null!, ex.message); throw new SCIMMY.Types.Error(ex.status, null!, ex.message);
} }
@ -102,10 +109,15 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
throw new Error('Anonymous user cannot access SCIM resources'); throw new Error('Anonymous user cannot access SCIM resources');
} }
if (!await isAuthorizedAction(mreq, installAdmin)) { return String(mreq.userId); // SCIMMYRouters requires the userId to be a string.
throw new SCIMMY.Types.Error(403, null!, 'Resource disallowed for non-admin users'); },
} context: async (mreq: RequestWithLogin): Promise<RequestContext> => {
return String(mreq.userId); // HACK: SCIMMYRouters requires the userId to be a string. const isAdmin = await installAdmin.isAdminReq(mreq);
const isScimUser = Boolean(
process.env.GRIST_SCIM_EMAIL && mreq.user?.loginEmail === process.env.GRIST_SCIM_EMAIL
);
const path = mreq.path;
return { isAdmin, isScimUser, path };
} }
}) as express.Router; // Have to cast it into express.Router. See https://github.com/scimmyjs/scimmy-routers/issues/24 }) as express.Router; // Have to cast it into express.Router. See https://github.com/scimmyjs/scimmy-routers/issues/24

View File

@ -18,7 +18,7 @@ function scimConfigForUser(user: string) {
const chimpy = scimConfigForUser('Chimpy'); const chimpy = scimConfigForUser('Chimpy');
const kiwi = scimConfigForUser('Kiwi'); const kiwi = scimConfigForUser('Kiwi');
const anon = configForUser('Anonymous'); const anon = scimConfigForUser('Anonymous');
const USER_CONFIG_BY_NAME = { const USER_CONFIG_BY_NAME = {
chimpy, chimpy,
@ -89,13 +89,14 @@ describe('Scim', () => {
function checkEndpointNotAccessibleForNonAdminUsers( function checkEndpointNotAccessibleForNonAdminUsers(
method: 'get' | 'post' | 'put' | 'patch' | 'delete', method: 'get' | 'post' | 'put' | 'patch' | 'delete',
path: string path: string,
validBody: object = {}
) { ) {
function makeCallWith(user: keyof UserConfigByName) { function makeCallWith(user: keyof UserConfigByName) {
if (method === 'get' || method === 'delete') { if (method === 'get' || method === 'delete') {
return axios[method](scimUrl(path), USER_CONFIG_BY_NAME[user]); return axios[method](scimUrl(path), USER_CONFIG_BY_NAME[user]);
} }
return axios[method](scimUrl(path), {}, USER_CONFIG_BY_NAME[user]); return axios[method](scimUrl(path), validBody, USER_CONFIG_BY_NAME[user]);
} }
it('should return 401 for anonymous', async function () { it('should return 401 for anonymous', async function () {
const res: any = await makeCallWith('anon'); const res: any = await makeCallWith('anon');
@ -104,7 +105,12 @@ describe('Scim', () => {
it('should return 401 for kiwi', async function () { it('should return 401 for kiwi', async function () {
const res: any = await makeCallWith('kiwi'); const res: any = await makeCallWith('kiwi');
assert.equal(res.status, 401); assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '403',
detail: 'You are not authorized to access this resource'
});
assert.equal(res.status, 403);
}); });
} }
@ -127,6 +133,27 @@ describe('Scim', () => {
const res = await axios.get(scimUrl('/Me'), anon); const res = await axios.get(scimUrl('/Me'), anon);
assert.equal(res.status, 401); assert.equal(res.status, 401);
}); });
it.skip('should allow operation like PATCH for kiwi', async function () {
// SKIPPING this test: only the GET verb is currently implemented by SCIMMY for the /Me endpoint.
// Issue created here: https://github.com/scimmyjs/scimmy/issues/47
const patchBody = {
schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
Operations: [{
op: "replace",
path: 'locale',
value: 'fr',
}],
};
const res = await axios.patch(scimUrl('/Me'), patchBody, kiwi);
assert.equal(res.status, 200);
assert.deepEqual(res.data, {
...personaToSCIMMYUserWithId('kiwi'),
locale: 'fr',
preferredLanguage: 'en',
});
});
}); });
describe('/Users/{id}', function () { describe('/Users/{id}', function () {
@ -170,12 +197,15 @@ describe('Scim', () => {
describe('POST /Users/.search', function () { describe('POST /Users/.search', function () {
const SEARCH_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:SearchRequest'; const SEARCH_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:SearchRequest';
it('should return all users for chimpy order by userName in descending order', async function () {
const res = await axios.post(scimUrl('/Users/.search'), { const searchExample = {
schemas: [SEARCH_SCHEMA], schemas: [SEARCH_SCHEMA],
sortBy: 'userName', sortBy: 'userName',
sortOrder: 'descending', sortOrder: 'descending',
}, chimpy); };
it('should return all users for chimpy order by userName in descending order', async function () {
const res = await axios.post(scimUrl('/Users/.search'), searchExample, chimpy);
assert.equal(res.status, 200); assert.equal(res.status, 200);
assert.isAbove(res.data.totalResults, 0, 'should have retrieved some users'); assert.isAbove(res.data.totalResults, 0, 'should have retrieved some users');
const users = res.data.Resources.map((r: any) => r.userName); const users = res.data.Resources.map((r: any) => r.userName);
@ -198,54 +228,70 @@ describe('Scim', () => {
"should have retrieved only chimpy's username and not other attribute"); "should have retrieved only chimpy's username and not other attribute");
}); });
checkEndpointNotAccessibleForNonAdminUsers('post', '/Users/.search'); checkEndpointNotAccessibleForNonAdminUsers('post', '/Users/.search', searchExample);
}); });
describe('POST /Users', function () { // Create a new users describe('POST /Users', function () { // Create a new users
async function withUserName(userName: string, cb: (userName: string) => Promise<void>) {
try {
await cb(userName);
} finally {
const user = await server.dbManager.getExistingUserByLogin(userName + "@getgrist.com");
if (user) {
await cleanupUser(user.id);
}
}
}
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); await withUserName('newuser1', async (userName) => {
const res = await axios.post(scimUrl('/Users'), toSCIMUserWithoutId(userName), chimpy);
assert.equal(res.status, 201); assert.equal(res.status, 201);
const newUserId = await getOrCreateUserId('newuser1'); const newUserId = await getOrCreateUserId(userName);
assert.deepEqual(res.data, toSCIMUserWithId('newuser1', newUserId)); assert.deepEqual(res.data, toSCIMUserWithId(userName, newUserId));
});
}); });
it('should allow creating a new user given only their email passed as username', async function () { it('should allow creating a new user given only their email passed as username', async function () {
await withUserName('new.user2', async (userName) => {
const res = await axios.post(scimUrl('/Users'), { const res = await axios.post(scimUrl('/Users'), {
schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
userName: 'new.user2@getgrist.com', userName: 'new.user2@getgrist.com',
}, chimpy); }, chimpy);
assert.equal(res.status, 201); assert.equal(res.status, 201);
assert.equal(res.data.userName, 'new.user2@getgrist.com'); assert.equal(res.data.userName, userName + '@getgrist.com');
assert.equal(res.data.displayName, 'new.user2'); assert.equal(res.data.displayName, userName);
});
}); });
it('should reject when passed email differs from username', async function () { it('should reject when passed email differs from username', async function () {
await withUserName('username', async (userName) => {
const res = await axios.post(scimUrl('/Users'), { const res = await axios.post(scimUrl('/Users'), {
schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
userName: 'username@getgrist.com', userName: userName + '@getgrist.com',
emails: [{ value: 'emails.value@getgrist.com' }], emails: [{ value: 'emails.value@getgrist.com' }],
}, chimpy); }, chimpy);
assert.equal(res.status, 400);
assert.deepEqual(res.data, { assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '400', status: '400',
detail: 'Email and userName must be the same', detail: 'Email and userName must be the same',
scimType: 'invalidValue' scimType: 'invalidValue'
}); });
assert.equal(res.status, 400);
});
}); });
it('should disallow creating a user with the same email', async function () { it('should disallow creating a user with the same email', async function () {
const res = await axios.post(scimUrl('/Users'), toSCIMUserWithoutId('chimpy'), chimpy); const res = await axios.post(scimUrl('/Users'), toSCIMUserWithoutId('chimpy'), chimpy);
assert.equal(res.status, 409);
assert.deepEqual(res.data, { assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '409', status: '409',
detail: 'An existing user with the passed email exist.', detail: 'An existing user with the passed email exist.',
scimType: 'uniqueness' scimType: 'uniqueness'
}); });
assert.equal(res.status, 409);
}); });
checkEndpointNotAccessibleForNonAdminUsers('post', '/Users'); checkEndpointNotAccessibleForNonAdminUsers('post', '/Users', toSCIMUserWithoutId('some-user'));
}); });
describe('PUT /Users/{id}', function () { describe('PUT /Users/{id}', function () {
@ -286,34 +332,34 @@ describe('Scim', () => {
userName: userToUpdateEmailLocalPart + '@getgrist.com', userName: userToUpdateEmailLocalPart + '@getgrist.com',
emails: [{ value: 'whatever@getgrist.com', primary: true }], emails: [{ value: 'whatever@getgrist.com', primary: true }],
}, chimpy); }, chimpy);
assert.equal(res.status, 400);
assert.deepEqual(res.data, { assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '400', status: '400',
detail: 'Email and userName must be the same', detail: 'Email and userName must be the same',
scimType: 'invalidValue' scimType: 'invalidValue'
}); });
assert.equal(res.status, 400);
}); });
it('should disallow updating a user with the same email', async function () { it('should disallow updating a user with the same email as another user\'s', async function () {
const res = await axios.put(scimUrl(`/Users/${userToUpdateId}`), toSCIMUserWithoutId('chimpy'), chimpy); const res = await axios.put(scimUrl(`/Users/${userToUpdateId}`), toSCIMUserWithoutId('chimpy'), chimpy);
assert.equal(res.status, 409);
assert.deepEqual(res.data, { assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '409', status: '409',
detail: 'SQLITE_CONSTRAINT: UNIQUE constraint failed: logins.email', detail: 'An existing user with the passed email exist.',
scimType: 'uniqueness' scimType: 'uniqueness'
}); });
assert.equal(res.status, 409);
}); });
it('should return 404 when the user is not found', async function () { it('should return 404 when the user is not found', async function () {
const res = await axios.put(scimUrl('/Users/1000'), toSCIMUserWithoutId('chimpy'), chimpy); const res = await axios.put(scimUrl('/Users/1000'), toSCIMUserWithoutId('whoever'), chimpy);
assert.equal(res.status, 404);
assert.deepEqual(res.data, { assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '404', status: '404',
detail: 'unable to find user to update' detail: 'unable to find user to update'
}); });
assert.equal(res.status, 404);
}); });
it('should deduce the name from the displayEmail when not provided', async function () { it('should deduce the name from the displayEmail when not provided', async function () {
@ -346,7 +392,7 @@ describe('Scim', () => {
}); });
}); });
checkEndpointNotAccessibleForNonAdminUsers('put', '/Users/1'); checkEndpointNotAccessibleForNonAdminUsers('put', '/Users/1', toSCIMUserWithoutId('chimpy'));
}); });
describe('PATCH /Users/{id}', function () { describe('PATCH /Users/{id}', function () {
@ -359,9 +405,7 @@ describe('Scim', () => {
await cleanupUser(userToPatchId); await cleanupUser(userToPatchId);
}); });
it('should replace values of an existing user', async function () { const validPatchBody = (newName: string) => ({
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'], schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
Operations: [{ Operations: [{
op: "replace", op: "replace",
@ -372,7 +416,11 @@ describe('Scim', () => {
path: "locale", path: "locale",
value: 'fr' value: 'fr'
}], }],
}, chimpy); });
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}`), validPatchBody(newName), chimpy);
assert.equal(res.status, 200); assert.equal(res.status, 200);
const refreshedUser = await axios.get(scimUrl(`/Users/${userToPatchId}`), chimpy); const refreshedUser = await axios.get(scimUrl(`/Users/${userToPatchId}`), chimpy);
assert.deepEqual(refreshedUser.data, { assert.deepEqual(refreshedUser.data, {
@ -384,7 +432,7 @@ describe('Scim', () => {
}); });
}); });
checkEndpointNotAccessibleForNonAdminUsers('patch', '/Users/1'); checkEndpointNotAccessibleForNonAdminUsers('patch', '/Users/1', validPatchBody('new name2'));
}); });
describe('DELETE /Users/{id}', function () { describe('DELETE /Users/{id}', function () {
@ -407,17 +455,200 @@ describe('Scim', () => {
it('should return 404 when the user is not found', async function () { it('should return 404 when the user is not found', async function () {
const res = await axios.delete(scimUrl('/Users/1000'), chimpy); const res = await axios.delete(scimUrl('/Users/1000'), chimpy);
assert.equal(res.status, 404);
assert.deepEqual(res.data, { assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ], schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '404', status: '404',
detail: 'user not found' detail: 'user not found'
}); });
assert.equal(res.status, 404);
}); });
checkEndpointNotAccessibleForNonAdminUsers('delete', '/Users/1'); checkEndpointNotAccessibleForNonAdminUsers('delete', '/Users/1');
}); });
it('should fix the 401 response for authenticated users', function () { describe('POST /Bulk', function () {
throw new Error("This is a reminder :)"); let usersToCleanupEmails: string[];
beforeEach(async function () {
usersToCleanupEmails = [];
usersToCleanupEmails.push('bulk-user1@getgrist.com');
usersToCleanupEmails.push('bulk-user2@getgrist.com');
});
afterEach(async function () {
for (const email of usersToCleanupEmails) {
const user = await server.dbManager.getExistingUserByLogin(email);
if (user) {
await cleanupUser(user.id);
}
}
});
it('should return statuses for each operation', async function () {
const putOnUnknownResource = { method: 'PUT', path: '/Users/1000', value: toSCIMUserWithoutId('chimpy') };
const validCreateOperation = {
method: 'POST', path: '/Users/', data: toSCIMUserWithoutId('bulk-user3'), bulkId: '1'
};
usersToCleanupEmails.push('bulk-user3');
const createOperationWithUserNameConflict = {
method: 'POST', path: '/Users/', data: toSCIMUserWithoutId('chimpy'), bulkId: '2'
};
const res = await axios.post(scimUrl('/Bulk'), {
schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkRequest'],
Operations: [
putOnUnknownResource,
validCreateOperation,
createOperationWithUserNameConflict,
],
}, chimpy);
assert.equal(res.status, 200);
assert.deepEqual(res.data, {
schemas: [ "urn:ietf:params:scim:api:messages:2.0:BulkResponse" ],
Operations: [
{
method: "PUT",
location: "/api/scim/v2/Users/1000",
status: "400",
response: {
schemas: [
"urn:ietf:params:scim:api:messages:2.0:Error"
],
status: "400",
scimType: "invalidSyntax",
detail: "Expected 'data' to be a single complex value in BulkRequest operation #1"
}
}, {
method: "POST",
bulkId: "1",
location: "/api/scim/v2/Users/26",
status: "201"
}, {
method: "POST",
bulkId: "2",
status: "409",
response: {
schemas: [
"urn:ietf:params:scim:api:messages:2.0:Error"
],
status: "409",
scimType: "uniqueness",
detail: "An existing user with the passed email exist."
}
}
]
});
});
it('should return 400 when no operations are provided', async function () {
const res = await axios.post(scimUrl('/Bulk'), {
schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkRequest'],
Operations: [],
}, chimpy);
assert.equal(res.status, 400);
assert.deepEqual(res.data, {
schemas: [ 'urn:ietf:params:scim:api:messages:2.0:Error' ],
status: '400',
detail: "BulkRequest request body must contain 'Operations' attribute with at least one operation",
scimType: 'invalidValue'
});
});
it('should disallow accessing resources to kiwi', async function () {
const creationOperation = {
method: 'POST', path: '/Users', data: toSCIMUserWithoutId('bulk-user4'), bulkId: '1'
};
usersToCleanupEmails.push('bulk-user4');
const selfPutOperation = { method: 'PUT', path: '/Me', value: toSCIMUserWithoutId('kiwi') };
const res = await axios.post(scimUrl('/Bulk'), {
schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkRequest'],
Operations: [
creationOperation,
selfPutOperation,
],
}, kiwi);
assert.equal(res.status, 200);
assert.deepEqual(res.data, {
schemas: [ "urn:ietf:params:scim:api:messages:2.0:BulkResponse" ],
Operations: [
{
method: "POST",
bulkId: "1",
status: "403",
response: {
detail: "You are not authorized to access this resource",
schemas: [ "urn:ietf:params:scim:api:messages:2.0:Error" ],
status: "403"
}
}, {
// When writing this test, the SCIMMY implementation does not yet support PUT operations on /Me.
// This reflects the current behavior, but it may change in the future.
// Change this test if the behavior changes.
// It is probably fine to allow altering oneself even for non-admins.
method: "PUT",
location: "/Me",
status: "400",
response: {
schemas: [
"urn:ietf:params:scim:api:messages:2.0:Error"
],
status: "400",
detail: "Invalid 'path' value '/Me' in BulkRequest operation #2",
scimType: "invalidValue"
}
}
]
});
});
it('should disallow accessing resources to anonymous', async function () {
const creationOperation = {
method: 'POST', path: '/Users', data: toSCIMUserWithoutId('bulk-user5'), bulkId: '1'
};
usersToCleanupEmails.push('bulk-user5');
const res = await axios.post(scimUrl('/Bulk'), {
schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkRequest'],
Operations: [creationOperation],
}, anon);
assert.equal(res.status, 401);
});
});
it('should allow fetching the Scim schema when autenticated', async function () {
const res = await axios.get(scimUrl('/Schemas'), kiwi);
assert.equal(res.status, 200);
assert.deepInclude(res.data, {
schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
});
assert.property(res.data, 'Resources');
assert.deepInclude(res.data.Resources[0], {
schemas: ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
id: 'urn:ietf:params:scim:schemas:core:2.0:User',
name: 'User',
description: 'User Account',
});
});
it('should allow fetching the Scim resource types when autenticated', async function () {
const res = await axios.get(scimUrl('/ResourceTypes'), kiwi);
assert.equal(res.status, 200);
assert.deepInclude(res.data, {
schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
});
assert.property(res.data, 'Resources');
assert.deepInclude(res.data.Resources[0], {
schemas: ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
name: 'User',
endpoint: '/Users',
});
});
it('should allow fetching the Scim service provider config when autenticated', async function () {
const res = await axios.get(scimUrl('/ServiceProviderConfig'), kiwi);
assert.equal(res.status, 200);
assert.deepInclude(res.data, {
schemas: ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
});
assert.property(res.data, 'patch');
assert.property(res.data, 'bulk');
assert.property(res.data, 'filter');
}); });
}); });