2021-04-09 13:46:53 +02:00
|
|
|
import assert from 'assert';
|
|
|
|
import bcrypt from 'bcrypt';
|
|
|
|
import owasp from 'owasp-password-strength-test';
|
|
|
|
import Joi from 'joi';
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
import { URL } from 'url';
|
2021-04-09 13:46:53 +02:00
|
|
|
import UserStore, { IUserSearch } from '../db/user-store';
|
|
|
|
import { Logger } from '../logger';
|
|
|
|
import User, { IUser } from '../user';
|
|
|
|
import isEmail from '../util/is-email';
|
2021-04-22 10:07:10 +02:00
|
|
|
import { AccessService, RoleName } from './access-service';
|
2021-04-09 13:46:53 +02:00
|
|
|
import { ADMIN } from '../permissions';
|
2021-04-16 15:29:23 +02:00
|
|
|
import ResetTokenService from './reset-token-service';
|
|
|
|
import InvalidTokenError from '../error/invalid-token-error';
|
|
|
|
import NotFoundError from '../error/notfound-error';
|
|
|
|
import OwaspValidationError from '../error/owasp-validation-error';
|
|
|
|
import { EmailService } from './email-service';
|
2021-04-22 10:07:10 +02:00
|
|
|
import { IUnleashConfig } from '../types/option';
|
2021-04-09 13:46:53 +02:00
|
|
|
|
|
|
|
export interface ICreateUser {
|
|
|
|
name?: string;
|
|
|
|
email?: string;
|
|
|
|
username?: string;
|
|
|
|
password?: string;
|
|
|
|
rootRole: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface IUpdateUser {
|
|
|
|
id: number;
|
|
|
|
name?: string;
|
|
|
|
email?: string;
|
|
|
|
rootRole?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IUserWithRole extends IUser {
|
|
|
|
rootRole: number;
|
|
|
|
}
|
2021-04-16 15:29:23 +02:00
|
|
|
interface IRoleDescription {
|
|
|
|
description: string;
|
|
|
|
name: string;
|
|
|
|
type: string;
|
|
|
|
}
|
|
|
|
interface ITokenUser extends IUpdateUser {
|
|
|
|
createdBy: string;
|
|
|
|
token: string;
|
|
|
|
role: IRoleDescription;
|
|
|
|
}
|
2021-04-09 13:46:53 +02:00
|
|
|
|
|
|
|
interface IStores {
|
|
|
|
userStore: UserStore;
|
|
|
|
}
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
interface IServices {
|
|
|
|
accessService: AccessService;
|
|
|
|
resetTokenService: ResetTokenService;
|
|
|
|
emailService: EmailService;
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
const saltRounds = 10;
|
|
|
|
|
|
|
|
class UserService {
|
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
private store: UserStore;
|
|
|
|
|
|
|
|
private accessService: AccessService;
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
private resetTokenService: ResetTokenService;
|
|
|
|
|
|
|
|
private emailService: EmailService;
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
constructor(
|
|
|
|
stores: IStores,
|
2021-04-22 10:07:10 +02:00
|
|
|
{
|
|
|
|
getLogger,
|
|
|
|
authentication,
|
|
|
|
}: Pick<IUnleashConfig, 'getLogger' | 'authentication'>,
|
|
|
|
{
|
|
|
|
accessService,
|
|
|
|
resetTokenService,
|
|
|
|
emailService,
|
|
|
|
}: Pick<
|
|
|
|
IServices,
|
|
|
|
'accessService' | 'resetTokenService' | 'emailService'
|
|
|
|
>,
|
2021-04-09 13:46:53 +02:00
|
|
|
) {
|
2021-04-22 10:07:10 +02:00
|
|
|
this.logger = getLogger('service/user-service.js');
|
2021-04-09 13:46:53 +02:00
|
|
|
this.store = stores.userStore;
|
|
|
|
this.accessService = accessService;
|
2021-04-16 15:29:23 +02:00
|
|
|
this.resetTokenService = resetTokenService;
|
|
|
|
this.emailService = emailService;
|
2021-04-22 10:07:10 +02:00
|
|
|
if (authentication && authentication.createAdminUser) {
|
2021-04-09 13:46:53 +02:00
|
|
|
process.nextTick(() => this.initAdminUser());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
validatePassword(password: string): boolean {
|
|
|
|
const result = owasp.test(password);
|
|
|
|
if (!result.strong) {
|
2021-04-16 15:29:23 +02:00
|
|
|
throw new OwaspValidationError(result);
|
2021-04-09 13:46:53 +02:00
|
|
|
} else return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async initAdminUser(): Promise<void> {
|
|
|
|
const hasAdminUser = await this.store.hasUser({ username: 'admin' });
|
|
|
|
|
|
|
|
if (!hasAdminUser) {
|
|
|
|
// create default admin user
|
|
|
|
try {
|
|
|
|
this.logger.info(
|
|
|
|
'Creating default user "admin" with password "admin"',
|
|
|
|
);
|
|
|
|
const user = await this.store.insert(
|
|
|
|
new User({
|
|
|
|
username: 'admin',
|
|
|
|
permissions: [ADMIN], // TODO: remove in v4
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
const passwordHash = await bcrypt.hash('admin', saltRounds);
|
|
|
|
await this.store.setPasswordHash(user.id, passwordHash);
|
|
|
|
|
|
|
|
const rootRoles = await this.accessService.getRootRoles();
|
|
|
|
const adminRole = rootRoles.find(
|
|
|
|
r => r.name === RoleName.ADMIN,
|
|
|
|
);
|
|
|
|
await this.accessService.setUserRootRole(user.id, adminRole.id);
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.error('Unable to create default user "admin"');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async getAll(): Promise<IUserWithRole[]> {
|
|
|
|
const users = await this.store.getAll();
|
2021-04-22 10:07:10 +02:00
|
|
|
const defaultRole = await this.accessService.getRootRole(
|
|
|
|
RoleName.VIEWER,
|
|
|
|
);
|
2021-04-09 13:46:53 +02:00
|
|
|
const userRoles = await this.accessService.getRootRoleForAllUsers();
|
|
|
|
const usersWithRootRole = users.map(u => {
|
|
|
|
const rootRole = userRoles.find(r => r.userId === u.id);
|
|
|
|
const roleId = rootRole ? rootRole.roleId : defaultRole.id;
|
|
|
|
return { ...u, rootRole: roleId };
|
|
|
|
});
|
|
|
|
return usersWithRootRole;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getUser(id: number): Promise<IUserWithRole> {
|
|
|
|
const roles = await this.accessService.getUserRootRoles(id);
|
2021-04-22 10:07:10 +02:00
|
|
|
const defaultRole = await this.accessService.getRootRole(
|
|
|
|
RoleName.VIEWER,
|
|
|
|
);
|
2021-04-09 13:46:53 +02:00
|
|
|
const roleId = roles.length > 0 ? roles[0].id : defaultRole.id;
|
|
|
|
const user = await this.store.get({ id });
|
|
|
|
return { ...user, rootRole: roleId };
|
|
|
|
}
|
|
|
|
|
|
|
|
async search(query: IUserSearch): Promise<User[]> {
|
|
|
|
return this.store.search(query);
|
|
|
|
}
|
|
|
|
|
2021-04-16 15:29:23 +02:00
|
|
|
async getByEmail(email: string): Promise<User> {
|
|
|
|
return this.store.get({ email });
|
|
|
|
}
|
|
|
|
|
2021-04-09 13:46:53 +02:00
|
|
|
async createUser({
|
|
|
|
username,
|
|
|
|
email,
|
|
|
|
name,
|
|
|
|
password,
|
|
|
|
rootRole,
|
|
|
|
}: ICreateUser): Promise<User> {
|
|
|
|
assert.ok(username || email, 'You must specify username or email');
|
|
|
|
|
|
|
|
if (email) {
|
|
|
|
Joi.assert(email, Joi.string().email(), 'Email');
|
|
|
|
}
|
|
|
|
|
|
|
|
const exists = await this.store.hasUser({ username, email });
|
|
|
|
if (exists) {
|
|
|
|
throw new Error('User already exists');
|
|
|
|
}
|
|
|
|
|
|
|
|
const user = await this.store.insert(
|
|
|
|
// TODO: remove permission in v4.
|
|
|
|
new User({ username, email, name, permissions: [ADMIN] }),
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.accessService.setUserRootRole(user.id, rootRole);
|
|
|
|
|
|
|
|
if (password) {
|
|
|
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
|
|
|
await this.store.setPasswordHash(user.id, passwordHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateUser({
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
email,
|
|
|
|
rootRole,
|
|
|
|
}: IUpdateUser): Promise<User> {
|
|
|
|
if (email) {
|
|
|
|
Joi.assert(email, Joi.string().email(), 'Email');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rootRole) {
|
|
|
|
await this.accessService.setUserRootRole(id, rootRole);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.store.update(id, { name, email });
|
|
|
|
}
|
|
|
|
|
|
|
|
async loginUser(usernameOrEmail: string, password: string): Promise<User> {
|
|
|
|
const idQuery = isEmail(usernameOrEmail)
|
|
|
|
? { email: usernameOrEmail }
|
|
|
|
: { username: usernameOrEmail };
|
|
|
|
const user = await this.store.get(idQuery);
|
|
|
|
const passwordHash = await this.store.getPasswordHash(user.id);
|
|
|
|
|
|
|
|
const match = await bcrypt.compare(password, passwordHash);
|
|
|
|
if (match) {
|
|
|
|
await this.store.successfullyLogin(user);
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
throw new Error('Wrong password, try again.');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Used to login users without specifying password. Used when integrating
|
|
|
|
* with external identity providers.
|
|
|
|
*
|
|
|
|
* @param usernameOrEmail
|
|
|
|
* @param autoCreateUser
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
async loginUserWithoutPassword(
|
|
|
|
email: string,
|
|
|
|
autoCreateUser: boolean = false,
|
|
|
|
): Promise<User> {
|
|
|
|
let user: User;
|
|
|
|
|
|
|
|
try {
|
|
|
|
user = await this.store.get({ email });
|
|
|
|
} catch (e) {
|
|
|
|
if (autoCreateUser) {
|
|
|
|
const defaultRole = await this.accessService.getRootRole(
|
2021-04-16 10:45:15 +02:00
|
|
|
RoleName.EDITOR,
|
2021-04-09 13:46:53 +02:00
|
|
|
);
|
|
|
|
user = await this.createUser({
|
|
|
|
email,
|
|
|
|
rootRole: defaultRole.id,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.store.successfullyLogin(user);
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
|
|
|
async changePassword(userId: number, password: string): Promise<void> {
|
|
|
|
this.validatePassword(password);
|
|
|
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
|
|
|
return this.store.setPasswordHash(userId, passwordHash);
|
|
|
|
}
|
|
|
|
|
|
|
|
async deleteUser(userId: number): Promise<void> {
|
|
|
|
const roles = await this.accessService.getRolesForUser(userId);
|
|
|
|
await Promise.all(
|
|
|
|
roles.map(role =>
|
|
|
|
this.accessService.removeUserFromRole(userId, role.id),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.store.delete(userId);
|
|
|
|
}
|
2021-04-16 15:29:23 +02:00
|
|
|
|
|
|
|
async getUserForToken(token: string): Promise<ITokenUser> {
|
|
|
|
const { createdBy, userId } = await this.resetTokenService.isValid(
|
|
|
|
token,
|
|
|
|
);
|
|
|
|
const user = await this.getUser(userId);
|
|
|
|
const role = await this.accessService.getRole(user.rootRole);
|
|
|
|
return {
|
|
|
|
token,
|
|
|
|
createdBy,
|
|
|
|
email: user.email,
|
|
|
|
name: user.name,
|
|
|
|
id: user.id,
|
|
|
|
role: {
|
|
|
|
description: role.role.description,
|
|
|
|
type: role.role.type,
|
|
|
|
name: role.role.name,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async resetPassword(token: string, password: string): Promise<void> {
|
|
|
|
this.validatePassword(password);
|
|
|
|
const user = await this.getUserForToken(token);
|
|
|
|
const allowed = await this.resetTokenService.useAccessToken({
|
|
|
|
userId: user.id,
|
|
|
|
token,
|
|
|
|
});
|
|
|
|
if (allowed) {
|
|
|
|
await this.changePassword(user.id, password);
|
|
|
|
} else {
|
|
|
|
throw new InvalidTokenError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async createResetPasswordEmail(
|
|
|
|
receiverEmail: string,
|
|
|
|
requester: string,
|
|
|
|
): Promise<URL> {
|
|
|
|
const receiver = await this.getByEmail(receiverEmail);
|
|
|
|
if (!receiver) {
|
|
|
|
throw new NotFoundError(`Could not find ${receiverEmail}`);
|
|
|
|
}
|
|
|
|
const resetLink = await this.resetTokenService.createResetPasswordUrl(
|
|
|
|
receiver.id,
|
|
|
|
requester,
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.emailService.sendResetMail(
|
|
|
|
receiver.name,
|
|
|
|
receiver.email,
|
|
|
|
resetLink.toString(),
|
|
|
|
);
|
|
|
|
return resetLink;
|
|
|
|
}
|
2021-04-09 13:46:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = UserService;
|
|
|
|
export default UserService;
|