Skip to content

Commit

Permalink
feat(api): add repository to save and find revoked user access tempor…
Browse files Browse the repository at this point in the history
…ary storage
  • Loading branch information
EmmanuelleBonnemay committed Jan 31, 2025
1 parent 916f4b7 commit 23a1209
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 0 deletions.
6 changes: 6 additions & 0 deletions api/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ REFRESH_TOKEN_LIFESPAN=7d
# default: '7d'
# REFRESH_TOKEN_LIFESPAN_PIX_ADMIN=7d

# Revoked user access lifespan
# presence: optional
# type: String
# default: '7d'
# REVOKED_USER_ACCESS_LIFESPAN=7d

# Saml access token lifespan
# presence: optional
# type: String
Expand Down
14 changes: 14 additions & 0 deletions api/src/identity-access-management/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ class UserCantBeCreatedError extends DomainError {
}
}

class RevokeUntilMustBeAnInstanceOfDate extends DomainError {
constructor(message = 'Revoke Until must be an instance of Date') {
super(message);
}
}

class UserIdIsRequiredError extends DomainError {
constructor(message = 'User Id is required') {
super(message);
}
}

class UserShouldChangePasswordError extends DomainError {
constructor(message = 'User password must be changed.', meta) {
super(message);
Expand All @@ -87,6 +99,8 @@ export {
OrganizationLearnerNotBelongToOrganizationIdentityError,
PasswordNotMatching,
PasswordResetDemandNotFoundError,
RevokeUntilMustBeAnInstanceOfDate,
UserCantBeCreatedError,
UserIdIsRequiredError,
UserShouldChangePasswordError,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { config } from '../../../../src/shared/config.js';
import { temporaryStorage } from '../../../../src/shared/infrastructure/key-value-storages/index.js';
import { UserIdIsRequiredError } from '../../domain/errors.js';
import { RevokeUntilMustBeAnInstanceOfDate } from '../../domain/errors.js';
import { RevokedUserAccess } from '../../domain/models/RevokedUserAccess.js';

const revokedUserAccessTemporaryStorage = temporaryStorage.withPrefix('revoked-user-access:');
const revokedUserAccessLifespanMs = config.authentication.revokedUserAccessLifespanMs;

const saveForUser = async function (userId, revokeUntil) {
if (!userId) {
throw new UserIdIsRequiredError();
}

if (!(revokeUntil instanceof Date)) {
throw new RevokeUntilMustBeAnInstanceOfDate();
}

await revokedUserAccessTemporaryStorage.save({
key: userId,
value: Math.floor(revokeUntil.getTime() / 1000),
expirationDelaySeconds: revokedUserAccessLifespanMs / 1000,
});
};

const findByUserId = async function (userId) {
const value = await revokedUserAccessTemporaryStorage.get(userId);
return new RevokedUserAccess(value);
};

export const revokedUserAccessRepository = { saveForUser, findByUserId };
1 change: 1 addition & 0 deletions api/src/shared/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const configuration = (function () {
'pix-certif': ms(process.env.REFRESH_TOKEN_LIFESPAN_PIX_CERTIF || '7d'),
'pix-admin': ms(process.env.REFRESH_TOKEN_LIFESPAN_PIX_ADMIN || '7d'),
},
revokedUserAccessLifespanMs: ms(process.env.REVOKED_USER_ACCESS_LIFESPAN || '7d'),
tokenForCampaignResultLifespan: process.env.CAMPAIGN_RESULT_ACCESS_TOKEN_LIFESPAN || '1h',
tokenForStudentReconciliationLifespan: '1h',
passwordResetTokenLifespan: '1h',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Joi from 'joi';

const schema = Joi.object({
ACCESS_TOKEN_LIFESPAN: Joi.string().optional(),
AUTH_SECRET: Joi.string().required(),
AUTONOMOUS_COURSES_ORGANIZATION_ID: Joi.number().required(),
API_DATA_URL: Joi.string().uri().optional(),
Expand Down Expand Up @@ -59,6 +60,7 @@ const schema = Joi.object({
POLE_EMPLOI_SENDING_URL: Joi.string().uri().optional(),
POLE_EMPLOI_TOKEN_URL: Joi.string().uri().optional(),
REDIS_URL: Joi.string().uri().optional(),
REVOKED_USER_ACCESS_LIFESPAN: Joi.string().optional(),
SCO_ACCOUNT_RECOVERY_KEY_LIFETIME_MINUTES: Joi.number().integer().min(1).optional(),
TEST_DATABASE_URL: Joi.string().optional(),
TEST_LOG_ENABLED: Joi.string().optional().valid('true', 'false'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { RevokedUserAccess } from '../../../../../src/identity-access-management/domain/models/RevokedUserAccess.js';
import { revokedUserAccessRepository } from '../../../../../src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js';
import { temporaryStorage } from '../../../../../src/shared/infrastructure/key-value-storages/index.js';
import { expect } from '../../../../test-helper.js';

const revokedUserAccessTemporaryStorage = temporaryStorage.withPrefix('revoked-user-access:');

describe('Integration | Identity Access Management | Infrastructure | Repository | revoked-user', function () {
beforeEach(async function () {
await revokedUserAccessTemporaryStorage.flushAll();
});

describe('#saveForUser', function () {
it('saves revoked user access in Redis', async function () {
// given
const revokeUntil = new Date();
const revokedTimeStamp = Math.floor(revokeUntil.getTime() / 1000);

// when
await revokedUserAccessRepository.saveForUser(12345, revokeUntil);

// then
const result = await revokedUserAccessTemporaryStorage.get(12345);
expect(result).to.equal(revokedTimeStamp);
});
});

describe('#findByUserId', function () {
it('finds revoked user access by user id', async function () {
// given
const revokeUntil = new Date();
const revokeTimeStamp = Math.floor(new Date().getTime() / 1000);
await revokedUserAccessRepository.saveForUser(12345, revokeUntil);

// when
const result = await revokedUserAccessRepository.findByUserId(12345);

// then
expect(result).to.deep.equal({
revokeTimeStamp: revokeTimeStamp,
});
expect(result).to.be.instanceOf(RevokedUserAccess);
});
});
});

0 comments on commit 23a1209

Please sign in to comment.