diff --git a/api/sample.env b/api/sample.env index 09e5aff77c8..2f10e17780b 100644 --- a/api/sample.env +++ b/api/sample.env @@ -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: '2d' +# REVOKED_USER_ACCESS_LIFESPAN=2d + # Saml access token lifespan # presence: optional # type: String diff --git a/api/src/identity-access-management/domain/errors.js b/api/src/identity-access-management/domain/errors.js index a3ba21d7c4f..26b6c1ee016 100644 --- a/api/src/identity-access-management/domain/errors.js +++ b/api/src/identity-access-management/domain/errors.js @@ -70,6 +70,18 @@ class UserCantBeCreatedError extends DomainError { } } +class RevokeDateMustBeAnInstanceOfDate extends DomainError { + constructor(message = 'Revoke date 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); @@ -87,6 +99,8 @@ export { OrganizationLearnerNotBelongToOrganizationIdentityError, PasswordNotMatching, PasswordResetDemandNotFoundError, + RevokeDateMustBeAnInstanceOfDate, UserCantBeCreatedError, + UserIdIsRequiredError, UserShouldChangePasswordError, }; diff --git a/api/src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js b/api/src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js new file mode 100644 index 00000000000..f9090d875e5 --- /dev/null +++ b/api/src/identity-access-management/infrastructure/repositories/revoked-user-access.repository.js @@ -0,0 +1,29 @@ +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 { RevokeDateMustBeAnInstanceOfDate } from '../../domain/errors.js'; +import { RevokedUserAccess } from '../../domain/models/revoked-user-access.js'; + +const revokedUserAccessTemporaryStorage = temporaryStorage.withPrefix('revoked-user-access:'); +const revokedUserAccessLifespanMs = config.authentication.revokedUserAccessLifespanMs; + +export const saveForUser = async function (userId, revokeDate) { + if (!userId) { + throw new UserIdIsRequiredError(); + } + + if (!(revokeDate instanceof Date)) { + throw new RevokeDateMustBeAnInstanceOfDate(); + } + + await revokedUserAccessTemporaryStorage.save({ + key: userId, + value: Math.floor(revokeDate.getTime() / 1000), + expirationDelaySeconds: revokedUserAccessLifespanMs / 1000, + }); +}; + +export const findByUserId = async function (userId) { + const value = await revokedUserAccessTemporaryStorage.get(userId); + return new RevokedUserAccess(value); +}; diff --git a/api/src/shared/config.js b/api/src/shared/config.js index 8f09553de0e..f91850c4ddd 100644 --- a/api/src/shared/config.js +++ b/api/src/shared/config.js @@ -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', diff --git a/api/tests/identity-access-management/integration/infrastructure/repositories/revoked-user-access.repository.test.js b/api/tests/identity-access-management/integration/infrastructure/repositories/revoked-user-access.repository.test.js new file mode 100644 index 00000000000..cc389dac0ec --- /dev/null +++ b/api/tests/identity-access-management/integration/infrastructure/repositories/revoked-user-access.repository.test.js @@ -0,0 +1,45 @@ +import { RevokedUserAccess } from '../../../../../src/identity-access-management/domain/models/revoked-user-access.js'; +import * as 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 revokeDate = new Date(); + const revokedTimeStamp = Math.floor(revokeDate.getTime() / 1000); + + // when + await revokedUserAccessRepository.saveForUser(12345, revokeDate); + + // 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 revokeDate = new Date(); + const revokeTimeStamp = Math.floor(new Date().getTime() / 1000); + await revokedUserAccessRepository.saveForUser(12345, revokeDate); + + // when + const result = await revokedUserAccessRepository.findByUserId(12345); + + // then + expect(result).to.deep.equal({ + revokeTimeStamp: revokeTimeStamp, + }); + expect(result).to.be.instanceOf(RevokedUserAccess); + }); + }); +});