diff --git a/clients/authentication/agent-connect/strategy.ts b/clients/authentication/agent-connect/strategy.ts index 29270ed7b..da3396469 100644 --- a/clients/authentication/agent-connect/strategy.ts +++ b/clients/authentication/agent-connect/strategy.ts @@ -1,6 +1,6 @@ -import { BaseClient, Issuer, generators } from 'openid-client'; import { HttpForbiddenError } from '#clients/exceptions'; import { IReqWithSession } from '#utils/session/with-session'; +import { BaseClient, Issuer, generators } from 'openid-client'; let _client = undefined as BaseClient | undefined; diff --git a/models/user/agent.ts b/models/user/agent.ts index 8b5233cdc..6fde8a1e7 100644 --- a/models/user/agent.ts +++ b/models/user/agent.ts @@ -52,6 +52,7 @@ export type IAgentInfo = { userType: string; isPrestataire: boolean; isMCP: boolean; + belongToATeam: boolean; }; const extractDomain = (email: string) => { @@ -65,7 +66,7 @@ const extractDomain = (email: string) => { export const getAgent = async ( userInfo: IAgentConnectUserInfo ): Promise => { - const { scopes, userType } = await getAgentScopes(userInfo?.email); + const { scopes, userType, team } = await getAgentScopes(userInfo?.email); const domain = extractDomain(userInfo?.email || ''); const isPrestataire = isLikelyPrestataire(domain); @@ -90,5 +91,6 @@ export const getAgent = async ( userType, isPrestataire, isMCP, + belongToATeam: team, }; }; diff --git a/models/user/scopes.ts b/models/user/scopes.ts index 2d6bbb458..e268cff49 100644 --- a/models/user/scopes.ts +++ b/models/user/scopes.ts @@ -39,7 +39,7 @@ const agentScope = [ */ export const getAgentScopes = async ( userEmail: string -): Promise<{ scopes: IAgentScope[]; userType: string }> => { +): Promise<{ scopes: IAgentScope[]; userType: string; team: boolean }> => { const isTestAccount = userEmail === 'user@yopmail.com' && (process.env.NODE_ENV !== 'production' || @@ -49,6 +49,7 @@ export const getAgentScopes = async ( return { scopes: [...agentScope, 'conformite', 'beneficiaires'], userType: 'Super-agent connecté', + team: false, }; } @@ -58,5 +59,6 @@ export const getAgentScopes = async ( scopes: [...agentScope, ...additionnalScopes], userType: additionnalScopes.length > 0 ? 'Super-agent connecté' : 'Agent connecté', + team: additionnalScopes.length > 0, }; }; diff --git a/pages/api/auth/agent-connect/callback.ts b/pages/api/auth/agent-connect/callback.ts index 30f1b17f1..a6fea4436 100644 --- a/pages/api/auth/agent-connect/callback.ts +++ b/pages/api/auth/agent-connect/callback.ts @@ -1,7 +1,10 @@ import { agentConnectAuthenticate } from '#clients/authentication/agent-connect/strategy'; import { HttpForbiddenError } from '#clients/exceptions'; +import { clientUniteLegaleRechercheEntreprise } from '#clients/recherche-entreprise/siren'; +import { isServicePublic } from '#models/core/types'; import { Exception } from '#models/exceptions'; -import { getAgent } from '#models/user/agent'; +import { IAgentInfo, getAgent } from '#models/user/agent'; +import { extractSirenFromSiret } from '#utils/helpers'; import { logFatalErrorInSentry } from '#utils/sentry'; import { cleanPathFrom, getPathFrom, setAgentSession } from '#utils/session'; import withSession from '#utils/session/with-session'; @@ -10,6 +13,9 @@ export default withSession(async function callbackRoute(req, res) { try { const userInfo = await agentConnectAuthenticate(req); const agent = await getAgent(userInfo); + + await verifyAgentHabilitation(agent); + const session = req.session; await setAgentSession(agent, session); @@ -23,14 +29,62 @@ export default withSession(async function callbackRoute(req, res) { } } catch (e: any) { logFatalErrorInSentry(new AgentConnectionFailedException({ cause: e })); + if (e instanceof CouldAServicePublicException) { + return res.redirect('/connexion/habilitation/requise'); + } if (e instanceof HttpForbiddenError) { - res.redirect('/connexion/echec-authorisation-requise'); + res.redirect('/connexion/habilitation/refuse'); } else { res.redirect('/connexion/echec-connexion'); } } }); +const verifyAgentHabilitation = async (agent: IAgentInfo) => { + if (agent.belongToATeam) { + return; + } + + const { isMCP } = agent; + + // MCP should always return a siret + if (isMCP && !agent.siret) { + throw new HttpForbiddenError('MCP user must have a siret'); + } + + const siren = extractSirenFromSiret(agent.siret); + // This doesn't work because it uses { cookies } from 'next/headers'; + // It doesn't work in pages/api + // const uniteLegale = await getUniteLegaleFromSlug(siren, { + // page: 0, + // isBot: false, + // }); + const uniteLegale = await clientUniteLegaleRechercheEntreprise(siren, 0); + + if (isServicePublic(uniteLegale)) { + return; + } + + const couldBeServicePublic = + uniteLegale.natureJuridique.startsWith('4') || + uniteLegale.natureJuridique.startsWith('8'); + + if (couldBeServicePublic) { + throw new CouldAServicePublicException({}); + } + + throw new HttpForbiddenError('Organization in not a service public'); +}; + +class CouldAServicePublicException extends Exception { + constructor(args: { cause?: any }) { + super({ + name: 'CouldAServicePublicException', + ...args, + }); + } +} + export class AgentConnectionFailedException extends Exception { constructor(args: { cause?: any }) { super({ diff --git a/pages/connexion/echec-authorisation-requise.tsx b/pages/connexion/habilitation/refuse.tsx similarity index 51% rename from pages/connexion/echec-authorisation-requise.tsx rename to pages/connexion/habilitation/refuse.tsx index d1832c0b8..3db06bc0e 100644 --- a/pages/connexion/echec-authorisation-requise.tsx +++ b/pages/connexion/habilitation/refuse.tsx @@ -1,22 +1,27 @@ -import { ReactElement } from 'react'; import connexionRefusedPicture from '#components-ui/illustrations/connexion-refused'; import { LayoutConnexion } from '#components/layouts/layout-connexion'; import Meta from '#components/meta/meta-client'; -import constants from '#models/constants'; import { NextPageWithLayout } from 'pages/_app'; +import { ReactElement } from 'react'; const ConnexionFailure: NextPageWithLayout = () => ( <> - -

Vous n’êtes pas autorisé(e) à accéder à cette partie du site

-

Cet espace est réservé aux agents publics habilités.

+ +

L’accès à l’espace agent vous est refusé

+
Seuls peuvent accéder à l’espace agent public :
+

- Vous êtes agent(e) du service public et vous souhaiter accéder au - service :{' '} - contactez-nous. + Votre organisation n’est pas un service public et par conséquent, l’accès + à l’espace agent vous est refusé.

← Retourner au moteur de recherche diff --git a/pages/connexion/habilitation/requise.tsx b/pages/connexion/habilitation/requise.tsx new file mode 100644 index 000000000..bda48cfda --- /dev/null +++ b/pages/connexion/habilitation/requise.tsx @@ -0,0 +1,52 @@ +import connexionRefusedPicture from '#components-ui/illustrations/connexion-refused'; +import { LayoutConnexion } from '#components/layouts/layout-connexion'; +import Meta from '#components/meta/meta-client'; +import { NextPageWithLayout } from 'pages/_app'; +import { ReactElement } from 'react'; + +const HabilitationRequise: NextPageWithLayout = () => ( + <> + +

Vous n’êtes pas autorisé(e) à accéder à l’espace agent

+
Seuls peuvent accéder à l’espace agent public :
+ +

+ Votre organisation{' '} + ne fait pas partie de la liste des services publics. Vous + pouvez{' '} + + demander l’ajout de votre organisation + {' '} + à la liste. +

+

+ Si votre demande est acceptée, vous obtiendrez automatiquement l’accès à + l’espace agent. +

+ ← Retourner au moteur de recherche + +); + +HabilitationRequise.getLayout = function getLayout(page: ReactElement) { + return ( + {page} + ); +}; + +export default HabilitationRequise;