Add support for master key clients to create user sessions (#7406)

* 6641: Implement support for user impersonation: master key clients can log in as any user, without access to the user's credentials, and without presuming the user already has a session

* reworded changelog

* rebuilt package lock

* fit test

* using lodash flatMap

* bump to node 12 for postgres test

* revert test fit

* add node version to postgres CI

* revert package-lock

Co-authored-by: gormanfletcher <git@gormanfletcher.com>
Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com>
This commit is contained in:
GormanFletcher
2021-06-04 19:55:00 -04:00
committed by GitHub
parent 754c127d96
commit 129f7bfa9b
6 changed files with 249 additions and 43 deletions

View File

@@ -31,6 +31,28 @@ export class UsersRouter extends ClassesRouter {
}
}
/**
* After retrieving a user directly from the database, we need to remove the
* password from the object (for security), and fix an issue some SDKs have
* with null values
*/
_sanitizeAuthData(user) {
delete user.password;
// Sometimes the authData still has null on that keys
// https://github.com/parse-community/parse-server/issues/935
if (user.authData) {
Object.keys(user.authData).forEach(provider => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
}
/**
* Validates a password request in login and verifyPassword
* @param {Object} req The request
@@ -117,20 +139,7 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.');
}
delete user.password;
// Sometimes the authData still has null on that keys
// https://github.com/parse-community/parse-server/issues/935
if (user.authData) {
Object.keys(user.authData).forEach(provider => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
this._sanitizeAuthData(user);
return resolve(user);
})
@@ -244,6 +253,57 @@ export class UsersRouter extends ClassesRouter {
return { response: user };
}
/**
* This allows master-key clients to create user sessions without access to
* user credentials. This enables systems that can authenticate access another
* way (API key, app administrators) to act on a user's behalf.
*
* We create a new session rather than looking for an existing session; we
* want this to work in situations where the user is logged out on all
* devices, since this can be used by automated systems acting on the user's
* behalf.
*
* For the moment, we're omitting event hooks and lockout checks, since
* immediate use cases suggest /loginAs could be used for semantically
* different reasons from /login
*/
async handleLogInAs(req) {
if (!req.auth.isMaster) {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required');
}
const userId = req.body.userId || req.query.userId;
if (!userId) {
throw new Parse.Error(
Parse.Error.INVALID_VALUE,
'userId must not be empty, null, or undefined'
);
}
const queryResults = await req.config.database.find('_User', { objectId: userId });
const user = queryResults[0];
if (!user) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'user not found');
}
this._sanitizeAuthData(user);
const { sessionData, createSession } = RestWrite.createSession(req.config, {
userId,
createdWith: {
action: 'login',
authProvider: 'masterkey',
},
installationId: req.info.installationId,
});
user.sessionToken = sessionData.sessionToken;
await createSession();
return { response: user };
}
handleVerifyPassword(req) {
return this._authenticateUserFromRequest(req)
.then(user => {
@@ -418,6 +478,9 @@ export class UsersRouter extends ClassesRouter {
this.route('POST', '/login', req => {
return this.handleLogIn(req);
});
this.route('POST', '/loginAs', req => {
return this.handleLogInAs(req);
});
this.route('POST', '/logout', req => {
return this.handleLogOut(req);
});