mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
SCIM: Implement egress + tests
This commit is contained in:
parent
92b4a65da0
commit
98f50d3c81
@ -441,6 +441,10 @@ export class HomeDBManager extends EventEmitter {
|
||||
return this._usersManager.getUser(userId, options);
|
||||
}
|
||||
|
||||
public async getUsers() {
|
||||
return this._usersManager.getUsers();
|
||||
}
|
||||
|
||||
public async getFullUser(userId: number) {
|
||||
return this._usersManager.getFullUser(userId);
|
||||
}
|
||||
@ -3271,7 +3275,7 @@ export class HomeDBManager extends EventEmitter {
|
||||
// Get the user objects which map to non-null values in the userDelta.
|
||||
const userIds = Object.keys(userDelta).filter(userId => userDelta[userId])
|
||||
.map(userIdStr => parseInt(userIdStr, 10));
|
||||
const users = await this._usersManager.getUsers(userIds, manager);
|
||||
const users = await this._usersManager.getUsersByIds(userIds, manager);
|
||||
|
||||
// Add unaffected users to the delta so that we have a record of where they are.
|
||||
groups.forEach(grp => {
|
||||
|
@ -657,10 +657,14 @@ export class UsersManager {
|
||||
return [this.getSupportUserId(), this.getAnonymousUserId(), this.getEveryoneUserId()];
|
||||
}
|
||||
|
||||
public async getUsers() {
|
||||
return await User.find({relations: ["logins"]});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise for an array of User entites for the given userIds.
|
||||
*/
|
||||
public async getUsers(userIds: number[], optManager?: EntityManager): Promise<User[]> {
|
||||
public async getUsersByIds(userIds: number[], optManager?: EntityManager): Promise<User[]> {
|
||||
if (userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
@ -161,6 +161,7 @@ export class MergedServer {
|
||||
await this.flexServer.addLandingPages();
|
||||
// todo: add support for home api to standalone app
|
||||
this.flexServer.addHomeApi();
|
||||
this.flexServer.addScimApi();
|
||||
this.flexServer.addBillingApi();
|
||||
this.flexServer.addNotifier();
|
||||
this.flexServer.addAuditLogger();
|
||||
|
@ -87,6 +87,7 @@ import * as path from 'path';
|
||||
import * as serveStatic from "serve-static";
|
||||
import {ConfigBackendAPI} from "app/server/lib/ConfigBackendAPI";
|
||||
import {IGristCoreConfig} from "app/server/lib/configCore";
|
||||
import { buildScimRouter } from './scim';
|
||||
|
||||
// Health checks are a little noisy in the logs, so we don't show them all.
|
||||
// We show the first N health checks:
|
||||
@ -887,6 +888,12 @@ export class FlexServer implements GristServer {
|
||||
new ApiServer(this, this.app, this._dbManager, this._widgetRepository = buildWidgetRepository(this));
|
||||
}
|
||||
|
||||
public addScimApi() {
|
||||
if (this._check('scim', 'api', 'homedb', 'json', 'api-mw')) { return; }
|
||||
this.app.use('/api/scim', buildScimRouter(this._dbManager, this._installAdmin));
|
||||
}
|
||||
|
||||
|
||||
public addBillingApi() {
|
||||
if (this._check('billing-api', 'homedb', 'json', 'api-mw')) { return; }
|
||||
this._getBilling();
|
||||
|
@ -1,21 +1,11 @@
|
||||
import * as express from 'express';
|
||||
import { buildUsersRoute, checkPermissionToUsersEndpoint } from './v2/users';
|
||||
|
||||
import { buildScimRouterv2 } from './v2/ScimV2Api';
|
||||
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||
import SCIMMY from "scimmy";
|
||||
import SCIMMYRouters from "scimmy-routers";
|
||||
import { InstallAdmin } from '../InstallAdmin';
|
||||
|
||||
type SCIMMYResource = typeof SCIMMY.Types.Resource;
|
||||
|
||||
const buildScimRouter = (dbManager: HomeDBManager) => {
|
||||
const v2 = express.Router();
|
||||
v2.use('/Users', checkPermissionToUsersEndpoint, buildUsersRoute(dbManager));
|
||||
|
||||
SCIMMY.Resources.User.ingress(handler)
|
||||
SCIMMY.Resources.declare(SCIMMY.Resources.User)
|
||||
.ingress((resource: SCIMMYResource, data) => {
|
||||
|
||||
|
||||
});
|
||||
const buildScimRouter = (dbManager: HomeDBManager, installAdmin: InstallAdmin) => {
|
||||
const v2 = buildScimRouterv2(dbManager, installAdmin);
|
||||
const scim = express.Router();
|
||||
scim.use('/v2', v2);
|
||||
return scim;
|
||||
|
31
app/server/lib/scim/v2/ScimUserUtils.ts
Normal file
31
app/server/lib/scim/v2/ScimUserUtils.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { User } from "app/gen-server/entity/User.js";
|
||||
import { SCIMMY } from "scimmy-routers";
|
||||
|
||||
/**
|
||||
* Converts a user from your database to a SCIMMY user
|
||||
*/
|
||||
export function toSCIMMYUser(user: User) {
|
||||
if (!user.logins) {
|
||||
throw new Error("User must have at least one login");
|
||||
}
|
||||
const preferredLanguage = user.options?.locale ?? "en";
|
||||
return new SCIMMY.Schemas.User({
|
||||
id: String(user.id),
|
||||
userName: user.loginEmail,
|
||||
displayName: user.name,
|
||||
name: {
|
||||
formatted: user.name,
|
||||
},
|
||||
preferredLanguage,
|
||||
locale: preferredLanguage, // Assume locale is the same as preferredLanguage
|
||||
photos: user.picture ? [{
|
||||
value: user.picture,
|
||||
type: "photo",
|
||||
primary: true
|
||||
}] : undefined,
|
||||
emails: [{
|
||||
value: user.loginEmail,
|
||||
primary: true,
|
||||
}],
|
||||
});
|
||||
}
|
76
app/server/lib/scim/v2/ScimV2Api.ts
Normal file
76
app/server/lib/scim/v2/ScimV2Api.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import * as express from 'express';
|
||||
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||
import SCIMMY from "scimmy";
|
||||
import SCIMMYRouters from "scimmy-routers";
|
||||
import { RequestWithLogin } from '../../Authorizer';
|
||||
import { InstallAdmin } from '../../InstallAdmin';
|
||||
import { toSCIMMYUser } from './ScimUserUtils';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const buildScimRouterv2 = (dbManager: HomeDBManager, installAdmin: InstallAdmin) => {
|
||||
const v2 = express.Router();
|
||||
|
||||
SCIMMY.Resources.declare(SCIMMY.Resources.User, {
|
||||
egress: async (resource: any) => {
|
||||
const { id, filter } = resource;
|
||||
if (id) {
|
||||
const user = await dbManager.getUser(id);
|
||||
if (!user) {
|
||||
throw new SCIMMY.Types.Error(404, null!, `User with ID ${id} not found`);
|
||||
}
|
||||
return toSCIMMYUser(user);
|
||||
}
|
||||
const scimmyUsers = (await dbManager.getUsers()).map(user => toSCIMMYUser(user));
|
||||
return filter ? filter.match(scimmyUsers) : scimmyUsers;
|
||||
},
|
||||
ingress: async (resource: any) => {
|
||||
try {
|
||||
const { id } = resource;
|
||||
if (id) {
|
||||
return null;
|
||||
}
|
||||
return [];
|
||||
} catch (ex) {
|
||||
// FIXME: remove this
|
||||
if (Math.random() > 1) {
|
||||
return null;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
},
|
||||
degress: () => {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const scimmyRouter = new SCIMMYRouters({
|
||||
type: 'bearer',
|
||||
handler: async (request: express.Request) => {
|
||||
const mreq = request as RequestWithLogin;
|
||||
if (mreq.userId === undefined) {
|
||||
// Note that any Error thrown here is automatically converted into a 403 response by SCIMMYRouters.
|
||||
throw new Error('You are not authorized to access this resource!');
|
||||
}
|
||||
|
||||
if (mreq.userId === dbManager.getAnonymousUserId()) {
|
||||
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.
|
||||
}
|
||||
}) as express.Router; // Have to cast it into express.Router. See https://github.com/scimmyjs/scimmy-routers/issues/24
|
||||
|
||||
return v2.use('/', scimmyRouter);
|
||||
};
|
||||
|
||||
export { buildScimRouterv2 };
|
@ -1,37 +0,0 @@
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||
import { expressWrap } from '../../expressWrap';
|
||||
import { integerParam } from '../../requestUtils';
|
||||
import { ApiError } from 'app/common/ApiError';
|
||||
import { RequestWithLogin } from '../../Authorizer';
|
||||
|
||||
function checkPermissionToUsersEndpoint(req: Request, res: Response, next: NextFunction) {
|
||||
const mreq = req as RequestWithLogin;
|
||||
const adminEmail = process.env.GRIST_DEFAULT_EMAIL;
|
||||
if (!adminEmail || mreq.user?.loginEmail !== adminEmail) {
|
||||
throw new ApiError('Permission denied', 403);
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
const buildUsersRoute = (dbManager: HomeDBManager) => {
|
||||
const userRoute = express.Router();
|
||||
|
||||
async function findUserOrFail(userId: number) {
|
||||
const user = await dbManager.getUser(userId);
|
||||
if (!user) {
|
||||
throw new ApiError('User not found', 404);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
userRoute.get('/:id', expressWrap(async (req, res) => {
|
||||
const userId = integerParam(req.params.id, 'id');
|
||||
const user = await findUserOrFail(userId);
|
||||
res.status(200).json(user);
|
||||
}));
|
||||
return userRoute;
|
||||
};
|
||||
|
||||
export { buildUsersRoute, checkPermissionToUsersEndpoint };
|
@ -189,7 +189,8 @@
|
||||
"redis": "3.1.1",
|
||||
"redlock": "3.1.2",
|
||||
"saml2-js": "4.0.2",
|
||||
"scimmy-routers": "^1.2.0",
|
||||
"scimmy": "1.2.3",
|
||||
"scimmy-routers": "1.2.1",
|
||||
"short-uuid": "3.1.1",
|
||||
"slugify": "1.6.6",
|
||||
"swagger-ui-dist": "5.11.0",
|
||||
|
@ -612,6 +612,7 @@ export async function createServer(port: number, initDb = createInitialDb): Prom
|
||||
flexServer.addAccessMiddleware();
|
||||
flexServer.addApiMiddleware();
|
||||
flexServer.addHomeApi();
|
||||
flexServer.addScimApi();
|
||||
flexServer.addApiErrorHandlers();
|
||||
await initDb(flexServer.getHomeDBManager().connection);
|
||||
flexServer.summary();
|
||||
|
@ -35,6 +35,7 @@ async function activateServer(home: FlexServer, docManager: DocManager) {
|
||||
await home.addLandingPages();
|
||||
home.addHomeApi();
|
||||
home.addAuditLogger();
|
||||
home.addScimApi();
|
||||
await home.addTelemetry();
|
||||
await home.addDoc();
|
||||
home.addApiErrorHandlers();
|
||||
|
16
yarn.lock
16
yarn.lock
@ -3801,7 +3801,7 @@ express-rate-limit@7.2.0:
|
||||
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.2.0.tgz#06ce387dd5388f429cab8263c514fc07bf90a445"
|
||||
integrity sha512-T7nul1t4TNyfZMJ7pKRKkdeVJWa2CqB8NA1P8BwYaoDI5QSBZARv5oMS43J7b7I5P+4asjVXjb7ONuwDKucahg==
|
||||
|
||||
express@4.19.2, express@^4.18.2:
|
||||
express@4.19.2, express@^4.19.2:
|
||||
version "4.19.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
|
||||
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
|
||||
@ -7346,15 +7346,15 @@ schema-utils@^3.2.0:
|
||||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
scimmy-routers@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/scimmy-routers/-/scimmy-routers-1.2.0.tgz#42090c616f127aefce194ebf4a3c8f4a4f62e30b"
|
||||
integrity sha512-+dT8yRglz2gMu0X1LlUYTi/PDgW6Zzu1YRWiv362I/wA64kud24XjYIoufNiW5OhskvBPQGT/P1aYOffcmxxsQ==
|
||||
scimmy-routers@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/scimmy-routers/-/scimmy-routers-1.2.1.tgz#a535ced83f051b2a0374e8df02da6e17424e8be0"
|
||||
integrity sha512-PDX4/mwOScSd+TjUPX0k3gH6v50WeYVgK1bisZ8f3p3eyJ0Qy4qYebDo6gzHqYCBjXNQniQxGSQvtlvG22NLdA==
|
||||
dependencies:
|
||||
express "^4.18.2"
|
||||
scimmy "^1.2.0"
|
||||
express "^4.19.2"
|
||||
scimmy "^1.2.3"
|
||||
|
||||
scimmy@^1.2.0:
|
||||
scimmy@1.2.3, scimmy@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/scimmy/-/scimmy-1.2.3.tgz#56ca9dbf11749b272e18090c923f81dbad4bc911"
|
||||
integrity sha512-16oXCvnieVeKxTDQqi275bLuyOCwXci8Jywm2/M+4dWNNYoduUz0Crj1nFY0ETYMsuYvCdareWov6/Mebu92xA==
|
||||
|
Loading…
Reference in New Issue
Block a user