mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Implement ingress
This commit is contained in:
parent
98f50d3c81
commit
c38107aadc
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user