Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CP V7.1: Story #12364: improve saml documentation & #13112: upgrade external providers #1945

Merged
merged 2 commits into from
Jul 16, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ public IdentityProviderDto mapToIdentityProviderDto(
dto.getIdentifierAttribute(),
dto.getMaximumAuthenticationLifetime(),
dto.getAuthnRequestBinding(),
Objects.isNull(dto.getWantsAssertionsSigned()) ? false : dto.getWantsAssertionsSigned(),
Objects.isNull(dto.getAuthnRequestSigned()) ? false : dto.getAuthnRequestSigned(),
Objects.isNull(dto.getWantsAssertionsSigned()) ? true : dto.getWantsAssertionsSigned(),
Objects.isNull(dto.getAuthnRequestSigned()) ? true : dto.getAuthnRequestSigned(),
dto.isPropagateLogout(),
dto.isAutoProvisioningEnabled(),
dto.getClientId(),
Expand Down
4 changes: 2 additions & 2 deletions cas/cas-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ To set up Saml V2 authentication with vitamui, please follow these steps:
keytool -importcert -keystore environments/keystores/server/my_server/keystore_cas-server.jks -storepass xxx -alias orga-saml -file environments/certs/orga-SAML.crt
```
- In Vitamui interface, we create an external provider of SAML type with the following informations:
- Email attribute: keep it empty, except if the idp provide the email after authentication
- Email attribute: The attribute containing the user email sent by the idp after authentication, please check that the attribute 'nameid-format' to 'emailAddress' instead of 'transient'
- Upload the CAS keystore file (with the associated password) (keystore_cas-server.jks)
- Upload the IDP metadata file (e.g., FederationMetadata.xml)
- After provider creation, we need to download the metadata file of the vitamui provider (spmetadata.xml), and
Expand Down Expand Up @@ -347,7 +347,7 @@ To test the saml authentication, bellow an example with an external CAS on versi
- Login in into Vitamui as a superadmin, create a SAML provider with the following information:
- Pattern: email domain configured before: ```mydomainmail.fr```
- Type: ```SAML```
- Email attribute: empty (except if the cas send the email after authentication)
- Email attribute: The attribute containing the user email sent by the idp after authentication, please check that the attribute 'nameid-format' to 'emailAddress' instead of 'transient'
- CAS Keystore: for testing: you upload any keystore with the right passowrd.
- IDP Metadata: upload the file generated by running external CAS, from the path ```/etc/cas/saml/idp-metadata.xml```
- Assertions : false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto;
import org.pac4j.core.client.IndirectClient;

import java.util.Objects;

/**
* Pac4j client identity provider.
*
Expand All @@ -61,7 +63,7 @@ public Pac4jClientIdentityProviderDto(final IdentityProviderDto dto, final Indir
setIdentifierAttribute(dto.getIdentifierAttribute());
setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled());
setProtocoleType(dto.getProtocoleType());
setPropagateLogout(dto.isPropagateLogout());
setPropagateLogout(Objects.isNull(dto.isPropagateLogout()) ? false : dto.isPropagateLogout());

setKeystoreBase64(dto.getKeystoreBase64());
setKeystorePassword(dto.getKeystorePassword());
Expand All @@ -70,8 +72,10 @@ public Pac4jClientIdentityProviderDto(final IdentityProviderDto dto, final Indir
setSpMetadata(dto.getSpMetadata());
setMaximumAuthenticationLifetime(dto.getMaximumAuthenticationLifetime());
setAuthnRequestBinding(dto.getAuthnRequestBinding());
setWantsAssertionsSigned(dto.getWantsAssertionsSigned());
setAuthnRequestSigned(dto.getAuthnRequestSigned());
setWantsAssertionsSigned(
Objects.isNull(dto.getWantsAssertionsSigned()) ? true : dto.getWantsAssertionsSigned()
);
setAuthnRequestSigned(Objects.isNull(dto.getAuthnRequestSigned()) ? true : dto.getAuthnRequestSigned());

setClientId(dto.getClientId());
setClientSecret(dto.getClientSecret());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
print("START 61_update_existing_external_providers_models.js.j2");

db = db.getSiblingDB('iam');

db.providers.updateMany(
{
internal: false,
$or: [
{ protocoleType: "SAML" },
{ protocoleType: "OIDC" }
],
wantsAssertionsSigned: { $exists: false }
},
[
{
$set: {
wantsAssertionsSigned: {
$cond: {
if: { $eq: [{ $type: "$spMetadata" }, "missing"] },
then: false,
else: {
$cond: {
if: { $regexMatch: { input: "$spMetadata", regex: /WantAssertionsSigned="true"/ } },
then: true,
else: false
}
}
}
}
}
}
]
);



db.providers.updateMany(
{
internal: false,
$or: [
{ protocoleType: "SAML" },
{ protocoleType: "OIDC" }
],
authnRequestSigned: { $exists: false }
},
[
{
$set: {
authnRequestSigned: {
$cond: {
if: { $eq: [{ $type: "$spMetadata" }, "missing"] },
then: false,
else: {
$cond: {
if: { $regexMatch: { input: "$spMetadata", regex: /AuthnRequestsSigned="true"/ } },
then: true,
else: false
}
}
}
}
}
}
]
);


db.providers.updateMany(
{
internal: false,
$or: [
{ protocoleType: "SAML" },
{ protocoleType: "OIDC" }
],
propagateLogout: { $exists: false }
},
{
$set: { propagateLogout: false }
}
);

print("END 61_update_existing_external_providers_models.js.j2");
2 changes: 1 addition & 1 deletion docs/developeurs/vitamui-conf-dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ of our environment trusted by the remote environment, to do that, we follow thes
```

3. Download an external public certificate (external_pub.pem && external_key.pem) provided in this
path: https://webdav.dev.programmevitam.fr/webdav/Certificats_vitam/
path: https://rec.part.programmevitam.fr/
4. Generate a keystore, for Vitam context, with this certificate. (example password: customer-password-ks , example in P12 format):

```shell script
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('IdentityProviderService', () => {
let httpTestingController: HttpTestingController;
let identityProviderService: IdentityProviderService;
let identityProviders: any[];
let externalIdentityProviders: any[];
let keystore: File;
let idpMetadata: File;

Expand All @@ -68,6 +69,19 @@ describe('IdentityProviderService', () => {
idpMetadata,
},
];
externalIdentityProviders = [
{
id: '44',
customerId: '1234',
name: 'Test IDP',
internal: false,
keystorePassword: 'testpassword1234',
patterns: ['test.com', 'test.fr'],
enabled: true,
keystore,
idpMetadata,
},
];
const snackBarSpy = jasmine.createSpyObj(['open']);

TestBed.configureTestingModule({
Expand Down Expand Up @@ -108,6 +122,23 @@ describe('IdentityProviderService', () => {
req.flush(identityProviders[0]);
});

it('should call /fake-api/providers and display a succes message to asking to restart service', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.create(externalIdentityProviders[0]).subscribe((response: IdentityProvider) => {
expect(response).toEqual(externalIdentityProviders[0]);
expect(snackBar.open).toHaveBeenCalledTimes(1);
expect(snackBar.open).toHaveBeenCalledWith({
message: 'SHARED.SNACKBAR.PROVIDER_CREATE_RESTART_NEED',
translateParams: {
param1: externalIdentityProviders[0].name,
},
});
}, fail);
const req = httpTestingController.expectOne('/fake-api/providers');
expect(req.request.method).toEqual('POST');
req.flush(externalIdentityProviders[0]);
});

it('should display an error message', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.create(identityProviders[0]).subscribe(fail, () => {
Expand Down Expand Up @@ -153,6 +184,27 @@ describe('IdentityProviderService', () => {
req.flush(identityProviders[0]);
});

it('should call PATCH with specific message /fake-api/providers/44', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.updated.subscribe(
(provider: IdentityProvider) => expect(provider).toEqual(externalIdentityProviders[0]),
fail,
);
identityProviderService.patch(externalIdentityProviders[0]).subscribe((provider: IdentityProvider) => {
expect(provider).toEqual(externalIdentityProviders[0]);
expect(snackBar.open).toHaveBeenCalledTimes(1);
expect(snackBar.open).toHaveBeenCalledWith({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED',
translateParams: {
param1: externalIdentityProviders[0].name,
},
});
}, fail);
const req = httpTestingController.expectOne('/fake-api/providers/44');
expect(req.request.method).toEqual('PATCH');
expect(req.request.body).toEqual(externalIdentityProviders[0]);
req.flush(externalIdentityProviders[0]);
});
it('should display an error message', () => {
const snackBar = TestBed.inject(VitamUISnackBarService);
identityProviderService.patch(identityProviders[0]).subscribe(fail, () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ export class IdentityProviderService {
map((updatedIdp: IdentityProvider) => this.addSpMetadataUrl(updatedIdp)),
tap(
(newIDP: IdentityProvider) => {
let messageKey = newIDP.internal ? 'SHARED.SNACKBAR.PROVIDER_CREATE' : 'SHARED.SNACKBAR.PROVIDER_CREATE_RESTART_NEED';

this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_CREATE',
message: messageKey,
translateParams: {
param1: newIDP.name,
},
Expand All @@ -81,8 +83,9 @@ export class IdentityProviderService {
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
let messageKey = updatedIdp.internal ? 'SHARED.SNACKBAR.PROVIDER_UPDATE' : 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED';
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
message: messageKey,
translateParams: {
param1: updatedIdp.name,
},
Expand All @@ -102,8 +105,9 @@ export class IdentityProviderService {
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
let messageKey = updatedIdp.internal ? 'SHARED.SNACKBAR.PROVIDER_UPDATE' : 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED';
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
message: messageKey,
translateParams: {
param1: updatedIdp.name,
},
Expand All @@ -122,8 +126,9 @@ export class IdentityProviderService {
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
let messageKey = updatedIdp.internal ? 'SHARED.SNACKBAR.PROVIDER_UPDATE' : 'SHARED.SNACKBAR.PROVIDER_UPDATE_RESTART_NEED';
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
message: messageKey,
translateParams: {
param1: updatedIdp.name,
},
Expand All @@ -135,27 +140,6 @@ export class IdentityProviderService {
),
);
}

updateSpMetadataFile(id: string, spMetadata: File): Observable<IdentityProvider> {
return this.providerApi.patchProviderSpMetadata(id, spMetadata).pipe(
map((updatedIdp: IdentityProvider) => this.addSpMetadataUrl(updatedIdp)),
tap((updatedIdp: IdentityProvider) => this.updated.next(updatedIdp)),
tap(
(updatedIdp: IdentityProvider) => {
this.snackBarService.open({
message: 'SHARED.SNACKBAR.PROVIDER_UPDATE',
translateParams: {
param1: updatedIdp.name,
},
});
},
(error) => {
this.snackBarService.open({ message: error.error.message, translate: false });
},
),
);
}

getAll(customerId?: string): Observable<IdentityProvider[]> {
const criterionArray: Criterion[] = [];
if (customerId) {
Expand Down
2 changes: 2 additions & 0 deletions ui/ui-frontend/projects/identity/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
"CUSTOMER_CREATE_ERROR": "The organization could not be created, please contact an administrator",
"CUSTOMER_UPDATE": "The organization {{ param1 }} has been successfully modified",
"PROVIDER_CREATE": "IDP {{ param1 }} has been successfully created",
"PROVIDER_CREATE_RESTART_NEED": "IDP {{ param1 }} has been successfully created, taking these parameter changes into account requires a restart of the authentication system",
"PROVIDER_UPDATE": "IDP {{ param1 }} has been successfully modified",
"PROVIDER_UPDATE_RESTART_NEED": "IDP {{ param1 }} has been successfully modified,, taking these parameter changes into account requires a restart of the authentication system",
"OWNER_CREATE": "The owner {{ param1 }} has been successfully created",
"OWNER_UPDATE": "The owner {{ param1 }} has been successfully modified",
"SAFE_CREATE": "The safe {{ param1 }} of owner {{ param2 }} has been successfully created",
Expand Down
2 changes: 2 additions & 0 deletions ui/ui-frontend/projects/identity/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
"CUSTOMER_CREATE_ERROR": "L'organisation n'a pas pu être créé, veuillez contacter un administrateur",
"CUSTOMER_UPDATE": "L'organisation {{ param1 }} a bien été modifié",
"PROVIDER_CREATE": "L'IDP {{ param1 }} a bien été créé",
"PROVIDER_CREATE_RESTART_NEED": "L'IDP {{ param1 }} a bien été créé, la prise en compte du changement de ces paramètres nécessite un redémarrage du système d'authentification",
"PROVIDER_UPDATE": "L'IDP {{ param1 }} a bien été modifié",
"PROVIDER_UPDATE_RESTART_NEED": "L'IDP {{ param1 }} a bien été modifié, la prise en compte du changement de ces paramètres nécessite un redémarrage du système d'authentification",
"OWNER_CREATE": "Le propriétaire {{ param1 }} a bien été créé",
"OWNER_UPDATE": "Le propriétaire {{ param1 }} a bien été modifié",
"SAFE_CREATE": "Le coffre {{ param1 }} du propriétaire {{ param2 }} a bien été créé",
Expand Down