Implement ingress

This commit is contained in:
fflorent 2024-09-03 23:53:25 +02:00
parent 98f50d3c81
commit c38107aadc
4 changed files with 80 additions and 14 deletions

View File

@ -541,6 +541,10 @@ export class HomeDBManager extends EventEmitter {
return this._usersManager.deleteUser(scope, userIdToDelete, name);
}
public async overrideUser(userId: number, props: UserProfile) {
return this._usersManager.overrideUser(userId, props);
}
/**
* Returns a QueryResult for the given organization. The orgKey
* can be a string (the domain from url) or the id of an org. If it is

View File

@ -386,7 +386,7 @@ export class UsersManager {
// Set the user's name if our provider knows it. Otherwise use their username
// from email, for lack of something better. If we don't have a profile at this
// time, then leave the name blank in the hopes of learning it when the user logs in.
user.name = (profile && (profile.name || email.split('@')[0])) || '';
user.name = (profile && this._getNameOrDeduceFromEmail(profile.name, email)) || '';
needUpdate = true;
}
if (!user.picture && profile && profile.picture) {
@ -561,6 +561,27 @@ export class UsersManager {
.filter(fullProfile => fullProfile);
}
/**
* Update users with passed property. Optional user properties that are missing will be reset to their default value.
*/
public async overrideUser(userId: number, props: UserProfile): Promise<User> {
return await this._connection.transaction(async manager => {
const user = await this.getUser(userId, {includePrefs: true});
if (!user) { throw new ApiError("unable to find user to update", 404); }
const login = user.logins[0];
user.name = this._getNameOrDeduceFromEmail(props.name, props.email);
user.picture = props.picture || '';
user.options = {...(user.options || {}), locale: props.locale ?? undefined};
if (props.email) {
login.email = normalizeEmail(props.email);
login.displayEmail = props.email;
}
await manager.save([user, login]);
return (await this.getUser(userId))!;
});
}
/**
* ==================================
*
@ -748,6 +769,10 @@ export class UsersManager {
return id;
}
private _getNameOrDeduceFromEmail(name: string, email: string) {
return name || email.split('@')[0];
}
// This deals with the problem posed by receiving a PermissionDelta specifying a
// role for both alice@x and Alice@x. We do not distinguish between such emails.
// If there are multiple indistinguishabe emails, we preserve just one of them,

View File

@ -1,5 +1,7 @@
import { normalizeEmail } from "app/common/emails";
import { UserProfile } from "app/common/LoginSessionAPI";
import { User } from "app/gen-server/entity/User.js";
import { SCIMMY } from "scimmy-routers";
import SCIMMY from "scimmy";
/**
* Converts a user from your database to a SCIMMY user
@ -8,7 +10,7 @@ export function toSCIMMYUser(user: User) {
if (!user.logins) {
throw new Error("User must have at least one login");
}
const preferredLanguage = user.options?.locale ?? "en";
const locale = user.options?.locale ?? "en";
return new SCIMMY.Schemas.User({
id: String(user.id),
userName: user.loginEmail,
@ -16,16 +18,29 @@ export function toSCIMMYUser(user: User) {
name: {
formatted: user.name,
},
preferredLanguage,
locale: preferredLanguage, // Assume locale is the same as preferredLanguage
locale,
preferredLanguage: locale, // Assume preferredLanguage is the same as locale
photos: user.picture ? [{
value: user.picture,
type: "photo",
primary: true
}] : undefined,
emails: [{
value: user.loginEmail,
value: user.logins[0].displayEmail,
primary: true,
}],
});
}
export function toUserProfile(scimUser: any, existingUser?: User): UserProfile {
const emailValue = scimUser.emails?.[0]?.value;
if (emailValue && normalizeEmail(emailValue) !== normalizeEmail(scimUser.userName)) {
throw new SCIMMY.Types.Error(400, 'invalidValue', 'Email and userName must be the same');
}
return {
name: scimUser.displayName ?? existingUser?.name,
picture: scimUser.photos?.[0]?.value,
locale: scimUser.locale,
email: emailValue ?? scimUser.userName ?? existingUser?.loginEmail,
};
}

View File

@ -4,12 +4,13 @@ import SCIMMY from "scimmy";
import SCIMMYRouters from "scimmy-routers";
import { RequestWithLogin } from '../../Authorizer';
import { InstallAdmin } from '../../InstallAdmin';
import { toSCIMMYUser } from './ScimUserUtils';
import { toSCIMMYUser, toUserProfile } from './ScimUserUtils';
import { ApiError } from 'app/common/ApiError';
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 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);
}
@ -30,18 +31,39 @@ 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) => {
ingress: async (resource: any, data: any) => {
try {
const { id } = resource;
if (id) {
return null;
const updatedUser = await dbManager.overrideUser(id, toUserProfile(data));
return toSCIMMYUser(updatedUser);
}
return [];
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
});
return toSCIMMYUser(newUser!);
} catch (ex) {
// FIXME: remove this
if (Math.random() > 1) {
return null;
if (ex instanceof ApiError) {
if (ex.status === 409) {
throw new SCIMMY.Types.Error(ex.status, 'uniqueness', ex.message);
}
throw new SCIMMY.Types.Error(ex.status, null!, ex.message);
}
if (ex.code?.startsWith('SQLITE')) {
switch (ex.code) {
case 'SQLITE_CONSTRAINT':
throw new SCIMMY.Types.Error(409, 'uniqueness', ex.message);
default:
throw new SCIMMY.Types.Error(500, 'serverError', ex.message);
}
}
throw ex;
}
},