Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion cypress/e2e/join_with_free_email_domain/fixtures.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ VALUES
INSERT INTO organizations
(id, siret, created_at, updated_at)
VALUES
(1, '19750663700010', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
(1, '19750663700010', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(2, '11000201100044', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

INSERT INTO users_organizations
(user_id, organization_id, is_external, verification_type, has_been_greeted)
VALUES
(1, 1, false, 'domain', true);

INSERT INTO email_domains
(organization_id, domain, verification_type, verified_at)
VALUES
(2, 'finances.gouv.fr', 'verified', CURRENT_TIMESTAMP),
(2, 'ext.finances.gouv.fr', 'external', CURRENT_TIMESTAMP)
;
15 changes: 15 additions & 0 deletions cypress/e2e/join_with_free_email_domain/index.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,19 @@ describe("restrict access for", () => {
"L’accès à ce site est limité aux agentes et agents possédant une adresse email d’une administration publique.",
);
});

it("ChorusPro", function () {
cy.focused().clear().type("11000201100044");

cy.contains("Enregistrer").click();

cy.title().should("include", "Domains restreintes dans l'organisation -");
cy.contains("Accès restreint");
cy.contains(
"Seules les adresses finances.gouv.fr peuvent rejoindre l’organisation « Services de l'etat pour la facturation electronique - Destination etat via chorus pro ».",
);
cy.contains(
"Soyez sûrs d’utiliser le SIRET de l’organisation pour laquelle vous travaillez.",
);
});
});
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/identite/src/data/organization/domains-whitelist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//

export const DOMAINS_WHITELIST = new Map<string, string[]>([
[
"11000201100044", // ChorusPro siret
["finances.gouv.fr"], // domains to whitelist
],
]);
3 changes: 3 additions & 0 deletions packages/identite/src/data/organization/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//

export * from "./domains-whitelist.js";
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function markDomainAsVerifiedFactory({

return match(domain_verification_type)
.with(
...EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES,
...EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES.options,
async (approved_verification_type) => {
await assignUserVerificationTypeToDomain(organization_id, domain);
return markDomainAsApproved({
Expand All @@ -69,7 +69,7 @@ export function markDomainAsVerifiedFactory({
},
)
.with(
...EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES,
...EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES.options,
(rejected_verification_type) =>
markDomainAsRejected({
organization_id,
Expand All @@ -93,7 +93,7 @@ export function markDomainAsVerifiedFactory({
organization_id,
domain,
domain_verification_types: [
...EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES,
...EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES.options,
null,
],
});
Expand All @@ -118,7 +118,7 @@ export function markDomainAsVerifiedFactory({
domain,
domain_verification_types: [
null,
...EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES,
...EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES.options,
],
});
return addDomain({
Expand Down
3 changes: 3 additions & 0 deletions packages/identite/src/services/organization/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
//

export * from "./is-domain-allowed-for-organization.js";
export * from "./is-entreprise-unipersonnelle.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DOMAINS_WHITELIST } from "@gouvfr-lasuite/proconnect.identite/data/organization";

export function isDomainAllowedForOrganization(siret: string, domain: string) {
const whitelist = DOMAINS_WHITELIST.get(siret);

// Allow unknown siret to ignore whitelisting
if (!whitelist) return true;

return whitelist.includes(domain);
}
28 changes: 20 additions & 8 deletions packages/identite/src/types/email-domain.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
export const EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES = [
//

import { z } from "zod";

//

export const EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES = z.enum([
"official_contact",
"trackdechets_postal_mail",
"verified",
] as const;
]);

export type EmailDomainApprovedVerificationType = z.output<
typeof EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES
>;

//

export const EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES = [
export const EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES = z.enum([
"blacklisted", // unused
"external", // domain used by external employees (eg. ext.numerique.gouv.fr)
"refused",
] as const;
]);

export type EmailDomainApprovedVerificationType =
(typeof EMAIL_DOMAIN_APPROVED_VERIFICATION_TYPES)[number];
export type EmailDomainRejectedVerificationType = z.output<
typeof EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES
>;

export type EmailDomainRejectedVerificationType =
(typeof EMAIL_DOMAIN_REJECTED_VERIFICATION_TYPES)[number];
//

export type EmailDomainVerificationType =
| EmailDomainApprovedVerificationType
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"data": {
"siret": "11000201100044",
"siege_social": true,
"etat_administratif": "A",
"date_fermeture": null,
"enseigne": "DESTINATION ETAT VIA CHORUS PRO",
"activite_principale": {
"code": "84.11Z",
"nomenclature": "NAFRev2",
"libelle": "Administration publique générale"
},
"tranche_effectif_salarie": {
"de": 50,
"a": 99,
"code": "21",
"date_reference": "2022",
"intitule": "50 à 99 salariés"
},
"diffusable_commercialement": true,
"status_diffusion": "diffusible",
"date_creation": 815785200,
"unite_legale": {
"siren": "110002011",
"rna": null,
"siret_siege_social": "11000201100044",
"type": "personne_morale",
"personne_morale_attributs": {
"raison_sociale": "SERVICES DE L'ETAT POUR LA FACTURATION ELECTRONIQUE",
"sigle": null
},
"personne_physique_attributs": {
"pseudonyme": null,
"prenom_usuel": null,
"prenom_1": null,
"prenom_2": null,
"prenom_3": null,
"prenom_4": null,
"nom_usage": null,
"nom_naissance": null,
"sexe": null
},
"categorie_entreprise": "PME",
"status_diffusion": "diffusible",
"diffusable_commercialement": true,
"forme_juridique": {
"code": "7120",
"libelle": "Service central d'un ministère"
},
"activite_principale": {
"code": "84.11Z",
"nomenclature": "NAFRev2",
"libelle": "Administration publique générale"
},
"tranche_effectif_salarie": {
"de": 50,
"a": 99,
"code": "21",
"date_reference": "2022",
"intitule": "50 à 99 salariés"
},
"economie_sociale_et_solidaire": false,
"date_creation": 362095200,
"etat_administratif": "A"
},
"adresse": {
"status_diffusion": "diffusible",
"complement_adresse": null,
"numero_voie": "139",
"indice_repetition_voie": null,
"type_voie": "RUE",
"libelle_voie": "DE BERCY",
"code_postal": "75012",
"libelle_commune": "PARIS",
"libelle_commune_etranger": null,
"distribution_speciale": null,
"code_commune": "75112",
"code_cedex": null,
"libelle_cedex": null,
"code_pays_etranger": null,
"libelle_pays_etranger": null,
"acheminement_postal": {
"l1": "SERVICES DE L'ETAT POUR LA FACTURATION ELECTRONIQUE",
"l2": "",
"l3": "",
"l4": "139 RUE DE BERCY",
"l5": "",
"l6": "75012 PARIS",
"l7": "FRANCE"
}
}
},
"links": {
"unite_legale": "https://entreprise.api.gouv.fr/v3/insee/sirene/unites_legales/110002011"
},
"meta": {
"date_derniere_mise_a_jour": 1737241200,
"redirect_from_siret": null
}
}
55 changes: 40 additions & 15 deletions src/config/errors.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,47 @@
export class InvalidEmailError extends Error {
constructor(public didYouMean: string) {
super();
this.didYouMean = didYouMean;
constructor(
public didYouMean: string,
options?: ErrorOptions,
) {
super(`Did you mean "${didYouMean}" ?`, options);
this.name = "InvalidEmailError";
}
}

export class ForbiddenError extends Error {}

export class UnableToAutoJoinOrganizationError extends Error {
constructor(public moderationId: number) {
super();
this.moderationId = moderationId;
constructor(
public moderationId: number,
options?: ErrorOptions,
) {
super(`Linked to moderation ${moderationId}`, options);
this.name = "UnableToAutoJoinOrganizationError";
}
}

export class UserInOrganizationAlreadyError extends Error {}

export class UserAlreadyAskedToJoinOrganizationError extends Error {
constructor(public moderationId: number) {
super();
this.moderationId = moderationId;
constructor(
public moderationId: number,
options: ErrorOptions,
) {
super(
`Moderation ${moderationId} already asked to join organization`,
options,
);
this.name = "UserAlreadyAskedToJoinOrganizationError";
}
}

export class UserMustConfirmToJoinOrganizationError extends Error {
constructor(public organizationId: number) {
super();
this.organizationId = organizationId;
constructor(
public organizationId: number,
options?: ErrorOptions,
) {
super(`Organization ${organizationId} confirmation is required`, options);
this.name = "UserMustConfirmToJoinOrganizationError";
}
}

Expand All @@ -37,6 +52,16 @@ export class AccessRestrictedToPublicServiceEmailError extends Error {
}
}

export class DomainRestrictedError extends Error {
constructor(
public organizationId: number,
options?: ErrorOptions,
) {
super(`Organization ${organizationId} is domain restricted`, options);
this.name = "DomainRestrictedError";
}
}

export class InvalidCredentialsError extends Error {}

export class EmailUnavailableError extends Error {}
Expand Down Expand Up @@ -99,10 +124,10 @@ export class OidcError extends Error {
constructor(
public error: string,
public error_description?: string,
options?: ErrorOptions,
) {
super();
this.error = error;
this.error_description = error_description;
super(`${error}: ${error_description}`, options);
this.name = "OidcError";
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/controllers/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NextFunction, Request, Response } from "express";
import { AssertionError } from "node:assert";
import Provider, { errors } from "oidc-provider";
import { z } from "zod";
import { FEATURE_ALWAYS_RETURN_EIDAS1_FOR_ACR } from "../config/env";
Expand Down Expand Up @@ -131,6 +132,13 @@ export const interactionEndControllerFactory =
new OidcError(
"access_denied",
"none of the requested ACRs could be obtained",
{
cause: new AssertionError({
expected: prompt,
actual: currentAcr,
operator: "isAcrSatisfied",
}),
},
),
);
}
Expand Down
Loading