fix(repository): Move backend code into subdirectory

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-10-02 20:10:32 +02:00 committed by David Mehren
parent 86584e705f
commit bf30cbcf48
272 changed files with 87 additions and 67 deletions

View file

@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../users/user.entity';
import { ProviderType } from './provider-type.enum';
/**
* The identity represents a single way for a user to login.
* A 'user' can have any number of these.
* Each one holds a type (local, github, twitter, etc.), if this type can have multiple instances (e.g. gitlab),
* it also saves the name of the instance. Also if this identity shall be the syncSource is saved.
*/
@Entity()
export class Identity {
@PrimaryGeneratedColumn()
id: number;
/**
* User that this identity corresponds to
*/
@ManyToOne((_) => User, (user) => user.identities, {
onDelete: 'CASCADE', // This deletes the Identity, when the associated User is deleted
})
user: Promise<User>;
/**
* The ProviderType of the identity
*/
@Column()
providerType: string;
/**
* The name of the provider.
* Only set if there are multiple provider of that type (e.g. gitlab)
*/
@Column({
nullable: true,
type: 'text',
})
providerName: string | null;
/**
* If the identity should be used as the sync source.
* See [authentication doc](../../docs/content/dev/authentication.md) for clarification
*/
@Column()
syncSource: boolean;
/**
* When the identity was created.
*/
@CreateDateColumn()
createdAt: Date;
/**
* When the identity was last updated.
*/
@UpdateDateColumn()
updatedAt: Date;
/**
* The unique identifier of a user from the login provider
*/
@Column({
nullable: true,
type: 'text',
})
providerUserId: string | null;
/**
* Token used to access the OAuth provider in the users name.
*/
@Column({
nullable: true,
type: 'text',
})
oAuthAccessToken: string | null;
/**
* The hash of the password
* Only set when the type of the identity is local
*/
@Column({
nullable: true,
type: 'text',
})
passwordHash: string | null;
public static create(
user: User,
providerType: ProviderType,
syncSource: boolean,
): Omit<Identity, 'id' | 'createdAt' | 'updatedAt'> {
const newIdentity = new Identity();
newIdentity.user = Promise.resolve(user);
newIdentity.providerType = providerType;
newIdentity.providerName = null;
newIdentity.syncSource = syncSource;
newIdentity.providerUserId = null;
newIdentity.oAuthAccessToken = null;
newIdentity.passwordHash = null;
return newIdentity;
}
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module';
import { User } from '../users/user.entity';
import { UsersModule } from '../users/users.module';
import { Identity } from './identity.entity';
import { IdentityService } from './identity.service';
import { LdapStrategy } from './ldap/ldap.strategy';
import { LocalStrategy } from './local/local.strategy';
@Module({
imports: [
TypeOrmModule.forFeature([Identity, User]),
UsersModule,
PassportModule,
LoggerModule,
],
controllers: [],
providers: [IdentityService, LocalStrategy, LdapStrategy],
exports: [IdentityService, LocalStrategy, LdapStrategy],
})
export class IdentityModule {}

View file

@ -0,0 +1,139 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigModule } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import appConfigMock from '../config/mock/app.config.mock';
import authConfigMock from '../config/mock/auth.config.mock';
import {
InvalidCredentialsError,
NoLocalIdentityError,
PasswordTooWeakError,
} from '../errors/errors';
import { LoggerModule } from '../logger/logger.module';
import { User } from '../users/user.entity';
import { checkPassword, hashPassword } from '../utils/password';
import { Identity } from './identity.entity';
import { IdentityService } from './identity.service';
import { ProviderType } from './provider-type.enum';
describe('IdentityService', () => {
let service: IdentityService;
let user: User;
let identityRepo: Repository<Identity>;
const password = 'AStrongPasswordToStartWith123';
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
IdentityService,
{
provide: getRepositoryToken(Identity),
useClass: Repository,
},
],
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfigMock, authConfigMock],
}),
LoggerModule,
],
}).compile();
service = module.get<IdentityService>(IdentityService);
user = User.create('test', 'Testy') as User;
identityRepo = module.get<Repository<Identity>>(
getRepositoryToken(Identity),
);
});
describe('createLocalIdentity', () => {
it('works', async () => {
jest
.spyOn(identityRepo, 'save')
.mockImplementationOnce(
async (identity: Identity): Promise<Identity> => identity,
);
const identity = await service.createLocalIdentity(user, password);
await checkPassword(password, identity.passwordHash ?? '').then(
(result) => expect(result).toBeTruthy(),
);
expect(await identity.user).toEqual(user);
});
});
describe('updateLocalPassword', () => {
beforeEach(async () => {
jest
.spyOn(identityRepo, 'save')
.mockImplementationOnce(
async (identity: Identity): Promise<Identity> => identity,
)
.mockImplementationOnce(
async (identity: Identity): Promise<Identity> => identity,
);
const identity = await service.createLocalIdentity(user, password);
user.identities = Promise.resolve([identity]);
});
it('works', async () => {
const newPassword = 'ThisIsAStrongNewP@ssw0rd';
const identity = await service.updateLocalPassword(user, newPassword);
await checkPassword(newPassword, identity.passwordHash ?? '').then(
(result) => expect(result).toBeTruthy(),
);
expect(await identity.user).toEqual(user);
});
it('fails, when user has no local identity', async () => {
user.identities = Promise.resolve([]);
await expect(service.updateLocalPassword(user, password)).rejects.toThrow(
NoLocalIdentityError,
);
});
it('fails, when new password is too weak', async () => {
await expect(
service.updateLocalPassword(user, 'password1'),
).rejects.toThrow(PasswordTooWeakError);
});
});
describe('loginWithLocalIdentity', () => {
it('works', async () => {
const identity = Identity.create(
user,
ProviderType.LOCAL,
false,
) as Identity;
identity.passwordHash = await hashPassword(password);
user.identities = Promise.resolve([identity]);
await expect(service.checkLocalPassword(user, password)).resolves.toEqual(
undefined,
);
});
describe('fails', () => {
it('when the password is wrong', async () => {
const identity = Identity.create(
user,
ProviderType.LOCAL,
false,
) as Identity;
identity.passwordHash = await hashPassword(password);
user.identities = Promise.resolve([identity]);
await expect(
service.checkLocalPassword(user, 'wrong_password'),
).rejects.toThrow(InvalidCredentialsError);
});
it('when user has no local identity', async () => {
user.identities = Promise.resolve([]);
await expect(
service.checkLocalPassword(user, password),
).rejects.toThrow(NoLocalIdentityError);
});
});
});
});

View file

@ -0,0 +1,206 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { zxcvbnAsync, zxcvbnOptions } from '@zxcvbn-ts/core';
import zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
import zxcvbnEnPackage from '@zxcvbn-ts/language-en';
import { Repository } from 'typeorm';
import authConfiguration, { AuthConfig } from '../config/auth.config';
import {
InvalidCredentialsError,
NoLocalIdentityError,
NotInDBError,
PasswordTooWeakError,
} from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { User } from '../users/user.entity';
import { checkPassword, hashPassword } from '../utils/password';
import { Identity } from './identity.entity';
import { ProviderType } from './provider-type.enum';
import { getFirstIdentityFromUser } from './utils';
@Injectable()
export class IdentityService {
constructor(
private readonly logger: ConsoleLoggerService,
@InjectRepository(Identity)
private identityRepository: Repository<Identity>,
@Inject(authConfiguration.KEY)
private authConfig: AuthConfig,
) {
this.logger.setContext(IdentityService.name);
const options = {
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary,
},
graphs: zxcvbnCommonPackage.adjacencyGraphs,
translations: zxcvbnEnPackage.translations,
};
zxcvbnOptions.setOptions(options);
}
/**
* @async
* Retrieve an identity by userId and providerType.
* @param {string} userId - the userId of the wanted identity
* @param {ProviderType} providerType - the providerType of the wanted identity
*/
async getIdentityFromUserIdAndProviderType(
userId: string,
providerType: ProviderType,
): Promise<Identity> {
const identity = await this.identityRepository.findOne({
where: {
providerUserId: userId,
providerType: providerType,
},
relations: ['user'],
});
if (identity === null) {
throw new NotInDBError(`Identity for user id '${userId}' not found`);
}
return identity;
}
/**
* @async
* Update the given Identity with the given information
* @param {Identity} identity - the identity to update
* @param {string | undefined} displayName - the displayName to update the user with
* @param {string | undefined} email - the email to update the user with
* @param {string | undefined} profilePicture - the profilePicture to update the user with
*/
async updateIdentity(
identity: Identity,
displayName?: string,
email?: string,
profilePicture?: string,
): Promise<Identity> {
if (identity.syncSource) {
// The identity is the syncSource and the user should be changed accordingly
const user = await identity.user;
let shouldSave = false;
if (displayName) {
user.displayName = displayName;
shouldSave = true;
}
if (email) {
user.email = email;
shouldSave = true;
}
if (profilePicture) {
// ToDo: sync image
}
if (shouldSave) {
identity.user = Promise.resolve(user);
return await this.identityRepository.save(identity);
}
}
return identity;
}
/**
* @async
* Create a new generic identity.
* @param {User} user - the user the identity should be added to
* @param {ProviderType} providerType - the providerType of the identity
* @param {string} userId - the userId the identity should have
* @return {Identity} the new local identity
*/
async createIdentity(
user: User,
providerType: ProviderType,
userId: string,
): Promise<Identity> {
const identity = Identity.create(user, providerType, false);
identity.providerUserId = userId;
return await this.identityRepository.save(identity);
}
/**
* @async
* Create a new identity for internal auth
* @param {User} user - the user the identity should be added to
* @param {string} password - the password the identity should have
* @return {Identity} the new local identity
*/
async createLocalIdentity(user: User, password: string): Promise<Identity> {
const identity = Identity.create(user, ProviderType.LOCAL, false);
await this.checkPasswordStrength(password);
identity.passwordHash = await hashPassword(password);
return await this.identityRepository.save(identity);
}
/**
* @async
* Update the internal password of the specified the user
* @param {User} user - the user, which identity should be updated
* @param {string} newPassword - the new password
* @throws {NoLocalIdentityError} the specified user has no internal identity
* @return {Identity} the changed identity
*/
async updateLocalPassword(
user: User,
newPassword: string,
): Promise<Identity> {
const internalIdentity: Identity | undefined =
await getFirstIdentityFromUser(user, ProviderType.LOCAL);
if (internalIdentity === undefined) {
this.logger.debug(
`The user with the username ${user.username} does not have a internal identity.`,
'updateLocalPassword',
);
throw new NoLocalIdentityError('This user has no internal identity.');
}
await this.checkPasswordStrength(newPassword);
internalIdentity.passwordHash = await hashPassword(newPassword);
return await this.identityRepository.save(internalIdentity);
}
/**
* @async
* Checks if the user and password combination matches
* @param {User} user - the user to use
* @param {string} password - the password to use
* @throws {InvalidCredentialsError} the password and user do not match
* @throws {NoLocalIdentityError} the specified user has no internal identity
*/
async checkLocalPassword(user: User, password: string): Promise<void> {
const internalIdentity: Identity | undefined =
await getFirstIdentityFromUser(user, ProviderType.LOCAL);
if (internalIdentity === undefined) {
this.logger.debug(
`The user with the username ${user.username} does not have a internal identity.`,
'checkLocalPassword',
);
throw new NoLocalIdentityError('This user has no internal identity.');
}
if (!(await checkPassword(password, internalIdentity.passwordHash ?? ''))) {
this.logger.debug(
`Password check for ${user.username} did not succeed.`,
'checkLocalPassword',
);
throw new InvalidCredentialsError('Password is not correct');
}
}
/**
* @async
* Check if the password is strong enough.
* This check is performed against the minimalPasswordStrength of the {@link AuthConfig}.
* @param {string} password - the password to check
* @throws {PasswordTooWeakError} the password is too weak
*/
private async checkPasswordStrength(password: string): Promise<void> {
const result = await zxcvbnAsync(password);
if (result.score < this.authConfig.local.minimalPasswordStrength) {
throw new PasswordTooWeakError();
}
}
}

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
export class LdapLoginDto {
@IsString()
username: string;
@IsString()
password: string;
}

View file

@ -0,0 +1,285 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard, PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import LdapAuth from 'ldapauth-fork';
import { Strategy, VerifiedCallback } from 'passport-custom';
import authConfiguration, {
AuthConfig,
LDAPConfig,
} from '../../config/auth.config';
import { NotInDBError } from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { UsersService } from '../../users/users.service';
import { Identity } from '../identity.entity';
import { IdentityService } from '../identity.service';
import { ProviderType } from '../provider-type.enum';
import { LdapLoginDto } from './ldap-login.dto';
const LDAP_ERROR_MAP: Record<string, string> = {
/* eslint-disable @typescript-eslint/naming-convention */
'530': 'Not Permitted to login at this time',
'531': 'Not permitted to logon at this workstation',
'532': 'Password expired',
'533': 'Account disabled',
'534': 'Account disabled',
'701': 'Account expired',
'773': 'User must reset password',
'775': 'User account locked',
default: 'Invalid username/password',
/* eslint-enable @typescript-eslint/naming-convention */
};
interface LdapPathParameters {
ldapIdentifier: string;
}
@Injectable()
export class LdapAuthGuard extends AuthGuard('ldap') {}
@Injectable()
export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') {
constructor(
private readonly logger: ConsoleLoggerService,
@Inject(authConfiguration.KEY)
private authConfig: AuthConfig,
private usersService: UsersService,
private identityService: IdentityService,
) {
super(
(
request: Request<LdapPathParameters, unknown, LdapLoginDto>,
doneCallBack: VerifiedCallback,
) => {
logger.setContext(LdapStrategy.name);
const ldapIdentifier = request.params.ldapIdentifier.toUpperCase();
const ldapConfig = this.getLDAPConfig(ldapIdentifier);
const username = request.body.username;
const password = request.body.password;
this.loginWithLDAP(ldapConfig, username, password, doneCallBack);
},
);
}
/**
* Try to log in the user with the given credentials.
* @param ldapConfig {LDAPConfig} - the ldap config to use
* @param username {string} - the username to login with
* @param password {string} - the password to login with
* @param doneCallBack {VerifiedCallback} - the callback to call if the login worked
* @returns {void}
* @throws {UnauthorizedException} - the user has given us incorrect credentials
* @throws {InternalServerErrorException} - if there are errors that we can't assign to wrong credentials
* @private
*/
private loginWithLDAP(
ldapConfig: LDAPConfig,
username: string,
password: string,
doneCallBack: VerifiedCallback,
): void {
// initialize LdapAuth lib
const auth = new LdapAuth({
url: ldapConfig.url,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter,
searchAttributes: ldapConfig.searchAttributes,
bindDN: ldapConfig.bindDn,
bindCredentials: ldapConfig.bindCredentials,
tlsOptions: {
ca: ldapConfig.tlsCaCerts,
},
});
auth.once('error', (error) => {
throw new InternalServerErrorException(error);
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
auth.on('error', () => {}); // Ignore further errors
auth.authenticate(
username,
password,
(error, user: Record<string, string>) => {
auth.close(() => {
// We don't care about the closing
});
if (error) {
try {
this.handleLDAPError(username, error);
} catch (error) {
doneCallBack(error, null);
return;
}
}
if (!user) {
doneCallBack(
new UnauthorizedException(LDAP_ERROR_MAP['default']),
null,
);
return;
}
const userId = user[ldapConfig.userIdField];
try {
this.createOrUpdateIdentity(userId, ldapConfig, user, username);
doneCallBack(null, username);
} catch (error) {
doneCallBack(error, null);
}
},
);
}
private createOrUpdateIdentity(
userId: string,
ldapConfig: LDAPConfig,
user: Record<string, string>,
username: string,
): void {
this.identityService
.getIdentityFromUserIdAndProviderType(userId, ProviderType.LDAP)
.then(async (identity) => {
await this.updateIdentity(
identity,
ldapConfig.displayNameField,
ldapConfig.profilePictureField,
user,
);
return;
})
.catch(async (error) => {
if (error instanceof NotInDBError) {
// The user/identity does not yet exist
const newUser = await this.usersService.createUser(
username,
// if there is no displayName we use the username
user[ldapConfig.displayNameField] ?? username,
);
const identity = await this.identityService.createIdentity(
newUser,
ProviderType.LDAP,
userId,
);
await this.updateIdentity(
identity,
ldapConfig.displayNameField,
ldapConfig.profilePictureField,
user,
);
return;
} else {
throw error;
}
});
}
/**
* Get and return the correct ldap config from the list of available configs.
* @param {string} ldapIdentifier- the identifier for the ldap config to be used
* @returns {LDAPConfig} - the ldap config with the given identifier
* @throws {BadRequestException} - there is no ldap config with the given identifier
* @private
*/
private getLDAPConfig(ldapIdentifier: string): LDAPConfig {
const ldapConfig: LDAPConfig | undefined = this.authConfig.ldap.find(
(config) => config.identifier === ldapIdentifier,
);
if (!ldapConfig) {
this.logger.warn(
`The LDAP Config '${ldapIdentifier}' was requested, but doesn't exist`,
);
throw new BadRequestException(
`There is no ldapConfig '${ldapIdentifier}'`,
);
}
return ldapConfig;
}
/**
* @async
* Update identity with data from the ldap user.
* @param {Identity} identity - the identity to sync
* @param {string} displayNameField - the field to be used as a display name
* @param {string} profilePictureField - the field to be used as a profile picture
* @param {Record<string, string>} user - the user object from ldap
* @private
*/
private async updateIdentity(
identity: Identity,
displayNameField: string,
profilePictureField: string,
user: Record<string, string>,
): Promise<Identity> {
let email: string | undefined = undefined;
if (user['mail']) {
if (Array.isArray(user['mail'])) {
email = user['mail'][0] as string;
} else {
email = user['mail'];
}
}
return await this.identityService.updateIdentity(
identity,
user[displayNameField],
email,
user[profilePictureField],
);
}
/**
* This method transforms the ldap error codes we receive into correct errors.
* It's very much inspired by https://github.com/vesse/passport-ldapauth/blob/b58c60000a7cc62165b112274b80c654adf59fff/lib/passport-ldapauth/strategy.js#L261
* @throws {UnauthorizedException} if error indicates that the user is not allowed to log in
* @throws {InternalServerErrorException} in every other cases
* @private
*/
private handleLDAPError(username: string, error: Error | string): void {
// Invalid credentials / user not found are not errors but login failures
let message = '';
if (typeof error === 'object') {
switch (error.name) {
case 'InvalidCredentialsError': {
message = 'Invalid username/password';
const ldapComment = error.message.match(
/data ([\da-fA-F]*), v[\da-fA-F]*/,
);
if (ldapComment && ldapComment[1]) {
message =
LDAP_ERROR_MAP[ldapComment[1]] || LDAP_ERROR_MAP['default'];
}
break;
}
case 'NoSuchObjectError':
message = 'Bad search base';
break;
case 'ConstraintViolationError':
message = 'Bad search base';
break;
default:
message = 'Invalid username/password';
break;
}
}
if (message !== '') {
this.logger.log(
`User with username '${username}' could not log in. Reason: ${message}`,
);
throw new UnauthorizedException(message);
}
// Other errors are (most likely) real errors
throw new InternalServerErrorException(error);
}
}

View file

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard, PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import {
InvalidCredentialsError,
NoLocalIdentityError,
} from '../../errors/errors';
import { ConsoleLoggerService } from '../../logger/console-logger.service';
import { UserRelationEnum } from '../../users/user-relation.enum';
import { User } from '../../users/user.entity';
import { UsersService } from '../../users/users.service';
import { IdentityService } from '../identity.service';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy, 'local') {
constructor(
private readonly logger: ConsoleLoggerService,
private userService: UsersService,
private identityService: IdentityService,
) {
super();
logger.setContext(LocalStrategy.name);
}
async validate(username: string, password: string): Promise<User> {
try {
const user = await this.userService.getUserByUsername(username, [
UserRelationEnum.IDENTITIES,
]);
await this.identityService.checkLocalPassword(user, password);
return user;
} catch (e) {
if (
e instanceof InvalidCredentialsError ||
e instanceof NoLocalIdentityError
) {
this.logger.log(
`User with username '${username}' could not log in. Reason: ${e.name}`,
);
throw new UnauthorizedException(
'This username and password combination is not valid.',
);
}
throw e;
}
}
}

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
export class LoginDto {
@IsString()
username: string;
@IsString()
password: string;
}

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
export class RegisterDto {
@IsString()
username: string;
@IsString()
displayName: string;
@IsString()
password: string;
}

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsString } from 'class-validator';
export class UpdatePasswordDto {
@IsString()
currentPassword: string;
@IsString()
newPassword: string;
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum ProviderType {
LOCAL = 'local',
LDAP = 'ldap',
SAML = 'saml',
OAUTH2 = 'oauth2',
GITLAB = 'gitlab',
GITHUB = 'github',
FACEBOOK = 'facebook',
TWITTER = 'twitter',
DROPBOX = 'dropbox',
GOOGLE = 'google',
}

View file

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { GuestAccess } from '../config/guest_access.enum';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { User } from '../users/user.entity';
import { UsersService } from '../users/users.service';
/**
* This guard checks if a session is present.
*
* If there is a username in `request.session.user` it will try to get this user from the database and put it into `request.user`. See {@link RequestUser}.
* If there is no `request.session.user`, but any GuestAccess is configured, `request.session.authProvider` is set to `guest` to indicate a guest user.
*
* @throws UnauthorizedException
*/
@Injectable()
export class SessionGuard implements CanActivate {
constructor(
private readonly logger: ConsoleLoggerService,
private userService: UsersService,
@Inject(noteConfiguration.KEY)
private noteConfig: NoteConfig,
) {
this.logger.setContext(SessionGuard.name);
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request & {
session?: { user: string; authProvider: string };
user?: User;
} = context.switchToHttp().getRequest();
if (!request.session?.user) {
if (this.noteConfig.guestAccess !== GuestAccess.DENY) {
if (request.session) {
request.session.authProvider = 'guest';
return true;
}
}
this.logger.debug('The user has no session.');
throw new UnauthorizedException("You're not logged in");
}
try {
request.user = await this.userService.getUserByUsername(
request.session.user,
);
return true;
} catch (e) {
if (e instanceof NotInDBError) {
this.logger.debug(
`The user '${request.session.user}' does not exist, but has a session.`,
);
throw new UnauthorizedException("You're not logged in");
}
throw e;
}
}
}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { User } from '../users/user.entity';
import { Identity } from './identity.entity';
import { ProviderType } from './provider-type.enum';
/**
* Get the first identity of a given type from the user
* @param {User} user - the user to get the identity from
* @param {ProviderType} providerType - the type of the identity
* @return {Identity | undefined} the first identity of the user or undefined, if such an identity can not be found
*/
export async function getFirstIdentityFromUser(
user: User,
providerType: ProviderType,
): Promise<Identity | undefined> {
const identities = await user.identities;
if (identities === undefined) {
return undefined;
}
return identities.find(
(aIdentity) => aIdentity.providerType === providerType,
);
}