Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCIM users endpoint #1199

Merged
merged 27 commits into from
Dec 4, 2024
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b47c66f
WIP
fflorent Aug 29, 2024
163ade3
SCIM: Implement egress + tests
fflorent Sep 2, 2024
642bca8
Implement ingress
fflorent Sep 3, 2024
a9f5193
Add tests
fflorent Sep 3, 2024
6db0737
SCIM: Implement DELETE
fflorent Sep 5, 2024
132bc3f
More tests and cleanups
fflorent Sep 6, 2024
a994ccd
Rebase fix + check GRIST_SCIM_USER
fflorent Sep 6, 2024
a57f042
Add GRIST_ENABLE_SCIM env variable
fflorent Sep 6, 2024
6215228
Move logic for Users to its own controller
fflorent Sep 6, 2024
44f2ee5
An unknown error should return a 500
fflorent Sep 7, 2024
5215e4f
Add a test for pagination
fflorent Sep 7, 2024
b0fe85e
Add tests for the new UsersManager methods
fflorent Sep 7, 2024
3041200
Document methods of the controller
fflorent Sep 7, 2024
efa742d
Rename ex → err in catch block
fflorent Sep 12, 2024
dc8478e
Bump Scimmy and Scimmy-Routers
fflorent Oct 18, 2024
61e8cd9
Log errors
fflorent Nov 22, 2024
ebf81a0
Only warn when the userName differ from the primary email
fflorent Nov 25, 2024
959f8b5
Rename overrideUser → overwriteUser
fflorent Nov 26, 2024
bc37751
Use full path for import
fflorent Nov 26, 2024
72cfdf0
Improve user deletion test description
fflorent Nov 26, 2024
d252f5b
Improve error message for anonymous users
fflorent Nov 26, 2024
c4f6712
Fix styling issue
fflorent Nov 26, 2024
090398c
Disallow deleting technical users
fflorent Nov 29, 2024
8312aa9
Disallow technical user modifications
fflorent Nov 29, 2024
4beef42
Update FIXME in ScimUserController
fflorent Nov 29, 2024
7ace7f8
rename "technical user" to "system user"
jordigh Nov 29, 2024
6f97aec
Document SCIM feature flag in the README
fflorent Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
More tests and cleanups
fflorent committed Nov 26, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 132bc3fa2b921e40f046c8a25d48fae1b1b55596
74 changes: 43 additions & 31 deletions app/server/lib/scim/v2/ScimV2Api.ts
Original file line number Diff line number Diff line change
@@ -10,20 +10,36 @@ import { parseInt } from 'lodash';

const WHITELISTED_PATHS_FOR_NON_ADMINS = [ "/Me", "/Schemas", "/ResourceTypes", "/ServiceProviderConfig" ];

async function isAuthorizedAction(mreq: RequestWithLogin, installAdmin: InstallAdmin): Promise<boolean> {
const isAdmin = await installAdmin.isAdminReq(mreq);
const isScimUser = Boolean(process.env.GRIST_SCIM_EMAIL && mreq.user?.loginEmail === process.env.GRIST_SCIM_EMAIL);
return isAdmin || isScimUser || WHITELISTED_PATHS_FOR_NON_ADMINS.includes(mreq.path);
interface RequestContext {
path: string;
isAdmin: boolean;
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 v2 = express.Router();

SCIMMY.Resources.declare(SCIMMY.Resources.User, {
egress: async (resource: any) => {
egress: async (resource: any, context: RequestContext) => {
checkAccess(context);

const { filter } = resource;
const id = parseInt(resource.id, 10);
if (id) {
if (!isNaN(id)) {
const user = await dbManager.getUser(id);
if (!user) {
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));
return filter ? filter.match(scimmyUsers) : scimmyUsers;
},
ingress: async (resource: any, data: any) => {
ingress: async (resource: any, data: any, context: RequestContext) => {
checkAccess(context);

try {
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));
return toSCIMMYUser(updatedUser);
}
await checkEmailIsUnique(dbManager, data.userName);
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, {
profile: userProfileToInsert
});
@@ -57,29 +73,20 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
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;
}
},
degress: async (resource: any) => {
degress: async (resource: any, context: RequestContext) => {
checkAccess(context);

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.
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);
}
@@ -102,10 +109,15 @@ const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin)
throw new Error('Anonymous user cannot access SCIM resources');
}

if (!await isAuthorizedAction(mreq, installAdmin)) {
throw new SCIMMY.Types.Error(403, null!, 'Resource disallowed for non-admin users');
}
return String(mreq.userId); // HACK: SCIMMYRouters requires the userId to be a string.
return String(mreq.userId); // SCIMMYRouters requires the userId to be a string.
},
context: async (mreq: RequestWithLogin): Promise<RequestContext> => {
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

Loading