Skip to content

Commit 24f58b4

Browse files
[FEATURE] Migrer les applications en BDD (PIX-16590).
#11434
2 parents 0078361 + 8f41d95 commit 24f58b4

File tree

17 files changed

+477
-42
lines changed

17 files changed

+477
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { cryptoService } from '../../../src/shared/domain/services/crypto-service.js';
2+
import { databaseBuffer } from '../database-buffer.js';
3+
4+
export function buildClientApplication({
5+
id = databaseBuffer.getNextId(),
6+
name = 'clientApplication',
7+
clientId = 'client-id',
8+
clientSecret = 'super-secret',
9+
scopes = ['scope1', 'scope2'],
10+
} = {}) {
11+
// eslint-disable-next-line no-sync
12+
const hashedSecret = cryptoService.hashPasswordSync(clientSecret);
13+
return databaseBuffer.pushInsertable({
14+
tableName: 'client_applications',
15+
values: {
16+
id,
17+
name,
18+
clientId,
19+
clientSecret: hashedSecret,
20+
scopes,
21+
},
22+
});
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const TABLE_NAME = 'client_applications';
2+
3+
/**
4+
* @param { import("knex").Knex } knex
5+
* @returns { Promise<void> }
6+
*/
7+
export async function up(knex) {
8+
await knex.schema.createTable(TABLE_NAME, function (table) {
9+
table.increments('id').primary().notNullable().comment('Identifiant d’une application tierce');
10+
table.string('name').notNullable().unique().comment('Nom de l’application tierce');
11+
table.string('clientId').notNullable().unique().comment('ID du client de l’application tierce');
12+
table.string('clientSecret').notNullable().comment('Secret du client de l’application tierce');
13+
table.specificType('scopes', 'varchar(255)[]').notNullable().comment('Scopes autorisés pour l’application');
14+
table.timestamps(false, true, true);
15+
table.comment('Applications tierces');
16+
});
17+
}
18+
19+
/**
20+
* @param { import("knex").Knex } knex
21+
* @returns { Promise<void> }
22+
*/
23+
export async function down(knex) {
24+
await knex.schema.dropTable(TABLE_NAME);
25+
}

Diff for: api/db/seeds/data/common/common-builder.js

+29
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const commonBuilder = async function ({ databaseBuilder }) {
1111
_createSupportAdmin(databaseBuilder);
1212
_createMetierAdmin(databaseBuilder);
1313

14+
createClientApplications(databaseBuilder);
15+
1416
await _createPublicTargetProfile(databaseBuilder);
1517
await databaseBuilder.commit();
1618
};
@@ -59,6 +61,33 @@ function _createCertifAdmin(databaseBuilder) {
5961
databaseBuilder.factory.buildPixAdminRole({ userId: REAL_PIX_SUPER_ADMIN_ID + 3, role: ROLES.CERTIF });
6062
}
6163

64+
function createClientApplications(databaseBuilder) {
65+
databaseBuilder.factory.buildClientApplication({
66+
name: 'livretScolaire',
67+
clientId: 'livretScolaire',
68+
clientSecret: 'livretScolaireSecret',
69+
scopes: ['organizations-certifications-result'],
70+
});
71+
databaseBuilder.factory.buildClientApplication({
72+
name: 'poleEmploi',
73+
clientId: 'poleEmploi',
74+
clientSecret: 'poleemploisecret',
75+
scopes: ['pole-emploi-participants-result'],
76+
});
77+
databaseBuilder.factory.buildClientApplication({
78+
name: 'pixData',
79+
clientId: 'pixData',
80+
clientSecret: 'pixdatasecret',
81+
scopes: ['statistics'],
82+
});
83+
databaseBuilder.factory.buildClientApplication({
84+
name: 'parcoursup',
85+
clientId: 'parcoursup',
86+
clientSecret: 'parcoursupsecret',
87+
scopes: ['parcoursup'],
88+
});
89+
}
90+
6291
function _createPublicTargetProfile(databaseBuilder) {
6392
return createTargetProfile({
6493
databaseBuilder,

Diff for: api/lib/domain/usecases/authenticate-application.js

+24-20
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,50 @@
1-
import lodash from 'lodash';
2-
31
import { config } from '../../../src/shared/config.js';
42
import {
53
ApplicationScopeNotAllowedError,
64
ApplicationWithInvalidClientIdError,
75
ApplicationWithInvalidClientSecretError,
86
} from '../../../src/shared/domain/errors.js';
9-
const { apimRegisterApplicationsCredentials, jwtConfig } = config;
10-
11-
const { find } = lodash;
127

13-
const authenticateApplication = async function ({ clientId, clientSecret, scope, tokenService }) {
14-
const application = find(apimRegisterApplicationsCredentials, { clientId });
15-
_checkClientId(application, clientId);
16-
_checkClientSecret(application, clientSecret);
8+
const { authentication } = config;
9+
10+
export async function authenticateApplication({
11+
clientId,
12+
clientSecret,
13+
scope,
14+
tokenService,
15+
clientApplicationRepository,
16+
cryptoService,
17+
}) {
18+
const application = await clientApplicationRepository.findByClientId(clientId);
19+
_checkApplication(application, clientId);
20+
await _checkClientSecret(application, clientSecret, cryptoService);
1721
_checkAppScope(application, scope);
1822

1923
return tokenService.createAccessTokenFromApplication(
2024
clientId,
21-
application.source,
25+
application.name,
2226
scope,
23-
jwtConfig[application.source].secret,
24-
jwtConfig[application.source].tokenLifespan,
27+
authentication.secret,
28+
authentication.accessTokenLifespanMs,
2529
);
26-
};
30+
}
2731

28-
function _checkClientId(application, clientId) {
29-
if (!application || application.clientId !== clientId) {
32+
function _checkApplication(application) {
33+
if (!application) {
3034
throw new ApplicationWithInvalidClientIdError('The client ID is invalid.');
3135
}
3236
}
3337

34-
function _checkClientSecret(application, clientSecret) {
35-
if (application.clientSecret !== clientSecret) {
38+
async function _checkClientSecret(application, clientSecret, cryptoService) {
39+
try {
40+
await cryptoService.checkPassword({ password: clientSecret, passwordHash: application.clientSecret });
41+
} catch {
3642
throw new ApplicationWithInvalidClientSecretError('The client secret is invalid.');
3743
}
3844
}
3945

4046
function _checkAppScope(application, scope) {
41-
if (application.scope !== scope) {
47+
if (!application.scopes.includes(scope)) {
4248
throw new ApplicationScopeNotAllowedError('The scope is invalid.');
4349
}
4450
}
45-
46-
export { authenticateApplication };

Diff for: api/lib/domain/usecases/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import * as resetPasswordService from '../../../src/identity-access-management/d
7979
import { scoAccountRecoveryService } from '../../../src/identity-access-management/domain/services/sco-account-recovery.service.js';
8080
import { accountRecoveryDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/account-recovery-demand.repository.js';
8181
import * as authenticationMethodRepository from '../../../src/identity-access-management/infrastructure/repositories/authentication-method.repository.js';
82+
import { clientApplicationRepository } from '../../../src/identity-access-management/infrastructure/repositories/client-application.repository.js';
8283
import { emailValidationDemandRepository } from '../../../src/identity-access-management/infrastructure/repositories/email-validation-demand.repository.js';
8384
// Not used in lib
8485
import { userAnonymizedEventLoggingJobRepository } from '../../../src/identity-access-management/infrastructure/repositories/jobs/user-anonymized-event-logging-job-repository.js';
@@ -244,6 +245,7 @@ const dependencies = {
244245
certificationCpfCityRepository,
245246
certificationOfficerRepository,
246247
challengeRepository,
248+
clientApplicationRepository,
247249
codeGenerator,
248250
codeUtils,
249251
competenceEvaluationRepository,

Diff for: api/scripts/prod/migrate-client-application.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { knex } from '../../db/knex-database-connection.js';
2+
import { Script } from '../../src/shared/application/scripts/script.js';
3+
import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js';
4+
import { config } from '../../src/shared/config.js';
5+
import { cryptoService } from '../../src/shared/domain/services/crypto-service.js';
6+
7+
export class MigrateClientApplicationScript extends Script {
8+
constructor() {
9+
super({
10+
description: 'This script will migrate client application from environment variables (config) to database.',
11+
permanent: false,
12+
options: {
13+
dryRun: {
14+
type: 'boolean',
15+
default: true,
16+
description: 'when true does not insert to database',
17+
},
18+
},
19+
});
20+
}
21+
22+
async handle({ options, logger }) {
23+
await knex.transaction(async (trx) => {
24+
const { apimRegisterApplicationsCredentials } = config;
25+
26+
for (const clientApplication of apimRegisterApplicationsCredentials) {
27+
const query = trx('client_applications').insert({
28+
clientId: clientApplication.clientId,
29+
clientSecret: await cryptoService.hashPassword(clientApplication.clientSecret),
30+
name: clientApplication.source,
31+
scopes: [clientApplication.scope],
32+
});
33+
logger.info({ event: 'MigrateClientApplicationScript' }, query.toString());
34+
await query;
35+
}
36+
if (options.dryRun) {
37+
logger.info({ event: 'MigrateClientApplicationScript' }, 'Dry run, rolling back insertions');
38+
await trx.rollback();
39+
}
40+
});
41+
}
42+
}
43+
44+
await ScriptRunner.execute(import.meta.url, MigrateClientApplicationScript);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class ClientApplication {
2+
constructor({ id, name, clientId, clientSecret, scopes }) {
3+
this.id = id;
4+
this.name = name;
5+
this.clientId = clientId;
6+
this.clientSecret = clientSecret;
7+
this.scopes = scopes;
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { knex } from '../../../../db/knex-database-connection.js';
2+
import { ClientApplication } from '../../domain/models/ClientApplication.js';
3+
4+
const TABLE_NAME = 'client_applications';
5+
6+
export const clientApplicationRepository = {
7+
async findByClientId(clientId) {
8+
const dto = await knex.select().from(TABLE_NAME).where({ clientId }).first();
9+
if (!dto) return undefined;
10+
return toDomain(dto);
11+
},
12+
};
13+
14+
function toDomain(dto) {
15+
return new ClientApplication(dto);
16+
}

Diff for: api/src/shared/config.js

+1
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,7 @@ const configuration = (function () {
658658
};
659659

660660
config.jwtConfig.livretScolaire.secret = 'test-secretOsmose';
661+
config.jwtConfig.livretScolaire.tokenLifespan = '4h';
661662
config.jwtConfig.poleEmploi.secret = 'test-secretPoleEmploi';
662663
config.jwtConfig.pixData.secret = 'test-secretPixData';
663664
config.jwtConfig.parcoursup.secret = 'test-secretPixParcoursup';

Diff for: api/tests/acceptance/application/authentication/authentication-route_test.js

+8
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ describe('Acceptance | Controller | authentication-controller', function () {
133133
'x-forwarded-host': 'app.pix.fr',
134134
},
135135
};
136+
137+
databaseBuilder.factory.buildClientApplication({
138+
name: 'osmose',
139+
clientId: OSMOSE_CLIENT_ID,
140+
clientSecret: OSMOSE_CLIENT_SECRET,
141+
scopes: [SCOPE],
142+
});
143+
await databaseBuilder.commit();
136144
});
137145

138146
it('should return an 200 with accessToken when clientId, client secret and scope are registred', async function () {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { clientApplicationRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/client-application.repository.js';
2+
import { databaseBuilder, domainBuilder, expect } from '../../../../test-helper.js';
3+
4+
describe('Integration | Identity Access Management | Infrastructure | Repository | client-application', function () {
5+
describe('#findByClientId', function () {
6+
let application2;
7+
8+
beforeEach(async function () {
9+
databaseBuilder.factory.buildClientApplication({
10+
name: 'appli1',
11+
clientId: 'clientId-appli1',
12+
clientSecret: 'secret-app1',
13+
scopes: ['scope1', 'scope2'],
14+
});
15+
application2 = databaseBuilder.factory.buildClientApplication({
16+
name: 'appli2',
17+
clientId: 'clientId-appli2',
18+
clientSecret: 'secret-app2',
19+
scopes: ['scope3', 'scope4', 'scope5'],
20+
});
21+
22+
await databaseBuilder.commit();
23+
});
24+
25+
context('when application name is not found', function () {
26+
it('should return undefined', async function () {
27+
// given
28+
const clientId = 'clientId-appli3';
29+
30+
// when
31+
const application = await clientApplicationRepository.findByClientId(clientId);
32+
33+
// then
34+
expect(application).to.be.undefined;
35+
});
36+
});
37+
38+
context('when application name is found', function () {
39+
it('should return the application model', async function () {
40+
// given
41+
const clientId = 'clientId-appli2';
42+
43+
// when
44+
const application = await clientApplicationRepository.findByClientId(clientId);
45+
46+
// then
47+
expect(application).to.deepEqualInstance(domainBuilder.buildClientApplication(application2));
48+
});
49+
});
50+
});
51+
});

0 commit comments

Comments
 (0)