Commit eb718584 authored by Sébastien DA ROCHA's avatar Sébastien DA ROCHA
Browse files

Split oidc and legacy services, add oidc user resource access

parent 24ae7457
Pipeline #8762 passed with stage
in 6 seconds
import {
Controller, Post, Body, HttpException, InternalServerErrorException,
HttpCode, Get, Req, Delete, Put, Query
HttpCode, Get, Req, Delete, Put, Query,
} from '@nestjs/common';
import { LegacyService } from './legacy.service';
import { LegacyServiceOIDC } from './legacy.service.oidc';
import { ApiOperation, ApiResponse, ApiUseTags, ApiImplicitHeader, ApiImplicitBody } from '@nestjs/swagger';
import {
LoginForm, UserCreationForm, UserInfoWithEcryptedPassword, JWTToken, Service, Resource, RestrictedAccessDataset,
AccessRequest, UpdatePasswordForm, UserUpdateForm, UserInfo, AccessDeletionResponse,
AccessRequestResponse, AccessRenewalResponse, UserAccountValidationRequest, PasswordResetForm, PasswordForgottenForm
AccessRequestResponse, AccessRenewalResponse, UserAccountValidationRequest, PasswordResetForm, PasswordForgottenForm,
} from './legacy.model';
import { handleError } from './errorHandlingHelper';
......@@ -17,6 +18,7 @@ export class LegacyController {
constructor(
private legacyService: LegacyService,
private legacyServiceOidc: LegacyServiceOIDC,
) { }
@Get('user')
......@@ -94,7 +96,7 @@ export class LegacyController {
@HttpCode(201)
async createOidcAccount(@Req() req) {
try {
return await this.legacyService.createOidcAccount(req.headers.token);
return await this.legacyServiceOidc.createAccount(req.headers.token);
} catch (error) {
if (error instanceof HttpException) {
throw error;
......@@ -243,6 +245,14 @@ export class LegacyController {
@Get('user/resources')
@ApiOperation({ title: 'Get the list of accessible resources by the specified user.' })
@ApiImplicitHeader({
name: 'Cookie',
description: 'The JWT token containing user information',
})
@ApiImplicitHeader({
name: 'X-Xsrf-Token',
description: 'Cross Site reference token contained in the JWT token',
})
@ApiImplicitHeader({
name: 'Cookie',
description: 'The JWT token is sent by the browser as a cookie (refer to the config of the Authentication project to know which key is used)',
......@@ -254,7 +264,12 @@ export class LegacyController {
async getUserResources(@Req() req): Promise<Resource[]> {
const token: JWTToken = req.headers.token;
try {
const userServices = await this.legacyService.getUserResources(token.username, token.authzKey);
let userServices;
if (token.authzKey == null) {
userServices = await this.legacyServiceOidc.getUserResources(token.email);
} else {
userServices = await this.legacyService.getUserResources(token.username, token.authzKey);
}
return userServices;
} catch (error) {
if (error instanceof HttpException) {
......
......@@ -2,7 +2,6 @@ import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger';
import { IsString, IsBoolean, IsNumber, IsInt, IsArray, MinLength, Matches } from 'class-validator';
import * as moment from 'moment';
export class LoginForm {
@ApiModelProperty()
@IsString()
......@@ -531,5 +530,5 @@ export class Email {
to: string[];
subject: string;
html: string; // As html
replyto?: string; //optional
replyto?: string; // optional
}
import { Module } from '@nestjs/common';
import { LegacyService } from './legacy.service';
import { LegacyServiceOIDC } from './legacy.service.oidc';
import { LegacyController } from './legacy.controller';
@Module({
providers: [LegacyService],
providers: [LegacyService, LegacyServiceOIDC],
controllers: [LegacyController],
})
export class LegacyModule {}
import {
Injectable, Logger, HttpException, BadRequestException, HttpStatus,
InternalServerErrorException, UnauthorizedException,
} from '@nestjs/common';
import {
LoginForm, UserInfoWithEcryptedPassword, UserCreationForm, Service, JWTToken, Resource, RestrictedAccessDataset,
AccessRequest, UpdatePasswordForm, UserUpdateForm, UserInfo, LegacyUserUpdateForm, LegacyUserCreationForm,
AccessDeletionResponse, AccessRequestResponse, AccessRenewalResponse, PasswordResetForm, PasswordForgottenForm, Email,
OIDCCreateForm,
} from './legacy.model';
import * as request from 'request-promise-native';
import { ConfigService } from '../configuration/config.service';
import { decrypt } from '../helpers/encryptionHelpers';
import { buildDataAccessDeletionAdminEmail, buildDataAccessDeletionUserEmail } from '../email-templates/data-access-deletion';
import { buildDataAccessRequestAdminEmail, buildDataAccessRequestUserEmail } from '../email-templates/data-access-request';
import { buildRenewDataAccessRequestAdminEmail, buildRenewDataAccessRequestUserEmail } from '../email-templates/data-access-renew';
import { handleError } from './errorHandlingHelper';
import * as uuid4 from 'uuid/v4';
import moment = require('moment-timezone');
import { Redis } from '../helpers/redis.helper';
import * as jwt from 'jsonwebtoken';
moment.tz.setDefault('Europe/Paris');
import { LegacyService } from './legacy.service'
@Injectable()
export class LegacyServiceOIDC extends LegacyService {
async createAccount(token): Promise<void> {
this.logger.log('Entering function', `${LegacyServiceOIDC.name} - ${this.createAccount.name}`);
try {
const userInfo = {username: token.id, email: token.email} as OIDCCreateForm;
if (userInfo) {
Logger.log(`User account validation for : ${userInfo.email}`, `${LegacyServiceOIDC.name} - ${this.createAccount.name}`);
let res = await request.post(`${this.conf.legacyAuthServiceUrl}/add_user_oidc/`).form(userInfo).catch((error) => {
const inres = JSON.parse(error.error);
// Normal use case
if (inres.message === 'Account already exists') {
return;
}
this.logger.error('Couldn\'t create user.', `${error}`, `${LegacyServiceOIDC.name} - ${this.createAccount.name}`);
throw new InternalServerErrorException({ error, message: 'Couldn\'t create user.' });
});
res = JSON.parse(res);
if (res.server_response && res.server_response === 'Success') {
Logger.log(`User account created.`, `${LegacyServiceOIDC.name} - ${this.createAccount.name}`);
return;
} else {
// Normal use case
if (res.message === 'Account already exists') {
return;
}
this.logger.error('Couldn\'t create user.', `${res}`, `${LegacyServiceOIDC.name} - ${this.createAccount.name}`);
if (res.message === 'Error during account creation') {
throw new InternalServerErrorException(res.message);
}
if (res.message === 'Uncomplete form') {
throw new BadRequestException('uncompleteForm');
} else {
throw new BadRequestException(res.message);
}
}
} else {
this.logger.warn('Token not found.', `${LegacyServiceOIDC.name} - ${this.createAccount.name}`);
throw new BadRequestException('tokenNotFound');
}
} catch (err) {
handleError(err, new InternalServerErrorException('Something went wrong.'));
}
}
async getUserResources(username: string): Promise<Resource[]> {
this.logger.log('Entering function', `${LegacyServiceOIDC.name} - ${this.getUserResources.name}`);
try {
// Get the list of the accessible services by the user
let res = await request.post(`${this.conf.legacyAuthServiceUrl}/get_user_service_oidc/`).form({ username}).catch((error) => {
this.logger.error('Couldn\'t get the user`\'s services list.', `${error}`, `${LegacyServiceOIDC.name} - ${this.getUserResources.name}`);
throw new InternalServerErrorException({ error, message: 'Couldn\'t get the user`\'s services list.' });
});
res = JSON.parse(res);
if (res.server_response && res.server_response === 'Success') {
const services = [];
if (res.services) {
// Create a new object for each service
res.services.forEach((service) => {
services.push(new Resource(service));
});
}
return services;
} else {
this.logger.error('Couldn\'t get the user`\'s services list.', `${res.message}`, `${LegacyServiceOIDC.name} - ${this.getUserResources.name}`);
throw new BadRequestException(res.message);
}
} catch (err) {
if (err instanceof HttpException) {
throw new HttpException(err.message, err.getStatus());
} else {
throw new InternalServerErrorException('Something went wrong.');
}
}
}
async getRestrictedAccessDatasets(): Promise<RestrictedAccessDataset[]> {
this.logger.log('Entering function', `${LegacyServiceOIDC.name} - ${this.getRestrictedAccessDatasets.name}`);
try {
let rawRestrictedService = await request.get(`${this.conf.legacyAuthServiceUrl}/get_services/`).catch((error) => {
this.logger.error('Couldn\'t get the services list.', `${error}`, `${LegacyServiceOIDC.name} - ${this.getRestrictedAccessDatasets.name}`);
throw new InternalServerErrorException({ error, message: 'Couldn\'t get the services list.' });
});
rawRestrictedService = JSON.parse(rawRestrictedService);
const restrictedServices = [];
if (rawRestrictedService.services) {
rawRestrictedService = rawRestrictedService.services.filter(service => service.visible === 'RESTRICTED');
rawRestrictedService.forEach((service) => {
restrictedServices.push(new RestrictedAccessDataset(service));
});
return restrictedServices;
} else {
this.logger.error(
'Couldn\'t get the different restricted services.',
`${rawRestrictedService}`,
`${LegacyServiceOIDC.name} - ${this.getRestrictedAccessDatasets.name}`,
);
throw new InternalServerErrorException('Couldn\'t get the different restricted services.');
}
} catch (err) {
if (err instanceof HttpException) {
throw new HttpException(err.message, err.getStatus());
} else {
throw new InternalServerErrorException('Something went wrong.');
}
}
}
async addUserResource(token: JWTToken, accessRequests: AccessRequest[]): Promise<AccessRequestResponse> {
this.logger.log('Entering function', `${LegacyServiceOIDC.name} - ${this.addUserResource.name}`);
try {
// Decrypt the password
const password = decrypt(token.authzKey, await this.configService.getPrivateKey());
if (!password) {
throw new UnauthorizedException('Invalid user credentials.');
}
// Get the list of datasets
const datasets = await this.getRestrictedAccessDatasets();
// Get the list of services
const services = await this.getServices();
const accessSuccessfullyRequested = [];
const accessUnsuccessfullyRequested = [];
// HTML to be displayed in the dedicated part of the admin and user email
let formatedDatasets = '';
for (const accessRequest of accessRequests) {
// Request access the the specified service and the specified modes
let res = await request.post(`${this.conf.legacyAuthServiceUrl}/add_user_service/`).form(
{
password,
username: token.username,
service_id: accessRequest.id,
modes: accessRequest.servicesId.toString(),
},
).catch((error) => {
this.logger.error('Couldn\'t request access to the resource.', `${error}`, `${LegacyServiceOIDC.name} - ${this.addUserResource.name}`);
throw new InternalServerErrorException({ error, message: 'Couldn\'t request access to the resource.' });
});
res = JSON.parse(res);
// Find the names of the services
const servicesName = [];
accessRequest.servicesId.forEach((s) => {
const service = services.find(e => e.id === s);
if (service) {
servicesName.push(service.name);
}
});
// Find the title of the dataset
const dataset = datasets.find(d => d.datasetId === accessRequest.id);
if (res.server_response && res.server_response === 'Success') {
if (dataset && servicesName.length > 0) {
servicesName.forEach((s) => {
formatedDatasets += `${dataset.datasetName} - Mode ${s}<br/>`;
});
accessSuccessfullyRequested.push(`${dataset.datasetName} (${servicesName.toString()})`);
}
} else {
this.logger.error('Couldn\'t request access to the resource.', `${res.message}`, `${LegacyServiceOIDC.name} - ${this.addUserResource.name}`);
accessUnsuccessfullyRequested.push(`${dataset.datasetName} (${servicesName.toString()})`);
}
}
const datetime = moment().format('DD/MM/YYYY à HH:mm');
// Building email temaplates
const adminEmail = buildDataAccessRequestAdminEmail({
datetime,
firstName: token.firstName,
lastName: token.lastName,
username: token.username,
datasets: formatedDatasets,
imageHost: this.conf.imageHost,
});
const userEmail = buildDataAccessRequestUserEmail({
datetime,
firstName: token.firstName,
datasets: formatedDatasets,
imageHost: this.conf.imageHost,
});
// Send the mail
await Promise.all([
this.sendEmail({
html: adminEmail,
to: [this.conf.userSupportMailbox],
subject: 'Demande d’accès aux données',
}),
this.sendEmail({
html: userEmail,
to: [token.email],
subject: 'Demande d’accès aux données',
}),
]);
return { successfullyRequested: accessSuccessfullyRequested, unsuccessfullyRequested: accessUnsuccessfullyRequested };
} catch (err) {
if (err instanceof HttpException) {
throw new HttpException(err.message, err.getStatus());
} else {
throw new InternalServerErrorException('Something went wrong.');
}
}
}
async renewUserResource(token: JWTToken, accessRequests: AccessRequest[]): Promise<AccessRenewalResponse> {
this.logger.log('Entering function', `${LegacyServiceOIDC.name} - ${this.renewUserResource.name}`);
try {
// Decrypt the password
const password = decrypt(token.authzKey, await this.configService.getPrivateKey());
if (!password) {
this.logger.warn('Invalid user credentials.', `${LegacyServiceOIDC.name} - ${this.renewUserResource.name}`);
throw new UnauthorizedException('Invalid user credentials.');
}
// Get the list of user access
const userAccess = await this.getUserResources(token.username);
// Get the list of datasets
const datasets = await this.getRestrictedAccessDatasets();
// Get the list of services
const services = await this.getServices();
const accessRenewalSuccessfullyRequested = [];
const accessRenewalUnsuccessfullyRequested = [];
// HTML to be displayed in the dedicated part of the admin and user email
let formatedDatasets = '';
// Find the name of the dataset and services for each accessRequest
accessRequests.forEach((accessRequest) => {
const servicesNameOk = [];
const servicesNameKo = [];
// Find the names of the services
accessRequest.servicesId.forEach((s) => {
const service = services.find(e => e.id === s);
if (service) {
// Check if the access has already been validated once (has a validUntil date)
const access = userAccess.find(e => e.datasetId === accessRequest.id && e.serviceName === service.name);
if (access && access.validUntil !== null) {
servicesNameOk.push(service.name);
} else {
servicesNameKo.push(service.name);
}
} else {
servicesNameKo.push(service.name);
}
});
// Find the title of the dataset
const dataset = datasets.find(d => d.datasetId === accessRequest.id);
// If both dataset and services are found then format a string that will be displayed in email body
// Also format an array of string that will be returned by the endpoint
if (dataset && servicesNameOk.length > 0) {
servicesNameOk.forEach((s) => {
formatedDatasets += `${dataset.datasetName} - Mode ${s}<br/>`;
});
accessRenewalSuccessfullyRequested.push(`${dataset.datasetName} (${servicesNameOk.toString()})`);
}
if (dataset && servicesNameKo.length > 0) {
accessRenewalUnsuccessfullyRequested.push(`${dataset.datasetName} (${servicesNameKo.toString()})`);
}
});
// Building email templates
const adminEmail = buildRenewDataAccessRequestAdminEmail({
firstName: token.firstName,
lastName: token.lastName,
username: token.username,
datasets: formatedDatasets,
datetime: moment().format('DD/MM/YYYY à HH:mm'),
imageHost: this.conf.imageHost,
});
const userEmail = buildRenewDataAccessRequestUserEmail({
firstName: token.firstName,
datasets: formatedDatasets,
datetime: moment().format('DD/MM/YYYY à HH:mm'),
imageHost: this.conf.imageHost,
});
// Send the mail
await Promise.all([
this.sendEmail({
html: adminEmail,
to: [this.conf.userSupportMailbox],
subject: 'Demande de renouvellement d’accès aux données',
}),
this.sendEmail({
html: userEmail,
to: [token.email],
subject: 'Demande de renouvellement d’accès aux données',
}),
]);
return {
successfullyRenewalRequested: accessRenewalSuccessfullyRequested,
unsuccessfullyRenewalRequested: accessRenewalUnsuccessfullyRequested,
};
} catch (err) {
if (err instanceof HttpException) {
throw new HttpException(err.message, err.getStatus());
} else {
throw new InternalServerErrorException('Something went wrong.');
}
}
}
async deleteUserResource(token: JWTToken, accessRequests: AccessRequest[]): Promise<AccessDeletionResponse> {
this.logger.log('Entering function', `${LegacyServiceOIDC.name} - ${this.deleteUserResource.name}`);
try {
// Decrypt the password
const password = decrypt(token.authzKey, await this.configService.getPrivateKey());
if (!password) {
this.logger.warn('Invalid user credentials.', `${LegacyServiceOIDC.name} - ${this.deleteUserResource.name}`);
throw new UnauthorizedException('Invalid user credentials.');
}
// Get the list of datasets
const datasets = await this.getRestrictedAccessDatasets();
// Get the list of services
const services = await this.getServices();
const datasetsSuccessfullyDeleted = [];
const datasetsUnsuccessfullyDeleted = [];
// HTML to be displayed in the dedicated part of the admin and user email
let formatedDatasets = '';
for (const accessRequest of accessRequests) {
// Delete access to the specified service and the specified modes
let res = await request.post(`${this.conf.legacyAuthServiceUrl}/del_user_service/`).form(
{
password,
username: token.username,
service_id: accessRequest.id,
modes: accessRequest.servicesId.toString(),
},
).catch((error) => {
this.logger.error('Couldn\'t remove access to the resource.', `${error}`, `${LegacyServiceOIDC.name} - ${this.deleteUserResource.name}`);
throw new InternalServerErrorException({ error, message: 'Couldn\'t remove access to the resource.' });
});
res = JSON.parse(res);
// Find the names of the services
const servicesName = [];
accessRequest.servicesId.forEach((s) => {
const service = services.find(e => e.id === s);
if (service) {
servicesName.push(service.name);
}
});
// Find the title of the dataset
const dataset = datasets.find(d => d.datasetId === accessRequest.id);
if (res.server_response && res.server_response === 'Success') {
if (dataset && servicesName.length > 0) {
servicesName.forEach((s) => {
formatedDatasets += `${dataset.datasetName} - Mode ${s}<br/>`;
});
datasetsSuccessfullyDeleted.push(`${dataset.datasetName} (${servicesName.toString()})`);
}
} else {
Logger.error(` [x] Error: ${res.message}`);
datasetsUnsuccessfullyDeleted.push(`${dataset.datasetName} (${servicesName.toString()})`);
}
}
// Building email temaplates
const adminEmail = buildDataAccessDeletionAdminEmail({
firstName: token.firstName,
lastName: token.lastName,
username: token.username,
datasets: formatedDatasets,
datetime: moment().format('DD/MM/YYYY à HH:mm'),
imageHost: this.conf.imageHost,
});
const userEmail = buildDataAccessDeletionUserEmail({
datasets: formatedDatasets,
datetime: moment().format('DD/MM/YYYY à HH:mm'),
firstName: token.firstName,
imageHost: this.conf.imageHost,
});
// Send the mail
await Promise.all([
this.sendEmail({
html: adminEmail,
to: [this.conf.userSupportMailbox],
subject: 'Demande de suppression d’accès aux données',
}),
this.sendEmail({
html: userEmail,
to: [token.email],
subject: 'Demande de suppression d’accès aux données',
}),
]);
return { successfullyDeleted: datasetsSuccessfullyDeleted, unsuccessfullyDeleted: datasetsUnsuccessfullyDeleted };
} catch (err) {
if (err instanceof HttpException) {
throw new HttpException(err.message, err.getStatus());
} else {
throw new InternalServerErrorException('Something went wrong.');
}
}
}
}
......@@ -6,9 +6,9 @@ import {
LoginForm, UserInfoWithEcryptedPassword, UserCreationForm, Service, JWTToken, Resource, RestrictedAccessDataset,
AccessRequest, UpdatePasswordForm, UserUpdateForm, UserInfo, LegacyUserUpdateForm, LegacyUserCreationForm,
AccessDeletionResponse, AccessRequestResponse, AccessRenewalResponse, PasswordResetForm, PasswordForgottenForm, Email,
OIDCCreateForm,
} from './legacy.model';
import * as request from 'request-promise-native';
import { Config } from '../configuration/config';
import { ConfigService } from '../configuration/config.service';
import { decrypt } from '../helpers/encryptionHelpers';
import { buildDataAccessDeletionAdminEmail, buildDataAccessDeletionUserEmail } from '../email-templates/data-access-deletion';
......@@ -20,17 +20,16 @@ import { buildAccountValidationEmail } from '../email-templates/account-creation
import { buildResetPasswordEmail } from '../email-templates/reset-password';
import moment = require('moment-timezone');
import { Redis } from '../helpers/redis.helper';
import * as jwt from 'jsonwebtoken';
moment.tz.setDefault('Europe/Paris');
@Injectable()
export class LegacyService {
conf: any;
private logger: Logger;
private redis: Redis;
protected conf: typeof Config;
protected logger: Logger;
protected redis: Redis;
constructor(
private configService: ConfigService,
protected configService: ConfigService,