diff --git a/Jenkinsfile b/Jenkinsfile index 7506a2c1bbddfec8ecb841949403711b30ac38eb..6b02ffcc4a97ed0fd6a2eeacb05d5dd75368646e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -62,8 +62,8 @@ pipeline { sh 'sudo yum install -y gcc-c++ make' sh 'sudo yum remove -y nodejs' sh 'curl -sL https://rpm.nodesource.com/setup_16.x | sudo -E bash -' - // sh 'sudo yum install -y nodejs' - sh 'sudo yum install -y nodejs-16.9.0-1nodesource' + sh 'sudo yum install -y nodejs' + // sh 'sudo yum install -y nodejs-16.9.0-1nodesource' sh 'node -v' sh '/usr/bin/node -v' sh 'npm -v' @@ -80,10 +80,13 @@ pipeline { environment { PUPPETEER_DOWNLOAD_HOST="${env.SERVICE_NEXUS_URL}/repository/puppeteer-chrome/" JAVA_TOOL_OPTIONS="" + NODE_OPTIONS="--max_old_space_size=12288" } steps { - sh 'node -v' + sh 'node -v' sh 'npmrc default' + //sh 'export NODE_OPTIONS="--max-old-space-size=12288"' + // sh ''' // $MVN_COMMAND clean verify org.owasp:dependency-check-maven:aggregate -Pvitam -pl '!cots/vitamui-nginx,!cots/vitamui-mongod,!cots/vitamui-logstash,!cots/vitamui-mongo-express' $JAVA_TOOL_OPTIONS // ''' diff --git a/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dsl/VitamQueryHelper.java b/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dsl/VitamQueryHelper.java index 47939e2368ad0a62af07f5b41a672900042d7e48..0e02510f183b6c1563ce65a5337885ec0791ffac 100644 --- a/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dsl/VitamQueryHelper.java +++ b/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dsl/VitamQueryHelper.java @@ -98,50 +98,6 @@ public class VitamQueryHelper { } - public static void addDatesCriteriaToQuery(BooleanQuery mainQuery, final String criteria, - final List<String> searchValues) - throws InvalidCreateOperationException { - BooleanQuery subQueryAnd = and(); - String searchCriteria = ArchiveSearchConsts.START_DATE_CRITERIA.equals(criteria) ? - ArchiveSearchConsts.START_DATE : - (ArchiveSearchConsts.END_DATE_CRITERIA.equals(criteria) ? - ArchiveSearchConsts.END_DATE : null); - - LOGGER.info("The search criteria Date is {} ", searchCriteria); - if (!CollectionUtils.isEmpty(searchValues)) { - if (searchValues.size() > 2) { - throw new IllegalArgumentException("criteria date should not contains more than 2 values"); - } - if (searchValues.size() == 1) { - //Equal date - LocalDateTime beginDate = - LocalDateTime.parse(searchValues.get(0), ArchiveSearchConsts.ISO_FRENCH_FORMATER).withHour(0) - .withMinute(0).withSecond(0).withNano(0); - - subQueryAnd.add(VitamQueryHelper - .buildSubQueryByOperator(searchCriteria, ArchiveSearchConsts.ONLY_DATE_FRENCH_FORMATER.format(beginDate.plusDays(1)), - ArchiveSearchConsts.CriteriaOperators.EQ)); - - } else { - LocalDateTime firstDate = - LocalDateTime.parse(searchValues.get(0), ArchiveSearchConsts.ISO_FRENCH_FORMATER); - LocalDateTime secondDate = - LocalDateTime.parse(searchValues.get(1), ArchiveSearchConsts.ISO_FRENCH_FORMATER); - - subQueryAnd.add(VitamQueryHelper.buildSubQueryByOperator(searchCriteria, - ArchiveSearchConsts.ONLY_DATE_FRENCH_FORMATER - .format(firstDate.isAfter(secondDate) ? secondDate.plusDays(1) : firstDate.plusDays(1)), - ArchiveSearchConsts.CriteriaOperators.GTE)); - subQueryAnd.add(VitamQueryHelper.buildSubQueryByOperator(searchCriteria, - ArchiveSearchConsts.ONLY_DATE_FRENCH_FORMATER - .format(firstDate.isAfter(secondDate) ? firstDate.plusDays(1) : secondDate.plusDays(1)), - ArchiveSearchConsts.CriteriaOperators.LTE)); - - } - } - mainQuery.add(subQueryAnd); - } - public static Query buildSubQueryByOperator(String searchKey, String value, ArchiveSearchConsts.CriteriaOperators operator) throws InvalidCreateOperationException { diff --git a/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dto/CriteriaValue.java b/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dto/CriteriaValue.java index da1f3fb4f6b553f0bab23d82d7994b1165d7a12d..1391e5084d17734835a087b24e9bea2c6617d89b 100644 --- a/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dto/CriteriaValue.java +++ b/api/api-archive-search/archive-search-commons/src/main/java/fr/gouv/vitamui/archives/search/common/dto/CriteriaValue.java @@ -35,6 +35,7 @@ import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor public class CriteriaValue { + private String id; private String value; private String beginInterval; private String endInterval; diff --git a/api/api-archive-search/archive-search-external/pom.xml b/api/api-archive-search/archive-search-external/pom.xml index 415bf66e564a989f839b07f6b01fb625e6b09ad3..aa63aaac9e9eed7834db9381e09d102815cf169a 100644 --- a/api/api-archive-search/archive-search-external/pom.xml +++ b/api/api-archive-search/archive-search-external/pom.xml @@ -110,7 +110,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-archive-search/archive-search-internal/pom.xml b/api/api-archive-search/archive-search-internal/pom.xml index 8483ba75e3c54cf22d124ac4d2faa9a0f312ab5b..58fdf49b144e16ec19913f1244a8b379a38f9f55 100644 --- a/api/api-archive-search/archive-search-internal/pom.xml +++ b/api/api-archive-search/archive-search-internal/pom.xml @@ -111,7 +111,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <dependency> diff --git a/api/api-archive-search/archive-search-internal/src/main/java/fr/gouv/vitamui/archive/internal/server/service/ArchivesSearchFieldsQueryBuilderService.java b/api/api-archive-search/archive-search-internal/src/main/java/fr/gouv/vitamui/archive/internal/server/service/ArchivesSearchFieldsQueryBuilderService.java index 5299bf5c5c3ac78bb1132af3a2b95cd57f249439..2268440ec48ef8765801e95063d16594015c0d8c 100644 --- a/api/api-archive-search/archive-search-internal/src/main/java/fr/gouv/vitamui/archive/internal/server/service/ArchivesSearchFieldsQueryBuilderService.java +++ b/api/api-archive-search/archive-search-internal/src/main/java/fr/gouv/vitamui/archive/internal/server/service/ArchivesSearchFieldsQueryBuilderService.java @@ -86,7 +86,12 @@ public class ArchivesSearchFieldsQueryBuilderService implements IArchivesSearchA searchCriteria.getValues().stream().map(value -> value.getValue()).collect( Collectors.toList()), ArchiveSearchConsts.CriteriaOperators.valueOf(searchCriteria.getOperator()))); - } + } else if (ArchiveSearchConsts.CriteriaDataType.DATE.name().equals(searchCriteria.getDataType())) { + queryToFill.add(buildStartDateEndDateQuery(searchCriteria.getCriteria(), + searchCriteria.getValues().stream().map(value -> value.getValue()).collect( + Collectors.toList()), + ArchiveSearchConsts.CriteriaOperators.valueOf(searchCriteria.getOperator()))); + } else { String mappedCriteriaName = @@ -202,5 +207,32 @@ public class ArchivesSearchFieldsQueryBuilderService implements IArchivesSearchA return subQueryAnd; } + private Query buildStartDateEndDateQuery(String searchCriteria, final List<String> searchValues, + ArchiveSearchConsts.CriteriaOperators operator) + throws InvalidCreateOperationException { + BooleanQuery subQueryAnd = and(); + BooleanQuery subQueryOr = or(); + String criteria = ArchiveSearchConsts.START_DATE_CRITERIA.equals(searchCriteria) ? + ArchiveSearchConsts.START_DATE : + (ArchiveSearchConsts.END_DATE_CRITERIA.equals(searchCriteria) ? + ArchiveSearchConsts.END_DATE : null); + + LOGGER.info("The search criteria Date is {} ", criteria); + if (!CollectionUtils.isEmpty(searchValues)) { + for (String value : searchValues) { + LocalDateTime searchDate = + LocalDateTime.parse(value, ArchiveSearchConsts.ISO_FRENCH_FORMATER).withHour(0) + .withMinute(0).withSecond(0).withNano(0); + subQueryOr + .add(VitamQueryHelper.buildSubQueryByOperator(criteria, + ArchiveSearchConsts.ONLY_DATE_FRENCH_FORMATER.format(searchDate.plusDays(1)), operator)); + } + + subQueryAnd.add(subQueryOr); + } + + return subQueryAnd; + } + } diff --git a/api/api-iam/iam-commons/pom.xml b/api/api-iam/iam-commons/pom.xml index 44bba5b1e8b45f90eb755196288445ca15f527e5..a11dfbdffec9f3cf9b2b431f094b91e69d780bf8 100644 --- a/api/api-iam/iam-commons/pom.xml +++ b/api/api-iam/iam-commons/pom.xml @@ -40,7 +40,12 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-oidc</artifactId> <scope>provided</scope> </dependency> diff --git a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java index f37f2d16b776758515dfb998debc9c9e3d47f46a..db5beea3016a432c8678f038e7ceec982364866d 100644 --- a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java +++ b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java @@ -37,6 +37,7 @@ package fr.gouv.vitamui.iam.common.dto; import java.util.List; +import java.util.Map; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -67,6 +68,7 @@ public class IdentityProviderDto extends CustomerIdDto { */ private static final long serialVersionUID = 2372968720503585884L; + // Common data to all providers private String identifier; @NotNull @@ -85,6 +87,18 @@ public class IdentityProviderDto extends CustomerIdDto { @Size(min = 1) private List<String> patterns; + private boolean readonly; + + + // Common data to external providers (SAML + OIDC) + private String mailAttribute; + + private String identifierAttribute; + + private boolean autoProvisioningEnabled; + + + // SAML provider data private String keystoreBase64; private String keystorePassword; @@ -97,13 +111,25 @@ public class IdentityProviderDto extends CustomerIdDto { private Integer maximumAuthenticationLifetime; - private boolean readonly; + private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; - private String mailAttribute; - private String identifierAttribute; + // OIDC provider data + private String clientId; - private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; + private String clientSecret; - private boolean autoProvisioningEnabled; + private String discoveryUrl; + + private String scope; + + private String preferredJwsAlgorithm; + + private Map<String, String> customParams; + + private Boolean useState; + + private Boolean useNonce; + + private Boolean usePkce; } diff --git a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Saml2ClientBuilder.java b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Pac4jClientBuilder.java similarity index 65% rename from api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Saml2ClientBuilder.java rename to api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Pac4jClientBuilder.java index 95401e1a3c4cbda61c56f8829f6ede6da392a70e..037c24f04ad6b6871ec2bbc6883503bd6e110312 100644 --- a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Saml2ClientBuilder.java +++ b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Pac4jClientBuilder.java @@ -37,14 +37,17 @@ package fr.gouv.vitamui.iam.common.utils; import java.util.Base64; +import java.util.Map; import java.util.Optional; +import com.nimbusds.jose.JWSAlgorithm; import fr.gouv.vitamui.iam.common.enums.AuthnRequestBindingEnum; import org.apache.commons.lang3.StringUtils; import org.opensaml.saml.common.xml.SAMLConstants; -import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.core.client.IndirectClient; import org.pac4j.core.exception.TechnicalException; -import org.pac4j.core.util.CommonHelper; +import org.pac4j.oidc.client.OidcClient; +import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.config.SAML2Configuration; import org.springframework.beans.factory.annotation.Value; @@ -59,24 +62,29 @@ import lombok.Getter; import lombok.Setter; /** - * A pac4j SAML2 client builder. + * A pac4j client builder. * * */ @Getter @Setter -public class Saml2ClientBuilder { +public class Pac4jClientBuilder { @Value("${login.url}") @NotNull private String casLoginUrl; - public Optional<SAML2Client> buildSaml2Client(final IdentityProviderDto provider) { + public Optional<IndirectClient> buildClient(final IdentityProviderDto provider) { final String technicalName = provider.getTechnicalName(); final String keystoreBase64 = provider.getKeystoreBase64(); final String keystorePassword = provider.getKeystorePassword(); final String privateKeyPassword = provider.getPrivateKeyPassword(); final String idpMetadata = provider.getIdpMetadata(); + + final String clientId = provider.getClientId(); + final String clientSecret = provider.getClientSecret(); + final String discoveryUrl = provider.getDiscoveryUrl(); + try { if (technicalName != null && keystoreBase64 != null && keystorePassword != null && privateKeyPassword != null && idpMetadata != null @@ -105,14 +113,59 @@ public class Saml2ClientBuilder { } final SAML2Client saml2Client = new SAML2Client(saml2Config); - saml2Client.setName(technicalName); - final String callbackUrl = CommonHelper.addParameter(casLoginUrl, Pac4jConstants.DEFAULT_CLIENT_NAME_PARAMETER, technicalName); - saml2Client.setCallbackUrl(callbackUrl); + setCallbackUrl(saml2Client, technicalName); saml2Client.init(); return Optional.of(saml2Client); + + } else if (clientId != null && clientSecret != null && discoveryUrl != null) { + + final OidcConfiguration oidcConfiguration = new OidcConfiguration(); + oidcConfiguration.setClientId(clientId); + oidcConfiguration.setSecret(clientSecret); + oidcConfiguration.setDiscoveryURI(discoveryUrl); + + final String scope = provider.getScope(); + if (scope != null) { + oidcConfiguration.setScope(scope); + } else { + oidcConfiguration.setScope("openid"); + } + final String algo = provider.getPreferredJwsAlgorithm(); + if (algo != null) { + oidcConfiguration.setPreferredJwsAlgorithm(JWSAlgorithm.parse(algo)); + } + final Map<String, String> customParams = provider.getCustomParams(); + if (customParams != null) { + oidcConfiguration.setCustomParams(customParams); + } + final Boolean useState = provider.getUseState(); + if (useState != null) { + oidcConfiguration.setWithState(useState); + } else { + oidcConfiguration.setWithState(true); + } + final Boolean useNonce = provider.getUseNonce(); + if (useNonce != null) { + oidcConfiguration.setUseNonce(useNonce); + } else { + oidcConfiguration.setUseNonce(true); + } + final Boolean usePkce = provider.getUsePkce(); + if (usePkce != null) { + oidcConfiguration.setDisablePkce(!usePkce); + } else { + oidcConfiguration.setDisablePkce(true); + } + + final OidcClient oidcClient = new OidcClient(); + setCallbackUrl(oidcClient, technicalName); + + oidcClient.init(); + return Optional.of(oidcClient); + } - } catch(final TechnicalException e) { + } catch (final TechnicalException e) { if(e.getMessage().contains("Error loading keystore")) { throw new InvalidFormatException(e.getMessage(), ErrorsConstants.ERRORS_VALID_KEYSPWD); } else if(e.getMessage().contains("Can't obtain SP private key")) { @@ -123,4 +176,9 @@ public class Saml2ClientBuilder { } return Optional.empty(); } + + private void setCallbackUrl(final IndirectClient client, final String technicalName) { + client.setName(technicalName); + client.setCallbackUrl(casLoginUrl); + } } diff --git a/api/api-iam/iam-external/pom.xml b/api/api-iam/iam-external/pom.xml index 400b1f5845150eb61fb97b2cd7c0980a8eb854fc..7d21f0435b52c20f9ae28053b579e9c5bf33f658 100644 --- a/api/api-iam/iam-external/pom.xml +++ b/api/api-iam/iam-external/pom.xml @@ -117,7 +117,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-iam/iam-internal/pom.xml b/api/api-iam/iam-internal/pom.xml index 567707dcf9863cefa3602eb9e4c3b31ed3d6befe..697423e185e7a9a5b04f3777c4edc1f9c9b46381 100644 --- a/api/api-iam/iam-internal/pom.xml +++ b/api/api-iam/iam-internal/pom.xml @@ -131,7 +131,15 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> + </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-oidc</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> </dependency> <!-- Spring Data Mongo--> @@ -217,6 +225,18 @@ <groupId>com.zaxxer</groupId> <artifactId>*</artifactId> </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>*</artifactId> + </exclusion> </exclusions> </dependency> <dependency> @@ -239,6 +259,18 @@ <groupId>com.zaxxer</groupId> <artifactId>*</artifactId> </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>*</artifactId> + </exclusion> </exclusions> </dependency> diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java index 7fcd6724a984016bd8206b0a3979c2da44f01cb3..8e47a28c8dda6f4a4cd1af3d1fc19229350d0699 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java @@ -53,7 +53,7 @@ import fr.gouv.vitamui.commons.vitam.api.administration.IngestContractService; import fr.gouv.vitamui.commons.vitam.api.config.VitamAccessConfig; import fr.gouv.vitamui.commons.vitam.api.config.VitamAdministrationConfig; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import fr.gouv.vitamui.iam.internal.server.application.converter.ApplicationConverter; import fr.gouv.vitamui.iam.internal.server.application.dao.ApplicationRepository; import fr.gouv.vitamui.iam.internal.server.application.service.ApplicationInternalService; @@ -200,8 +200,8 @@ public class ApiIamServerConfig extends AbstractContextConfiguration { } @Bean - public Saml2ClientBuilder saml2ClientBuilder() { - return new Saml2ClientBuilder(); + public Pac4jClientBuilder pac4jClientBuilder() { + return new Pac4jClientBuilder(); } @Bean diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java index c3c29b7d8df60b9ec78f323d9e06edd5a0116b7e..8640001513593f3c5d39d65a8c359a12deb9f31d 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java @@ -79,6 +79,24 @@ public class IdentityProviderConverter implements Converter<IdentityProviderDto, public static final String AUTO_PROVISIONING_ENABLED_KEY = "Mise à jour automatique des utilisateurs"; + public static final String CLIENT_ID_KEY = "Identifiant client"; + + public static final String CLIENT_SECRET_KEY = "Secret client"; + + public static final String DISCOVERY_URL_KEY = "URL de découverte des metadata"; + + public static final String SCOPE_KEY = "Périmètre"; + + public static final String PREFERRED_JWS_ALGORITHM_KEY = "Algorithme JWS préféré"; + + public static final String CUSTOM_PARAMS_KEY = "Paramètres spécifiques"; + + public static final String USE_STATE_KEY = "Avec state"; + + public static final String USE_NONCE_KEY = "Avec nonce"; + + public static final String USE_PKCE_KEY = "Avec PKCE"; + private final SpMetadataGenerator spMetadataGenerator; public IdentityProviderConverter(final SpMetadataGenerator spMetadataGenerator) { @@ -98,12 +116,15 @@ public class IdentityProviderConverter implements Converter<IdentityProviderDto, logbookData.put(AUTHENTICATION_REQUEST_BINDING_KEY, String.valueOf(dto.getAuthnRequestBinding())); logbookData.put(MAXIMUM_AUTHENTICATION_LIFE_TIME, String.valueOf(dto.getMaximumAuthenticationLifetime())); logbookData.put(AUTO_PROVISIONING_ENABLED_KEY, String.valueOf(dto.isAutoProvisioningEnabled())); + logbookData.put(CLIENT_ID_KEY, String.valueOf(dto.getClientId())); + logbookData.put(DISCOVERY_URL_KEY, String.valueOf(dto.getDiscoveryUrl())); return ApiUtils.toJson(logbookData); } @Override public IdentityProvider convertDtoToEntity(final IdentityProviderDto dto) { final IdentityProvider provider = new IdentityProvider(); + // Common provider.setId(dto.getId()); provider.setIdentifier(dto.getIdentifier()); provider.setName(dto.getName()); @@ -111,20 +132,31 @@ public class IdentityProviderConverter implements Converter<IdentityProviderDto, provider.setInternal(dto.getInternal()); provider.setTechnicalName(dto.getTechnicalName()); convertPatterns(dto, provider); + provider.setReadonly(dto.isReadonly()); + provider.setCustomerId(dto.getCustomerId()); + // SAML + OIDC + provider.setMailAttribute(dto.getMailAttribute()); + provider.setIdentifierAttribute(dto.getIdentifierAttribute()); + provider.setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled()); + // SAML provider.setKeystoreBase64(dto.getKeystoreBase64()); provider.setKeystorePassword(dto.getKeystorePassword()); provider.setPrivateKeyPassword(dto.getKeystorePassword()); dto.setPrivateKeyPassword(dto.getKeystorePassword()); provider.setIdpMetadata(dto.getIdpMetadata()); - provider.setMailAttribute(dto.getMailAttribute()); - provider.setIdentifierAttribute(dto.getIdentifierAttribute()); provider.setAuthnRequestBinding(dto.getAuthnRequestBinding()); - final String spMetadata = spMetadataGenerator.generate(dto); - provider.setSpMetadata(spMetadata); - provider.setCustomerId(dto.getCustomerId()); + provider.setSpMetadata(spMetadataGenerator.generate(dto)); provider.setMaximumAuthenticationLifetime(dto.getMaximumAuthenticationLifetime()); - provider.setReadonly(dto.isReadonly()); - provider.setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled()); + // OIDC + provider.setClientId(dto.getClientId()); + provider.setClientSecret(dto.getClientSecret()); + provider.setDiscoveryUrl(dto.getDiscoveryUrl()); + provider.setScope(dto.getScope()); + provider.setPreferredJwsAlgorithm(dto.getPreferredJwsAlgorithm()); + provider.setCustomParams(dto.getCustomParams()); + provider.setUseState(dto.getUseState()); + provider.setUseNonce(dto.getUseNonce()); + provider.setUsePkce(dto.getUsePkce()); return provider; } diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java index 0f3854800bfb09f8a1b3f0c579fd9098fb1d687b..e94fe837888199d18a53092469f5b0c7bf264a89 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java @@ -37,6 +37,7 @@ package fr.gouv.vitamui.iam.internal.server.idp.domain; import java.util.List; +import java.util.Map; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -66,6 +67,7 @@ import lombok.ToString; @ToString(callSuper = true, exclude = { "keystoreBase64", "keystorePassword", "privateKeyPassword", "idpMetadata", "spMetadata" }) public class IdentityProvider extends CustomerIdDocument { + // Common data to all providers @NotNull @Length(min = 1, max = 12) private String identifier; @@ -87,6 +89,18 @@ public class IdentityProvider extends CustomerIdDocument { @Size(min = 1) private List<String> patterns; + private boolean readonly; + + + // Common data to external providers (SAML + OIDC) + private String mailAttribute; + + private String identifierAttribute; + + private boolean autoProvisioningEnabled; + + + // SAML provider data private String keystoreBase64; private String keystorePassword; @@ -99,13 +113,25 @@ public class IdentityProvider extends CustomerIdDocument { private Integer maximumAuthenticationLifetime; - private boolean readonly; + private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; - private String mailAttribute; - private String identifierAttribute; + // OIDC provider data + private String clientId; - private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; + private String clientSecret; - private boolean autoProvisioningEnabled; + private String discoveryUrl; + + private String scope; + + private String preferredJwsAlgorithm; + + private Map<String, String> customParams; + + private Boolean useState; + + private Boolean useNonce; + + private Boolean usePkce; } diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java index ae1cfd189c1aee865e716a790c8df1c516864fd1..aa48afc913bd9d69e4b8c05a2360aacc82d774ea 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java @@ -247,6 +247,18 @@ public class IdentityProviderInternalService extends VitamUICrudService<Identity entity.setPatterns(patterns); } + break; + case "mailAttribute" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.MAIL_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); + entity.setMailAttribute(CastUtils.toString(entry.getValue())); + break; + case "identifierAttribute" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.IDENTIFIER_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); + entity.setIdentifierAttribute(CastUtils.toString(entry.getValue())); + break; + case "autoProvisioningEnabled" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.AUTO_PROVISIONING_ENABLED_KEY, entity.isAutoProvisioningEnabled(), entry.getValue())); + entity.setAutoProvisioningEnabled(CastUtils.toBoolean(entry.getValue())); break; case "keystoreBase64" : logbooks.add(new EventDiffDto(IdentityProviderConverter.KEYSTORE_BASE_64_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); @@ -271,23 +283,48 @@ public class IdentityProviderInternalService extends VitamUICrudService<Identity maximumAuthenticationLifeTime)); entity.setMaximumAuthenticationLifetime(maximumAuthenticationLifeTime); break; - case "mailAttribute" : - logbooks.add(new EventDiffDto(IdentityProviderConverter.MAIL_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); - entity.setMailAttribute(CastUtils.toString(entry.getValue())); - break; - case "identifierAttribute" : - logbooks.add(new EventDiffDto(IdentityProviderConverter.IDENTIFIER_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); - entity.setIdentifierAttribute(CastUtils.toString(entry.getValue())); - break; case "authnRequestBinding" : final String authnRequestBindingAsString = CastUtils.toString(entry.getValue()); final AuthnRequestBindingEnum newAuthnRequestBinding = EnumUtils.stringToEnum(AuthnRequestBindingEnum.class, authnRequestBindingAsString); logbooks.add(new EventDiffDto(IdentityProviderConverter.AUTHENTICATION_REQUEST_BINDING_KEY, entity.getAuthnRequestBinding(), newAuthnRequestBinding)); entity.setAuthnRequestBinding(newAuthnRequestBinding); break; - case "autoProvisioningEnabled" : - logbooks.add(new EventDiffDto(IdentityProviderConverter.AUTO_PROVISIONING_ENABLED_KEY, entity.isAutoProvisioningEnabled(), entry.getValue())); - entity.setAutoProvisioningEnabled(CastUtils.toBoolean(entry.getValue())); + case "clientId" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.CLIENT_ID_KEY, entity.getClientId(), entry.getValue())); + entity.setClientId(CastUtils.toString(entry.getValue())); + break; + case "clientSecret" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.CLIENT_SECRET_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); + entity.setClientSecret(CastUtils.toString(entry.getValue())); + break; + case "discoveryUrl" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.DISCOVERY_URL_KEY, entity.getDiscoveryUrl(), entry.getValue())); + entity.setDiscoveryUrl(CastUtils.toString(entry.getValue())); + break; + case "scope" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.SCOPE_KEY, entity.getScope(), entry.getValue())); + entity.setScope(CastUtils.toString(entry.getValue())); + break; + case "preferredJwsAlgorithm" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.PREFERRED_JWS_ALGORITHM_KEY, entity.getPreferredJwsAlgorithm(), entry.getValue())); + entity.setPreferredJwsAlgorithm(CastUtils.toString(entry.getValue())); + break; + case "customParams" : + Map<String, String> customParams = CastUtils.toMap(entry.getValue()); + logbooks.add(new EventDiffDto(IdentityProviderConverter.CUSTOM_PARAMS_KEY, entity.getCustomParams(), customParams)); + entity.setCustomParams(customParams); + break; + case "useState" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.USE_STATE_KEY, entity.getUseState(), entry.getValue())); + entity.setUseState(CastUtils.toBoolean(entry.getValue())); + break; + case "useNonce" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.USE_NONCE_KEY, entity.getUseNonce(), entry.getValue())); + entity.setUseNonce(CastUtils.toBoolean(entry.getValue())); + break; + case "usePkce" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.USE_PKCE_KEY, entity.getUsePkce(), entry.getValue())); + entity.setUsePkce(CastUtils.toBoolean(entry.getValue())); break; default : throw new IllegalArgumentException("Unable to patch provider " + entity.getId() + ": key " + entry.getKey() + " is not allowed"); diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java index 319d2a36459e7bbd5153323b7e2b9ac15bf9883b..319f0bb480e16aa525725e050e65653bcce372bf 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java @@ -36,14 +36,12 @@ */ package fr.gouv.vitamui.iam.internal.server.idp.service; -import java.io.IOException; -import java.util.Optional; - +import org.pac4j.core.client.IndirectClient; import org.pac4j.saml.client.SAML2Client; import org.springframework.beans.factory.annotation.Autowired; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; /** * A service provider metadata generator. @@ -53,25 +51,21 @@ import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; public class SpMetadataGenerator { @Autowired - private Saml2ClientBuilder saml2ClientBuilder; + private Pac4jClientBuilder pac4jClientBuilder; public String generate(final IdentityProviderDto provider) { - final Optional<SAML2Client> optionalSaml2Client = saml2ClientBuilder.buildSaml2Client(provider); - if (optionalSaml2Client.isPresent()) { - try { - return optionalSaml2Client.get().getServiceProviderMetadataResolver().getMetadata(); - } catch (final IOException e) { - throw new RuntimeException(e); - } + final IndirectClient client = pac4jClientBuilder.buildClient(provider).orElse(null); + if (client instanceof SAML2Client) { + return ((SAML2Client) client).getServiceProviderMetadataResolver().getMetadata(); } return null; } - public Saml2ClientBuilder getSaml2ClientBuilder() { - return saml2ClientBuilder; + public Pac4jClientBuilder getPac4jClientBuilder() { + return pac4jClientBuilder; } - public void setSaml2ClientBuilder(final Saml2ClientBuilder saml2ClientBuilder) { - this.saml2ClientBuilder = saml2ClientBuilder; + public void setPac4jClientBuilder(final Pac4jClientBuilder pac4jClientBuilder) { + this.pac4jClientBuilder = pac4jClientBuilder; } } diff --git a/api/api-iam/iam-internal/src/main/resources/spring.properties b/api/api-iam/iam-internal/src/main/resources/spring.properties new file mode 100644 index 0000000000000000000000000000000000000000..da97d1db45d4b791eb6e6cb64d083f3695641f62 --- /dev/null +++ b/api/api-iam/iam-internal/src/main/resources/spring.properties @@ -0,0 +1 @@ +spring.index.ignore=true diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java index 0261f40eeec230e2908902b97d96ca9549819f6f..22ed9180ca96d12faacc56c6dd8caaf4df783368 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java @@ -3,6 +3,8 @@ package fr.gouv.vitamui.iam.internal.server.idp.converter; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; +import java.util.List; +import java.util.Map; import fr.gouv.vitamui.iam.common.enums.AuthnRequestBindingEnum; import org.junit.Test; @@ -18,6 +20,32 @@ import fr.gouv.vitamui.iam.internal.server.idp.service.SpMetadataGenerator; public class IdentityProviderConverterTest { + private static final String CUSTOMER_ID = "customerId"; + private static final boolean ENABLED = true; + private static final String ID = "id"; + private static final String IDENTIFIER = "identifier"; + private static final String IDP_METADATA = "idpMetadata"; + private static final boolean INTERNAL = true; + private static final String KEYSTORE_BASE64 = "keystoreBase64"; + private static final String KEYSTORE_PASSWORD = "keystorePassword"; + private static final int MAXIMUM_AUTHENTICATION_LIFETIME = 5; + private static final String NAME = "name"; + private static final List<String> PATTERNS = Arrays.asList("@test.com"); + private static final String PRIVATE_KEY_PASSWORD = "privateKeyPassword"; + private static final String SP_METADATA = "spMetadata"; + private static final String TECHNICAL_NAME = "technicalname"; + private static final String MAIL_ATTRIBUTE = "mailAttribute"; + private static final String IDENTIFIER_ATTRIBUTE = "identifierAttribute"; + private static final AuthnRequestBindingEnum AUTHN_REQUEST_BINDING = AuthnRequestBindingEnum.POST; + private static final String SECRET = "secret"; + private static final String DISCOVERY_URL = "http://discoveryurl"; + private static final String SCOPE = "openid"; + private static final String PREFERRED_JWS_ALGORITHM = "HS256"; + private static final Map CUSTOM_PARAMS = Map.of("prompt", "none"); + private static final boolean USE_STATE = true; + private static final boolean USE_NONCE = true; + private static final boolean USE_PKCE = true; + private final SpMetadataGenerator spMetadataGenerator = Mockito.mock(SpMetadataGenerator.class); private final IdentityProviderConverter converter = new IdentityProviderConverter(spMetadataGenerator); @@ -25,23 +53,32 @@ public class IdentityProviderConverterTest { @Test public void testConvertEntityToDto() { IdentityProvider idp = new IdentityProvider(); - idp.setCustomerId("customerId"); - idp.setEnabled(true); - idp.setId("id"); - idp.setIdentifier("identifier"); - idp.setIdpMetadata("idpMetadata"); - idp.setInternal(true); - idp.setKeystoreBase64("keystoreBase64"); - idp.setKeystorePassword("keystorePassword"); - idp.setMaximumAuthenticationLifetime(5); - idp.setName("name"); - idp.setPatterns(Arrays.asList("@test.com")); - idp.setPrivateKeyPassword("privateKeyPassword"); - idp.setSpMetadata("spMetadata"); - idp.setTechnicalName("technicalname"); - idp.setMailAttribute("mailAttribute"); - idp.setIdentifierAttribute("identifierAttribute"); - idp.setAuthnRequestBinding(AuthnRequestBindingEnum.POST); + idp.setCustomerId(CUSTOMER_ID); + idp.setEnabled(ENABLED); + idp.setId(ID); + idp.setIdentifier(IDENTIFIER); + idp.setIdpMetadata(IDP_METADATA); + idp.setInternal(INTERNAL); + idp.setKeystoreBase64(KEYSTORE_BASE64); + idp.setKeystorePassword(KEYSTORE_PASSWORD); + idp.setMaximumAuthenticationLifetime(MAXIMUM_AUTHENTICATION_LIFETIME); + idp.setName(NAME); + idp.setPatterns(PATTERNS); + idp.setPrivateKeyPassword(PRIVATE_KEY_PASSWORD); + idp.setSpMetadata(SP_METADATA); + idp.setTechnicalName(TECHNICAL_NAME); + idp.setMailAttribute(MAIL_ATTRIBUTE); + idp.setIdentifierAttribute(IDENTIFIER_ATTRIBUTE); + idp.setAuthnRequestBinding(AUTHN_REQUEST_BINDING); + idp.setClientId(ID); + idp.setClientSecret(SECRET); + idp.setDiscoveryUrl(DISCOVERY_URL); + idp.setScope(SCOPE); + idp.setPreferredJwsAlgorithm(PREFERRED_JWS_ALGORITHM); + idp.setCustomParams(CUSTOM_PARAMS); + idp.setUseState(USE_STATE); + idp.setUseNonce(USE_NONCE); + idp.setUsePkce(USE_PKCE); IdentityProviderDto res = converter.convertEntityToDto(idp); assertThat(res).isEqualToIgnoringGivenFields(idp); } @@ -49,23 +86,32 @@ public class IdentityProviderConverterTest { @Test public void testConvertDtoToEntity() { IdentityProviderDto idp = new IdentityProviderDto(); - idp.setCustomerId("customerId"); - idp.setEnabled(true); - idp.setId("id"); - idp.setIdentifier("identifier"); - idp.setIdpMetadata("idpMetadata"); - idp.setInternal(true); - idp.setKeystoreBase64("keystoreBase64"); - idp.setKeystorePassword("keystorePassword"); - idp.setMaximumAuthenticationLifetime(5); - idp.setName("name"); - idp.setPatterns(Arrays.asList("@test.com")); - idp.setPrivateKeyPassword("privateKeyPassword"); - idp.setSpMetadata("spMetadata"); - idp.setTechnicalName("technicalname"); - idp.setMailAttribute("mailAttribute"); - idp.setIdentifierAttribute("identifierAttribute"); - idp.setAuthnRequestBinding(AuthnRequestBindingEnum.POST); + idp.setCustomerId(CUSTOMER_ID); + idp.setEnabled(ENABLED); + idp.setId(ID); + idp.setIdentifier(IDENTIFIER); + idp.setIdpMetadata(IDP_METADATA); + idp.setInternal(INTERNAL); + idp.setKeystoreBase64(KEYSTORE_BASE64); + idp.setKeystorePassword(KEYSTORE_PASSWORD); + idp.setMaximumAuthenticationLifetime(MAXIMUM_AUTHENTICATION_LIFETIME); + idp.setName(NAME); + idp.setPatterns(PATTERNS); + idp.setPrivateKeyPassword(PRIVATE_KEY_PASSWORD); + idp.setSpMetadata(SP_METADATA); + idp.setTechnicalName(TECHNICAL_NAME); + idp.setMailAttribute(MAIL_ATTRIBUTE); + idp.setIdentifierAttribute(IDENTIFIER_ATTRIBUTE); + idp.setAuthnRequestBinding(AUTHN_REQUEST_BINDING); + idp.setClientId(ID); + idp.setClientSecret(SECRET); + idp.setDiscoveryUrl(DISCOVERY_URL); + idp.setScope(SCOPE); + idp.setPreferredJwsAlgorithm(PREFERRED_JWS_ALGORITHM); + idp.setCustomParams(CUSTOM_PARAMS); + idp.setUseState(USE_STATE); + idp.setUseNonce(USE_NONCE); + idp.setUsePkce(USE_PKCE); IdentityProvider res = converter.convertDtoToEntity(idp); assertThat(res).isEqualToIgnoringGivenFields(idp, "spMetadata"); } @@ -73,23 +119,32 @@ public class IdentityProviderConverterTest { @Test public void testConvertToLogbook() throws InvalidParseOperationException { IdentityProviderDto idp = new IdentityProviderDto(); - idp.setCustomerId("customerId"); - idp.setEnabled(true); - idp.setId("id"); - idp.setIdentifier("identifier"); - idp.setIdpMetadata("idpMetadata"); - idp.setInternal(true); - idp.setKeystoreBase64("keystoreBase64"); - idp.setKeystorePassword("keystorePassword"); - idp.setMaximumAuthenticationLifetime(5); - idp.setName("name"); - idp.setPatterns(Arrays.asList("@test.com")); - idp.setPrivateKeyPassword("privateKeyPassword"); - idp.setSpMetadata("spMetadata"); - idp.setTechnicalName("technicalname"); - idp.setMailAttribute("mailAttribute"); - idp.setIdentifierAttribute("identifierAttribute"); - idp.setAuthnRequestBinding(AuthnRequestBindingEnum.POST); + idp.setCustomerId(CUSTOMER_ID); + idp.setEnabled(ENABLED); + idp.setId(ID); + idp.setIdentifier(IDENTIFIER); + idp.setIdpMetadata(IDP_METADATA); + idp.setInternal(INTERNAL); + idp.setKeystoreBase64(KEYSTORE_BASE64); + idp.setKeystorePassword(KEYSTORE_PASSWORD); + idp.setMaximumAuthenticationLifetime(MAXIMUM_AUTHENTICATION_LIFETIME); + idp.setName(NAME); + idp.setPatterns(PATTERNS); + idp.setPrivateKeyPassword(PRIVATE_KEY_PASSWORD); + idp.setSpMetadata(SP_METADATA); + idp.setTechnicalName(TECHNICAL_NAME); + idp.setMailAttribute(MAIL_ATTRIBUTE); + idp.setIdentifierAttribute(IDENTIFIER_ATTRIBUTE); + idp.setAuthnRequestBinding(AUTHN_REQUEST_BINDING); + idp.setClientId(ID); + idp.setClientSecret(SECRET); + idp.setDiscoveryUrl(DISCOVERY_URL); + idp.setScope(SCOPE); + idp.setPreferredJwsAlgorithm(PREFERRED_JWS_ALGORITHM); + idp.setCustomParams(CUSTOM_PARAMS); + idp.setUseState(USE_STATE); + idp.setUseNonce(USE_NONCE); + idp.setUsePkce(USE_PKCE); String json = converter.convertToLogbook(idp); @@ -103,6 +158,7 @@ public class IdentityProviderConverterTest { assertThat(jsonNode.get(IdentityProviderConverter.MAIL_ATTRIBUTE_KEY)).isNotNull(); assertThat(jsonNode.get(IdentityProviderConverter.IDENTIFIER_ATTRIBUTE_KEY)).isNotNull(); assertThat(jsonNode.get(IdentityProviderConverter.AUTHENTICATION_REQUEST_BINDING_KEY)).isNotNull(); + assertThat(jsonNode.get(IdentityProviderConverter.CLIENT_ID_KEY)).isNotNull(); + assertThat(jsonNode.get(IdentityProviderConverter.DISCOVERY_URL_KEY)).isNotNull(); } - } diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java index 28f05d4ec675b70af6881ec17ec039976ff8e4a7..4958a3445af67311b383d30efc35c8999b2d512e 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java @@ -140,10 +140,18 @@ public class IdentityProviderInternalIntegrationTest extends AbstractLogbookInte service.patch(partialDto); partialDto.remove("autoProvisioningEnabled"); + partialDto.put("clientId", "1"); + service.patch(partialDto); + partialDto.remove("clientId"); + + partialDto.put("discoveryUrl", "http://url"); + service.patch(partialDto); + partialDto.remove("discoveryUrl"); + final Criteria criteria = Criteria.where("obId").is(dto.getIdentifier()).and("obIdReq").is(MongoDbCollections.PROVIDERS).and("evType") .is(EventType.EXT_VITAMUI_UPDATE_IDP); final Collection<Event> events = eventRepository.findAll(Query.query(criteria)); - assertThat(events).hasSize(6); + assertThat(events).hasSize(8); } private IdentityProviderDto createIdp() { diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java index 380432ce9c35dd0b73e13521292b9c33192cba16..60e617c524a9d55b44937e0d211866480509255e 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java @@ -305,6 +305,17 @@ public class IdentityProviderInternalServiceTest extends AbstractServerIdentityB partialDto.put("keystorePassword", "keyspwd"); partialDto.put("idpMetadata", "<xml></xml>"); partialDto.put("autoProvisioningEnabled", true); + partialDto.put("clientId", "1"); + partialDto.put("clientSecret", "secret"); + partialDto.put("discoveryUrl", "http://url"); + partialDto.put("scope", "openid"); + partialDto.put("preferredJwsAlgorithm", "HS256"); + Map<String, String> customParams = new HashMap<>(); + customParams.put("prompt", "login"); + partialDto.put("customParams", customParams); + partialDto.put("useState", true); + partialDto.put("useNonce", true); + partialDto.put("usePkce", true); service.processPatch(idp, partialDto); assertThat(idp.getName()).isEqualTo("nameTest"); @@ -315,6 +326,15 @@ public class IdentityProviderInternalServiceTest extends AbstractServerIdentityB assertThat(idp.getIdpMetadata()).isEqualTo("<xml></xml>"); assertThat(idp.getPatterns()).isEqualTo(Arrays.asList(".*@vitamui.com", ".*@vitamui.com")); assertThat(idp.isAutoProvisioningEnabled()).isTrue(); + assertThat(idp.getClientId()).isEqualTo("1"); + assertThat(idp.getClientSecret()).isEqualTo("secret"); + assertThat(idp.getDiscoveryUrl()).isEqualTo("http://url"); + assertThat(idp.getScope()).isEqualTo("openid"); + assertThat(idp.getPreferredJwsAlgorithm()).isEqualTo("HS256"); + assertThat(idp.getCustomParams()).isEqualTo(customParams); + assertThat(idp.getUseState()).isTrue(); + assertThat(idp.getUseNonce()).isTrue(); + assertThat(idp.getUsePkce()).isTrue(); } @Test diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java index 1cda03a9429f5009b2eb225a0d1bdfca00b08787..378b6740dd64867e5827a13781a8308e17ec3f5a 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java @@ -10,10 +10,10 @@ import org.springframework.core.io.ClassPathResource; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderBuilder; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; /** - * Tests {@link Saml2ClientBuilder} and {@link SpMetadataGenerator}. + * Tests {@link Pac4jClientBuilder} and {@link SpMetadataGenerator}. * * */ @@ -21,17 +21,17 @@ public final class SpMetadataGeneratorTest { private static final String CAS_URL = "http://cas/login"; - private Saml2ClientBuilder builder; + private Pac4jClientBuilder builder; @Before public void setUp() { - builder = new Saml2ClientBuilder(); + builder = new Pac4jClientBuilder(); builder.setCasLoginUrl(CAS_URL); } @Test public void testSimpleProvider() { - assertFalse(builder.buildSaml2Client(new IdentityProviderDto()).isPresent()); + assertFalse(builder.buildClient(new IdentityProviderDto()).isPresent()); } @Test @@ -40,7 +40,7 @@ public final class SpMetadataGeneratorTest { new ClassPathResource("test-idp/sp-test-keystore.jks"), "password", "password", new ClassPathResource("test-idp/idp-test-metadata.xml"), "clientId", false, "mailAttribute", "identifierAttribute", AuthnRequestBindingEnum.POST, false).build(); final SpMetadataGenerator generator = new SpMetadataGenerator(); - generator.setSaml2ClientBuilder(builder); + generator.setPac4jClientBuilder(builder); final String metadata = generator.generate(provider); assertTrue(metadata.contains("entityID=\"http://cas/login/idp0\"")); assertTrue(metadata.contains( diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java index f85f19632d6d6bb8953c7f78c97f2276b6ff9d9a..9c13a493c9f9c466af8d8e1f950f3855de11c48b 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java @@ -256,10 +256,11 @@ public final class CasControllerTest extends AbstractServerIdentityBuilder { } @Test - public void testLoginKoNullPasswords() { + public void testLoginKoBlankPasswords() { user.setType(UserTypeEnum.NOMINATIVE); final LoginRequestDto request = new LoginRequestDto(); request.setUsername(EMAIL); + request.setPassword(""); try { controller.login(request); fail("should fail"); diff --git a/api/api-iam/iam-security/pom.xml b/api/api-iam/iam-security/pom.xml index 3b8d726477c761ea19d73291e6c87b150539552c..c57ba5373607346a25b56bfecb4011b5daafcd00 100644 --- a/api/api-iam/iam-security/pom.xml +++ b/api/api-iam/iam-security/pom.xml @@ -51,7 +51,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> <scope>provided</scope> </dependency> diff --git a/api/api-ingest/ingest-internal/src/main/resources/spring.properties b/api/api-ingest/ingest-internal/src/main/resources/spring.properties new file mode 100644 index 0000000000000000000000000000000000000000..da97d1db45d4b791eb6e6cb64d083f3695641f62 --- /dev/null +++ b/api/api-ingest/ingest-internal/src/main/resources/spring.properties @@ -0,0 +1 @@ +spring.index.ignore=true diff --git a/api/api-referential/referential-commons/pom.xml b/api/api-referential/referential-commons/pom.xml index 105ebf3cf8085e6693cfbd855230fd6b84b768e4..20100c25b4be01ca8425a4a078b07f3d8528a884 100644 --- a/api/api-referential/referential-commons/pom.xml +++ b/api/api-referential/referential-commons/pom.xml @@ -35,7 +35,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> <scope>provided</scope> </dependency> diff --git a/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dsl/VitamQueryHelper.java b/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dsl/VitamQueryHelper.java index 75139f0be29db6b89939971d442c4814e881735d..235a40aa8f98d650b73d74535be3e0a9c1807c92 100644 --- a/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dsl/VitamQueryHelper.java +++ b/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dsl/VitamQueryHelper.java @@ -50,8 +50,9 @@ import fr.gouv.vitamui.commons.api.domain.DirectionDto; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -64,7 +65,6 @@ import static fr.gouv.vitam.common.database.builder.query.QueryHelper.eq; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.gt; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.in; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.lt; -import static fr.gouv.vitam.common.database.builder.query.QueryHelper.lte; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.matchPhrasePrefix; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.ne; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.nin; @@ -105,13 +105,14 @@ public class VitamQueryHelper { private static final String ACQUISITION_INFORMATIONS = "acquisitionInformations"; private static final String EVENTS_OPTYPE = "Events.OpType"; private static final String ELIMINATION = "elimination"; - private static final String TRANSFER = "transfer"; - private static final String PRESERVATION = "preservation"; + private static final String TRANSFER_REPLY = "transfer_reply"; /* */ private static final String DATE_FORMAT = "dd/MM/yyyy"; public static final Collection<String> staticAcquisitionInformations = List.of("Versement", "Protocole", "Achat", - "Copie", "Dation", "Dépôt", "Dévolution", "Don", "Legs", "Réintégration", "Autres", "Non renseigné"); + "Copie", "Dation", "Dépôt", "Dévolution", "Don", "Legs", "Réintégration", "Autres", + VitamQueryHelper.ACQUISITION_INFORMATION_NON_RENSEIGNE); + public static final String ACQUISITION_INFORMATION_NON_RENSEIGNE = "Non renseigné"; private VitamQueryHelper() { throw new UnsupportedOperationException("Utility class"); @@ -215,8 +216,7 @@ public class VitamQueryHelper { addAcquisitionInformationsToQuery(query, (ArrayList<String>) entry.getValue()); break; case ELIMINATION: - case TRANSFER: - case PRESERVATION: + case TRANSFER_REPLY: addEventsToQuery(query, (String) entry.getValue(), searchKey.toUpperCase()); break; default: @@ -228,18 +228,18 @@ public class VitamQueryHelper { setQuery(select, query, queryOr, isEmpty, haveOrParameters); - LOGGER.debug("Final query: {}", select.getFinalSelect().toPrettyString()); + LOGGER.debug("Final query Details: {}", select.getFinalSelect().toPrettyString()); return select.getFinalSelect(); } - private static void manageFilters(Optional<String> orderBy, Optional<DirectionDto> direction, Select select) - throws InvalidParseOperationException { + private static void manageFilters(Optional<String> orderBy, Optional<DirectionDto> direction, Select select) throws InvalidParseOperationException, InvalidCreateOperationException { // Manage Filters if (orderBy.isPresent()) { + String order = orderBy.get(); if (direction.isPresent() && DirectionDto.DESC.equals(direction.get())) { - select.addOrderByDescFilter(orderBy.get()); + select.addOrderByDescFilter(order); } else { - select.addOrderByAscFilter(orderBy.get()); + select.addOrderByAscFilter(order); } } } @@ -270,8 +270,13 @@ public class VitamQueryHelper { private static void addAcquisitionInformationsToQuery(BooleanQuery query, List<String> data) throws InvalidCreateOperationException { List<String> acquisitionInformations = new ArrayList<>(staticAcquisitionInformations); acquisitionInformations.removeAll(data); + if(!acquisitionInformations.isEmpty()) { - query.add(nin(ACQUISITION_INFORMATION, acquisitionInformations.toArray(new String[] {}))); + if(data.contains(ACQUISITION_INFORMATION_NON_RENSEIGNE) ) { + query.add(nin(ACQUISITION_INFORMATION, acquisitionInformations.toArray(new String[] {}))); + } else { + query.add(in(ACQUISITION_INFORMATION, data.toArray(new String[] {}))); + } } } @@ -280,24 +285,24 @@ public class VitamQueryHelper { ObjectMapper mapper = new ObjectMapper(); AccessionRegisterDetailsSearchStatsDto.EndDateInterval dateInterval = mapper.convertValue(value, new TypeReference<>() {}); - SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DATE_FORMAT).withZone(ZoneOffset.UTC); String dateMinStr = dateInterval.getEndDateMin(); String dateMaxStr = dateInterval.getEndDateMax(); - if(dateMinStr != null && dateMaxStr == null) { - query.add(lte(END_DATE, formatter.parse(dateMinStr))); + if (dateMinStr != null && dateMaxStr == null) { + query.add(range(END_DATE, LocalDate.parse(dateMinStr, dtf).toString(), true, LocalDate.now().toString(), true)); } - if(dateMinStr == null && dateMaxStr != null) { - query.add(lte(END_DATE, formatter.parse(dateMaxStr))); + if (dateMinStr == null && dateMaxStr != null) { + query.add(range(END_DATE, LocalDate.now().toString(), true, LocalDate.parse(dateMaxStr, dtf).toString(), true)); } - if(dateMinStr != null && dateMaxStr != null) { - query.add(range(END_DATE, formatter.parse(dateMinStr), true, formatter.parse(dateMaxStr), false)); + if (dateMinStr != null && dateMaxStr != null) { + query.add(range(END_DATE, LocalDate.parse(dateMinStr, dtf).toString(), true, LocalDate.parse(dateMaxStr, dtf).toString(), true)); } - } catch (InvalidCreateOperationException | ParseException e) { - LOGGER.error("Can not find binding for StartDate key: \n {}", e); + } catch (InvalidCreateOperationException e) { + LOGGER.error("Can not find binding for EndDate key: \n {}", e); } } diff --git a/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDetailDto.java b/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDetailDto.java index 6ebd5de0e9c9648dcf56b603727ae4d6eb776c38..3c6e8ad95f16d1312c94a62cb36ecca5cc3b444c 100644 --- a/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDetailDto.java +++ b/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDetailDto.java @@ -84,6 +84,14 @@ public class AccessionRegisterDetailDto extends AccessionRegisterDto { private List<String> operationsIds; - private String messageIdentifier; + private String archivalProfile; + + private String operationType; + + private String legalStatus; + + private String obIdIn; + + private List<String> comment; } diff --git a/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDto.java b/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDto.java index f7d8510542887f7899b8bcda008b718ec55913f7..1ae2a817bcb7588a7f5dc520e2fd1077fdf0611f 100644 --- a/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDto.java +++ b/api/api-referential/referential-commons/src/main/java/fr/gouv/vitamui/referential/common/dto/AccessionRegisterDto.java @@ -36,7 +36,6 @@ */ package fr.gouv.vitamui.referential.common.dto; -import fr.gouv.vitam.common.model.administration.RegisterValueDetailModel; import fr.gouv.vitamui.commons.api.domain.IdDto; import lombok.Getter; import lombok.Setter; diff --git a/api/api-referential/referential-external/pom.xml b/api/api-referential/referential-external/pom.xml index 171b2c21824014757c183cacba9c78b5f54ea3fc..1558130e22454e17b7e2c3afdf4b5af7edc807cc 100644 --- a/api/api-referential/referential-external/pom.xml +++ b/api/api-referential/referential-external/pom.xml @@ -115,7 +115,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-referential/referential-internal/pom.xml b/api/api-referential/referential-internal/pom.xml index eb3c3ba3b1edec026f524c1e369965d943c3f529..dbc2b43df81bfb829428c3fb1650bc70e107f14b 100644 --- a/api/api-referential/referential-internal/pom.xml +++ b/api/api-referential/referential-internal/pom.xml @@ -157,7 +157,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-referential/referential-internal/src/main/java/fr/gouv/vitamui/referential/internal/server/accessionregister/summary/AccessionRegisterSummaryInternalService.java b/api/api-referential/referential-internal/src/main/java/fr/gouv/vitamui/referential/internal/server/accessionregister/summary/AccessionRegisterSummaryInternalService.java index 43cdc97aa7ed0f5a471ec7bc92514b070c718bf5..9d3d178617a4c8407e1d64563ce61b3161874ac4 100644 --- a/api/api-referential/referential-internal/src/main/java/fr/gouv/vitamui/referential/internal/server/accessionregister/summary/AccessionRegisterSummaryInternalService.java +++ b/api/api-referential/referential-internal/src/main/java/fr/gouv/vitamui/referential/internal/server/accessionregister/summary/AccessionRegisterSummaryInternalService.java @@ -68,7 +68,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -80,7 +82,6 @@ import java.util.stream.Collectors; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.and; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.eq; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.in; -import static fr.gouv.vitam.common.database.builder.query.QueryHelper.lte; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.ne; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.nin; import static fr.gouv.vitam.common.database.builder.query.QueryHelper.range; @@ -107,8 +108,7 @@ public class AccessionRegisterSummaryInternalService { private static final String ACQUISITION_INFORMATION = "AcquisitionInformation"; private static final String EVENTS_OPTYPE = "Events.OpType"; private static final String ELIMINATION = "ELIMINATION"; - private static final String TRANSFER = "TRANSFER"; - private static final String PRESERVATION = "PRESERVATION"; + private static final String TRANSFER_REPLY = "TRANSFER_REPLY"; @Autowired AccessionRegisterSummaryInternalService(AccessionRegisterService accessionRegisterService, @@ -169,6 +169,7 @@ public class AccessionRegisterSummaryInternalService { LOGGER.debug("Context application Session ID : {} ", context.getApplicationSessionId()); JsonNode detailsQuery = buildCustomAccessionRegisterDetailsQuery(detailsSearchDto); + LOGGER.debug("Final query Summary: {}", detailsQuery.toPrettyString()); RequestResponse<AccessionRegisterDetailModel> accessionRegisterDetails = adminExternalClient .findAccessionRegisterDetails(context, detailsQuery); @@ -215,7 +216,7 @@ public class AccessionRegisterSummaryInternalService { query.add(in(STATUS, stringValues.toArray(new String[] {}))); } - addStartDateToQuery(query, detailsSearchDto.getDateInterval()); + addEndDateToQuery(query, detailsSearchDto.getDateInterval()); if(detailsSearchDto.getAdvancedSearch() != null) { AccessionRegisterDetailsSearchStatsDto.AdvancedSearchData advancedSearch = detailsSearchDto.getAdvancedSearch(); @@ -224,17 +225,10 @@ public class AccessionRegisterSummaryInternalService { addQueryFrom(query, advancedSearch.getArchivalAgreements(), ARCHIVAL_AGREEMENT); addQueryFrom(query, advancedSearch.getArchivalProfiles(), ARCHIVAL_PROFILE); - if(CollectionUtils.isNotEmpty(advancedSearch.getAcquisitionInformations())) { - List<String> acquisitionInformations = new ArrayList<>(VitamQueryHelper.staticAcquisitionInformations); - acquisitionInformations.removeAll(advancedSearch.getAcquisitionInformations()); - if(!acquisitionInformations.isEmpty()) { - query.add(nin(ACQUISITION_INFORMATION, acquisitionInformations.toArray(new String[] {}))); - } - } + addAcquisitionInformationsToQuery(query, advancedSearch); addEventOpTypeQuery(query, advancedSearch.getElimination(), ELIMINATION); - addEventOpTypeQuery(query, advancedSearch.getElimination(), TRANSFER); - addEventOpTypeQuery(query, advancedSearch.getElimination(), PRESERVATION); + addEventOpTypeQuery(query, advancedSearch.getTransferReply(), TRANSFER_REPLY); } @@ -245,24 +239,41 @@ public class AccessionRegisterSummaryInternalService { return select.getFinalSelect(); } - private static void addStartDateToQuery(BooleanQuery query, AccessionRegisterDetailsSearchStatsDto.EndDateInterval dateInterval) - throws ParseException, InvalidCreateOperationException { + private static void addAcquisitionInformationsToQuery(BooleanQuery query, + AccessionRegisterDetailsSearchStatsDto.AdvancedSearchData advancedSearch) + throws InvalidCreateOperationException { + List<String> acquisitionInformationsFromIhm = advancedSearch.getAcquisitionInformations(); + if(CollectionUtils.isNotEmpty(acquisitionInformationsFromIhm)) { + List<String> acquisitionInformations = new ArrayList<>(VitamQueryHelper.staticAcquisitionInformations); + acquisitionInformations.removeAll(acquisitionInformationsFromIhm); + if(!acquisitionInformations.isEmpty()) { + if(acquisitionInformationsFromIhm.contains(VitamQueryHelper.ACQUISITION_INFORMATION_NON_RENSEIGNE) ) { + query.add(nin(ACQUISITION_INFORMATION, acquisitionInformations.toArray(new String[] {}))); + } else { + query.add(in(ACQUISITION_INFORMATION, acquisitionInformationsFromIhm.toArray(new String[] {}))); + } + } + } + } + + private static void addEndDateToQuery(BooleanQuery query, AccessionRegisterDetailsSearchStatsDto.EndDateInterval dateInterval) + throws InvalidCreateOperationException { if(dateInterval != null) { - SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT); + DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DATE_FORMAT).withZone(ZoneOffset.UTC); String dateMinStr = dateInterval.getEndDateMin(); String dateMaxStr = dateInterval.getEndDateMax(); if (dateMinStr != null && dateMaxStr == null) { - query.add(lte(END_DATE, formatter.parse(dateMinStr))); + query.add(range(END_DATE, LocalDate.parse(dateMinStr, dtf).toString(), true, LocalDate.now().toString(), true)); } if (dateMinStr == null && dateMaxStr != null) { - query.add(lte(END_DATE, formatter.parse(dateMaxStr))); + query.add(range(END_DATE, LocalDate.now().toString(), true, LocalDate.parse(dateMaxStr, dtf).toString(), true)); } if (dateMinStr != null && dateMaxStr != null) { - query.add(range(END_DATE, formatter.parse(dateMinStr), true, formatter.parse(dateMaxStr), false)); + query.add(range(END_DATE, LocalDate.parse(dateMinStr, dtf).toString(), true, LocalDate.parse(dateMaxStr, dtf).toString(), true)); } } } diff --git a/cas/cas-server/pom.xml b/cas/cas-server/pom.xml index 3cb4ab9c52e69dc3f5187b61d0e0b9b206195c5c..cb418f60e12f90cf7ce269beda99d0dab344e17b 100644 --- a/cas/cas-server/pom.xml +++ b/cas/cas-server/pom.xml @@ -12,17 +12,19 @@ <properties> <assertj-core.version>3.11.1</assertj-core.version> - <jackson.version>2.10.0</jackson.version> - <lombok.version>1.18.10</lombok.version> - <micrometer.version>1.3.0</micrometer.version> - <mockito.version>1.10.19</mockito.version> - <spring.boot.version>2.2.0.RELEASE</spring.boot.version> - <spring.cloud.consul.version>2.2.2.RELEASE</spring.cloud.consul.version> - <spring.security.version>5.2.0.RELEASE</spring.security.version> - <spring.test.version>5.2.0.RELEASE</spring.test.version> - <spring.version>5.2.0.RELEASE</spring.version> - <swagger.version>1.5.18</swagger.version> - <thymeleaf-spring5.version>3.0.11.RELEASE</thymeleaf-spring5.version> + <jackson.version>2.12.4</jackson.version> + <lombok.version>1.18.20</lombok.version> + <micrometer.version>1.7.3</micrometer.version> + <mockito.version>3.12.1</mockito.version> + <spring.boot.version>2.5.4</spring.boot.version> + <spring.cloud.consul.version>3.0.3</spring.cloud.consul.version> + <spring.security.version>5.5.2</spring.security.version> + <spring.test.version>5.3.9</spring.test.version> + <spring.version>5.3.9</spring.version> + <swagger.version>2.1.10</swagger.version> + <mongo.version>4.3.1</mongo.version> + <spring-webmvc-pac4j.version>5.0.0</spring-webmvc-pac4j.version> + <thymeleaf-spring5.version>3.0.12.RELEASE</thymeleaf-spring5.version> <rpm.skip>false</rpm.skip> <rpm.jar-file>${project.build.finalName}.war</rpm.jar-file> @@ -46,6 +48,14 @@ <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </exclusion> + <exclusion> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + </exclusion> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + </exclusion> </exclusions> </dependency> @@ -88,6 +98,11 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.mongodb</groupId> + <artifactId>mongodb-driver-sync</artifactId> + <version>${mongo.version}</version> + </dependency> <!-- authentication delegation --> <dependency> @@ -139,11 +154,11 @@ <dependency> <groupId>org.pac4j</groupId> <artifactId>spring-webmvc-pac4j</artifactId> - <version>${pac4j.version}</version> + <version>${spring-webmvc-pac4j.version}</version> </dependency> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> @@ -158,6 +173,18 @@ <version>${cas.version}</version> </dependency> + <!-- X509 authentication --> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-support-x509-webflow</artifactId> + <version>${cas.version}</version> + </dependency> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-support-x509-core</artifactId> + <version>${cas.version}</version> + </dependency> + <!-- subrogation --> <dependency> <groupId>org.apereo.cas</groupId> @@ -196,6 +223,11 @@ <artifactId>cas-server-support-pm-core</artifactId> <version>${cas.version}</version> </dependency> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-core-notifications</artifactId> + <version>${cas.version}</version> + </dependency> <!-- multi-factor authentication --> <dependency> @@ -223,6 +255,11 @@ <artifactId>cas-server-support-sms-twilio</artifactId> <version>${cas.version}</version> </dependency> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-core-authentication-mfa-api</artifactId> + <version>${cas.version}</version> + </dependency> <!-- throttling --> <dependency> @@ -282,8 +319,6 @@ <artifactId>opentracing-spring-jaeger-web-starter</artifactId> </dependency> - - <!-- OAuth support --> <dependency> <groupId>org.apereo.cas</groupId> @@ -334,6 +369,16 @@ </dependency> <!-- logs --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-logging</artifactId> + <exclusions> + <exclusion> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-to-slf4j</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> @@ -348,6 +393,11 @@ <artifactId>jul-to-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> + <dependency> + <groupId>org.gandon.tomcat</groupId> + <artifactId>juli-to-slf4j</artifactId> + <version>1.1.1</version> + </dependency> <!-- UTIL --> <dependency> @@ -359,6 +409,21 @@ <artifactId>thymeleaf-spring5</artifactId> <version>${thymeleaf-spring5.version}</version> </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>1.15</version> + </dependency> + <dependency> + <groupId>org.webjars</groupId> + <artifactId>jquery-ui</artifactId> + <version>1.12.1</version> + </dependency> + <dependency> + <groupId>org.webjars</groupId> + <artifactId>font-awesome</artifactId> + <version>5.11.2</version> + </dependency> <!-- TEST --> <dependency> @@ -425,6 +490,7 @@ <exclude>WEB-INF/lib/log4j-web-*.jar</exclude> <exclude>WEB-INF/lib/log4j-slf4j-impl-*.jar</exclude> <exclude>WEB-INF/lib/log4j-slf4j18-impl-*.jar</exclude> + <exclude>WEB-INF/lib/log4j-layout-template-json-*.jar</exclude> <exclude>WEB-INF/lib/spring-boot-starter-log4j2-*.jar</exclude> <exclude>WEB-INF/lib/slf4j-api-1.8.0-beta4.jar</exclude> <exclude>WEB-INF/lib/jcl-over-slf4j-1.8.0-beta4.jar</exclude> @@ -432,6 +498,8 @@ <exclude>WEB-INF/lib/jackson-core-2.8.10.jar</exclude> <exclude>WEB-INF/lib/jackson-databind-2.8.10.jar</exclude> <exclude>WEB-INF/lib/jackson-dataformat-yaml-2.8.10.jar</exclude> + <exclude>WEB-INF/lib/cas-server-core-logging-api-*.jar</exclude> + <exclude>WEB-INF/lib/slf4j-api-*.jar</exclude> </excludes> </overlay> </overlays> @@ -444,13 +512,16 @@ WEB-INF/lib/log4j-web-*.jar, WEB-INF/lib/log4j-slf4j-impl-*.jar, WEB-INF/lib/log4j-slf4j18-impl-*.jar, + WEB-INF/lib/log4j-layout-template-json-*.jar, WEB-INF/lib/spring-boot-starter-log4j2-*.jar, WEB-INF/lib/slf4j-api-1.8.0-beta4.jar, WEB-INF/lib/jcl-over-slf4j-1.8.0-beta4.jar, WEB-INF/lib/jul-to-slf4j-1.8.0-beta4.jar, WEB-INF/lib/jackson-core-2.8.10.jar, WEB-INF/lib/jackson-databind-2.8.10.jar, - WEB-INF/lib/jackson-dataformat-yaml-2.8.10.jar + WEB-INF/lib/jackson-dataformat-yaml-2.8.10.jar, + WEB-INF/lib/cas-server-core-logging-api-*.jar, + WEB-INF/lib/slf4j-api-*.jar </packagingExcludes> </configuration> </plugin> diff --git a/cas/cas-server/src/main/config/cas-server-application-dev.yml b/cas/cas-server/src/main/config/cas-server-application-dev.yml index 53d7b185f59fba63b2995eebaeba95cf5c388331..a7ed5d45fe3e631f8ad15d11ccabe7c7433992ec 100644 --- a/cas/cas-server/src/main/config/cas-server-application-dev.yml +++ b/cas/cas-server/src/main/config/cas-server-application-dev.yml @@ -49,7 +49,7 @@ iam-client: cas.authn.accept.users: -cas.messageBundle.baseNames: classpath:overriden_messages,classpath:messages +cas.message-bundle.base-names: classpath:overriden_messages,classpath:messages cas.tgc.path: /cas @@ -61,45 +61,46 @@ cas.authn.pm.reset.crypto.enabled: true cas.server.prefix: https://dev.vitamui.com:8080/cas login.url: ${cas.server.prefix}/login -cas.serviceRegistry.mongo.clientUri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas +cas.service-registry.mongo.client-uri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas -#cas.serviceRegistry.mongo.port: 27018 -#cas.serviceRegistry.mongo.databaseName: cas -#cas.serviceRegistry.mongo.authenticationDatabaseName: cas -#cas.serviceRegistry.mongo.replicaSet: rs0 -cas.serviceRegistry.mongo.collection: services -#cas.serviceRegistry.mongo.userId: mongod_dbuser_cas -#cas.serviceRegistry.mongo.password: mongod_dbpwd_cas +#cas.service-registry.mongo.port: 27018 +#cas.service-registry.mongo.database-name: cas +#cas.service-registry.mongo.authentication-database-name: cas +#cas.service-registry.mongo.replica-set: rs0 +cas.service-registry.mongo.collection: services +#cas.service-registry.mongo.user-id: mongod_dbuser_cas +#cas.service-registry.mongo.password: mongod_dbpwd_cas cas.authn.surrogate.separator: "," -cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +cas.authn.surrogate.sms.attribute-name: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS # 24 hours cache for login delegation -cas.ticket.tst.timeToKillInSeconds: 86400 +# Must be at least 24 hours as this cache is also used for password management and its 24-hour-creation email +cas.ticket.tst.time-to-kill-in-seconds: 86400 -cas.authn.pm.enabled: true -cas.authn.pm.policyPattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{${password.length},}$' +cas.authn.pm.core.enabled: true +cas.authn.pm.core.policy-pattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{${password.length},}$' cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe cas.authn.pm.reset.mail.text: "Changez de mot de passe via le lien: %s" cas.authn.pm.reset.mail.from: serveur-cas@noreply.com # 1 Day : 24 * 60 Minutes to reset password -cas.authn.pm.reset.expirationMinutes: 1440 -cas.authn.pm.reset.mail.attributeName: email -cas.authn.pm.reset.securityQuestionsEnabled: false -cas.authn.pm.reset.includeServerIpAddress: false -cas.authn.pm.autoLogin: true +cas.authn.pm.reset.expiration-minutes: 1440 +cas.authn.pm.reset.mail.attribute-name: email +cas.authn.pm.reset.security-questions-enabled: false +cas.authn.pm.reset.include-server-ip-address: false +cas.authn.pm.core.auto-login: true cas.authn.mfa.simple.sms.from: 'changeme' cas.authn.mfa.simple.sms.text: 'Code : %s' -cas.authn.mfa.simple.sms.attributeName: mobile -cas.authn.mfa.simple.timeToKillInSeconds: 3600 -cas.authn.mfa.simple.tokenLength: 4 -cas.authn.mfa.globalPrincipalAttributeNameTriggers: computedOtp -cas.authn.mfa.globalPrincipalAttributeValueRegex: 'true' +cas.authn.mfa.simple.sms.attribute-name: mobile +cas.authn.mfa.simple.time-to-kill-in-seconds: 3600 +cas.authn.mfa.simple.token-length: 4 +cas.authn.mfa.triggers.principal.global-principal-attribute-name-triggers: computedOtp +cas.authn.mfa.triggers.principal.global-principal-attribute-value-regex: 'true' cas.authn.mfa.simple.mail.text: xxx @@ -113,13 +114,13 @@ spring.mail.properties.mail.smtp.starttls.enable: false cas.authn.throttle.failure.threshold: 2 -cas.authn.throttle.failure.rangeSeconds: 3 +cas.authn.throttle.failure.range-seconds: 3 cas: logout: - followServiceRedirects: true - redirectParameter: next + follow-service-redirects: true + redirect-parameter: next management.endpoints.enabled-by-default: true @@ -128,8 +129,8 @@ cas.monitor.endpoints.endpoint.defaults.access[0]: PERMIT # for SMS: -cas.smsProvider.twilio.accountId: changeme -cas.smsProvider.twilio.token: changeme +cas.sms-provider.twilio.account-id: changeme +cas.sms-provider.twilio.token: changeme vitamui.portal.url: https://dev.vitamui.com:4200/ @@ -142,7 +143,8 @@ ip.header: X-Real-IP # 8 hours in seconds -api.token.ttl: 28800 +# the old api.token.ttl property +cas.authn.oauth.access-token.max-time-to-live-in-seconds: 28800 server-identity: @@ -155,7 +157,6 @@ server-identity: theme: # vitamui-platform-name: VITAM-UI # vitamui-favicon: /absolute/path/to/favicon.ico - # vitam-logo: /absolute/path/to/logo.png # vitamui-logo-large: /absolute/path/to/logo.png primary: '#702382' secondary: '#241f63' @@ -171,7 +172,7 @@ opentracing: host: localhost port: 6831 -debug: true +#debug: true logging: config: src/main/config/logback-dev.xml level: @@ -181,11 +182,11 @@ logging: org.apereo.inspektr.audit.support: 'OFF' # Cas CORS (necessary for mobile app) -cas.httpWebRequest.cors.enabled: true -cas.httpWebRequest.cors.allowCredentials: false -cas.httpWebRequest.cors.allowOrigins: [ '*' ] -cas.httpWebRequest.cors.allowMethods: [ '*' ] -cas.httpWebRequest.cors.allowHeaders: [ '*' ] +cas.http-web-request.cors.enabled: true +cas.http-web-request.cors.allow-credentials: false +cas.http-web-request.cors.allow-origins: [ '*' ] +cas.http-web-request.cors.allow-methods: [ '*' ] +cas.http-web-request.cors.allow-headers: [ '*' ] # Password configuration password: diff --git a/cas/cas-server/src/main/config/cas-server-application-recette.yml b/cas/cas-server/src/main/config/cas-server-application-recette.yml index bf82b4c93b57cc614352cede5136251bec05c9ff..6b47e561e665a04cb1735cea1fb42a7500bdba5c 100644 --- a/cas/cas-server/src/main/config/cas-server-application-recette.yml +++ b/cas/cas-server/src/main/config/cas-server-application-recette.yml @@ -41,7 +41,7 @@ iam-client: cas.authn.accept.users: -cas.messageBundle.baseNames: classpath:overriden_messages,classpath:messages +cas.message-bundle.base-names: classpath:overriden_messages,classpath:messages cas.tgc.path: /cas @@ -53,45 +53,46 @@ cas.authn.pm.reset.crypto.enabled: true cas.server.prefix: https://dev.vitamui.com:8080/cas login.url: ${cas.server.prefix}/login -cas.serviceRegistry.mongo.clientUri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas +cas.service-registry.mongo.client-uri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas -#cas.serviceRegistry.mongo.port: 27018 -#cas.serviceRegistry.mongo.databaseName: cas -#cas.serviceRegistry.mongo.authenticationDatabaseName: cas -#cas.serviceRegistry.mongo.replicaSet: rs0 -cas.serviceRegistry.mongo.collection: services -#cas.serviceRegistry.mongo.userId: mongod_dbuser_cas -#cas.serviceRegistry.mongo.password: mongod_dbpwd_cas +#cas.service-registry.mongo.port: 27018 +#cas.service-registry.mongo.database-name: cas +#cas.service-registry.mongo.authentication-database-name: cas +#cas.service-registry.mongo.replica-set: rs0 +cas.service-registry.mongo.collection: services +#cas.service-registry.mongo.user-id: mongod_dbuser_cas +#cas.service-registry.mongo.password: mongod_dbpwd_cas cas.authn.surrogate.separator: "," -cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +cas.authn.surrogate.sms.attribute-name: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS # 24 hours cache for login delegation -cas.ticket.tst.timeToKillInSeconds: 86400 +# Must be at least 24 hours as this cache is also used for password management and its 24-hour-creation email +cas.ticket.tst.time-to-kill-in-seconds: 86400 -cas.authn.pm.enabled: true -cas.authn.pm.policyPattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{12,}$' +cas.authn.pm.core.enabled: true +cas.authn.pm.core.policy-pattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{12,}$' cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe cas.authn.pm.reset.mail.text: "Changez de mot de passe via le lien: %s" cas.authn.pm.reset.mail.from: serveur-cas@noreply.com # 1 Day : 24 * 60 Minutes to reset password -cas.authn.pm.reset.expirationMinutes: 1440 -cas.authn.pm.reset.mail.attributeName: email -cas.authn.pm.reset.securityQuestionsEnabled: false -cas.authn.pm.reset.includeServerIpAddress: false -cas.authn.pm.autoLogin: true +cas.authn.pm.reset.expiration-minutes: 1440 +cas.authn.pm.reset.mail.attribute-name: email +cas.authn.pm.reset.security-questions-enabled: false +cas.authn.pm.reset.include-server-ip-address: false +cas.authn.pm.core.auto-login: true cas.authn.mfa.simple.sms.from: 'changeme' cas.authn.mfa.simple.sms.text: 'Code : %s' -cas.authn.mfa.simple.sms.attributeName: mobile -cas.authn.mfa.simple.timeToKillInSeconds: 3600 -cas.authn.mfa.simple.tokenLength: 4 -cas.authn.mfa.globalPrincipalAttributeNameTriggers: computedOtp -cas.authn.mfa.globalPrincipalAttributeValueRegex: 'true' +cas.authn.mfa.simple.sms.attribute-name: mobile +cas.authn.mfa.simple.time-to-kill-in-seconds: 3600 +cas.authn.mfa.simple.token-length: 4 +cas.authn.mfa.triggers.principal.global-principal-attribute-name-triggers: computedOtp +cas.authn.mfa.triggers.principal.global-principal-attribute-value-regex: 'true' cas.authn.mfa.simple.mail.text: xxx spring.mail.host: localhost @@ -104,13 +105,13 @@ spring.mail.properties.mail.smtp.starttls.enable: false cas.authn.throttle.failure.threshold: 2 -cas.authn.throttle.failure.rangeSeconds: 3 +cas.authn.throttle.failure.range-seconds: 3 cas: logout: - followServiceRedirects: true - redirectParameter: next + follow-service-redirects: true + redirect-parameter: next management.endpoints.enabled-by-default: true @@ -119,8 +120,8 @@ cas.monitor.endpoints.endpoint.defaults.access[0]: PERMIT # for SMS: -cas.smsProvider.twilio.accountId: changeme -cas.smsProvider.twilio.token: changeme +cas.sms-provider.twilio.account-id: changeme +cas.sms-provider.twilio.token: changeme vitamui.portal.url: https://dev.vitamui.com:9000/ @@ -133,7 +134,8 @@ ip.header: X-Real-IP # 8 hours in seconds -api.token.ttl: 28800 +# the old api.token.ttl property +cas.authn.oauth.access-token.max-time-to-live-in-seconds: 28800 server-identity: @@ -164,11 +166,11 @@ logging: # org.apereo.inspektr.audit.support: 'OFF' # Cas CORS (necessary for mobile app) -cas.httpWebRequest.cors.enabled: true -cas.httpWebRequest.cors.allowCredentials: false -cas.httpWebRequest.cors.allowOrigins: [ '*' ] -cas.httpWebRequest.cors.allowMethods: [ '*' ] -cas.httpWebRequest.cors.allowHeaders: [ '*' ] +cas.http-web-request.cors.enabled: true +cas.http-web-request.cors.allow-credentials: false +cas.http-web-request.cors.allow-origins: [ '*' ] +cas.http-web-request.cors.allow-methods: [ '*' ] +cas.http-web-request.cors.allow-headers: [ '*' ] # Password configuration password: diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java index bfdc73ae9a22bf0728ab70a73237032eb4fe0a23..ebb69020e1f27327431fe1f9323e89281121aae7 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java @@ -52,6 +52,8 @@ import org.springframework.webflow.execution.RequestContextHolder; import lombok.val; +import java.util.Collections; + /** * Post-processor which also handles the surrogation in the authentication delegation. * @@ -82,7 +84,7 @@ public class DelegatedSurrogateAuthenticationPostProcessor extends SurrogateAuth val requestContext = RequestContextHolder.getRequestContext(); val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); - val webContext = new JEEContext(request, response, sessionStore); + val webContext = new JEEContext(request, response); val surrogateInSession = sessionStore.get(webContext, Constants.SURROGATE).orElse(null); if (surrogateInSession != null) { LOGGER.debug("surrogate: {} found after authentication delegation -> overriding credential", surrogateInSession); @@ -91,7 +93,7 @@ public class DelegatedSurrogateAuthenticationPostProcessor extends SurrogateAuth newCredential.setSurrogateUsername((String) surrogateInSession); WebUtils.putCredential(requestContext, newCredential); - final AuthenticationTransaction newTransaction = DefaultAuthenticationTransaction.of(transaction.getService(), newCredential); + final AuthenticationTransaction newTransaction = new DefaultAuthenticationTransaction(transaction.getService(), Collections.singletonList(newCredential)); super.process(builder, newTransaction); } else { return; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java index a25433a70462d4c5adbf4a840c5e0d083f3c656e..94deb8add1cad92b0a287f05ebbe78d0c8cb6638 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java @@ -37,6 +37,7 @@ package fr.gouv.vitamui.cas.authentication; import java.util.List; +import java.util.Optional; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; @@ -51,8 +52,6 @@ import fr.gouv.vitamui.iam.common.enums.SubrogationStatusEnum; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.val; -import lombok.val; - /** * Specific surrogate service based on the IAM API. * @@ -74,8 +73,8 @@ public class IamSurrogateAuthenticationService extends BaseSurrogateAuthenticati } @Override - public boolean canAuthenticateAsInternal(final String surrogate, final Principal principal, final Service service) { - val id = (String) principal.getAttributes().get(UserPrincipalResolver.SUPER_USER_ID_ATTRIBUTE).get(0); + public boolean canAuthenticateAsInternal(String surrogate, Principal principal, Optional<Service> service) { + val id = principal.getId(); boolean canAuthenticate = false; try { val subrogations = casExternalRestClient.getSubrogationsBySuperUserId(utils.buildContext(id), id); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java index c068c04c59ba93162140b668dec6642ddae100c9..3bfd91ee7e621c8f3a56ba7558b6ba8ee4e33c5c 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java @@ -36,14 +36,20 @@ */ package fr.gouv.vitamui.cas.authentication; +import java.security.cert.CertificateParsingException; import java.util.*; +import java.util.regex.Pattern; import fr.gouv.vitamui.cas.provider.ProvidersService; -import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; +import fr.gouv.vitamui.cas.x509.CertificateParser; +import fr.gouv.vitamui.cas.x509.X509AttributeMapping; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang.StringUtils; +import org.apereo.cas.adaptors.x509.authentication.principal.X509CertificateCredential; import org.apereo.cas.authentication.AuthenticationHandler; import org.apereo.cas.authentication.Credential; +import org.apereo.cas.authentication.SurrogatePrincipal; import org.apereo.cas.authentication.SurrogateUsernamePasswordCredential; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.authentication.principal.ClientCredential; @@ -81,6 +87,7 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.*; @RequiredArgsConstructor public class UserPrincipalResolver implements PrincipalResolver { + public static final String EMAIL_VALID_REGEXP = "^[_a-z0-9]+(((\\.|-)[_a-z0-9]+))*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,})$"; public static final String SUPER_USER_ID_ATTRIBUTE = "superUserId"; public static final String COMPUTED_OTP = "computedOtp"; @@ -98,19 +105,50 @@ public class UserPrincipalResolver implements PrincipalResolver { private final ProvidersService providersService; + private final X509AttributeMapping x509EmailAttributeMapping; + + private final X509AttributeMapping x509IdentifierAttributeMapping; + + private final String x509DefaultDomain; + @Override public Principal resolve(final Credential credential, final Optional<Principal> optPrincipal, final Optional<AuthenticationHandler> handler) { val principal = optPrincipal.get(); - val userId = principal.getId(); + val principalId = principal.getId(); val requestContext = RequestContextHolder.getRequestContext(); final boolean surrogationCall; - final String username; + String username; final String superUsername; - final String userProviderId; + String userProviderId; final Optional<String> technicalUserId; - if (credential instanceof SurrogateUsernamePasswordCredential) { + // x509 certificate + if (credential instanceof X509CertificateCredential) { + try { + val certificate = ((X509CertificateCredential) credential).getCertificate(); + username = CertificateParser.extract(certificate, x509EmailAttributeMapping); + technicalUserId = Optional.ofNullable(CertificateParser.extract(certificate, x509IdentifierAttributeMapping)); + } catch (final CertificateParsingException e) { + throw new RuntimeException(e.getMessage()); + } + superUsername = null; + userProviderId = null; + surrogationCall = false; + + String userDomain = username; + + // If the certificate does not contain the user mail, then we use the default domain configured + if (StringUtils.isBlank(userDomain) || !Pattern.matches(EMAIL_VALID_REGEXP, userDomain)) { + userDomain = String.format("@%s", x509DefaultDomain); + username = null; + } + + val userProvider = identityProviderHelper.findByUserIdentifier(providersService.getProviders(), userDomain); + if (userProvider.isPresent()) { + userProviderId = userProvider.get().getId(); + } + } else if (credential instanceof SurrogateUsernamePasswordCredential) { // login/password + surrogation val surrogationCredential = (SurrogateUsernamePasswordCredential) credential; username = surrogationCredential.getSurrogateUsername(); @@ -120,7 +158,7 @@ public class UserPrincipalResolver implements PrincipalResolver { surrogationCall = true; } else if (credential instanceof UsernamePasswordCredential) { // login/password - username = userId; + username = principalId; superUsername = null; userProviderId = null; technicalUserId = Optional.empty(); @@ -129,12 +167,12 @@ public class UserPrincipalResolver implements PrincipalResolver { // authentication delegation (+ surrogation) val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); - val webContext = new JEEContext(request, response, sessionStore); + val webContext = new JEEContext(request, response); val clientCredential = (ClientCredential) credential; val providerName = clientCredential.getClientName(); val provider = identityProviderHelper.findByTechnicalName(providersService.getProviders(), providerName).get(); val mailAttribute = provider.getMailAttribute(); - String email = userId; + String email = principalId; if (CommonHelper.isNotBlank(mailAttribute)) { val mails = principal.getAttributes().get(mailAttribute); if (mails == null || mails.size() == 0 || CommonHelper.isBlank((String) mails.get(0))) { @@ -142,13 +180,13 @@ public class UserPrincipalResolver implements PrincipalResolver { return NullPrincipal.getInstance(); } else { val mail = (String) mails.get(0); - LOGGER.info("Provider: '{}' requested specific mail attribute: '{}' for id: '{}' replaced by: '{}'", providerName, mailAttribute, userId, mail); + LOGGER.info("Provider: '{}' requested specific mail attribute: '{}' for id: '{}' replaced by: '{}'", providerName, mailAttribute, principalId, mail); email = mail; } } val identifierAttribute = provider.getIdentifierAttribute(); - String identifier = userId; + String identifier = principalId; if (CommonHelper.isNotBlank(identifierAttribute)) { val identifiers = principal.getAttributes().get(identifierAttribute); if (identifiers == null || identifiers.size() == 0 || CommonHelper.isBlank((String) identifiers.get(0))) { @@ -156,7 +194,7 @@ public class UserPrincipalResolver implements PrincipalResolver { return NullPrincipal.getInstance(); } else { val identifierAttr = (String) identifiers.get(0); - LOGGER.info("Provider: '{}' requested specific identifier attribute: '{}' for id: '{}' replaced by: '{}'", providerName, identifierAttribute, userId, identifierAttr); + LOGGER.info("Provider: '{}' requested specific identifier attribute: '{}' for id: '{}' replaced by: '{}'", providerName, identifierAttribute, principalId, identifierAttr); identifier = identifierAttr; } } @@ -221,10 +259,14 @@ public class UserPrincipalResolver implements PrincipalResolver { attributes.put(ADDRESS_ATTRIBUTE, Collections.singletonList(new CasJsonWrapper(user.getAddress()))); attributes.put(ANALYTICS_ATTRIBUTE, Collections.singletonList(new CasJsonWrapper(user.getAnalytics()))); attributes.put(INTERNAL_CODE, Collections.singletonList(user.getInternalCode())); - attributes.put(INTERNAL_CODE, Collections.singletonList(user.getInternalCode())); + UserDto superUser = null; if (surrogationCall) { attributes.put(SUPER_USER_ATTRIBUTE, Collections.singletonList(superUsername)); - final UserDto superUser = casExternalRestClient.getUser(utils.buildContext(superUsername), superUsername, null, Optional.empty(), Optional.empty()); + superUser = casExternalRestClient.getUser(utils.buildContext(superUsername), superUsername, null, Optional.empty(), Optional.empty()); + if (superUser == null) { + LOGGER.debug("No super user found for: {}", superUsername); + return NullPrincipal.getInstance(); + } attributes.put(SUPER_USER_IDENTIFIER_ATTRIBUTE, Collections.singletonList(superUser.getIdentifier())); attributes.put(SUPER_USER_ID_ATTRIBUTE, Collections.singletonList(superUser.getId())); } @@ -242,13 +284,19 @@ public class UserPrincipalResolver implements PrincipalResolver { profiles.forEach(profile -> profile.getRoles().forEach(role -> roles.add(role.getName()))); attributes.put(ROLES_ATTRIBUTE, new ArrayList(roles)); } - return principalFactory.createPrincipal(user.getId(), attributes); + val createdPrincipal = principalFactory.createPrincipal(user.getId(), attributes); + if (surrogationCall) { + val createdSuperPrincipal = principalFactory.createPrincipal(superUser.getId()); + return new SurrogatePrincipal(createdSuperPrincipal, createdPrincipal); + } else { + return createdPrincipal; + } } @Override public boolean supports(final Credential credential) { return credential instanceof UsernamePasswordCredential || credential instanceof ClientCredential - || credential instanceof SurrogateUsernamePasswordCredential; + || credential instanceof SurrogateUsernamePasswordCredential || credential instanceof X509CertificateCredential; } @Override diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java index ef5aeb42eb757b517cca05004fa35fb77ce6b04d..cb3898a742f103e7dbece3c3b20e06ca37004872 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java @@ -45,6 +45,7 @@ import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.ticket.CustomOAuth20DefaultAccessTokenFactory; import fr.gouv.vitamui.cas.ticket.DynamicTicketGrantingTicketFactory; import fr.gouv.vitamui.cas.util.Utils; +import fr.gouv.vitamui.cas.x509.X509AttributeMapping; import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; import fr.gouv.vitamui.commons.api.identity.ServerIdentityConfiguration; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; @@ -52,7 +53,7 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.commons.security.client.config.password.PasswordConfiguration; import fr.gouv.vitamui.commons.security.client.password.PasswordValidator; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import fr.gouv.vitamui.iam.external.client.IamExternalRestClientFactory; import fr.gouv.vitamui.iam.external.client.IdentityProviderExternalRestClient; @@ -68,12 +69,10 @@ import org.apereo.cas.authentication.principal.PrincipalFactory; import org.apereo.cas.authentication.principal.PrincipalResolver; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.configuration.support.Beans; import org.apereo.cas.pm.PasswordHistoryService; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.services.ServicesManager; -import org.apereo.cas.support.oauth.authenticator.Authenticators; -import org.apereo.cas.support.oauth.web.OAuth20HandlerInterceptorAdapter; -import org.apereo.cas.support.oauth.web.response.accesstoken.ext.AccessTokenGrantRequestExtractor; import org.apereo.cas.ticket.BaseTicketCatalogConfigurer; import org.apereo.cas.ticket.ExpirationPolicyBuilder; import org.apereo.cas.ticket.TicketCatalog; @@ -85,12 +84,8 @@ import org.apereo.cas.ticket.accesstoken.OAuth20DefaultAccessToken; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.token.JwtBuilder; import org.apereo.cas.util.crypto.CipherExecutor; -import org.pac4j.core.client.Client; -import org.pac4j.core.client.DirectClient; -import org.pac4j.core.config.Config; +import org.pac4j.core.client.Clients; import org.pac4j.core.context.session.SessionStore; -import org.pac4j.core.http.adapter.JEEHttpActionAdapter; -import org.pac4j.springframework.web.SecurityInterceptor; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -104,12 +99,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.util.Collection; -import java.util.Objects; -import java.util.stream.Collectors; /** * Configure all beans to customize the CAS server. @@ -191,9 +182,6 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Qualifier("delegatedClientDistributedSessionStore") private SessionStore delegatedClientDistributedSessionStore; - @Autowired - private TicketRegistry ticketRegistry; - @Autowired @Qualifier("centralAuthenticationService") private ObjectProvider<CentralAuthenticationService> centralAuthenticationService; @@ -206,12 +194,12 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Qualifier("passwordHistoryService") private PasswordHistoryService passwordHistoryService; + @Autowired + private PasswordConfiguration passwordConfiguration; + @Value("${token.api.cas}") private String tokenApiCas; - @Value("${api.token.ttl}") - private Integer apiTokenTtl; - @Value("${ip.header}") private String ipHeaderName; @@ -221,44 +209,41 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Value("${vitamui.cas.identity}") private String casIdentity; - @Autowired - @Qualifier("oauthSecConfig") - private ObjectProvider<Config> oauthSecConfig; + @Value("${theme.vitamui-logo-large:#{null}}") + private String vitamuiLargeLogoPath; - @Autowired - @Qualifier("accessTokenGrantRequestExtractors") - private Collection<AccessTokenGrantRequestExtractor> accessTokenGrantRequestExtractors; + @Value("${theme.vitamui-favicon:#{null}}") + private String vitamuiFaviconPath; - @Bean - public SecurityInterceptor requiresAuthenticationAuthorizeInterceptor() { - val interceptor = new SecurityInterceptor(oauthSecConfig.getObject(), Authenticators.CAS_OAUTH_CLIENT, JEEHttpActionAdapter.INSTANCE); - interceptor.setAuthorizers("none"); - return interceptor; - } + @Value("${vitamui.authn.x509.emailAttribute:}") + private String x509EmailAttribute; - @Bean - public PasswordValidator passwordValidator() { - return new PasswordValidator(); - } + @Value("${vitamui.authn.x509.emailAttributeParsing:}") + private String x509EmailAttributeParsing; - @Autowired - private PasswordConfiguration passwordConfiguration; + @Value("${vitamui.authn.x509.emailAttributeExpansion:}") + private String x509EmailAttributeExpansion; - @Bean - public SecurityInterceptor requiresAuthenticationAccessTokenInterceptor() { - val secConfig = oauthSecConfig.getObject(); - val clients = - Objects.requireNonNull(secConfig).getClients().findAllClients().stream().filter(client -> client instanceof DirectClient).map(Client::getName) - .collect(Collectors.joining(",")); - val interceptor = new SecurityInterceptor(oauthSecConfig.getObject(), clients, JEEHttpActionAdapter.INSTANCE); - interceptor.setAuthorizers("none"); - return interceptor; - } + @Value("${vitamui.authn.x509.identifierAttribute:}") + private String x509IdentifierAttribute; + + @Value("${vitamui.authn.x509.identifierAttributeParsing:}") + private String x509IdentifierAttributeParsing; + + @Value("${vitamui.authn.x509.identifierAttributeExpansion:}") + private String x509IdentifierAttributeExpansion; + + @Value("${vitamui.authn.x509.defaultDomain:}") + private String x509DefaultDomain; + + // position matters unfortunately: the ticketRegistry must be autowired after (= under) others + // as it depends on the catalog instantiated above + @Autowired + private TicketRegistry ticketRegistry; @Bean - public HandlerInterceptor oauthHandlerInterceptorAdapter() { - return new OAuth20HandlerInterceptorAdapter(requiresAuthenticationAccessTokenInterceptor(), requiresAuthenticationAuthorizeInterceptor(), - accessTokenGrantRequestExtractors); + public PasswordValidator passwordValidator() { + return new PasswordValidator(); } @Bean @@ -269,8 +254,16 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Bean @RefreshScope public PrincipalResolver surrogatePrincipalResolver() { - return new UserPrincipalResolver(principalFactory, casRestClient(), utils(), delegatedClientDistributedSessionStore, identityProviderHelper(), - providersService()); + val emailMapping = new X509AttributeMapping(x509EmailAttribute, x509EmailAttributeParsing, x509EmailAttributeExpansion); + val identifierMapping = new X509AttributeMapping(x509IdentifierAttribute, x509IdentifierAttributeParsing, x509IdentifierAttributeExpansion); + return new UserPrincipalResolver(principalFactory, casRestClient(), utils(), delegatedClientDistributedSessionStore, + identityProviderHelper(), providersService(), emailMapping, identifierMapping, x509DefaultDomain); + } + + @Bean + @RefreshScope + public PrincipalResolver x509SubjectDNPrincipalResolver() { + return surrogatePrincipalResolver(); } @Bean @@ -292,6 +285,14 @@ public class AppConfig extends BaseTicketCatalogConfigurer { registeredServiceAccessStrategyEnforcer, surrogateEligibilityAuditableExecution, delegatedClientDistributedSessionStore); } + // overrides the CAS specific message converter to prevent + // the CasRestExternalClient to use the 'application/vnd.cas.services+yaml;charset=UTF-8' + // content type and to fail + @Bean + public HttpMessageConverter yamlHttpMessageConverter() { + return null; + } + @Bean public IamExternalRestClientFactory iamRestClientFactory() { LOGGER.debug("Iam client factory: {}", iamClientProperties); @@ -308,14 +309,20 @@ public class AppConfig extends BaseTicketCatalogConfigurer { return iamRestClientFactory().getIdentityProviderExternalRestClient(); } + @RefreshScope + @Bean + public Clients builtClients() { + return new Clients(casProperties.getServer().getLoginUrl()); + } + @Bean public ProvidersService providersService() { - return new ProvidersService(); + return new ProvidersService(builtClients(), identityProviderCrudRestClient(), pac4jClientBuilder(), utils()); } @Bean - public Saml2ClientBuilder saml2ClientBuilder() { - return new Saml2ClientBuilder(); + public Pac4jClientBuilder pac4jClientBuilder() { + return new Pac4jClientBuilder(); } @Bean @@ -331,7 +338,7 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Bean public TicketGrantingTicketFactory defaultTicketGrantingTicketFactory() { return new DynamicTicketGrantingTicketFactory(ticketGrantingTicketUniqueIdGenerator, grantingTicketExpirationPolicy.getObject(), - protocolTicketCipherExecutor); + protocolTicketCipherExecutor, servicesManager, utils()); } @Bean @@ -343,8 +350,10 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Override public void configureTicketCatalog(final TicketCatalog plan) { final TicketDefinition metadata = buildTicketDefinition(plan, "TOK", OAuth20DefaultAccessToken.class, Ordered.HIGHEST_PRECEDENCE); - metadata.getProperties().setStorageName("oauthAccessTokensCache"); - metadata.getProperties().setStorageTimeout(apiTokenTtl); + metadata.getProperties().setStorageName(casProperties.getAuthn().getOauth().getAccessToken().getStorageName()); + val timeout = Beans.newDuration(casProperties.getAuthn().getOauth().getAccessToken().getMaxTimeToLiveInSeconds()).getSeconds(); + metadata.getProperties().setStorageTimeout(timeout); + metadata.getProperties().setExcludeFromCascade(casProperties.getLogout().isRemoveDescendantTickets()); registerTicketDefinition(plan, metadata); } @@ -365,7 +374,7 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Bean public ServletContextInitializer servletContextInitializer() { - return new InitContextConfiguration(); + return new InitContextConfiguration(vitamuiLargeLogoPath, vitamuiFaviconPath); } @Bean diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java index 84fbf421f4a94a51325a34aa31b04ff7b629af17..4f2a38a0e7decfd202d8dcfaf97bbc9131ba20af 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java @@ -37,7 +37,6 @@ package fr.gouv.vitamui.cas.config; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; import fr.gouv.vitamui.commons.rest.client.configuration.RestClientConfiguration; @@ -46,7 +45,6 @@ import fr.gouv.vitamui.commons.rest.client.configuration.RestClientConfiguration * * */ -@Component @ConfigurationProperties(value = "iam-client") public class IamClientConfigurationProperties extends RestClientConfiguration { diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java index 9bbdf4113f80425851de90252944bd75d0ca1da9..2455197324c512116adb5e168a62738c0e4e16c3 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java @@ -41,7 +41,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; import org.springframework.boot.web.servlet.ServletContextInitializer; import javax.servlet.ServletContext; @@ -53,35 +53,20 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; /** - * Custom initial flow action to retrieve pre-filled inputs. + * Custom context initializer to pre-fill logo and favicon. */ +@RequiredArgsConstructor public class InitContextConfiguration implements ServletContextInitializer { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(InitContextConfiguration.class); - @Value("${theme.vitam-logo:#{null}}") - private String vitamLogoPath; - - @Value("${theme.vitamui-logo-large:#{null}}") - private String vitamuiLargeLogoPath; - - @Value("${theme.vitamui-favicon:#{null}}") - private String vitamuiFaviconPath; + private final String vitamuiLargeLogoPath; + private final String vitamuiFaviconPath; @Override public void onStartup(final ServletContext servletContext) throws ServletException { - if (vitamLogoPath != null) { - try { - final Path logoFile = Paths.get(vitamLogoPath); - final String logo = DatatypeConverter.printBase64Binary(Files.readAllBytes(logoFile)); - servletContext.setAttribute(Constants.VITAM_LOGO, logo); - } catch (final IOException e) { - LOGGER.warn("Can't find vitam logo"); - e.printStackTrace(); - } - } if (vitamuiLargeLogoPath != null) { try { final Path logoFile = Paths.get(vitamuiLargeLogoPath); @@ -105,7 +90,4 @@ public class InitContextConfiguration implements ServletContextInitializer { } } - } - - diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java index bbda59c6fcc193ff2e239b99919dc53658c501b7..905ea1417b1888c77033735b32b33bf3d18ca0f6 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java @@ -48,7 +48,7 @@ import javax.servlet.ServletException; import java.util.Objects; /** - * Custom flow action for password complexity configuration. + * Custom context initializer for password complexity configuration. */ public class InitPasswordConstraintsConfiguration implements ServletContextInitializer { @@ -126,5 +126,3 @@ public class InitPasswordConstraintsConfiguration implements ServletContextIniti } } } - - diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java index 6e4a9b523aa8a563d697b7a97b644032246884b1..8faab1e7187b344b3d3878ac94770ca3dea2dbf1 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java @@ -36,41 +36,38 @@ */ package fr.gouv.vitamui.cas.config; +import fr.gouv.vitamui.cas.webflow.configurer.CustomCasSimpleMultifactorWebflowConfigurer; +import fr.gouv.vitamui.cas.x509.CustomRequestHeaderX509CertificateExtractor; import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationEventExecutionPlan; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.mfa.simple.web.flow.CasSimpleMultifactorWebflowConfigurer; +import org.apereo.cas.logout.LogoutManager; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCommunicationStrategy; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicketFactory; +import org.apereo.cas.notifications.CommunicationsManager; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.TransientSessionTicket; -import org.apereo.cas.ticket.TransientSessionTicketFactory; import org.apereo.cas.ticket.factory.DefaultTicketFactory; import org.apereo.cas.ticket.factory.DefaultTransientSessionTicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.ticket.registry.TicketRegistrySupport; -import org.apereo.cas.util.CollectionUtils; -import org.apereo.cas.util.io.CommunicationsManager; -import org.apereo.cas.web.DelegatedClientWebflowManager; import org.apereo.cas.web.cookie.CasCookieBuilder; import org.apereo.cas.web.flow.CasWebflowConfigurer; -import org.apereo.cas.web.flow.CasWebflowConstants; -import org.apereo.cas.web.flow.SingleSignOnParticipationStrategy; +import org.apereo.cas.web.flow.DelegatedClientAuthenticationConfigurationContext; +import org.apereo.cas.web.flow.X509CertificateCredentialsRequestHeaderAction; +import org.apereo.cas.web.flow.actions.ConsumerExecutionAction; +import org.apereo.cas.web.flow.actions.StaticEventExecutionAction; import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; -import org.apereo.cas.web.support.ArgumentExtractor; -import org.pac4j.core.client.Clients; +import org.apereo.cas.web.flow.util.MultifactorAuthenticationWebflowUtils; import org.pac4j.core.context.session.SessionStore; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -78,11 +75,9 @@ import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.webflow.config.FlowDefinitionRegistryBuilder; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; import org.springframework.webflow.engine.builder.support.FlowBuilderServices; import org.springframework.webflow.execution.Action; -import org.thymeleaf.spring5.SpringTemplateEngine; import fr.gouv.vitamui.cas.pm.PmTransientSessionTicketExpirationPolicyBuilder; import fr.gouv.vitamui.cas.pm.ResetPasswordController; @@ -91,11 +86,9 @@ import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.cas.webflow.actions.CheckMfaTokenAction; import fr.gouv.vitamui.cas.webflow.actions.CustomDelegatedClientAuthenticationAction; import fr.gouv.vitamui.cas.webflow.actions.CustomSendTokenAction; -import fr.gouv.vitamui.cas.webflow.actions.CustomVerifyPasswordResetRequestAction; import fr.gouv.vitamui.cas.webflow.actions.DispatcherAction; import fr.gouv.vitamui.cas.webflow.actions.GeneralTerminateSessionAction; import fr.gouv.vitamui.cas.webflow.actions.I18NSendPasswordResetInstructionsAction; -import fr.gouv.vitamui.cas.webflow.actions.NoOpAction; import fr.gouv.vitamui.cas.webflow.actions.TriggerChangePasswordAction; import fr.gouv.vitamui.cas.webflow.configurer.CustomLoginWebflowConfigurer; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; @@ -108,10 +101,6 @@ import lombok.val; @Configuration public class WebflowConfig { - @Autowired - @Qualifier("servicesManager") - private ObjectProvider<ServicesManager> servicesManager; - @Autowired @Qualifier("ticketGrantingTicketCookieGenerator") private ObjectProvider<CasCookieBuilder> ticketGrantingTicketCookieGenerator; @@ -120,14 +109,6 @@ public class WebflowConfig { @Qualifier("warnCookieGenerator") private ObjectProvider<CasCookieBuilder> warnCookieGenerator; - @Autowired - @Qualifier("authenticationServiceSelectionPlan") - private ObjectProvider<AuthenticationServiceSelectionPlan> authenticationRequestServiceSelectionStrategies; - - @Autowired - @Qualifier("authenticationEventExecutionPlan") - private ObjectProvider<AuthenticationEventExecutionPlan> AuthenticationEventExecutionStrategies; - @Autowired @Qualifier("communicationsManager") private CommunicationsManager communicationsManager; @@ -157,76 +138,70 @@ public class WebflowConfig { private FlowDefinitionRegistry loginFlowDefinitionRegistry; @Autowired - private ApplicationContext applicationContext; + private ConfigurableApplicationContext applicationContext; @Autowired private CasExternalRestClient casRestClient; @Autowired - @Qualifier("initialAuthenticationAttemptWebflowEventResolver") - private ObjectProvider<CasDelegatingWebflowEventResolver> initialAuthenticationAttemptWebflowEventResolver; - - @Autowired - @Qualifier("builtClients") - private ObjectProvider<Clients> builtClients; - - @Autowired - @Qualifier("serviceTicketRequestWebflowEventResolver") - private ObjectProvider<CasWebflowEventResolver> serviceTicketRequestWebflowEventResolver; + private TicketRegistry ticketRegistry; @Autowired - @Qualifier("adaptiveAuthenticationPolicy") - private ObjectProvider<AdaptiveAuthenticationPolicy> adaptiveAuthenticationPolicy; + @Qualifier("centralAuthenticationService") + private ObjectProvider<CentralAuthenticationService> centralAuthenticationService; @Autowired - @Qualifier("registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer") - private ObjectProvider<AuditableExecution> registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer; + @Qualifier("delegatedClientDistributedSessionStore") + private ObjectProvider<SessionStore> delegatedClientDistributedSessionStore; @Autowired - @Qualifier("defaultAuthenticationSystemSupport") - private ObjectProvider<AuthenticationSystemSupport> authenticationSystemSupport; + private Utils utils; @Autowired - private TicketRegistry ticketRegistry; + private TicketRegistrySupport ticketRegistrySupport; @Autowired - @Qualifier("argumentExtractor") - private ObjectProvider<ArgumentExtractor> argumentExtractor; + @Qualifier("messageSource") + private HierarchicalMessageSource messageSource; @Autowired - @Qualifier("centralAuthenticationService") - private ObjectProvider<CentralAuthenticationService> centralAuthenticationService; + @Qualifier("casSimpleMultifactorAuthenticationTicketFactory") + private CasSimpleMultifactorAuthenticationTicketFactory casSimpleMultifactorAuthenticationTicketFactory; @Autowired - @Qualifier("singleSignOnParticipationStrategy") - private ObjectProvider<SingleSignOnParticipationStrategy> webflowSingleSignOnParticipationStrategy; + private LogoutManager logoutManager; @Autowired - @Qualifier("delegatedClientDistributedSessionStore") - private ObjectProvider<SessionStore> delegatedClientDistributedSessionStore; + @Qualifier(DelegatedClientAuthenticationConfigurationContext.DEFAULT_BEAN_NAME) + private DelegatedClientAuthenticationConfigurationContext delegatedClientAuthenticationConfigurationContext; @Autowired - private Utils utils; + @Qualifier("mfaSimpleMultifactorTokenCommunicationStrategy") + private CasSimpleMultifactorTokenCommunicationStrategy mfaSimpleMultifactorTokenCommunicationStrategy; @Autowired - private DelegatedClientWebflowManager delegatedClientWebflowManager; + @Qualifier("mfaSimpleAuthenticatorFlowRegistry") + private FlowDefinitionRegistry mfaSimpleAuthenticatorFlowRegistry; @Autowired - private TicketRegistrySupport ticketRegistrySupport; + @Qualifier("servicesManager") + private ServicesManager servicesManager; @Autowired - @Qualifier("messageSource") - private HierarchicalMessageSource messageSource; + @Qualifier("frontChannelLogoutAction") + private Action frontChannelLogoutAction; @Autowired - private SpringTemplateEngine springTemplateEngine; + @Qualifier("adaptiveAuthenticationPolicy") + private ObjectProvider<AdaptiveAuthenticationPolicy> adaptiveAuthenticationPolicy; @Autowired - private ThymeleafProperties thymeleafProperties; + @Qualifier("serviceTicketRequestWebflowEventResolver") + private ObjectProvider<CasWebflowEventResolver> serviceTicketRequestWebflowEventResolver; @Autowired - @Qualifier("casSimpleMultifactorAuthenticationTicketFactory") - private TransientSessionTicketFactory casSimpleMultifactorAuthenticationTicketFactory; + @Qualifier("initialAuthenticationAttemptWebflowEventResolver") + private ObjectProvider<CasDelegatingWebflowEventResolver> initialAuthenticationAttemptWebflowEventResolver; @Value("${vitamui.portal.url}") private String vitamuiPortalUrl; @@ -234,6 +209,15 @@ public class WebflowConfig { @Value("${cas.authn.surrogate.separator}") private String surrogationSeparator; + @Value("${theme.vitamui-platform-name:VITAM-UI}") + private String vitamuiPlatformName; + + @Value("${vitamui.authn.x509.enabled:false}") + private boolean x509AuthnEnabled; + + @Value("${vitamui.authn.x509.mandatory:false}") + private boolean x509AuthnMandatory; + @Bean public DispatcherAction dispatcherAction() { return new DispatcherAction(providersService, identityProviderHelper, casRestClient, surrogationSeparator, utils, @@ -252,7 +236,7 @@ public class WebflowConfig { pmTicketFactory.addTicketFactory(TransientSessionTicket.class, pmTicketFactory()); return new I18NSendPasswordResetInstructionsAction(casProperties, communicationsManager, passwordManagementService, ticketRegistry, pmTicketFactory, - messageSource, providersService, identityProviderHelper, utils); + messageSource, providersService, identityProviderHelper, utils, vitamuiPlatformName); } @Bean @@ -274,36 +258,17 @@ public class WebflowConfig { @Bean @Lazy public Action delegatedAuthenticationAction() { - return new CustomDelegatedClientAuthenticationAction( - initialAuthenticationAttemptWebflowEventResolver.getObject(), - serviceTicketRequestWebflowEventResolver.getObject(), - adaptiveAuthenticationPolicy.getObject(), - builtClients.getObject(), - servicesManager.getObject(), - registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer.getObject(), - delegatedClientWebflowManager, - authenticationSystemSupport.getObject(), - casProperties, - authenticationRequestServiceSelectionStrategies.getObject(), - centralAuthenticationService.getObject(), - webflowSingleSignOnParticipationStrategy.getObject(), - delegatedClientDistributedSessionStore.getObject(), - CollectionUtils.wrap(argumentExtractor.getObject()), - identityProviderHelper, - providersService, - utils, - ticketRegistry, - vitamuiPortalUrl, - surrogationSeparator); + return new CustomDelegatedClientAuthenticationAction(delegatedClientAuthenticationConfigurationContext, identityProviderHelper, + providersService, utils, ticketRegistry, vitamuiPortalUrl, surrogationSeparator); } @Bean @RefreshScope public Action terminateSessionAction() { return new GeneralTerminateSessionAction(centralAuthenticationService.getObject(), - ticketGrantingTicketCookieGenerator.getObject(), - warnCookieGenerator.getObject(), - casProperties.getLogout()); + ticketGrantingTicketCookieGenerator.getObject(), warnCookieGenerator.getObject(), + logoutManager, applicationContext, utils, casRestClient, servicesManager, casProperties, + frontChannelLogoutAction); } @Bean @@ -314,30 +279,25 @@ public class WebflowConfig { @Bean public Action loadSurrogatesListAction() { - return new NoOpAction("success"); + return StaticEventExecutionAction.SUCCESS; } @Bean @RefreshScope public Action mfaSimpleMultifactorSendTokenAction() { val simple = casProperties.getAuthn().getMfa().getSimple(); - return new CustomSendTokenAction(ticketRegistry, communicationsManager, - casSimpleMultifactorAuthenticationTicketFactory, simple, utils); - } - - @Bean - public FlowDefinitionRegistry customMfaSimpleAuthenticatorFlowRegistry() { - val builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.flowBuilderServices); - builder.setBasePath(CasWebflowConstants.BASE_CLASSPATH_WEBFLOW); - builder.addFlowLocationPattern("/mfa-simple/mfa-simple-custom-webflow.xml"); - return builder.build(); + return new CustomSendTokenAction(ticketRegistry, communicationsManager, casSimpleMultifactorAuthenticationTicketFactory, + simple, mfaSimpleMultifactorTokenCommunicationStrategy, utils); } @Bean @DependsOn("defaultWebflowConfigurer") public CasWebflowConfigurer mfaSimpleMultifactorWebflowConfigurer() { - return new CasSimpleMultifactorWebflowConfigurer(flowBuilderServices, loginFlowDefinitionRegistry, - customMfaSimpleAuthenticatorFlowRegistry(), applicationContext, casProperties); + val cfg = new CustomCasSimpleMultifactorWebflowConfigurer(flowBuilderServices, + loginFlowDefinitionRegistry, mfaSimpleAuthenticatorFlowRegistry, applicationContext, casProperties, + MultifactorAuthenticationWebflowUtils.getMultifactorAuthenticationWebflowCustomizers(applicationContext)); + cfg.setOrder(100); + return cfg; } @Bean @@ -349,13 +309,28 @@ public class WebflowConfig { @Lazy @RefreshScope public Action delegatedAuthenticationClientLogoutAction() { - return new NoOpAction(null); + return new ConsumerExecutionAction(ctx -> {}); } @Bean @RefreshScope - public Action verifyPasswordResetRequestAction() { - return new CustomVerifyPasswordResetRequestAction(casProperties, passwordManagementService, centralAuthenticationService.getObject()); + public Action delegatedAuthenticationClientFinishLogoutAction() { + return new ConsumerExecutionAction(ctx -> {}); } + @Bean + @RefreshScope + public Action x509Check() { + if (x509AuthnEnabled) { + val sslHeaderName = casProperties.getAuthn().getX509().getSslHeaderName(); + val certificateExtractor = new CustomRequestHeaderX509CertificateExtractor(sslHeaderName, x509AuthnMandatory); + + return new X509CertificateCredentialsRequestHeaderAction(initialAuthenticationAttemptWebflowEventResolver.getObject(), + serviceTicketRequestWebflowEventResolver.getObject(), + adaptiveAuthenticationPolicy.getObject(), + certificateExtractor, casProperties); + } else { + return new StaticEventExecutionAction("error"); + } + } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java index 8c786840d7992b573b250abdc5f0843393a098f5..c2b5c2d2df1070145f9c454761767f1b2d029210 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java @@ -55,10 +55,7 @@ import org.apereo.cas.authentication.Credential; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties; -import org.apereo.cas.pm.BasePasswordManagementService; -import org.apereo.cas.pm.InvalidPasswordException; -import org.apereo.cas.pm.PasswordChangeRequest; -import org.apereo.cas.pm.PasswordHistoryService; +import org.apereo.cas.pm.*; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.crypto.CipherExecutor; import org.apereo.cas.web.support.WebUtils; @@ -129,8 +126,10 @@ public class IamPasswordManagementService extends BasePasswordManagementService val requestContext = RequestContextHolder.getRequestContext(); val authentication = WebUtils.getAuthentication(requestContext); if (authentication != null) { + // login/pwd subrogation String superUsername = (String) utils.getAttributeValue(authentication.getAttributes(), SurrogateAuthenticationService.AUTHENTICATION_ATTR_SURROGATE_PRINCIPAL); if (superUsername == null) { + // authn delegation subrogation superUsername = (String) utils.getAttributeValue(authentication.getPrincipal().getAttributes(), SUPER_USER_ATTRIBUTE); } LOGGER.debug("is it currently a superUser: {}", superUsername); @@ -152,7 +151,7 @@ public class IamPasswordManagementService extends BasePasswordManagementService throw new PasswordConfirmException(); } - if (!passwordValidator.isValid(getProperties().getPolicyPattern(), bean.getPassword())) { + if (!passwordValidator.isValid(getProperties().getCore().getPolicyPattern(), bean.getPassword())) { throw new PasswordNotMatchRegexException(); } @@ -194,7 +193,8 @@ public class IamPasswordManagementService extends BasePasswordManagementService } @Override - public String findEmail(final String username) { + public String findEmail(final PasswordManagementQuery query) { + val username = query.getUsername(); String email = null; val usernameWithLowercase = username.toLowerCase().trim(); try { @@ -210,7 +210,7 @@ public class IamPasswordManagementService extends BasePasswordManagementService } @Override - public Map<String, String> getSecurityQuestions(final String username) { + public Map<String, String> getSecurityQuestions(final PasswordManagementQuery query) { throw new UnsupportedOperationException("security questions/answers are not available"); } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java index d9f5df004a84dc2626667baa3c7baf03eaa99231..54290cd4e4b85d12b6757f9a1e569f0550afa3e8 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java @@ -38,7 +38,6 @@ package fr.gouv.vitamui.cas.pm; import java.util.Locale; -import org.apache.commons.lang3.StringUtils; import org.springframework.context.HierarchicalMessageSource; import lombok.EqualsAndHashCode; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java index 49e07c48a7688ec7d5321a64a6f2366a3b09a9b4..2104ba191a38bfd64ab51aaf58d86f63dc3186c7 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java @@ -63,7 +63,7 @@ public class PmTransientSessionTicketExpirationPolicyBuilder extends TransientSe val attributes = RequestContextHolder.getRequestAttributes(); if (attributes != null) { try { - val expInMinutes = (Integer) attributes.getAttribute(PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, 0); + val expInMinutes = (Long) attributes.getAttribute(PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, 0); if (expInMinutes != null) { return new HardTimeoutExpirationPolicy(expInMinutes * 60); } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java index 46a02a858d8c1c7c0c9b07d3298a05e3819d9306..57aef3f8c894617123cc920d3eeee12719178662 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java @@ -44,12 +44,13 @@ import lombok.val; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.notifications.CommunicationsManager; +import org.apereo.cas.pm.PasswordManagementQuery; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.pm.web.flow.PasswordManagementWebflowUtils; import org.apereo.cas.ticket.factory.DefaultTransientSessionTicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.CollectionUtils; -import org.apereo.cas.util.io.CommunicationsManager; import org.apereo.cas.web.flow.CasWebflowConfigurer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.HierarchicalMessageSource; @@ -105,23 +106,23 @@ public class ResetPasswordController { return false; } - communicationsManager.validate(); if (!communicationsManager.isMailSenderDefined()) { LOGGER.warn("CAS is unable to send password-reset emails given no settings are defined to account for email servers"); return false; } val usernameLower = username.toLowerCase().trim(); - val email = passwordManagementService.findEmail(usernameLower); + val query = PasswordManagementQuery.builder().username(usernameLower).build(); + val email = passwordManagementService.findEmail(query); if (StringUtils.isBlank(email)) { LOGGER.warn("No recipient is provided"); return false; } - final String url = buildPasswordResetUrl(usernameLower, casProperties); final Locale locale = new Locale(language); final long expMinutes = PmMessageToSend.ONE_DAY.equals(ttl) ? 24 * 60L : casProperties.getAuthn().getPm().getReset().getExpirationMinutes(); - final PmMessageToSend messageToSend = PmMessageToSend.buildMessage(messageSource, firstname, lastname, String.valueOf(expMinutes), url, vitamuiPlatformName, locale); request.setAttribute(PmTransientSessionTicketExpirationPolicyBuilder.PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, expMinutes); + final String url = buildPasswordResetUrl(usernameLower, casProperties); + final PmMessageToSend messageToSend = PmMessageToSend.buildMessage(messageSource, firstname, lastname, String.valueOf(expMinutes), url, vitamuiPlatformName, locale); LOGGER.debug("Generated password reset URL [{}] for: {} ({}); Link is only active for the next [{}] minute(s)", utils.sanitizePasswordResetUrl(url), email, messageToSend.getSubject(), expMinutes); @@ -130,7 +131,8 @@ public class ResetPasswordController { } protected String buildPasswordResetUrl(final String username, final CasConfigurationProperties casProperties) { - val token = passwordManagementService.createToken(username); + val query = PasswordManagementQuery.builder().username(username).build(); + val token = passwordManagementService.createToken(query); val properties = CollectionUtils.<String, Serializable>wrap(PasswordManagementWebflowUtils.FLOWSCOPE_PARAMETER_NAME_TOKEN, token); val ticket = pmTicketFactory.create((Service) null, properties); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/SamlIdentityProviderDto.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/Pac4jClientIdentityProviderDto.java similarity index 74% rename from cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/SamlIdentityProviderDto.java rename to cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/Pac4jClientIdentityProviderDto.java index de43a123f9a94875caaff487358839b7aa713286..7828f20296520b5a3783a6380a4e7da648048d2b 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/SamlIdentityProviderDto.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/Pac4jClientIdentityProviderDto.java @@ -37,37 +37,51 @@ package fr.gouv.vitamui.cas.provider; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; -import org.pac4j.saml.client.SAML2Client; +import org.pac4j.core.client.IndirectClient; /** - * SAML identity provider. + * Pac4j client identity provider. * * */ -public class SamlIdentityProviderDto extends IdentityProviderDto { +public class Pac4jClientIdentityProviderDto extends IdentityProviderDto { - private final SAML2Client saml2Client; + private final IndirectClient client; - public SamlIdentityProviderDto(final IdentityProviderDto dto, final SAML2Client saml2Client) { + public Pac4jClientIdentityProviderDto(final IdentityProviderDto dto, final IndirectClient client) { setId(dto.getId()); setName(dto.getName()); setTechnicalName(dto.getTechnicalName()); setInternal(dto.getInternal()); + setEnabled(dto.getEnabled()); setPatterns(dto.getPatterns()); + setReadonly(dto.isReadonly()); + + setMailAttribute(dto.getMailAttribute()); + setIdentifierAttribute(dto.getIdentifierAttribute()); + setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled()); + setKeystoreBase64(dto.getKeystoreBase64()); setKeystorePassword(dto.getKeystorePassword()); setPrivateKeyPassword(dto.getPrivateKeyPassword()); setIdpMetadata(dto.getIdpMetadata()); setSpMetadata(dto.getSpMetadata()); - setPatterns(dto.getPatterns()); setMaximumAuthenticationLifetime(dto.getMaximumAuthenticationLifetime()); - setMailAttribute(dto.getMailAttribute()); - setIdentifierAttribute(dto.getIdentifierAttribute()); setAuthnRequestBinding(dto.getAuthnRequestBinding()); - this.saml2Client = saml2Client; + + setClientId(dto.getClientId()); + setClientSecret(dto.getClientSecret()); + setDiscoveryUrl(dto.getDiscoveryUrl()); + setScope(dto.getScope()); + setPreferredJwsAlgorithm(dto.getPreferredJwsAlgorithm()); + setCustomParams(dto.getCustomParams()); + setUseState(dto.getUseState()); + setUseNonce(dto.getUseNonce()); + setUsePkce(dto.getUsePkce()); + this.client = client; } - public SAML2Client getSaml2Client() { - return saml2Client; + public IndirectClient getClient() { + return client; } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java index 3bb2cc36f0cebd218a0cb3b12e83a87b67a2ad16..07ae844122422c47c1a0a4feb30e83f417e63b26 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java @@ -36,20 +36,16 @@ */ package fr.gouv.vitamui.cas.provider; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.pac4j.core.client.Client; import org.pac4j.core.client.Clients; -import org.pac4j.saml.client.SAML2Client; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import org.pac4j.core.client.IndirectClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.util.Assert; @@ -58,10 +54,9 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.dto.common.ProviderEmbeddedOptions; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import fr.gouv.vitamui.iam.external.client.IdentityProviderExternalRestClient; import lombok.Getter; -import lombok.Setter; /** * Retrieve all the identity providers from the IAM API. @@ -69,29 +64,20 @@ import lombok.Setter; * */ @Getter -@Setter +@RequiredArgsConstructor public class ProvidersService { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(ProvidersService.class); private List<IdentityProviderDto> providers = new ArrayList<>(); - @Autowired - @Qualifier("builtClients") - private Clients clients; + private final Clients clients; - @Autowired - private IdentityProviderExternalRestClient identityProviderExternalRestClient; + private final IdentityProviderExternalRestClient identityProviderExternalRestClient; - @Autowired - private Saml2ClientBuilder saml2ClientBuilder; + private final Pac4jClientBuilder pac4jClientBuilder; - @Autowired - private Utils utils; - - public ProvidersService() { - // do nothing - } + private final Utils utils; @PostConstruct public void afterPropertiesSet() { @@ -114,18 +100,18 @@ public class ProvidersService { final List<IdentityProviderDto> temporaryProviders = identityProviderExternalRestClient.getAll(utils.buildContext(null), Optional.empty(), Optional.of(ProviderEmbeddedOptions.KEYSTORE + "," + ProviderEmbeddedOptions.IDPMETADATA)); // sort by identifier. This is needed in order to take the internal provider first. - Collections.sort(temporaryProviders, (provider1, provider2) -> provider1.getIdentifier().compareTo(provider2.getIdentifier())); + Collections.sort(temporaryProviders, Comparator.comparing(IdentityProviderDto::getIdentifier)); LOGGER.debug("Reloaded {} providers: {}", temporaryProviders.size(), StringUtils.join(temporaryProviders.stream().map(IdentityProviderDto::getId).collect(Collectors.toList()), ", ")); final List<Client> newClients = new ArrayList<>(); final List<IdentityProviderDto> newProviders = new ArrayList<>(); temporaryProviders.forEach(p -> { - final SAML2Client saml2Client = saml2ClientBuilder.buildSaml2Client(p).orElse(null); - if (saml2Client != null) { - newClients.add(saml2Client); + final IndirectClient client = pac4jClientBuilder.buildClient(p).orElse(null); + if (client != null) { + newClients.add(client); } - newProviders.add(new SamlIdentityProviderDto(p, saml2Client)); + newProviders.add(new Pac4jClientIdentityProviderDto(p, client)); }); clients.setClients(newClients); providers = newProviders; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java index 3416cc57bc760f75aba039009ad9f48f258f14d8..e753dcf7f8037d412d84ff25a76e84285df01db2 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java @@ -40,25 +40,17 @@ import fr.gouv.vitamui.commons.api.CommonConstants; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; -import org.apereo.cas.configuration.support.Beans; import org.apereo.cas.services.ServicesManager; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; +import org.apereo.cas.support.oauth.OAuth20GrantTypes; +import org.apereo.cas.support.oauth.OAuth20ResponseTypes; import org.apereo.cas.support.oauth.util.OAuth20Utils; -import org.apereo.cas.ticket.ExpirationPolicy; import org.apereo.cas.ticket.ExpirationPolicyBuilder; -import org.apereo.cas.ticket.Ticket; -import org.apereo.cas.ticket.TicketFactory; import org.apereo.cas.ticket.TicketGrantingTicket; -import org.apereo.cas.ticket.accesstoken.OAuth20AccessToken; -import org.apereo.cas.ticket.accesstoken.OAuth20AccessTokenExpirationPolicy; -import org.apereo.cas.ticket.accesstoken.OAuth20AccessTokenFactory; -import org.apereo.cas.ticket.accesstoken.OAuth20DefaultAccessToken; +import org.apereo.cas.ticket.accesstoken.*; import org.apereo.cas.token.JwtBuilder; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.val; -import org.apache.commons.lang3.StringUtils; import java.util.Collection; import java.util.List; @@ -69,55 +61,39 @@ import java.util.Map; * * */ -@RequiredArgsConstructor @Getter -public class CustomOAuth20DefaultAccessTokenFactory implements OAuth20AccessTokenFactory { +public class CustomOAuth20DefaultAccessTokenFactory extends OAuth20DefaultAccessTokenFactory { - /** - * ExpirationPolicy for refresh tokens. - */ - protected final ExpirationPolicyBuilder<OAuth20AccessToken> expirationPolicy; - - /** - * JWT builder instance. - */ - protected final JwtBuilder jwtBuilder; - - /** - * Services manager. - */ - protected final ServicesManager servicesManager; + public CustomOAuth20DefaultAccessTokenFactory(final ExpirationPolicyBuilder<OAuth20AccessToken> expirationPolicy, + final JwtBuilder jwtBuilder, + final ServicesManager servicesManager) { + super(expirationPolicy, jwtBuilder, servicesManager); + } @Override - public OAuth20AccessToken create(final Service service, final Authentication authentication, + public OAuth20AccessToken create(final Service service, + final Authentication authentication, final TicketGrantingTicket ticketGrantingTicket, - final Collection<String> scopes, final String clientId, - final Map<String, Map<String, Object>> requestClaims) { + final Collection<String> scopes, + final String token, + final String clientId, + final Map<String, Map<String, Object>> requestClaims, + final OAuth20ResponseTypes responseType, + final OAuth20GrantTypes grantType) { val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(jwtBuilder.getServicesManager(), clientId); val expirationPolicyToUse = determineExpirationPolicyForService(registeredService); + // CUSTO: don't generate the identifier, but use the token of the principal val accessTokenId = generateAccessTokenId(authentication); val at = new OAuth20DefaultAccessToken(accessTokenId, service, authentication, - expirationPolicyToUse, ticketGrantingTicket, scopes, - clientId, requestClaims); + expirationPolicyToUse, ticketGrantingTicket, token, scopes, + clientId, requestClaims, responseType, grantType); if (ticketGrantingTicket != null) { ticketGrantingTicket.getDescendantTickets().add(at.getId()); } return at; } - @Override - public OAuth20AccessToken create(final Service service, final Authentication authentication, - final Collection<String> scopes, final String clientId, - final Map<String, Map<String, Object>> requestClaims) { - val accessTokenId = generateAccessTokenId(authentication); - val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(jwtBuilder.getServicesManager(), clientId); - val expirationPolicyToUse = determineExpirationPolicyForService(registeredService); - return new OAuth20DefaultAccessToken(accessTokenId, service, authentication, - expirationPolicyToUse, null, - scopes, clientId, requestClaims); - } - private String generateAccessTokenId(final Authentication authentication) { final Principal principal = authentication.getPrincipal(); final List<Object> authToken = principal.getAttributes().get(CommonConstants.AUTHTOKEN_ATTRIBUTE); @@ -126,23 +102,4 @@ public class CustomOAuth20DefaultAccessTokenFactory implements OAuth20AccessToke } return (String) authToken.get(0); } - - @Override - public TicketFactory get(final Class<? extends Ticket> clazz) { - return this; - } - - private ExpirationPolicy determineExpirationPolicyForService(final OAuthRegisteredService registeredService) { - if (registeredService != null && registeredService.getAccessTokenExpirationPolicy() != null) { - val policy = registeredService.getAccessTokenExpirationPolicy(); - val maxTime = policy.getMaxTimeToLive(); - val ttl = policy.getTimeToKill(); - if (StringUtils.isNotBlank(maxTime) && StringUtils.isNotBlank(ttl)) { - return new OAuth20AccessTokenExpirationPolicy( - Beans.newDuration(maxTime).getSeconds(), - Beans.newDuration(ttl).getSeconds()); - } - } - return this.expirationPolicy.buildTicketExpirationPolicy(); - } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java index 789a542a0efba99ab2e3f5fa93ffe8a4b263684a..88c6a6fb38f6fe0c22676e7b51157adaa9c644a8 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java @@ -40,11 +40,12 @@ import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.commons.api.enums.UserTypeEnum; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.authentication.principal.Service; +import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.*; import org.apereo.cas.ticket.expiration.HardTimeoutExpirationPolicy; import org.apereo.cas.ticket.factory.DefaultTicketGrantingTicketFactory; import org.apereo.cas.util.crypto.CipherExecutor; -import org.springframework.beans.factory.annotation.Autowired; import java.io.Serializable; import java.util.List; @@ -59,18 +60,20 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.*; */ public class DynamicTicketGrantingTicketFactory extends DefaultTicketGrantingTicketFactory { - @Autowired - private Utils utils; + private final Utils utils; public DynamicTicketGrantingTicketFactory(final UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator, final ExpirationPolicyBuilder<TicketGrantingTicket> ticketGrantingTicketExpirationPolicy, - final CipherExecutor<Serializable, String> cipherExecutor) { - super(ticketGrantingTicketUniqueTicketIdGenerator, ticketGrantingTicketExpirationPolicy, cipherExecutor); + final CipherExecutor<Serializable, String> cipherExecutor, + final ServicesManager servicesManager, + final Utils utils) { + super(ticketGrantingTicketUniqueTicketIdGenerator, ticketGrantingTicketExpirationPolicy, cipherExecutor, servicesManager); + this.utils = utils; } @Override protected <T extends TicketGrantingTicket> T produceTicket(final Authentication authentication, - final String tgtId, final Class<T> clazz) { + final String tgtId, final Service service, final Class<T> clazz) { final Principal principal = authentication.getPrincipal(); final Map<String, List<Object>> attributes = principal.getAttributes(); final String superUser = (String) utils.getAttributeValue(attributes, SUPER_USER_ATTRIBUTE); @@ -79,7 +82,7 @@ public class DynamicTicketGrantingTicketFactory extends DefaultTicketGrantingTic return (T) new TicketGrantingTicketImpl( tgtId, authentication, new HardTimeoutExpirationPolicy(170 * 60)); } else { - return super.produceTicket(authentication, tgtId, clazz); + return super.produceTicket(authentication, tgtId, service, clazz); } } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java index 1595e8389d7818d52807a6c66ef268d1527f1599..866985641c35a8023386341cd52419aa060d40ed 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java @@ -53,8 +53,6 @@ public abstract class Constants { // web: public static final String PORTAL_URL = "portalUrl"; - public static final String VITAM_LOGO = "vitamLogo"; - public static final String VITAM_UI_LARGE_LOGO = "vitamuiLargeLogo"; public static final String VITAM_UI_FAVICON = "vitamuiFavicon"; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java index bb0c3d9e4f5a5990d79805826d504a0b0fb1c85e..65b900ed37d5bdc80ddc73f78980ee7736c32314 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java @@ -46,9 +46,9 @@ import org.apereo.cas.CasProtocolConstants; import org.apereo.cas.configuration.model.support.cookie.TicketGrantingCookieProperties; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.support.WebUtils; +import org.pac4j.core.client.IndirectClient; import org.pac4j.core.util.Pac4jConstants; import org.pac4j.core.util.CommonHelper; -import org.pac4j.saml.client.SAML2Client; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.webflow.context.ExternalContext; @@ -92,7 +92,7 @@ public class Utils { return new ExternalHttpContext(casTenantIdentifier, casToken, "cas+" + username, casIdentity); } - public Event performClientRedirection(final Action action, final SAML2Client client, final RequestContext requestContext) throws IOException { + public Event performClientRedirection(final Action action, final IndirectClient client, final RequestContext requestContext) throws IOException { final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); val service = WebUtils.getService(requestContext); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java index 508107480033cad86088b2da82a5258640da8114..b306699f6f970bdc726dfd861d75e6d3deaa8b57 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java @@ -39,9 +39,8 @@ package fr.gouv.vitamui.cas.webflow.actions; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.apereo.cas.mfa.simple.CasSimpleMultifactorAuthenticationTicketFactory; import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential; -import org.apereo.cas.ticket.TransientSessionTicket; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicket; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.web.support.WebUtils; import org.springframework.webflow.action.AbstractAction; @@ -64,11 +63,11 @@ public class CheckMfaTokenAction extends AbstractAction { protected Event doExecute(final RequestContext requestContext) { val credential = WebUtils.getCredential(requestContext); val tokenCredential = (CasSimpleMultifactorTokenCredential) credential; - val token = CasSimpleMultifactorAuthenticationTicketFactory.PREFIX + "-" + tokenCredential.getToken(); + val token = CasSimpleMultifactorAuthenticationTicket.PREFIX + "-" + tokenCredential.getToken(); LOGGER.debug("Checking token: {}", token); WebUtils.putCredential(requestContext, new CasSimpleMultifactorTokenCredential(token)); - val acct = this.ticketRegistry.getTicket(token, TransientSessionTicket.class); + val acct = this.ticketRegistry.getTicket(token, CasSimpleMultifactorAuthenticationTicket.class); if (acct != null) { val creationTime = acct.getCreationTime(); val now_less_one_minute = ZonedDateTime.now().minus(60, ChronoUnit.SECONDS); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java index a9c876d6afcc452114540dc4bf7c1f22856995b4..59af1cb117859722adf6d03b518f989970fd0243 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java @@ -36,7 +36,7 @@ */ package fr.gouv.vitamui.cas.webflow.actions; -import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; +import fr.gouv.vitamui.cas.provider.Pac4jClientIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; @@ -44,31 +44,16 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import org.apache.commons.lang3.StringUtils; -import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.AuthenticationSystemSupport; -import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.web.DelegatedClientWebflowManager; import org.apereo.cas.web.flow.DelegatedClientAuthenticationAction; -import org.apereo.cas.web.flow.SingleSignOnParticipationStrategy; -import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; -import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; -import org.apereo.cas.web.support.ArgumentExtractor; +import org.apereo.cas.web.flow.DelegatedClientAuthenticationConfigurationContext; import org.apereo.cas.web.support.WebUtils; -import org.pac4j.core.client.Clients; -import org.pac4j.core.context.JEEContext; -import org.pac4j.core.context.session.SessionStore; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import java.io.IOException; -import java.util.List; import lombok.val; @@ -88,8 +73,6 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu private final ProvidersService providersService; - private final CasConfigurationProperties casProperties; - private final Utils utils; private final TicketRegistry ticketRegistry; @@ -98,32 +81,15 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu private final String surrogationSeparator; - public CustomDelegatedClientAuthenticationAction(final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, - final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, - final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, - final Clients clients, - final ServicesManager servicesManager, - final AuditableExecution delegatedAuthenticationPolicyEnforcer, - final DelegatedClientWebflowManager delegatedClientWebflowManager, - final AuthenticationSystemSupport authenticationSystemSupport, - final CasConfigurationProperties casProperties, - final AuthenticationServiceSelectionPlan authenticationRequestServiceSelectionStrategies, - final CentralAuthenticationService centralAuthenticationService, - final SingleSignOnParticipationStrategy singleSignOnParticipationStrategy, - final SessionStore<JEEContext> sessionStore, - final List<ArgumentExtractor> argumentExtractors, + public CustomDelegatedClientAuthenticationAction(final DelegatedClientAuthenticationConfigurationContext context, final IdentityProviderHelper identityProviderHelper, final ProvidersService providersService, final Utils utils, final TicketRegistry ticketRegistry, final String vitamuiPortalUrl, final String surrogationSeparator) { - super(initialAuthenticationAttemptWebflowEventResolver, serviceTicketRequestWebflowEventResolver, adaptiveAuthenticationPolicy, - clients, servicesManager, delegatedAuthenticationPolicyEnforcer, delegatedClientWebflowManager, authenticationSystemSupport, - casProperties, authenticationRequestServiceSelectionStrategies, centralAuthenticationService, singleSignOnParticipationStrategy, - sessionStore, argumentExtractors); + super(context); this.identityProviderHelper = identityProviderHelper; - this.casProperties = casProperties; this.providersService = providersService; this.utils = utils; this.ticketRegistry = ticketRegistry; @@ -185,8 +151,8 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu val optProvider = identityProviderHelper.findByTechnicalName(providersService.getProviders(), idp); if (optProvider.isPresent()) { val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); - response.addCookie(utils.buildIdpCookie(idp, casProperties.getTgc())); - val client = ((SamlIdentityProviderDto) optProvider.get()).getSaml2Client(); + response.addCookie(utils.buildIdpCookie(idp, configContext.getCasProperties().getTgc())); + val client = ((Pac4jClientIdentityProviderDto) optProvider.get()).getClient(); LOGGER.debug("Force redirect to the SAML IdP: {}", client.getName()); try { return utils.performClientRedirection(this, client, context); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java index a2c46c79771a090df63dad64a6a83ddb9154fd7b..8353e44268567093dddff40ffecfee164b14ddfd 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java @@ -37,78 +37,83 @@ package fr.gouv.vitamui.cas.webflow.actions; import fr.gouv.vitamui.cas.util.Utils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.principal.Principal; -import org.apereo.cas.configuration.model.support.mfa.CasSimpleMultifactorProperties; -import org.apereo.cas.mfa.simple.CasSimpleMultifactorAuthenticationHandler; -import org.apereo.cas.mfa.simple.CasSimpleMultifactorAuthenticationTicketFactory; +import org.apereo.cas.configuration.model.support.mfa.CasSimpleMultifactorAuthenticationProperties; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCommunicationStrategy; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicket; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicketFactory; +import org.apereo.cas.mfa.simple.web.flow.CasSimpleMultifactorSendTokenAction; +import org.apereo.cas.notifications.CommunicationsManager; import org.apereo.cas.ticket.Ticket; -import org.apereo.cas.ticket.TransientSessionTicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.util.CollectionUtils; -import org.apereo.cas.util.io.CommunicationsManager; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.support.WebUtils; -import org.springframework.binding.message.MessageBuilder; -import org.springframework.webflow.action.AbstractAction; -import org.springframework.webflow.action.EventFactorySupport; -import org.springframework.webflow.core.collection.LocalAttributeMap; + +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; +/** + * The custom action to send SMS for the MFA simple token. + */ @Slf4j -@RequiredArgsConstructor -public class CustomSendTokenAction extends AbstractAction { - private static final String MESSAGE_MFA_TOKEN_SENT = "cas.mfa.simple.label.tokensent"; +public class CustomSendTokenAction extends CasSimpleMultifactorSendTokenAction { - private final TicketRegistry ticketRegistry; - private final CommunicationsManager communicationsManager; - private final TransientSessionTicketFactory ticketFactory; - private final CasSimpleMultifactorProperties properties; private final Utils utils; + public CustomSendTokenAction(final TicketRegistry ticketRegistry, + final CommunicationsManager communicationsManager, + final CasSimpleMultifactorAuthenticationTicketFactory ticketFactory, + final CasSimpleMultifactorAuthenticationProperties properties, + final CasSimpleMultifactorTokenCommunicationStrategy tokenCommunicationStrategy, + final Utils utils) { + super(ticketRegistry, communicationsManager, ticketFactory, properties, tokenCommunicationStrategy); + this.utils = utils; + } + + @Override + protected boolean isSmsSent(final CommunicationsManager communicationsManager, + final CasSimpleMultifactorAuthenticationProperties properties, + final Principal principal, + final Ticket token) { + if (communicationsManager.isSmsSenderDefined()) { + val smsProperties = properties.getSms(); + String smsText = StringUtils.isNotBlank(smsProperties.getText()) + ? smsProperties.getFormattedText(token.getId()) + : token.getId(); + // CUSTO: remove the prefix + smsText = smsText.replace(CasSimpleMultifactorAuthenticationTicket.PREFIX + "-", ""); + return communicationsManager.sms(principal, smsProperties.getAttributeName(), smsText, smsProperties.getFrom()); + } + return false; + } + @Override protected Event doExecute(final RequestContext requestContext) { val authentication = WebUtils.getInProgressAuthentication(); - val principal = authentication.getPrincipal(); - val principalAttributes = principal.getAttributes(); + val principal = resolvePrincipal(authentication.getPrincipal()); - // custo + // check for a principal attribute and redirect to a custom page when missing + val principalAttributes = principal.getAttributes(); val mobile = (String) utils.getAttributeValue(principalAttributes, "mobile"); if (mobile == null) { requestContext.getFlowScope().put("firstname", utils.getAttributeValue(principalAttributes, "firstname")); return getEventFactorySupport().event(this, "missingPhone"); } - val service = WebUtils.getService(requestContext); - val token = ticketFactory.create(service, CollectionUtils.wrap(CasSimpleMultifactorAuthenticationHandler.PROPERTY_PRINCIPAL, principal)); - LOGGER.debug("Created multifactor authentication token [{}] for service [{}]", token, service); + // remove token + WebUtils.removeSimpleMultifactorAuthenticationToken(requestContext); - val smsSent = isSmsSent(communicationsManager, properties, principal, token); - val emailSent = isMailSent(communicationsManager, properties, principal, token); + val event = super.doExecute(requestContext); - if (smsSent || emailSent) { - ticketRegistry.addTicket(token); - LOGGER.debug("Successfully submitted token via SMS and/or email to [{}]", principal.getId()); - - val resolver = new MessageBuilder() - .info() - .code(MESSAGE_MFA_TOKEN_SENT) - .defaultText(MESSAGE_MFA_TOKEN_SENT) - .build(); - requestContext.getMessageContext().addMessage(resolver); - - // custo + // add the obfuscated phone to the webflow in case of success + if (CasWebflowConstants.TRANSITION_ID_SUCCESS.equals(event.getId())) { requestContext.getFlowScope().put("mobile", obfuscateMobile(mobile)); - - val attributes = new LocalAttributeMap("token", token.getId()); - return new EventFactorySupport().event(this, CasWebflowConstants.TRANSITION_ID_SUCCESS, attributes); } - LOGGER.error("Both email and SMS communication strategies failed to submit token [{}] to user", token); - return error(); + + return event; } private String obfuscateMobile(final String mobile) { @@ -116,31 +121,4 @@ public class CustomSendTokenAction extends AbstractAction { m = m.substring(0, 2) + " XX XX XX " + m.substring(m.length() - 2); return m; } - - private boolean isSmsSent(final CommunicationsManager communicationsManager, - final CasSimpleMultifactorProperties properties, - final Principal principal, - final Ticket token) { - if (communicationsManager.isSmsSenderDefined()) { - val smsProperties = properties.getSms(); - String smsText = StringUtils.isNotBlank(smsProperties.getText()) - ? smsProperties.getFormattedText(token.getId()) - : token.getId(); - // custo: remove the CAS prefix - smsText = smsText.replace(CasSimpleMultifactorAuthenticationTicketFactory.PREFIX + "-", ""); - return communicationsManager.sms(principal, smsProperties.getAttributeName(), smsText, smsProperties.getFrom()); - } - return false; - } - - private boolean isMailSent(final CommunicationsManager communicationsManager, - final CasSimpleMultifactorProperties properties, - final Principal principal, - final Ticket token) { - if (communicationsManager.isMailSenderDefined()) { - val mailProperties = properties.getMail(); - return communicationsManager.email(principal, mailProperties.getAttributeName(), mailProperties, mailProperties.getFormattedBody(token.getId())); - } - return false; - } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java deleted file mode 100644 index ec00f2d7f3d4cd566a8171caa9d9d5d826f5a3cb..0000000000000000000000000000000000000000 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java +++ /dev/null @@ -1,80 +0,0 @@ -package fr.gouv.vitamui.cas.webflow.actions; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.apache.commons.lang3.StringUtils; -import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.pm.BasePasswordManagementService; -import org.apereo.cas.pm.PasswordManagementService; -import org.apereo.cas.pm.web.flow.PasswordManagementWebflowUtils; -import org.apereo.cas.ticket.InvalidTicketException; -import org.apereo.cas.ticket.TransientSessionTicket; -import org.apereo.cas.web.support.WebUtils; -import org.springframework.webflow.action.AbstractAction; -import org.springframework.webflow.action.EventFactorySupport; -import org.springframework.webflow.execution.Event; -import org.springframework.webflow.execution.RequestContext; - -/** - * A custom verify passwod rest action which deals with expired (removed from registry) tickets. - */ -@Slf4j -@RequiredArgsConstructor -public class CustomVerifyPasswordResetRequestAction extends AbstractAction { - private final CasConfigurationProperties casProperties; - private final PasswordManagementService passwordManagementService; - private final CentralAuthenticationService centralAuthenticationService; - - @Override - protected Event doExecute(final RequestContext requestContext) { - - val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); - val transientTicket = request.getParameter(PasswordManagementWebflowUtils.REQUEST_PARAMETER_NAME_PASSWORD_RESET_TOKEN); - - if (StringUtils.isBlank(transientTicket)) { - LOGGER.error("Password reset token is missing"); - return error(); - } - - TransientSessionTicket tst = null; - try { - tst = this.centralAuthenticationService.getTicket(transientTicket, TransientSessionTicket.class); - } catch (final InvalidTicketException e) {} - if (tst == null) { - LOGGER.error("Unable to locate token [{}] in the ticket registry", transientTicket); - return error(); - } - - val token = tst.getProperties().get(PasswordManagementWebflowUtils.FLOWSCOPE_PARAMETER_NAME_TOKEN).toString(); - val username = passwordManagementService.parseToken(token); - if (StringUtils.isBlank(username)) { - LOGGER.error("Password reset token could not be verified"); - return error(); - } - this.centralAuthenticationService.deleteTicket(tst.getId()); - - PasswordManagementWebflowUtils.putPasswordResetToken(requestContext, token); - val pm = casProperties.getAuthn().getPm(); - if (pm.getReset().isSecurityQuestionsEnabled()) { - val questions = BasePasswordManagementService - .canonicalizeSecurityQuestions(passwordManagementService.getSecurityQuestions(username)); - if (questions.isEmpty()) { - LOGGER.error("No security questions could be found for [{}]", username); - return error(); - } - PasswordManagementWebflowUtils.putPasswordResetSecurityQuestions(requestContext, questions); - } else { - LOGGER.debug("Security questions are not enabled"); - } - - PasswordManagementWebflowUtils.putPasswordResetUsername(requestContext, username); - PasswordManagementWebflowUtils.putPasswordResetSecurityQuestionsEnabled(requestContext, pm.getReset().isSecurityQuestionsEnabled()); - - if (pm.getReset().isSecurityQuestionsEnabled()) { - return success(); - } - return new EventFactorySupport().event(this, "questionsDisabled"); - } -} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java index c16043b0aba3c5db8d118b8ddc95b3fba2bad13a..18ae0d8ef70072d9396531d48810fa5bde68b807 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java @@ -36,7 +36,7 @@ */ package fr.gouv.vitamui.cas.webflow.actions; -import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; +import fr.gouv.vitamui.cas.provider.Pac4jClientIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; @@ -133,14 +133,14 @@ public class DispatcherAction extends AbstractAction { val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); boolean isInternal; - val provider = (SamlIdentityProviderDto) identityProviderHelper.findByUserIdentifier(providersService.getProviders(), dispatchedUser).orElse(null); + val provider = (Pac4jClientIdentityProviderDto) identityProviderHelper.findByUserIdentifier(providersService.getProviders(), dispatchedUser).orElse(null); if (provider != null) { isInternal = provider.getInternal(); } else { return new Event(this, BAD_CONFIGURATION); } val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); - val webContext = new JEEContext(request, response, sessionStore); + val webContext = new JEEContext(request, response); if (isInternal) { sessionStore.set(webContext, Constants.SURROGATE, null); LOGGER.debug("Redirect the user to the password page..."); @@ -153,7 +153,7 @@ public class DispatcherAction extends AbstractAction { sessionStore.set(webContext, Constants.SURROGATE, surrogate); } - return utils.performClientRedirection(this, provider.getSaml2Client(), requestContext); + return utils.performClientRedirection(this, provider.getClient(), requestContext); } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java index 9d24bf8ed5728c02fbb2c45ce056d7be72f0cc17..44e835c8b268d7775e714272da21a77a8023cd65 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java @@ -36,7 +36,6 @@ */ package fr.gouv.vitamui.cas.webflow.actions; -import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; @@ -44,7 +43,6 @@ import fr.gouv.vitamui.commons.rest.client.ExternalHttpContext; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.CentralAuthenticationService; @@ -55,9 +53,9 @@ import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.principal.SimpleWebApplicationServiceImpl; import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.configuration.model.core.logout.LogoutProperties; import org.apereo.cas.logout.LogoutManager; -import org.apereo.cas.logout.slo.SingleLogoutRequest; +import org.apereo.cas.logout.SingleLogoutExecutionRequest; +import org.apereo.cas.logout.slo.SingleLogoutRequestContext; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.InvalidTicketException; @@ -65,23 +63,16 @@ import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.TicketGrantingTicketImpl; import org.apereo.cas.ticket.expiration.NeverExpiresExpirationPolicy; import org.apereo.cas.web.cookie.CasCookieBuilder; -import org.apereo.cas.web.flow.logout.FrontChannelLogoutAction; import org.apereo.cas.web.flow.logout.TerminateSessionAction; import org.apereo.cas.web.support.WebUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.webflow.execution.Action; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.xml.bind.DatatypeConverter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import static fr.gouv.vitamui.commons.api.CommonConstants.*; @@ -91,44 +82,37 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.*; * * */ -@Setter @Getter public class GeneralTerminateSessionAction extends TerminateSessionAction { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(GeneralTerminateSessionAction.class); - @Autowired - private Utils utils; + private final Utils utils; - @Autowired - private CasExternalRestClient casExternalRestClient; + private final CasExternalRestClient casExternalRestClient; - @Autowired - private LogoutManager logoutManager; + private final ServicesManager servicesManager; - @Autowired - private ServicesManager servicesManager; + private final CasConfigurationProperties casProperties; - @Autowired - private CasConfigurationProperties casProperties; - - @Autowired - private FrontChannelLogoutAction frontChannelLogoutAction; - - @Value("${theme.vitam-logo:#{null}}") - private String vitamLogoPath; - - @Value("${theme.vitamui-logo-large:#{null}}") - private String vitamuiLargeLogoPath; - - @Value("${theme.vitamui-favicon:#{null}}") - private String vitamuiFaviconPath; + private final Action frontChannelLogoutAction; public GeneralTerminateSessionAction(final CentralAuthenticationService centralAuthenticationService, final CasCookieBuilder ticketGrantingTicketCookieGenerator, final CasCookieBuilder warnCookieGenerator, - final LogoutProperties logoutProperties) { - super(centralAuthenticationService, ticketGrantingTicketCookieGenerator, warnCookieGenerator, logoutProperties); + final LogoutManager logoutManager, + final ConfigurableApplicationContext applicationContext, + final Utils utils, + final CasExternalRestClient casExternalRestClient, + final ServicesManager servicesManager, + final CasConfigurationProperties casProperties, + final Action frontChannelLogoutAction) { + super(centralAuthenticationService, ticketGrantingTicketCookieGenerator, warnCookieGenerator, casProperties.getLogout(), logoutManager, applicationContext); + this.utils = utils; + this.casExternalRestClient = casExternalRestClient; + this.servicesManager = servicesManager; + this.casProperties = casProperties; + this.frontChannelLogoutAction = frontChannelLogoutAction; } @Override @SneakyThrows @@ -176,58 +160,25 @@ public class GeneralTerminateSessionAction extends TerminateSessionAction { // fallback cases: // no CAS cookie -> general logout if (tgtId == null) { - final List<SingleLogoutRequest> logoutRequests = performGeneralLogout("nocookie"); + final List<SingleLogoutRequestContext> logoutRequests = performGeneralLogout("nocookie"); WebUtils.putLogoutRequests(context, logoutRequests); // no ticket or expired -> general logout } else if (ticket == null || ticket.isExpired()) { - final List<SingleLogoutRequest> logoutRequests = performGeneralLogout(tgtId); + final List<SingleLogoutRequestContext> logoutRequests = performGeneralLogout(tgtId); WebUtils.putLogoutRequests(context, logoutRequests); } // if we are in the login webflow, compute the logout URLs if ("login".equals(context.getFlowExecutionContext().getDefinition().getId())) { logger.debug("Computing front channel logout URLs"); - frontChannelLogoutAction.doExecute(context); - } - - final MutableAttributeMap<Object> flowScope = context.getFlowScope(); - if (vitamLogoPath != null) { - try { - final Path logoFile = Paths.get(vitamLogoPath); - final String logo = DatatypeConverter.printBase64Binary(Files.readAllBytes(logoFile)); - flowScope.put(Constants.VITAM_LOGO, logo); - } - catch (final IOException e) { - LOGGER.warn("Can't find vitam logo", e); - } - } - if (vitamuiLargeLogoPath != null) { - try { - final Path logoFile = Paths.get(vitamuiLargeLogoPath); - final String logo = DatatypeConverter.printBase64Binary(Files.readAllBytes(logoFile)); - flowScope.put(Constants.VITAM_UI_LARGE_LOGO, logo); - } - catch (final IOException e) { - LOGGER.warn("Can't find vitam ui large logo", e); - } - } - - if (vitamuiFaviconPath != null) { - try { - final Path faviconFile = Paths.get(vitamuiFaviconPath); - final String favicon = DatatypeConverter.printBase64Binary(Files.readAllBytes(faviconFile)); - flowScope.put(Constants.VITAM_UI_FAVICON, favicon); - } - catch (final IOException e) { - LOGGER.warn("Can't find vitam ui favicon", e); - } + frontChannelLogoutAction.execute(context); } return event; } - protected List<SingleLogoutRequest> performGeneralLogout(final String tgtId) { + protected List<SingleLogoutRequestContext> performGeneralLogout(final String tgtId) { try { final Map<String, AuthenticationHandlerExecutionResult> successes = new HashMap<>(); @@ -254,7 +205,10 @@ public class GeneralTerminateSessionAction extends TerminateSessionAction { } } - return logoutManager.performLogout(fakeTgt); + return logoutManager.performLogout( + SingleLogoutExecutionRequest.builder() + .ticketGrantingTicket(fakeTgt) + .build()); } catch (final RuntimeException e) { logger.error("Unable to perform general logout", e); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java index 7e85426f5b93cc3bc5ef4fc0cc3aa44a84248747..7310ad8e6475e6ad77c027eeae9edb3022fdb671 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java @@ -37,15 +37,18 @@ package fr.gouv.vitamui.cas.webflow.actions; import org.apache.commons.lang3.StringUtils; +import org.apereo.cas.audit.AuditActionResolvers; +import org.apereo.cas.audit.AuditResourceResolvers; +import org.apereo.cas.audit.AuditableActions; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.notifications.CommunicationsManager; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.pm.web.flow.actions.SendPasswordResetInstructionsAction; import org.apereo.cas.ticket.TicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.util.io.CommunicationsManager; import org.apereo.cas.web.support.WebUtils; -import org.springframework.beans.factory.annotation.Value; +import org.apereo.inspektr.audit.annotation.Audit; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.webflow.core.collection.MutableAttributeMap; @@ -78,8 +81,7 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn private final Utils utils; - @Value("${theme.vitamui-platform-name:VITAM-UI}") - private String vitamuiPlatformName; + private final String vitamuiPlatformName; public I18NSendPasswordResetInstructionsAction(final CasConfigurationProperties casProperties, final CommunicationsManager communicationsManager, @@ -89,27 +91,31 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn final HierarchicalMessageSource messageSource, final ProvidersService providersService, final IdentityProviderHelper identityProviderHelper, - final Utils utils) { - super(casProperties, communicationsManager, passwordManagementService, ticketRegistry, ticketFactory); + final Utils utils, + final String vitamuiPlatformName) { + super(casProperties, communicationsManager, passwordManagementService, ticketRegistry, ticketFactory, null); this.messageSource = messageSource; this.providersService = providersService; this.identityProviderHelper = identityProviderHelper; this.utils = utils; + this.vitamuiPlatformName = vitamuiPlatformName; } + @Audit(action = AuditableActions.REQUEST_CHANGE_PASSWORD, + principalResolverName = "REQUEST_CHANGE_PASSWORD_PRINCIPAL_RESOLVER", + actionResolverName = AuditActionResolvers.REQUEST_CHANGE_PASSWORD_ACTION_RESOLVER, + resourceResolverName = AuditResourceResolvers.REQUEST_CHANGE_PASSWORD_RESOURCE_RESOLVER) @Override protected Event doExecute(final RequestContext requestContext) { - communicationsManager.validate(); - if (!communicationsManager.isMailSenderDefined()) { + if (!communicationsManager.isMailSenderDefined() && !communicationsManager.isSmsSenderDefined()) { return getErrorEvent("contact.failed", "Unable to send email as no mail sender is defined", requestContext); } + // CUSTO: try to get the username from the credentials also (after a password expiration) val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); request.removeAttribute(PmTransientSessionTicketExpirationPolicyBuilder.PM_EXPIRATION_IN_MINUTES_ATTRIBUTE); String username = request.getParameter("username"); - // added from CAS: if (StringUtils.isBlank(username)) { - // try to get the username from the credentials also (after a password expiration) final MutableAttributeMap<Object> flowScope = requestContext.getFlowScope(); final Object credential = flowScope.get("credential"); if (credential instanceof UsernamePasswordCredential) { @@ -117,13 +123,13 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn username = usernamePasswordCredential.getUsername(); } } + val query = buildPasswordManagementQuery(requestContext); if (StringUtils.isBlank(username)) { - LOGGER.warn("No username parameter is provided"); return getErrorEvent("username.required", "No username is provided", requestContext); } - // changed from CAS: - final String email = passwordManagementService.findEmail(username); + val email = passwordManagementService.findEmail(query); + // CUSTO: only retrieve email (and not phone) and force success event (instead of error) when failure if (StringUtils.isBlank(email)) { LOGGER.warn("No recipient is provided; nonetheless, we return to the success page"); return success(); @@ -133,23 +139,26 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn } val service = WebUtils.getService(requestContext); - val url = buildPasswordResetUrl(username, passwordManagementService, casProperties, service); + val url = buildPasswordResetUrl(query.getUsername(), passwordManagementService, casProperties, service); if (StringUtils.isNotBlank(url)) { val pm = casProperties.getAuthn().getPm(); - LOGGER.debug("Generated password reset URL [{}]; Link is only active for the next [{}] minute(s)", utils.sanitizePasswordResetUrl(url), - pm.getReset().getExpirationMinutes()); - if (sendPasswordResetEmailToAccount(email, url)) { - return success(); + LOGGER.debug("Generated password reset URL [{}]; Link is only active for the next [{}] minute(s)", + url, pm.getReset().getExpirationMinutes()); + // CUSTO: only send email (and not SMS) + val sendEmail = sendPasswordResetEmailToAccount(query.getUsername(), email, url, requestContext); + if (sendEmail) { + return success(url); } } else { LOGGER.error("No password reset URL could be built and sent to [{}]", email); } LOGGER.error("Failed to notify account [{}]", email); - return getErrorEvent("contact.failed", "Failed to send the password reset link to the given email address or phone number", requestContext); + return getErrorEvent("contact.failed", "Failed to send the password reset link via email address or phone", requestContext); } @Override - protected boolean sendPasswordResetEmailToAccount(final String to, final String url) { + protected boolean sendPasswordResetEmailToAccount(final String username, final String to, final String url, + final RequestContext requestContext) { final PmMessageToSend messageToSend = PmMessageToSend.buildMessage(messageSource, "", "", String.valueOf(casProperties.getAuthn().getPm().getReset().getExpirationMinutes()), url, vitamuiPlatformName, LocaleContextHolder.getLocale()); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..3b132ba0f3ca9b113bff487a72fe037195cee08e --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java @@ -0,0 +1,125 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package fr.gouv.vitamui.cas.webflow.configurer; + +import lombok.val; +import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential; +import org.apereo.cas.util.CollectionUtils; +import org.apereo.cas.web.flow.CasWebflowConstants; +import org.apereo.cas.web.flow.configurer.AbstractCasMultifactorWebflowConfigurer; +import org.apereo.cas.web.flow.configurer.CasMultifactorWebflowCustomizer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.builder.support.FlowBuilderServices; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Custom webflow for simple MFA. + */ +public class CustomCasSimpleMultifactorWebflowConfigurer extends AbstractCasMultifactorWebflowConfigurer { + + public static final String MFA_SIMPLE_EVENT_ID = "mfa-simple"; + + public CustomCasSimpleMultifactorWebflowConfigurer(final FlowBuilderServices flowBuilderServices, + final FlowDefinitionRegistry loginFlowDefinitionRegistry, + final FlowDefinitionRegistry flowDefinitionRegistry, + final ConfigurableApplicationContext applicationContext, + final CasConfigurationProperties casProperties, + final List<CasMultifactorWebflowCustomizer> mfaFlowCustomizers) { + super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext, + casProperties, Optional.of(flowDefinitionRegistry), mfaFlowCustomizers); + } + + @Override + protected void doInitialize() { + multifactorAuthenticationFlowDefinitionRegistries.forEach(registry -> { + val flow = getFlow(registry, MFA_SIMPLE_EVENT_ID); + createFlowVariable(flow, CasWebflowConstants.VAR_ID_CREDENTIAL, CasSimpleMultifactorTokenCredential.class); + flow.getStartActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_INITIAL_FLOW_SETUP)); + + val initLoginFormState = createActionState(flow, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM, + createEvaluateAction(CasWebflowConstants.ACTION_ID_INIT_LOGIN_ACTION)); + createTransitionForState(initLoginFormState, CasWebflowConstants.TRANSITION_ID_SUCCESS, "sendSimpleToken"); + setStartState(flow, initLoginFormState); + createEndState(flow, CasWebflowConstants.STATE_ID_SUCCESS); + createEndState(flow, CasWebflowConstants.STATE_ID_UNAVAILABLE); + + val sendSimpleToken = createActionState(flow, "sendSimpleToken", + createEvaluateAction("mfaSimpleMultifactorSendTokenAction")); + createTransitionForState(sendSimpleToken, CasWebflowConstants.TRANSITION_ID_ERROR, CasWebflowConstants.STATE_ID_UNAVAILABLE); + createTransitionForState(sendSimpleToken, CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM); + // CUSTO: + createTransitionForState(sendSimpleToken, "missingPhone", "missingPhone"); + createViewState(flow, "missingPhone", "casSmsMissingPhoneView"); + // + + val setPrincipalAction = createSetAction("viewScope.principal", "conversationScope.authentication.principal"); + val propertiesToBind = CollectionUtils.wrapList("token"); + val binder = createStateBinderConfiguration(propertiesToBind); + val viewLoginFormState = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, + "simple-mfa/casSimpleMfaLoginView", binder); + createStateModelBinding(viewLoginFormState, CasWebflowConstants.VAR_ID_CREDENTIAL, CasSimpleMultifactorTokenCredential.class); + viewLoginFormState.getEntryActionList().add(setPrincipalAction); + + // CUSTO: instead of CasWebflowConstants.STATE_ID_REAL_SUBMIT, send to intermediateSubmit + createTransitionForState(viewLoginFormState, CasWebflowConstants.TRANSITION_ID_SUBMIT, + "intermediateSubmit", Map.of("bind", Boolean.TRUE, "validate", Boolean.TRUE)); + createTransitionForState(viewLoginFormState, CasWebflowConstants.TRANSITION_ID_RESEND, "sendSimpleToken", + Map.of("bind", Boolean.FALSE, "validate", Boolean.FALSE)); + + // CUSTO: + val intermediateSubmit = createActionState(flow, "intermediateSubmit", createEvaluateAction("checkMfaTokenAction")); + createTransitionForState(intermediateSubmit, CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_REAL_SUBMIT); + createTransitionForState(intermediateSubmit, CasWebflowConstants.TRANSITION_ID_ERROR, "codeExpired"); + val codeExpired = createViewState(flow, "codeExpired", "casSmsCodeExpiredView"); + createTransitionForState(codeExpired, "resend", CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + // + + val realSubmitState = createActionState(flow, CasWebflowConstants.STATE_ID_REAL_SUBMIT, + createEvaluateAction(CasWebflowConstants.ACTION_ID_OTP_AUTHENTICATION_ACTION)); + createTransitionForState(realSubmitState, CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_SUCCESS); + createTransitionForState(realSubmitState, CasWebflowConstants.TRANSITION_ID_ERROR, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM); + }); + + registerMultifactorProviderAuthenticationWebflow(getLoginFlow(), MFA_SIMPLE_EVENT_ID, + casProperties.getAuthn().getMfa().getSimple().getId()); + } +} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java index 59ab31491719f0fedc26217e1926d6dd3e0f7994..a3f4cbcb3dbf4da7022b9659be5b13521d3186db 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java @@ -36,10 +36,7 @@ */ package fr.gouv.vitamui.cas.webflow.configurer; -import javax.security.auth.login.AccountLockedException; -import javax.security.auth.login.AccountNotFoundException; -import javax.security.auth.login.CredentialExpiredException; -import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.*; import fr.gouv.vitamui.cas.webflow.actions.DispatcherAction; import lombok.val; @@ -56,7 +53,7 @@ import org.apereo.cas.ticket.UnsatisfiedAuthenticationPolicyException; import org.apereo.cas.util.CollectionUtils; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.flow.configurer.DefaultLoginWebflowConfigurer; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; import org.springframework.webflow.engine.ActionState; import org.springframework.webflow.engine.Flow; @@ -84,16 +81,8 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer private static final String BAD_CONFIGURATION_VIEW = "casAccountBadConfigurationView"; - public static final String DIRECT_RESULT = "direct"; - - private static final String DIRECT_ACTION = "directRedirection"; - - public static final String INDIRECT_RESULT = "indirect"; - - private static final String INDIRECT_ACTION = "indirectRedirection"; - public CustomLoginWebflowConfigurer(final FlowBuilderServices flowBuilderServices, final FlowDefinitionRegistry flowDefinitionRegistry, - final ApplicationContext applicationContext, final CasConfigurationProperties casProperties) { + final ConfigurableApplicationContext applicationContext, final CasConfigurationProperties casProperties) { super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties); } @@ -106,7 +95,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer CasWebflowConstants.STATE_ID_GATEWAY_REQUEST_CHECK); createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_TICKET_GRANTING_TICKET_INVALID, CasWebflowConstants.STATE_ID_TERMINATE_SESSION); - // instead of STATE_ID_HAS_SERVICE_CHECK, send to STATE_ID_TRIGGER_CHANGE_PASSWORD + // CUSTO: instead of STATE_ID_HAS_SERVICE_CHECK, send to STATE_ID_TRIGGER_CHANGE_PASSWORD createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_TICKET_GRANTING_TICKET_VALID, STATE_ID_TRIGGER_CHANGE_PASSWORD); @@ -115,7 +104,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer private void createTriggerChangePasswordAction(final Flow flow) { final ActionState action = createActionState(flow, STATE_ID_TRIGGER_CHANGE_PASSWORD, "triggerChangePasswordAction"); - createTransitionForState(action, TriggerChangePasswordAction.EVENT_ID_CHANGE_PASSWORD, CasWebflowConstants.VIEW_ID_MUST_CHANGE_PASSWORD); + createTransitionForState(action, TriggerChangePasswordAction.EVENT_ID_CHANGE_PASSWORD, CasWebflowConstants.STATE_ID_MUST_CHANGE_PASSWORD); createTransitionForState(action, TriggerChangePasswordAction.EVENT_ID_CONTINUE, CasWebflowConstants.STATE_ID_HAS_SERVICE_CHECK); } @@ -123,21 +112,27 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer protected void createHandleAuthenticationFailureAction(final Flow flow) { val handler = createActionState(flow, CasWebflowConstants.STATE_ID_HANDLE_AUTHN_FAILURE, CasWebflowConstants.ACTION_ID_AUTHENTICATION_EXCEPTION_HANDLER); - createTransitionForState(handler, AccountDisabledException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_ACCOUNT_DISABLED); - createTransitionForState(handler, AccountLockedException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_ACCOUNT_LOCKED); - // custo: - createTransitionForState(handler, AccountPasswordMustChangeException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_SEND_RESET_PASSWORD_ACCT_INFO); - createTransitionForState(handler, CredentialExpiredException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_EXPIRED_PASSWORD); - createTransitionForState(handler, InvalidLoginLocationException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_INVALID_WORKSTATION); - createTransitionForState(handler, InvalidLoginTimeException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_INVALID_AUTHENTICATION_HOURS); + createTransitionForState(handler, AccountDisabledException.class.getSimpleName(), CasWebflowConstants.STATE_ID_ACCOUNT_DISABLED); + createTransitionForState(handler, AccountLockedException.class.getSimpleName(), CasWebflowConstants.STATE_ID_ACCOUNT_LOCKED); + createTransitionForState(handler, AccountExpiredException.class.getSimpleName(), CasWebflowConstants.STATE_ID_EXPIRED_PASSWORD); + createTransitionForState(handler, AccountLockedException.class.getSimpleName(), CasWebflowConstants.STATE_ID_ACCOUNT_LOCKED); + // CUSTO: instead of STATE_ID_MUST_CHANGE_PASSWORD, send to STATE_ID_SEND_RESET_PASSWORD_ACCT_INFO + createTransitionForState(handler, AccountPasswordMustChangeException.class.getSimpleName(), CasWebflowConstants.STATE_ID_SEND_RESET_PASSWORD_ACCT_INFO); + createTransitionForState(handler, CredentialExpiredException.class.getSimpleName(), CasWebflowConstants.STATE_ID_EXPIRED_PASSWORD); + createTransitionForState(handler, InvalidLoginLocationException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INVALID_WORKSTATION); + createTransitionForState(handler, InvalidLoginTimeException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INVALID_AUTHENTICATION_HOURS); createTransitionForState(handler, FailedLoginException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); createTransitionForState(handler, AccountNotFoundException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); - createTransitionForState(handler, UnauthorizedServiceForPrincipalException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + createTransitionForState(handler, UnauthorizedServiceForPrincipalException.class.getSimpleName(), + CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); createTransitionForState(handler, PrincipalException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); - createTransitionForState(handler, UnsatisfiedAuthenticationPolicyException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); - createTransitionForState(handler, UnauthorizedAuthenticationException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_AUTHENTICATION_BLOCKED); + createTransitionForState(handler, UnsatisfiedAuthenticationPolicyException.class.getSimpleName(), + CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + createTransitionForState(handler, UnauthorizedAuthenticationException.class.getSimpleName(), + CasWebflowConstants.STATE_ID_AUTHENTICATION_BLOCKED); createTransitionForState(handler, CasWebflowConstants.STATE_ID_SERVICE_UNAUTHZ_CHECK, CasWebflowConstants.STATE_ID_SERVICE_UNAUTHZ_CHECK); createStateDefaultTransition(handler, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + handler.getEntryActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_CLEAR_WEBFLOW_CREDENTIALS)); } @Override @@ -145,7 +140,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer val propertiesToBind = CollectionUtils.wrapList("username"); val binder = createStateBinderConfiguration(propertiesToBind); - val state = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, "casLoginView", binder); + val state = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, "login/casLoginView", binder); state.getRenderActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_RENDER_LOGIN_FORM)); createStateModelBinding(state, CasWebflowConstants.VAR_ID_CREDENTIAL, UsernamePasswordCredential.class); @@ -163,7 +158,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer val action = createActionState(flow, INTERMEDIATE_SUBMIT, "dispatcherAction"); createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_SUCCESS, VIEW_PWD_FORM); createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_STOP, CasWebflowConstants.STATE_ID_STOP_WEBFLOW); - createTransitionForState(action, DispatcherAction.DISABLED, CasWebflowConstants.VIEW_ID_ACCOUNT_DISABLED); + createTransitionForState(action, DispatcherAction.DISABLED, CasWebflowConstants.STATE_ID_ACCOUNT_DISABLED); createTransitionForState(action, DispatcherAction.BAD_CONFIGURATION, BAD_CONFIGURATION_VIEW); createEndState(flow, BAD_CONFIGURATION_VIEW, BAD_CONFIGURATION_VIEW); @@ -183,6 +178,6 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer attributes.put("validate", Boolean.TRUE); attributes.put("history", History.INVALIDATE); - createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_RESET_PASSWORD, CasWebflowConstants.VIEW_ID_SEND_RESET_PASSWORD_ACCT_INFO); + createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_RESET_PASSWORD, CasWebflowConstants.STATE_ID_SEND_RESET_PASSWORD_ACCT_INFO); } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java new file mode 100644 index 0000000000000000000000000000000000000000..b90f56a096b71bda341edbb1b12d25abd3ad6416 --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java @@ -0,0 +1,94 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package fr.gouv.vitamui.cas.x509; + +import lombok.val; +import org.apache.commons.lang.StringUtils; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.regex.Pattern; + +/** + * Certificate parser + */ +public class CertificateParser { + + private CertificateParser() {} + + public static String extract(final X509Certificate cert, final X509AttributeMapping mapping) throws CertificateParsingException { + val name = mapping.getName(); + String value = null; + if (X509CertificateAttributes.ISSUER_DN.name().equalsIgnoreCase(name)) { + value = cert.getIssuerDN().getName(); + } else if (X509CertificateAttributes.SUBJECT_DN.name().equalsIgnoreCase(name)) { + value = cert.getSubjectDN().getName(); + } else if (X509CertificateAttributes.SUBJECT_ALTERNATE_NAME.name().equalsIgnoreCase(name)) { + val altNames = cert.getSubjectAlternativeNames(); + if (altNames != null && altNames.size() > 0) { + val altName = altNames.iterator().next(); + if (altName != null && altName.size() == 2) { + value = (String) altName.get(1); + } + } + } + if (value == null) { + throw new CertificateParsingException("Cannot find X509 value for: " + name); + } + val parsing = mapping.getParsing(); + val expansion = mapping.getExpansion(); + if (StringUtils.isNotBlank(parsing)) { + val pattern = Pattern.compile(parsing); + val matcher = pattern.matcher(value); + if (matcher.matches()) { + val groupCount = matcher.groupCount(); + if (groupCount == 0) { + throw new CertificateParsingException("Parsing fails for X509 value: " + value); + } + if (StringUtils.isBlank(expansion)) { + value = matcher.group(1); + } else { + value = expansion; + for (int i = 0; i < groupCount; i++) { + value = value.replace("{" + i + "}", matcher.group(i + 1)); + } + } + } + } + return value; + } +} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..3f04e0229955c068704fb7216cb353bde873ac72 --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java @@ -0,0 +1,126 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package fr.gouv.vitamui.cas.x509; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apereo.cas.adaptors.x509.authentication.X509CertificateExtractor; + +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; + +/** + * Custom certificate extractor from the request. + */ +@Slf4j +public class CustomRequestHeaderX509CertificateExtractor implements X509CertificateExtractor { + + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERT = "-----END CERTIFICATE-----"; + + private final String customCertificateHeader; + + private final boolean x509AuthnMandatory; + + public CustomRequestHeaderX509CertificateExtractor(final String customCertificateHeader, final boolean x509AuthnMandatory) { + this.customCertificateHeader = customCertificateHeader; + this.x509AuthnMandatory = x509AuthnMandatory; + } + + @Override + public X509Certificate[] extract(final HttpServletRequest request) { + final X509Certificate[] certs = internalExtract(request); + if (x509AuthnMandatory && certs == null) { + throw new RuntimeException("Client certificate is mandatory!"); + } + return certs; + } + + protected X509Certificate[] internalExtract(final HttpServletRequest request) { + final String certHeader = request.getHeader(customCertificateHeader); + if (StringUtils.isBlank(certHeader)) { + LOGGER.debug("Certificates not found via custom header: {}", customCertificateHeader); + return null; + } + + X509Certificate cert = null; + try { + cert = parseCertificateGeneratedByNginx(certHeader); + } catch (final Exception e) { + LOGGER.debug("Nginx parsing exception: {}", e.getMessage()); + try { + cert = parseCertificateGeneratedByApache(certHeader); + } catch (final Exception e2) { + LOGGER.debug("Apache parsing exception: {}", e2.getMessage()); + } + } + if (cert == null) { + LOGGER.error("Cannot parse certificate from Apache and Nginx"); + return null; + } + + final X509Certificate[] certificates = new X509Certificate[1]; + certificates[0] = cert; + + LOGGER.debug("[{}] Certificate(s) found via custom header: [{}]", certificates.length, Arrays.toString(certificates)); + return certificates; + } + + protected X509Certificate parseCertificateGeneratedByNginx(final String header) throws CertificateException { + final String data = header.replaceAll("\t", "\n"); + + final String decoded = URLDecoder.decode(data, StandardCharsets.UTF_8); + final String cert = decoded.replace(BEGIN_CERT, "").replace(END_CERT, "").replaceAll(" ","+").replaceAll("\\n", ""); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(cert))); + } + + protected X509Certificate parseCertificateGeneratedByApache(final String header) throws CertificateException { + final String cert = header.replace(BEGIN_CERT, "").replace(END_CERT, "").replaceAll(" ", "").replaceAll("\\n", ""); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(cert))); + } +} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/NoOpAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509AttributeMapping.java similarity index 75% rename from cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/NoOpAction.java rename to cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509AttributeMapping.java index 7f80f6f16884d4c7e71ef8f215bb682504f13017..3418246edb2ad944238677595d988ac7ee52648b 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/NoOpAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509AttributeMapping.java @@ -34,26 +34,34 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -package fr.gouv.vitamui.cas.webflow.actions; - -import lombok.RequiredArgsConstructor; -import org.springframework.webflow.action.AbstractAction; -import org.springframework.webflow.execution.Event; -import org.springframework.webflow.execution.RequestContext; +package fr.gouv.vitamui.cas.x509; /** - * A no-op action returning a specific result. + * Attribute mapping definition for X509 authentication */ -@RequiredArgsConstructor -public class NoOpAction extends AbstractAction { +public class X509AttributeMapping { + + private final String name; + + private final String parsing; - private final String eventId; + private final String expansion; + + public X509AttributeMapping(final String name, final String parsing, final String expansion) { + this.name = name; + this.parsing = parsing; + this.expansion = expansion; + } + + public String getName() { + return name; + } + + public String getParsing() { + return parsing; + } - @Override - protected Event doExecute(final RequestContext requestContext) { - if (eventId != null) { - return getEventFactorySupport().event(this, eventId); - } - return null; + public String getExpansion() { + return expansion; } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/VitamStatusCode.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509CertificateAttributes.java similarity index 55% rename from cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/VitamStatusCode.java rename to cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509CertificateAttributes.java index b871034928ebe0fefe75848321e23e67dd59cd1d..5fa843eeb5d0bcbb6030fb9c35a2b69f5dd98d67 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/VitamStatusCode.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509CertificateAttributes.java @@ -34,70 +34,11 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -package fr.gouv.vitamui.cas.util; +package fr.gouv.vitamui.cas.x509; /** - * Enum StatusCode - * - * different constants status code for workflow , action handler and process + * X509 certificate attributes */ -public enum VitamStatusCode { - - /** - * UNKNOWN : indicates that the workflow or the action handler or the process is in unknown status! - */ - UNKNOWN, - - /** - * STARTED : indicates that the workflow or the action handler or the process has been started - */ - STARTED, - - /** - * ALREADY_EXECUTED : indicates that a particular step / action has already been processed - */ - ALREADY_EXECUTED, - /** - * OK : indicates the successful without warning - */ - OK, - - /** - * WARNING : indicates successful with a general warning. Warning are often useful in preventing future Action - * problems - */ - WARNING, - - /** - * KO : indicates the failed execution of the action - */ - KO, - - /** - * FATAL : indicates a critical error such as technical Exception ( runtime exception, illegal argument exception, - * null pointer exception ...) - */ - FATAL; - - /** - * @return Status Level - */ - public int getStatusLevel() { - return ordinal(); - } - - /** - * @return True if the status is greater or equal to OK - */ - public boolean isGreaterOrEqualToKo() { - return compareTo(KO) >= 0; - } - - /** - * @return True if the status is greater or equal to FATAL - */ - public boolean isGreaterOrEqualToFatal() { - return compareTo(FATAL) >= 0; - } - +public enum X509CertificateAttributes { + ISSUER_DN, SUBJECT_DN, SUBJECT_ALTERNATE_NAME } diff --git a/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java b/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java index 18cc916e62b5817e790df0b735307e2daf3f554f..2c19e8b8534e3247b95e7e37a000cb8484ec7d4a 100644 --- a/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java +++ b/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java @@ -1,40 +1,65 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ package org.apereo.cas; +import org.apereo.cas.util.logging.LoggingInitialization; +import org.apereo.cas.util.spring.boot.AbstractCasBanner; + import lombok.experimental.UtilityClass; import lombok.val; -import org.apereo.cas.util.spring.boot.AbstractCasBanner; +import org.apache.commons.lang3.StringUtils; import org.springframework.boot.Banner; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.jfr.FlightRecorderApplicationStartup; -import java.util.HashMap; -import java.util.Map; +import java.util.Optional; /** - * Copy/pasted from the original CAS class, directly using a custom banner (avoid the search by reflection). + * Copy/pasted from the original CAS class with no reflection to avoid unwanted logs and to retrieve the custom banner. */ @UtilityClass public class CasEmbeddedContainerUtils { - /** - * Property to dictate to the environment whether embedded container is running CAS. - */ - public static final String EMBEDDED_CONTAINER_CONFIG_ACTIVE = "CasEmbeddedContainerConfigurationActive"; - - /** - * Gets runtime properties. - * - * @param embeddedContainerActive the embedded container active - * @return the runtime properties - */ - public static Map<String, Object> getRuntimeProperties(final Boolean embeddedContainerActive) { - val properties = new HashMap<String, Object>(); - properties.put(EMBEDDED_CONTAINER_CONFIG_ACTIVE, embeddedContainerActive); - return properties; + private static final int APPLICATION_EVENTS_CAPACITY = 5_000; + + public static Optional<LoggingInitialization> getLoggingInitialization() { + return Optional.empty(); } - /** - * Gets cas banner instance. - * - * @return the cas banner instance - */ public static Banner getCasBannerInstance() { return new CustomCasBanner(); } @@ -53,4 +78,15 @@ public class CasEmbeddedContainerUtils { " \n"; } } + + public static ApplicationStartup getApplicationStartup() { + val type = StringUtils.defaultIfBlank(System.getProperty("CAS_APP_STARTUP"), "default"); + if (StringUtils.equalsIgnoreCase("jfr", type)) { + return new FlightRecorderApplicationStartup(); + } + if (StringUtils.equalsIgnoreCase("buffering", type)) { + return new BufferingApplicationStartup(APPLICATION_EVENTS_CAPACITY); + } + return ApplicationStartup.DEFAULT; + } } diff --git a/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java b/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java deleted file mode 100644 index bd5f88e3b8b01905416aeeb346b5f9dd05603cf2..0000000000000000000000000000000000000000 --- a/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.apereo.cas.config; - -import org.apereo.cas.configuration.CasConfigurationProperties; - -import org.apereo.cas.ticket.TicketCatalog; -import org.apereo.cas.ticket.TicketDefinition; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - * Override the transient session ticket map timeout. - */ -@Configuration(value = "hazelcastTicketRegistryTicketMetadataCatalogConfiguration", proxyBeanMethods = false) -@EnableConfigurationProperties(CasConfigurationProperties.class) -public class HazelcastTicketRegistryTicketCatalogConfiguration extends BaseTicketDefinitionBuilderSupportConfiguration { - - private final CasConfigurationProperties casProperties; - - public HazelcastTicketRegistryTicketCatalogConfiguration(final CasConfigurationProperties casProperties) { - super(casProperties, new CasTicketCatalogConfigurationValuesProvider() {}); - this.casProperties = casProperties; - } - - @Override - protected void buildAndRegisterTransientSessionTicketDefinition(final TicketCatalog plan, final TicketDefinition metadata) { - final CasTicketCatalogConfigurationValuesProvider configurationValuesProvider = new CasTicketCatalogConfigurationValuesProvider() {}; - metadata.getProperties().setStorageName(configurationValuesProvider.getTransientSessionStorageName().apply(casProperties)); - // changed from CAS: set one day of cache - metadata.getProperties().setStorageTimeout(86400); - super.buildAndRegisterTransientSessionTicketDefinition(plan, metadata); - } -} diff --git a/cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java b/cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..e62c6011955f703a50ccec80b64d3fb54b853148 --- /dev/null +++ b/cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java @@ -0,0 +1,245 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package org.apereo.cas.web.flow.resolver.impl; + +import org.apereo.cas.adaptors.x509.authentication.principal.X509CertificateCredential; +import org.apereo.cas.audit.AuditableContext; +import org.apereo.cas.authentication.AuthenticationException; +import org.apereo.cas.authentication.Credential; +import org.apereo.cas.authentication.principal.Service; +import org.apereo.cas.authentication.principal.WebApplicationService; +import org.apereo.cas.services.RegisteredService; +import org.apereo.cas.ticket.AbstractTicketException; +import org.apereo.cas.util.CollectionUtils; +import org.apereo.cas.util.LoggingUtils; +import org.apereo.cas.util.function.FunctionUtils; +import org.apereo.cas.web.flow.CasWebflowConstants; +import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; +import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; +import org.apereo.cas.web.support.WebUtils; + +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Copy/paste of the original CAS class with a customisation to return an error for a failed X509 login process when mandatory. + */ +@Slf4j +public class DefaultCasDelegatingWebflowEventResolver extends AbstractCasWebflowEventResolver implements CasDelegatingWebflowEventResolver { + + private final List<CasWebflowEventResolver> orderedResolvers = new ArrayList<>(0); + + private final CasWebflowEventResolver selectiveResolver; + + // + // CUSTO + @Value("${vitamui.authn.x509.mandatory:false}") + private boolean x509AuthnMandatory; + // + // + + public DefaultCasDelegatingWebflowEventResolver(final CasWebflowEventResolutionConfigurationContext configurationContext, + final CasWebflowEventResolver selectiveResolver) { + super(configurationContext); + this.selectiveResolver = selectiveResolver; + } + + @Override + public Set<Event> resolveInternal(final RequestContext context) { + val credential = getCredentialFromContext(context); + val service = WebUtils.getService(context); + try { + + if (credential != null) { + val builder = getConfigurationContext().getAuthenticationSystemSupport() + .handleInitialAuthenticationTransaction(service, credential); + builder.getInitialAuthentication().ifPresent(authn -> { + WebUtils.putAuthenticationResultBuilder(builder, context); + WebUtils.putAuthentication(authn, context); + }); + } + + val registeredService = determineRegisteredServiceForEvent(context, service); + LOGGER.trace("Attempting to resolve candidate authentication events for service [{}]", service); + val resolvedEvents = resolveCandidateAuthenticationEvents(context, service, registeredService); + if (!resolvedEvents.isEmpty()) { + LOGGER.trace("Authentication events resolved for [{}] are [{}]. Selecting final event...", service, resolvedEvents); + WebUtils.putResolvedEventsAsAttribute(context, resolvedEvents); + val finalResolvedEvent = this.selectiveResolver.resolveSingle(context); + LOGGER.debug("The final authentication event resolved for [{}] is [{}]", service, finalResolvedEvent); + if (finalResolvedEvent != null) { + return CollectionUtils.wrapSet(finalResolvedEvent); + } + } else { + LOGGER.trace("No candidate authentication events were resolved for service [{}]", service); + } + + val builder = WebUtils.getAuthenticationResultBuilder(context); + if (builder == null) { + val msg = "Unable to locate authentication object in the webflow context"; + throw new IllegalArgumentException(new AuthenticationException(msg)); + } + return CollectionUtils.wrapSet(grantTicketGrantingTicketToAuthenticationResult(context, builder, service)); + } catch (final Exception exception) { + var event = returnAuthenticationExceptionEventIfNeeded(exception, credential, service); + if (event == null) { + FunctionUtils.doIf(LOGGER.isDebugEnabled(), + e -> LOGGER.debug(exception.getMessage(), exception), + e -> LoggingUtils.warn(LOGGER, exception.getMessage(), exception)) + .accept(exception); + event = newEvent(CasWebflowConstants.TRANSITION_ID_ERROR, exception); + } + val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return CollectionUtils.wrapSet(event); + } + } + + @Override + public void addDelegate(final CasWebflowEventResolver r) { + if (r != null) { + orderedResolvers.add(r); + } + } + + @Override + public void addDelegate(final CasWebflowEventResolver r, final int index) { + if (r != null) { + orderedResolvers.add(index, r); + } + } + + /** + * Resolve candidate authentication events set. + * + * @param context the context + * @param service the service + * @param registeredService the registered service + * @return the set + */ + protected Collection<Event> resolveCandidateAuthenticationEvents(final RequestContext context, + final Service service, + final RegisteredService registeredService) { + return this.orderedResolvers + .stream() + .map(resolver -> { + LOGGER.debug("Resolving candidate authentication event for service [{}] using [{}]", service, resolver.getName()); + return resolver.resolveSingle(context); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Event::getId)) + .collect(Collectors.toList()); + } + + private RegisteredService determineRegisteredServiceForEvent(final RequestContext context, final Service service) { + if (service == null) { + return null; + } + LOGGER.trace("Locating authentication event in the request context..."); + val authn = WebUtils.getAuthentication(context); + if (authn == null) { + val msg = "Unable to locate authentication object in the webflow context"; + throw new IllegalArgumentException(new AuthenticationException(msg)); + } + LOGGER.trace("Locating service [{}] in service registry to determine authentication policy", service); + val registeredService = getConfigurationContext().getServicesManager().findServiceBy(service); + LOGGER.trace("Enforcing access strategy policies for registered service [{}] and principal [{}]", + registeredService, authn.getPrincipal()); + val unauthorizedRedirectUrl = registeredService.getAccessStrategy().getUnauthorizedRedirectUrl(); + if (unauthorizedRedirectUrl != null) { + WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, unauthorizedRedirectUrl); + } + + val audit = AuditableContext.builder() + .service(service) + .authentication(authn) + .registeredService(registeredService) + .build(); + val result = getConfigurationContext().getRegisteredServiceAccessStrategyEnforcer().execute(audit); + result.throwExceptionIfNeeded(); + return registeredService; + } + + private Event returnAuthenticationExceptionEventIfNeeded(final Exception exception, + final Credential credential, + final WebApplicationService service) { + + // + // CUSTO + if (x509AuthnMandatory) { + if (credential instanceof X509CertificateCredential) { + throw new IllegalArgumentException("Authentication failure for mandatory X509 login"); + } + } + // + // + + val result = (exception instanceof AuthenticationException || exception instanceof AbstractTicketException) + ? Optional.of(exception) + : (exception.getCause() instanceof AuthenticationException || exception.getCause() instanceof AbstractTicketException) + ? Optional.of(exception.getCause()) + : Optional.empty(); + return result + .map(Exception.class::cast) + .map(ex -> { + FunctionUtils.doIf(LOGGER.isDebugEnabled(), + e -> LOGGER.debug(ex.getMessage(), ex), + e -> LOGGER.warn(ex.getMessage())) + .accept(exception); + val attributes = new LocalAttributeMap<Serializable>(CasWebflowConstants.TRANSITION_ID_ERROR, ex); + attributes.put(Credential.class.getName(), credential); + attributes.put(WebApplicationService.class.getName(), service); + return newEvent(CasWebflowConstants.TRANSITION_ID_AUTHENTICATION_FAILURE, attributes); + }) + .orElse(null); + } +} diff --git a/cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java b/cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java deleted file mode 100644 index 943b3a42286de52cd27c53df29073a32cd219f09..0000000000000000000000000000000000000000 --- a/cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.pac4j.core.client; - -import java.util.*; - -import org.pac4j.core.authorization.generator.AuthorizationGenerator; -import org.pac4j.core.exception.TechnicalException; -import org.pac4j.core.http.ajax.AjaxRequestResolver; -import org.pac4j.core.http.callback.CallbackUrlResolver; -import org.pac4j.core.http.url.UrlResolver; -import org.pac4j.core.util.CommonHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Backported from pac4j v5.1. - */ -@SuppressWarnings({ "unchecked" }) -public class Clients { - - private static final Logger LOGGER = LoggerFactory.getLogger(Clients.class); - - private List<Client> clients; - - private Map<String, Client> clientsMap; - - private Integer oldClientsHash; - - private String callbackUrl; - - private AjaxRequestResolver ajaxRequestResolver; - - private UrlResolver urlResolver; - - private CallbackUrlResolver callbackUrlResolver; - - private List<AuthorizationGenerator> authorizationGenerators = new ArrayList<>(); - - private String defaultSecurityClients; - - public Clients() { - } - - public Clients(final String callbackUrl, final List<Client> clients) { - setCallbackUrl(callbackUrl); - setClients(clients); - } - - public Clients(final String callbackUrl, final Client... clients) { - setCallbackUrl(callbackUrl); - setClients(clients); - } - - public Clients(final List<Client> clients) { - setClients(clients); - } - - public Clients(final Client... clients) { - setClients(clients); - } - - /** - * Populate the resolvers, callback URL and authz generators in the Client - * if defined in Clients and not already in the Client itself. And check the client name. - */ - protected void init() { - // the clients list has changed or has not been initialized yet - if (oldClientsHash == null || oldClientsHash.intValue() != clients.hashCode()) { - synchronized (this) { - if (oldClientsHash == null || oldClientsHash.intValue() != clients.hashCode()) { - clientsMap = new HashMap<>(); - for (final var client : this.clients) { - final var name = client.getName(); - CommonHelper.assertNotBlank("name", name); - final var lowerTrimmedName = name.toLowerCase().trim(); - if (clientsMap.containsKey(lowerTrimmedName)) { - throw new TechnicalException("Duplicate name in clients: " + name); - } - clientsMap.put(lowerTrimmedName, client); - if (client instanceof IndirectClient) { - final var indirectClient = (IndirectClient) client; - if (this.callbackUrl != null && indirectClient.getCallbackUrl() == null) { - indirectClient.setCallbackUrl(this.callbackUrl); - } - if (this.urlResolver != null && indirectClient.getUrlResolver() == null) { - indirectClient.setUrlResolver(this.urlResolver); - } - if (this.callbackUrlResolver != null && indirectClient.getCallbackUrlResolver() == null) { - indirectClient.setCallbackUrlResolver(this.callbackUrlResolver); - } - if (this.ajaxRequestResolver != null && indirectClient.getAjaxRequestResolver() == null) { - indirectClient.setAjaxRequestResolver(this.ajaxRequestResolver); - } - } - final var baseClient = (BaseClient) client; - if (!authorizationGenerators.isEmpty()) { - baseClient.addAuthorizationGenerators(this.authorizationGenerators); - } - } - this.oldClientsHash = this.clients.hashCode(); - } - } - } - } - - /** - * Return the right client according to the specific name. - * - * @param name name of the client - * @return the right client - */ - public Optional<Client> findClient(final String name) { - CommonHelper.assertNotBlank("name", name); - init(); - - final var foundClient = clientsMap.get(name.toLowerCase().trim()); - LOGGER.debug("Found client: {} for name: {}", foundClient, name); - return Optional.ofNullable(foundClient); - } - - /** - * Use {@link #findClient(String)} instead. - */ - @Deprecated - public <C extends Client> Optional<C> findClient(final Class<C> clazz) { - CommonHelper.assertNotNull("clazz", clazz); - init(); - - C foundClient = null; - for (final var client : getClients()) { - if (clazz.isAssignableFrom(client.getClass())) { - foundClient = (C) client; - break; - } - } - LOGGER.debug("Found client: {} for class: {}", foundClient, clazz); - return Optional.ofNullable(foundClient); - } - - /** - * Find all the clients (initialized). - * - * @return all the clients (initialized) - */ - public List<Client> findAllClients() { - init(); - - return getClients(); - } - - public String getCallbackUrl() { - return this.callbackUrl; - } - - public void setCallbackUrl(final String callbackUrl) { - this.callbackUrl = callbackUrl; - } - - public void setClients(final List<Client> clients) { - CommonHelper.assertNotNull("clients", clients); - this.clients = clients; - } - - public void setClients(final Client... clients) { - CommonHelper.assertNotNull("clients", clients); - setClients(new ArrayList<>(Arrays.asList(clients))); - } - - public List<Client> getClients() { - return this.clients; - } - - public AjaxRequestResolver getAjaxRequestResolver() { - return ajaxRequestResolver; - } - - public void setAjaxRequestResolver(final AjaxRequestResolver ajaxRequestResolver) { - this.ajaxRequestResolver = ajaxRequestResolver; - } - - public CallbackUrlResolver getCallbackUrlResolver() { - return callbackUrlResolver; - } - - public void setCallbackUrlResolver(final CallbackUrlResolver callbackUrlResolver) { - this.callbackUrlResolver = callbackUrlResolver; - } - - public List<AuthorizationGenerator> getAuthorizationGenerators() { - return this.authorizationGenerators; - } - - public void setAuthorizationGenerators(final List<AuthorizationGenerator> authorizationGenerators) { - CommonHelper.assertNotNull("authorizationGenerators", authorizationGenerators); - this.authorizationGenerators = authorizationGenerators; - } - - public void setAuthorizationGenerators(final AuthorizationGenerator... authorizationGenerators) { - CommonHelper.assertNotNull("authorizationGenerators", authorizationGenerators); - this.authorizationGenerators = Arrays.asList(authorizationGenerators); - } - - public void setAuthorizationGenerator(final AuthorizationGenerator authorizationGenerator) { - addAuthorizationGenerator(authorizationGenerator); - } - - public void addAuthorizationGenerator(final AuthorizationGenerator authorizationGenerator) { - CommonHelper.assertNotNull("authorizationGenerator", authorizationGenerator); - this.authorizationGenerators.add(authorizationGenerator); - } - - public String getDefaultSecurityClients() { - return defaultSecurityClients; - } - - public void setDefaultSecurityClients(final String defaultSecurityClients) { - this.defaultSecurityClients = defaultSecurityClients; - } - - public UrlResolver getUrlResolver() { - return urlResolver; - } - - public void setUrlResolver(final UrlResolver urlResolver) { - this.urlResolver = urlResolver; - } - - @Override - public String toString() { - return CommonHelper.toNiceString(this.getClass(), "callbackUrl", this.callbackUrl, "clients", getClients(), - "ajaxRequestResolver", ajaxRequestResolver, "callbackUrlResolver", callbackUrlResolver, - "authorizationGenerators", authorizationGenerators, "defaultSecurityClients", defaultSecurityClients, - "urlResolver", this.urlResolver); - } -} diff --git a/cas/cas-server/src/main/resources/application.properties b/cas/cas-server/src/main/resources/application.properties index d38b16389a1deda0b8f9965e57ed7fb83b645152..682fbe1e9908d810165d70d42d022e262e218382 100644 --- a/cas/cas-server/src/main/resources/application.properties +++ b/cas/cas-server/src/main/resources/application.properties @@ -11,9 +11,9 @@ server.ssl.enabled=true server.port=8443 server.servlet.context-path=/cas server.max-http-header-size=2097152 +## CUSTO: NATIVE -> NONE server.forward-headers-strategy=NONE -server.connection-timeout=PT20S -# CUSTO: ALWAYS -> NEVER +## CUSTO: ALWAYS -> NEVER server.error.include-stacktrace=NEVER server.compression.enabled=true @@ -22,17 +22,21 @@ server.compression.mime-types=application/javascript,application/json,applicatio ## # CAS Web Application Embedded Tomcat Configuration # -server.tomcat.max-http-post-size=2097152 +server.tomcat.max-http-form-post-size=2097152 server.tomcat.basedir=build/tomcat +server.tomcat.connection-timeout=PT20S server.tomcat.accesslog.enabled=true server.tomcat.accesslog.pattern=%t %a "%r" %s (%D ms) server.tomcat.accesslog.suffix=.log -server.tomcat.min-spare-threads=10 -server.tomcat.max-threads=200 -server.tomcat.port-header=X-Forwarded-Port -server.tomcat.protocol-header=X-Forwarded-Proto -server.tomcat.protocol-header-https-value=https -server.tomcat.remote-ip-header=X-FORWARDED-FOR +server.tomcat.background-processor-delay=0s +server.tomcat.threads.min-spare=10 +server.tomcat.threads.max=200 + +server.tomcat.remoteip.port-header=X-Forwarded-Port +server.tomcat.remoteip.protocol-header=X-Forwarded-Proto +server.tomcat.remoteip.protocol-header-https-value=https +server.tomcat.remoteip.remote-ip-header=X-FORWARDED-FOR + server.tomcat.uri-encoding=UTF-8 server.tomcat.additional-tld-skip-patterns=*.jar @@ -44,9 +48,9 @@ spring.jmx.enabled=false ## # CAS Web Application Http Encoding Configuration # -spring.http.encoding.charset=UTF-8 -spring.http.encoding.enabled=true -spring.http.encoding.force=true +server.servlet.encoding.charset=UTF-8 +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true ## # Allow configuration classes to override bean definitions from Spring Boot @@ -59,13 +63,15 @@ spring.main.lazy-initialization=false # spring.cloud.bus.enabled=false -# Indicates that systemPropertiesOverride can be used. Set to false to prevent users from changing the default accidentally. Default true. +# Indicates that systemPropertiesOverride can be used. Set to false to +# prevent users from changing the default accidentally. Default true. spring.cloud.config.allow-override=true # External properties should override system properties. spring.cloud.config.override-system-properties=false -# When allowOverride is true, external properties should take lowest priority, and not override any existing property sources (including local config files). +# When allowOverride is true, external properties should take lowest priority, +# and not override any existing property sources (including local config files). spring.cloud.config.override-none=false # spring.cloud.bus.refresh.enabled=true @@ -85,12 +91,6 @@ management.endpoints.web.base-path=/actuator management.endpoints.web.exposure.include=info,health,status,configurationMetadata management.endpoints.jmx.exposure.exclude=* -management.metrics.export.atlas.enabled=false -management.metrics.export.graphite.enabled=false -management.metrics.export.influx.enabled=false -management.metrics.export.newrelic.enabled=false -management.metrics.export.signalfx.enabled=false -management.metrics.export.wavefront.enabled=false # management.endpoints.web.exposure.include=* # management.endpoints.web.path-mapping.health=status @@ -104,7 +104,7 @@ spring.security.user.name=casuser # spring.security.user.roles= # Define a CAS-specific "WARN" status code and its order -management.health.status.order=WARN,DOWN,OUT_OF_SERVICE,UNKNOWN,UP +management.endpoint.health.status.order=WARN,DOWN,OUT_OF_SERVICE,UNKNOWN,UP # Define health indicator behavior (requires cas-server-core-monitor) management.health.memoryHealthIndicator.enabled=true @@ -118,13 +118,9 @@ spring.cloud.discovery.client.composite-indicator.enabled=false ## # CAS Web Application Session Configuration # -# 4 (hours) * 60 (minutes) * 60 (seconds) -server.servlet.session.timeout=PT14400S +server.servlet.session.timeout=PT300S server.servlet.session.cookie.http-only=true -server.servlet.session.cookie.secure=true server.servlet.session.tracking-modes=COOKIE -cas.ticket.tgt.hardTimeout.timeToKillInSeconds=14400 - ## # CAS Thymeleaf View Configuration # @@ -142,7 +138,7 @@ server.servlet.context-parameters.isLog4jAutoInitializationDisabled=true ## # CAS Metrics Configuration # -management.metrics.web.server.auto-time-requests=true +management.metrics.web.server.request.autotime.enabled=true management.metrics.export.atlas.enabled=false management.metrics.export.datadog.enabled=false @@ -164,6 +160,14 @@ management.metrics.enable.process.cpu=true management.metrics.enable.process.uptime=true management.metrics.enable.process.start.time=true +## +# CAS Swagger Configuration +# +springdoc.show-actuator=true +springdoc.model-and-view-allowed=true +springdoc.writer-with-default-pretty-printer=true +springdoc.swagger-ui.display-request-duration=true + ## # CAS AspectJ Configuration # @@ -171,17 +175,35 @@ spring.aop.auto=true spring.aop.proxy-target-class=true ## -# CAS View Settings +# CAS Authentication Credentials # -cas.view.cas2.v3ForwardCompatible=true +cas.authn.accept.enabled=true +cas.authn.accept.users=casuser::Mellon +cas.authn.accept.name=Static Credentials +## +# CAS Template Configuration +# +spring.groovy.template.enabled=false + +# CAS doesn't rely on this, Spring Boot will warn it is on if not set +spring.jpa.open-in-view=false +## +# Excluded auto-configuration classes +# +spring.autoconfigure.exclude= \ + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ + org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + +# CUSTO: ## # Theme logo path and color hex. # May be overridden by YAML config # -# theme.vitam-logo=/path/to/vitam-logo/vitam-logo.png # theme.vitamui-logo-large=/path/to/vitam-logo/vitamui-logo-large.png +# theme.vitamui-favicon theme.vitamui-platform-name=VITAM-UI theme.primary=#702382 @@ -190,11 +212,3 @@ theme.background=#0F0D2D; # Inline-CSS added in every html template, on body tag theme.body.style=--vitamui-primary:${theme.primary};--vitamui-secondary:${theme.secondary};--vitamui-background:${theme.background}; - - - -## -# CAS Authentication Credentials -# -cas.authn.accept.users=casuser::Mellon -cas.authn.accept.name=Static Credentials diff --git a/cas/cas-server/src/main/resources/bootstrap.properties b/cas/cas-server/src/main/resources/bootstrap.properties index 40c0c886b981f75f38f21ad84849d8c981a09ee8..a246766918a6c34cb94bbddb86fdff86dbf61ee6 100644 --- a/cas/cas-server/src/main/resources/bootstrap.properties +++ b/cas/cas-server/src/main/resources/bootstrap.properties @@ -4,6 +4,7 @@ # Name of the application for which environment settings and properties should be fetched. # This should map to a cas.yml or cas.properties file. spring.application.name=cas +# CUSTO: spring.profiles.active=default # Define where the configuration server is running @@ -26,4 +27,4 @@ health.config.enabled=true # If you wish to change the configuration directory, it's best to not # overlay this file, but specify the directory location via command-line # parameters or system properties via -D. -# cas.standalone.configurationDirectory=/etc/cas/config +# cas.standalone.configuration-directory=/etc/cas/config diff --git a/cas/cas-server/src/main/resources/overriden_messages.properties b/cas/cas-server/src/main/resources/overriden_messages.properties index f899e2cca9be8aea63390e64805fd47f42b8b966..f67dcdbd28edf72e6df04710bbd8bb7224fdead0 100644 --- a/cas/cas-server/src/main/resources/overriden_messages.properties +++ b/cas/cas-server/src/main/resources/overriden_messages.properties @@ -1,3 +1,6 @@ +webjars.jqueryui.js=/webjars/jquery-ui/1.12.1/jquery-ui.min.js +webjars.fontawesomemin.css=/webjars/font-awesome/5.11.2/css/all.min.css + screen.accountlocked.heading=Your account has been locked for the next 20 minutes screen.accountlocked.message=Please wait before trying again diff --git a/cas/cas-server/src/main/resources/templates/casPwdView.html b/cas/cas-server/src/main/resources/templates/casPwdView.html index 0d3904fe832c4d0b9251ed9ca33a36ecb5dd8c23..c5947096614be6f79dc6fa8e42ab1228b80ca081 100644 --- a/cas/cas-server/src/main/resources/templates/casPwdView.html +++ b/cas/cas-server/src/main/resources/templates/casPwdView.html @@ -11,9 +11,8 @@ <link th:href="@{/css/cas.css}" rel="stylesheet"/> <script type="text/javascript" th:src="@{#{webjars.zxcvbn.js}}"></script> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body th:styleappend="${@environment.getProperty('theme.body.style')}"> diff --git a/cas/cas-server/src/main/resources/templates/casPac4jStopWebflow.html b/cas/cas-server/src/main/resources/templates/delegated-authn/casDelegatedAuthnStopWebflow.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casPac4jStopWebflow.html rename to cas/cas-server/src/main/resources/templates/delegated-authn/casDelegatedAuthnStopWebflow.html diff --git a/cas/cas-server/src/main/resources/templates/casServiceErrorView.html b/cas/cas-server/src/main/resources/templates/error/casServiceErrorView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casServiceErrorView.html rename to cas/cas-server/src/main/resources/templates/error/casServiceErrorView.html diff --git a/cas/cas-server/src/main/resources/templates/fragments/scripts.html b/cas/cas-server/src/main/resources/templates/fragments/scripts.html index 5a764724cc99776872f18aa33dd9680be2de33d6..4a11e81c44dfbe52ce658f0a8ae1fc018759b894 100644 --- a/cas/cas-server/src/main/resources/templates/fragments/scripts.html +++ b/cas/cas-server/src/main/resources/templates/fragments/scripts.html @@ -1,9 +1,9 @@ <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> -<script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey != null and recaptchaVersion=='v2'}"></script> -<script th:src="${'//www.google.com/recaptcha/api.js?render=' + recaptchaSiteKey}" th:if="${recaptchaSiteKey != null and recaptchaVersion == 'v3'}"></script> <script type="text/javascript" th:src="@{#{webjars.headmin.js}}"></script> -<script type="text/javascript" th:src="@{${#themes.code('cas.javascript.file')}}"></script> +<span th:remove="tag" th:each="file : ${#strings.arraySplit(#themes.code('cas.standard.js.file'), ',')}"> + <script type="text/javascript" th:src="@{${file}}"></script> +</span> <script th:inline="javascript"> head.ready(document, function () { @@ -53,25 +53,3 @@ function notifyResourcesAreLoaded(callback) { /*]]>*/ </script> - -<span th:if="${recaptchaVersion=='v2'}" th:remove="tag"> - <script type="text/javascript" th:if="${recaptchaSiteKey != null AND recaptchaInvisible != null AND recaptchaInvisible}" th:inline="javascript"> - function onRecaptchaV2Submit(token) { - $('#fm1').submit(); - } - </script> -</span> - -<span th:if="${recaptchaVersion=='v3'}" th:remove="tag"> - <script type="text/javascript" th:if="${recaptchaSiteKey != null}" th:inline="javascript"> - grecaptcha.ready(function() { - grecaptcha.execute(/*[[${recaptchaSiteKey}]]*/, {action: 'login'}) - .then(function(token) { - $("#g-recaptcha-token").val(token) - }); - }); - </script> -</span> - - - diff --git a/cas/cas-server/src/main/resources/templates/layout.html b/cas/cas-server/src/main/resources/templates/layout.html index dfe7f39658103fa504eeb62f6e203e53c8b2b1d6..01d9cdaddfcc26b13314f544472d330b7a309ae5 100644 --- a/cas/cas-server/src/main/resources/templates/layout.html +++ b/cas/cas-server/src/main/resources/templates/layout.html @@ -14,7 +14,7 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> </head> diff --git a/cas/cas-server/src/main/resources/templates/casAccountDisabledView.html b/cas/cas-server/src/main/resources/templates/login-error/casAccountDisabledView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casAccountDisabledView.html rename to cas/cas-server/src/main/resources/templates/login-error/casAccountDisabledView.html diff --git a/cas/cas-server/src/main/resources/templates/casAccountLockedView.html b/cas/cas-server/src/main/resources/templates/login-error/casAccountLockedView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casAccountLockedView.html rename to cas/cas-server/src/main/resources/templates/login-error/casAccountLockedView.html diff --git a/cas/cas-server/src/main/resources/templates/casAuthenticationBlockedView.html b/cas/cas-server/src/main/resources/templates/login-error/casAuthenticationBlockedView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casAuthenticationBlockedView.html rename to cas/cas-server/src/main/resources/templates/login-error/casAuthenticationBlockedView.html diff --git a/cas/cas-server/src/main/resources/templates/casExpiredPassView.html b/cas/cas-server/src/main/resources/templates/login-error/casExpiredPassView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casExpiredPassView.html rename to cas/cas-server/src/main/resources/templates/login-error/casExpiredPassView.html diff --git a/cas/cas-server/src/main/resources/templates/casMustChangePassView.html b/cas/cas-server/src/main/resources/templates/login-error/casMustChangePassView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casMustChangePassView.html rename to cas/cas-server/src/main/resources/templates/login-error/casMustChangePassView.html diff --git a/cas/cas-server/src/main/resources/templates/casGenericSuccessView.html b/cas/cas-server/src/main/resources/templates/login/casGenericSuccessView.html similarity index 86% rename from cas/cas-server/src/main/resources/templates/casGenericSuccessView.html rename to cas/cas-server/src/main/resources/templates/login/casGenericSuccessView.html index b6eba3924891b847aaf0fba3a64de1b9158958aa..cd5ca473ec55e43a48839bab8d988a869766384a 100644 --- a/cas/cas-server/src/main/resources/templates/casGenericSuccessView.html +++ b/cas/cas-server/src/main/resources/templates/login/casGenericSuccessView.html @@ -14,9 +14,8 @@ <link th:href="@{/css/cas.css}" rel="stylesheet"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body class="bg-portal" th:styleappend="${@environment.getProperty('theme.body.style')}"> diff --git a/cas/cas-server/src/main/resources/templates/casLoginView.html b/cas/cas-server/src/main/resources/templates/login/casLoginView.html similarity index 94% rename from cas/cas-server/src/main/resources/templates/casLoginView.html rename to cas/cas-server/src/main/resources/templates/login/casLoginView.html index 987063907d896c8e73a4780f443b21ba452c044c..1968e2b9360209126390b5c80c3f7f9e46aa09ad 100644 --- a/cas/cas-server/src/main/resources/templates/casLoginView.html +++ b/cas/cas-server/src/main/resources/templates/login/casLoginView.html @@ -9,9 +9,8 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"> <link th:href="@{/css/cas.css}" rel="stylesheet"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body th:styleappend="${@environment.getProperty('theme.body.style')}"> @@ -64,8 +63,8 @@ </div> <div class="form-control" th:if="${!#strings.isEmpty(superUser)}"> - <label for="surrogate" th:utext="#{screen.welcome.label.surrogate}"/> <span th:utext="${surrogate}" /><br /> - <label for="superUser" th:utext="#{screen.welcome.label.superuser}"/> <span th:utext="${superUser}" /><br /> + <label for="surrogate" th:utext="#{screen.welcome.label.surrogate}"/> <span th:text="${surrogate}" /><br /> + <label for="superUser" th:utext="#{screen.welcome.label.superuser}"/> <span th:text="${superUser}" /><br /> <input type="hidden" id="username" name="username" th:value="${surrogate + ',' + superUser}" /> <br><br> diff --git a/cas/cas-server/src/main/resources/templates/casLogoutView.html b/cas/cas-server/src/main/resources/templates/logout/casLogoutView.html similarity index 89% rename from cas/cas-server/src/main/resources/templates/casLogoutView.html rename to cas/cas-server/src/main/resources/templates/logout/casLogoutView.html index 0b456dcab1828db632d6ef3b51bef1904c14f3df..f8867387508b9dfed4d45e11c99e74608705e1a8 100644 --- a/cas/cas-server/src/main/resources/templates/casLogoutView.html +++ b/cas/cas-server/src/main/resources/templates/logout/casLogoutView.html @@ -9,9 +9,8 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"> <link th:href="@{/css/cas.css}" rel="stylesheet"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body th:styleappend="${@environment.getProperty('theme.body.style')}"> <div class="login"> diff --git a/cas/cas-server/src/main/resources/templates/casPropagateLogoutView.html b/cas/cas-server/src/main/resources/templates/logout/casPropagateLogoutView.html similarity index 95% rename from cas/cas-server/src/main/resources/templates/casPropagateLogoutView.html rename to cas/cas-server/src/main/resources/templates/logout/casPropagateLogoutView.html index fa2bb8f65202a0066b0a54a4721a2ef796e45321..aafc04861b637798cc879f3a4adda3ccf7cb6eee 100644 --- a/cas/cas-server/src/main/resources/templates/casPropagateLogoutView.html +++ b/cas/cas-server/src/main/resources/templates/logout/casPropagateLogoutView.html @@ -14,9 +14,8 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> <script th:inline="javascript"> /*<![CDATA[*/ diff --git a/cas/cas-server/src/main/resources/templates/casPasswordUpdateSuccessView.html b/cas/cas-server/src/main/resources/templates/password-reset/casPasswordUpdateSuccessView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casPasswordUpdateSuccessView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casPasswordUpdateSuccessView.html diff --git a/cas/cas-server/src/main/resources/templates/casResetPasswordErrorView.html b/cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordErrorView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casResetPasswordErrorView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordErrorView.html diff --git a/cas/cas-server/src/main/resources/templates/casResetPasswordSendInstructionsView.html b/cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSendInstructionsView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casResetPasswordSendInstructionsView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSendInstructionsView.html diff --git a/cas/cas-server/src/main/resources/templates/casResetPasswordSentInstructionsView.html b/cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSentInstructionsView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casResetPasswordSentInstructionsView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSentInstructionsView.html diff --git a/cas/cas-server/src/main/resources/templates/casSimpleMfaLoginView.html b/cas/cas-server/src/main/resources/templates/simple-mfa/casSimpleMfaLoginView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casSimpleMfaLoginView.html rename to cas/cas-server/src/main/resources/templates/simple-mfa/casSimpleMfaLoginView.html diff --git a/cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml b/cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml deleted file mode 100644 index 412b7296e7cbd2701c9ee37a89331081ede406f3..0000000000000000000000000000000000000000 --- a/cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml +++ /dev/null @@ -1,61 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<flow xmlns="http://www.springframework.org/schema/webflow" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://www.springframework.org/schema/webflow - http://www.springframework.org/schema/webflow/spring-webflow.xsd"> - - <var name="credential" class="org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential" /> - <on-start> - <evaluate expression="initialFlowSetupAction" /> - </on-start> - - <action-state id="initializeLoginForm"> - <evaluate expression="initializeLoginAction" /> - <transition on="success" to="sendSimpleToken"/> - </action-state> - - <action-state id="sendSimpleToken"> - <evaluate expression="mfaSimpleMultifactorSendTokenAction" /> - <transition on="success" to="viewLoginForm"/> - <transition on="error" to="unavailable"/> - <!-- custo --> - <transition on="missingPhone" to="missingPhone"/> - </action-state> - - <!-- custo --> - <view-state id="missingPhone" view="casSmsMissingPhoneView" /> - - <view-state id="viewLoginForm" view="casSimpleMfaLoginView" model="credential"> - <binder> - <binding property="token" required="true"/> - </binder> - <on-entry> - <set name="viewScope.principal" value="conversationScope.authentication.principal" /> - </on-entry> - <!-- custo --> - <transition on="submit" bind="true" validate="true" to="intermediateSubmit"/> - <transition on="resend" bind="false" validate="false" to="sendSimpleToken"/> - - </view-state> - - <!-- custo --> - <action-state id="intermediateSubmit"> - <evaluate expression="checkMfaTokenAction" /> - <transition on="success" to="realSubmit"/> - <transition on="error" to="codeExpired"/> - </action-state> - - <!-- custo --> - <view-state id="codeExpired" view="casSmsCodeExpiredView"> - <transition on="resend" to="initializeLoginForm"/> - </view-state> - - <action-state id="realSubmit"> - <evaluate expression="oneTimeTokenAuthenticationWebflowAction" /> - <transition on="success" to="success" /> - <transition on="error" to="viewLoginForm" /> - </action-state> - - <end-state id="success" /> - <end-state id="unavailable" /> -</flow> diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java index 5dc4fb86e7e066f5243cc60de8d653890e4a10a9..5791a4b1bc77a9ae8c549579fcd048b5ba06c4c7 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java @@ -64,6 +64,7 @@ public abstract class BaseWebflowActionTest { when(context.getActiveFlow()).thenReturn(flow); when(context.getRequestScope()).thenReturn(flowParameters); + when(context.getFlashScope()).thenReturn(flowParameters); when(context.getConversationScope()).thenReturn(flowParameters); final ServletExternalContext servletExternalContext = mock(ServletExternalContext.class); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java index 05df354af1f1bef700fa8f0ae81bf761f340b5ce..ee95ad49ebbc82850244e8f225fd53d1b6952da9 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java @@ -8,8 +8,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Arrays; -import java.util.HashMap; -import java.util.List; import org.apereo.cas.authentication.principal.DefaultPrincipalFactory; import org.apereo.cas.authentication.principal.Principal; @@ -29,8 +27,6 @@ import fr.gouv.vitamui.iam.common.enums.SubrogationStatusEnum; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.val; -import lombok.val; - /** * Tests {@link IamSurrogateAuthenticationService}. */ @@ -90,11 +86,8 @@ public final class IamSurrogateAuthenticationServiceTest { } private Principal principal() { - val attributes = new HashMap<String, List<Object>>(); - attributes.put(UserPrincipalResolver.SUPER_USER_ID_ATTRIBUTE, Arrays.asList(SU_ID)); - val factory = new DefaultPrincipalFactory(); - return factory.createPrincipal("x", attributes); + return factory.createPrincipal(SU_ID); } private SubrogationDto surrogation() { diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java index ec01363bf46a11b3deb74f542a0ea61ad52547f8..e78af99e62dc7b0818a0f0c70cea3ce024fa1411 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java @@ -4,6 +4,7 @@ import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.cas.BaseWebflowActionTest; +import fr.gouv.vitamui.cas.x509.X509AttributeMapping; import fr.gouv.vitamui.commons.api.CommonConstants; import fr.gouv.vitamui.commons.api.domain.AddressDto; import fr.gouv.vitamui.commons.api.domain.GroupDto; @@ -19,6 +20,7 @@ import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.val; +import org.apereo.cas.adaptors.x509.authentication.principal.X509CertificateCredential; import org.apereo.cas.authentication.SurrogateUsernamePasswordCredential; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.authentication.principal.ClientCredential; @@ -33,6 +35,8 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import java.security.Principal; +import java.security.cert.X509Certificate; import java.util.*; import static fr.gouv.vitamui.commons.api.CommonConstants.IDENTIFIER_ATTRIBUTE; @@ -94,8 +98,10 @@ public final class UserPrincipalResolverTest extends BaseWebflowActionTest { sessionStore = mock(SessionStore.class); identityProviderHelper = mock(IdentityProviderHelper.class); providersService = mock(ProvidersService.class); + val emailMapping = new X509AttributeMapping("subject_dn", null, null); + val identifierMapping = new X509AttributeMapping("issuer_dn", null, null); resolver = new UserPrincipalResolver(principalFactory, casExternalRestClient, utils, sessionStore, - identityProviderHelper, providersService); + identityProviderHelper, providersService, emailMapping, identifierMapping, ""); } @Test @@ -113,6 +119,28 @@ public final class UserPrincipalResolverTest extends BaseWebflowActionTest { assertNull(attributes.get(SUPER_USER_ATTRIBUTE)); } + @Test + public void testResolveX509() { + when(casExternalRestClient.getUser(any(ExternalHttpContext.class), eq(USERNAME), eq(null), eq(Optional.of(IDENTIFIER)), + eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))).thenReturn(userProfile(UserStatusEnum.ENABLED)); + val cert = mock(X509Certificate.class); + val subjectDn = mock(Principal.class); + when(subjectDn.getName()).thenReturn(USERNAME); + when(cert.getSubjectDN()).thenReturn(subjectDn); + val issuerDn = mock(Principal.class); + when(issuerDn.getName()).thenReturn(IDENTIFIER); + when(cert.getIssuerDN()).thenReturn(issuerDn); + + val principal = resolver.resolve(new X509CertificateCredential(new X509Certificate[] { cert }), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty()); + + assertEquals(USERNAME_ID, principal.getId()); + final Map<String, List<Object>> attributes = principal.getAttributes(); + assertEquals(USERNAME, attributes.get(CommonConstants.EMAIL_ATTRIBUTE).get(0)); + assertEquals(Arrays.asList(ROLE_NAME), attributes.get(CommonConstants.ROLES_ATTRIBUTE)); + assertNull(attributes.get(SUPER_USER_ATTRIBUTE)); + } + @Test public void testResolveAuthnDelegation() { val provider = new IdentityProviderDto(); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java index 1d1597467637a44b5548412f4a978fedd4fd7f07..dd035a39b317975ae51113d4e2dbe6d53cd95161 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java @@ -61,6 +61,7 @@ import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties; import org.apereo.cas.pm.PasswordChangeRequest; +import org.apereo.cas.pm.PasswordManagementQuery; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -128,7 +129,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes private PasswordManagementProperties passwordManagementProperties; - @Value("${cas.authn.pm.policyPattern}") + @Value("${cas.authn.pm.core.policy-pattern}") private String policyPattern; private PasswordConfiguration passwordConfiguration; @@ -144,7 +145,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes identityProviderDto = new IdentityProviderDto(); identityProviderDto.setInternal(true); passwordManagementProperties = new PasswordManagementProperties(); - passwordManagementProperties.setPolicyPattern(encode(policyPattern)); + passwordManagementProperties.getCore().setPolicyPattern(encode(policyPattern)); passwordConfiguration = new PasswordConfiguration(); passwordConfiguration.setCheckOccurrence(true); passwordConfiguration.setOccurrencesCharsNumber(4); @@ -307,7 +308,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(user(UserStatusEnum.ENABLED)); - assertEquals(EMAIL, service.findEmail(EMAIL)); + assertEquals(EMAIL, service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test @@ -315,7 +316,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenThrow(new BadRequestException("error")); - assertNull(service.findEmail(EMAIL)); + assertNull(service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test @@ -323,7 +324,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(null); - assertNull(service.findEmail(EMAIL)); + assertNull(service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test @@ -331,7 +332,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(user(UserStatusEnum.DISABLED)); - assertNull(service.findEmail(EMAIL)); + assertNull(service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test(expected = UnsupportedOperationException.class) @@ -339,7 +340,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(user(UserStatusEnum.ENABLED)); - service.getSecurityQuestions(EMAIL); + service.getSecurityQuestions(PasswordManagementQuery.builder().username(EMAIL).build()); } private UserDto user(final UserStatusEnum status) { diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java index 558d83819c633eef61352370b8dbaf4dff5584b5..27149c75d7bf8ad717606152b4b50a7bc8833c00 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java @@ -7,7 +7,7 @@ import fr.gouv.vitamui.iam.common.dto.common.ProviderEmbeddedOptions; import fr.gouv.vitamui.iam.external.client.IdentityProviderExternalRestClient; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import lombok.val; import org.junit.Before; import org.junit.Test; @@ -49,15 +49,11 @@ public final class ProvidersServiceTest { @Before public void setUp() { - service = new ProvidersService(); val clients = new Clients(); - service.setClients(clients); - val builder = mock(Saml2ClientBuilder.class); - service.setSaml2ClientBuilder(builder); + val builder = mock(Pac4jClientBuilder.class); restClient = mock(IdentityProviderExternalRestClient.class); - service.setIdentityProviderExternalRestClient(restClient); val utils = new Utils(null, 0, null, null); - service.setUtils(utils); + service = new ProvidersService(clients, restClient, builder, utils); provider = new IdentityProviderDto(); provider.setId(PROVIDER_ID); @@ -66,7 +62,7 @@ public final class ProvidersServiceTest { saml2Client = new SAML2Client(); saml2Client.setName("testSAML2Client"); - when(builder.buildSaml2Client(provider)).thenReturn(Optional.of(saml2Client)); + when(builder.buildClient(provider)).thenReturn(Optional.of(saml2Client)); identityProviderHelper = new IdentityProviderHelper(); } @@ -84,7 +80,7 @@ public final class ProvidersServiceTest { val userProvider = identityProviderHelper.findByUserIdentifier(service.getProviders(), "jerome@company.com"); assertTrue(userProvider.isPresent()); assertEquals(PROVIDER_ID, userProvider.get().getId()); - assertEquals(saml2Client, ((SamlIdentityProviderDto) userProvider.get()).getSaml2Client()); + assertEquals(saml2Client, ((Pac4jClientIdentityProviderDto) userProvider.get()).getClient()); } @Test diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e80a88ccceecced5f515a77ba480c88fcf6ebd20 --- /dev/null +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java @@ -0,0 +1,74 @@ +package fr.gouv.vitamui.cas.webflow.actions; + +import fr.gouv.vitamui.cas.BaseWebflowActionTest; +import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; +import lombok.val; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicket; +import org.apereo.cas.ticket.registry.TicketRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests {@link CheckMfaTokenAction}. + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ServerIdentityAutoConfiguration.class) +@TestPropertySource(locations = "classpath:/application-test.properties") +public class CheckMfaTokenActionTest extends BaseWebflowActionTest { + + private static final String TOKEN = "000"; + + private TicketRegistry ticketRegistry; + + private CheckMfaTokenAction action; + + private CasSimpleMultifactorAuthenticationTicket ticket; + + @Override + @Before + public void setUp() { + super.setUp(); + + ticketRegistry = mock(TicketRegistry.class); + action = new CheckMfaTokenAction(ticketRegistry); + + val credential = mock(CasSimpleMultifactorTokenCredential.class); + when(credential.getToken()).thenReturn(TOKEN); + when(credential.getId()).thenReturn(TOKEN); + flowParameters.put("credential", credential); + + ticket = mock(CasSimpleMultifactorAuthenticationTicket.class); + when(ticketRegistry.getTicket("CASMFA-" + TOKEN, CasSimpleMultifactorAuthenticationTicket.class)).thenReturn(ticket); + } + + @Test + public void tokenNotExpired() { + val creationDate = ZonedDateTime.now().minus(30, ChronoUnit.SECONDS); + when(ticket.getCreationTime()).thenReturn(creationDate); + + val event = action.doExecute(context); + + assertEquals("success", event.getId()); + } + + @Test + public void tokenExpired() { + val creationDate = ZonedDateTime.now().minus(70, ChronoUnit.SECONDS); + when(ticket.getCreationTime()).thenReturn(creationDate); + + val event = action.doExecute(context); + + assertEquals("error", event.getId()); + } +} diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java index 2c515caf34735aefff3c80d6d6a2b52fbd52b5fb..efae1c98d014c228417bb49b19262b4c416f7a9d 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java @@ -3,36 +3,26 @@ package fr.gouv.vitamui.cas.webflow.actions; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import java.util.ArrayList; - import fr.gouv.vitamui.cas.BaseWebflowActionTest; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.AuthenticationSystemSupport; -import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; +import lombok.val; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.web.DelegatedClientWebflowManager; -import org.apereo.cas.web.flow.SingleSignOnParticipationStrategy; -import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; -import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; +import org.apereo.cas.web.flow.DelegatedClientAuthenticationConfigurationContext; +import org.apereo.cas.web.flow.DelegatedClientIdentityProviderConfigurationProducer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.pac4j.core.client.Clients; -import org.pac4j.core.context.session.SessionStore; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; +import static org.mockito.Mockito.*; + /** * Tests {@link CustomDelegatedClientAuthenticationAction}. * @@ -54,14 +44,10 @@ public final class CustomDelegatedClientAuthenticationActionTest extends BaseWeb public void setUp() { super.setUp(); - action = new CustomDelegatedClientAuthenticationAction(mock(CasDelegatingWebflowEventResolver.class), - mock(CasWebflowEventResolver.class), mock(AdaptiveAuthenticationPolicy.class), - mock(Clients.class), mock(ServicesManager.class), mock(AuditableExecution.class), - mock(DelegatedClientWebflowManager.class), mock(AuthenticationSystemSupport.class), - mock(CasConfigurationProperties.class), mock(AuthenticationServiceSelectionPlan.class), - mock(CentralAuthenticationService.class), mock(SingleSignOnParticipationStrategy.class), - mock(SessionStore.class), new ArrayList<>(), mock(IdentityProviderHelper.class), - mock(ProvidersService.class), mock(Utils.class), mock(TicketRegistry.class), "", ","); + val configContext = mock(DelegatedClientAuthenticationConfigurationContext.class); + when(configContext.getDelegatedClientIdentityProvidersProducer()).thenReturn(mock(DelegatedClientIdentityProviderConfigurationProducer.class)); + action = new CustomDelegatedClientAuthenticationAction(configContext, mock(IdentityProviderHelper.class), + mock(ProvidersService.class), mock(Utils.class), mock(TicketRegistry.class), "", ","); } @Test diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java index 3253377f3615735b7a9d0f3fe0fc660400b195ec..baef17e0dd78bd73554169f9e77e630633636264 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java @@ -27,7 +27,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.webflow.execution.Event; -import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; +import fr.gouv.vitamui.cas.provider.Pac4jClientIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; @@ -57,7 +57,7 @@ public final class DispatcherActionTest extends BaseWebflowActionTest { private DispatcherAction action; - private SamlIdentityProviderDto provider; + private Pac4jClientIdentityProviderDto provider; @Override @Before @@ -72,7 +72,7 @@ public final class DispatcherActionTest extends BaseWebflowActionTest { action = new DispatcherAction(providersService, identityProviderHelper, casExternalRestClient, ",", utils, mock(SessionStore.class)); final SAML2Client client = new SAML2Client(); - provider = new SamlIdentityProviderDto(new IdentityProviderDto(), client); + provider = new Pac4jClientIdentityProviderDto(new IdentityProviderDto(), client); provider.setInternal(true); when(identityProviderHelper.findByUserIdentifier(any(LinkedList.class), eq(USERNAME))) .thenReturn(Optional.of(provider)); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java index 298e658c5dc368a0140c47f5387050e8c28e4f95..765e8917a455da31ba25e2ccf51f605dac19b689 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java @@ -1,5 +1,6 @@ package fr.gouv.vitamui.cas.webflow.actions; +import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.logout.LogoutManager; import org.apereo.cas.services.RegexRegisteredService; import org.apereo.cas.services.ServicesManager; @@ -27,9 +28,8 @@ public final class GeneralTerminateSessionActionTest { final LogoutManager logoutManager = mock(LogoutManager.class); - final GeneralTerminateSessionAction action = new GeneralTerminateSessionAction(null, null, null, null); - action.setServicesManager(servicesManager); - action.setLogoutManager(logoutManager); + final GeneralTerminateSessionAction action = new GeneralTerminateSessionAction(null, null, + null, logoutManager, null, null, null, servicesManager, new CasConfigurationProperties(), null); action.performGeneralLogout("tgtId"); } diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2d030a75bb8f4c1b589d510e662c7099aa8e9f30 --- /dev/null +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java @@ -0,0 +1,59 @@ +package fr.gouv.vitamui.cas.webflow.actions; + +import fr.gouv.vitamui.cas.BaseWebflowActionTest; +import fr.gouv.vitamui.cas.util.Utils; +import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; +import lombok.val; +import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.ticket.registry.TicketRegistrySupport; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Tests {@link TriggerChangePasswordAction}. + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ServerIdentityAutoConfiguration.class) +@TestPropertySource(locations = "classpath:/application-test.properties") +public class TriggerChangePasswordActionTest extends BaseWebflowActionTest { + + private TriggerChangePasswordAction action; + + @Override + @Before + public void setUp() { + super.setUp(); + + val tgtId = "TGT-1"; + + flowParameters.put("ticketGrantingTicketId", tgtId); + + val ticketRegistrySupport = mock(TicketRegistrySupport.class); + action = new TriggerChangePasswordAction(ticketRegistrySupport, mock(Utils.class)); + + when(ticketRegistrySupport.getAuthenticatedPrincipalFrom(tgtId)).thenReturn(mock(Principal.class)); + } + + @Test + public void changePassword() { + requestParameters.put("doChangePassword", "yes"); + + val event = action.doExecute(context); + + assertEquals("changePassword", event.getId()); + } + + @Test + public void dontChangePassword() { + val event = action.doExecute(context); + + assertEquals("continue", event.getId()); + } +} diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..48b8c54c62d7c5de0008e2cf6227bec6ff8bc1d9 --- /dev/null +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java @@ -0,0 +1,59 @@ +package fr.gouv.vitamui.cas.x509; + +import org.apereo.cas.util.crypto.CertUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Tests {@link CertificateParser}. + */ +public class CertificateParserTest { + + private static final String ISSUER_DN = "EMAILADDRESS=admin@vitam, CN=admin, O=Vitam, ST=Some-State, C=FR"; + private static final String SUBJECT_DN = "EMAILADDRESS=bob@email, CN=bob, OU=IT, O=MyCompany, ST=Some-State, C=FR"; + + private static X509Certificate cert; + + @BeforeClass + public static void beforeClass() throws IOException { + cert = CertUtils.readCertificate(new FileInputStream("src/test/resources/client.crt")); + } + + @Test + public void testIssuerDnNoParsingExpansion() throws CertificateParsingException { + assertEquals(ISSUER_DN, CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.ISSUER_DN.toString(), null, null))); + } + + @Test + public void testIssuerDnParsingNoExpansion() throws CertificateParsingException { + assertEquals("Vitam", CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.ISSUER_DN.toString(), ".*O=(.*), ST=.*", null))); + } + + @Test + public void testIssuerDnParsingExpansion() throws CertificateParsingException { + assertEquals("Vitam@email", CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.ISSUER_DN.toString(), ".*O=(.*), ST=.*", "{0}@email"))); + } + + @Test + public void testSubjectDnNoParsingExpansion() throws CertificateParsingException { + assertEquals(SUBJECT_DN, CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.SUBJECT_DN.toString(), null, null))); + } + + @Test + public void testSubjectAlternateNameNoParsingExpansion() { + try { + CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.SUBJECT_ALTERNATE_NAME.toString(), null, null)); + fail(); + } catch (final CertificateParsingException e) { + assertEquals("Cannot find X509 value for: SUBJECT_ALTERNATE_NAME", e.getMessage()); + } + } +} diff --git a/cas/cas-server/src/test/resources/application-test.properties b/cas/cas-server/src/test/resources/application-test.properties index 3977e9b8fba88c4161b506f7a53ef96ed6eaa68b..690d4e00a1cb2e5e6faa82b97ec8fd710fae7a34 100644 --- a/cas/cas-server/src/test/resources/application-test.properties +++ b/cas/cas-server/src/test/resources/application-test.properties @@ -1,5 +1,5 @@ -server-identity.identityName=CAS -server-identity.identityRole=SSO -server-identity.identityServerId=1 +server-identity.identity-name=CAS +server-identity.identity-role=SSO +server-identity.identity-server-id=1 -cas.authn.pm.policyPattern=^(?=(.*[$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]){2})(?=(?:.*[a-z]){2})(?=(?:.*[A-Z]){2})(?=(?:.*[\\d]){2})[A-Za-z\u00c0-\u00ff0-9$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]{12,}$ +cas.authn.pm.core.policy-pattern=^(?=(.*[$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]){2})(?=(?:.*[a-z]){2})(?=(?:.*[A-Z]){2})(?=(?:.*[\\d]){2})[A-Za-z\u00c0-\u00ff0-9$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]{12,}$ diff --git a/cas/cas-server/src/test/resources/client.crt b/cas/cas-server/src/test/resources/client.crt new file mode 100644 index 0000000000000000000000000000000000000000..8c4810bf21550bf02473f10dc21b69ea56ad9968 --- /dev/null +++ b/cas/cas-server/src/test/resources/client.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICvDCCAiUCAQEwDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCRlIxEzARBgNV +BAgMClNvbWUtU3RhdGUxDjAMBgNVBAoMBVZpdGFtMQ4wDAYDVQQDDAVhZG1pbjEa +MBgGCSqGSIb3DQEJARYLYWRtaW5Adml0YW0wHhcNMjEwMzAxMTQxNTAzWhcNMzEw +MjI3MTQxNTAzWjBrMQswCQYDVQQGEwJGUjETMBEGA1UECAwKU29tZS1TdGF0ZTES +MBAGA1UECgwJTXlDb21wYW55MQswCQYDVQQLDAJJVDEMMAoGA1UEAwwDYm9iMRgw +FgYJKoZIhvcNAQkBFglib2JAZW1haWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDs1MhEdAxoupbY6lc2awKf+bKFVY2D94gNz41evmFGsvXGHJj9uvKL +76wJOMsSPhbm5cC1wJvvIsgo73bgWG6qfkxmIJiRgc2KOu8j1Et4+BFlUFmmtMhg +TQio5TsA6AiXgfPOTlLGeZbxv0RdUkv/fi35W6kfLqNByv5amZ8iG+ISAI10t0YY +Pj2Zl5VQdX3mUAzF4QETruStxIqVXySiOn7Sia2bUTgAI/DVXgrbPmXWxnk+lG7S +4qvpLjvcNnzsU/G1TKOvArJqjytg/Ze9vswkBYtRSTxbXWwjFBVqX2nFIav8JTnv +Lkitkl5qGUdS9uMHvrWEcVQ14nM7BQYTAgMBAAEwDQYJKoZIhvcNAQELBQADgYEA +RiQshNhSUi1vUBk5veFctdQRxXTcqmhK0bVc1kreZhula5iC+gHGfZsIJZ1/5ksr +d2rFv/ps4n7GZtGE8t5WTC0EcjbBy8L9VbpzFcAJUqD9wDC6ph8xA8paUB4d9F1Q +qfCIpvHbACc2Vtv3zEF4nE2BebWVJgbo92dpNDH2rs4= +-----END CERTIFICATE----- diff --git a/commons/commons-api/src/main/java/fr/gouv/vitamui/commons/api/domain/AccessionRegisterDetailsSearchStatsDto.java b/commons/commons-api/src/main/java/fr/gouv/vitamui/commons/api/domain/AccessionRegisterDetailsSearchStatsDto.java index 0d0613aea026a7fa5e5897c5627deeeaf5fda3a3..9df67dbb0106fbfccb09e4577d32deb3454c394b 100644 --- a/commons/commons-api/src/main/java/fr/gouv/vitamui/commons/api/domain/AccessionRegisterDetailsSearchStatsDto.java +++ b/commons/commons-api/src/main/java/fr/gouv/vitamui/commons/api/domain/AccessionRegisterDetailsSearchStatsDto.java @@ -1,5 +1,6 @@ package fr.gouv.vitamui.commons.api.domain; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -25,8 +26,8 @@ public class AccessionRegisterDetailsSearchStatsDto { private List<String> archivalProfiles; private List<String> acquisitionInformations; private String elimination; - private String transfer; - private String preservation; + @JsonProperty("transfer_reply") + private String transferReply; } } diff --git a/deployment/environments/group_vars/all/vitamui_vars.yml b/deployment/environments/group_vars/all/vitamui_vars.yml index 383ade785e1d251dadbbfa25f3366d7f86cffa09..3eebd78c8f9b76ad2d228c40edc10976dc0486c1 100755 --- a/deployment/environments/group_vars/all/vitamui_vars.yml +++ b/deployment/environments/group_vars/all/vitamui_vars.yml @@ -239,7 +239,7 @@ vitamui: cas_server: host: "cas-server.service.{{ consul_domain }}" vitamui_component: "cas-server" - vitamui_component_type: "external" + vitamui_component_type: "ui" package_name: "vitamui-cas-server" store_name: "cas-server" service_name: "vitamui-cas-server" diff --git a/deployment/roles/vitamui/tasks/ui.yml b/deployment/roles/vitamui/tasks/ui.yml index fe17884bb4bc85dc6b928ef9bcdee78359b9b66e..223d35af756520230398e76852be5b95bd266b3b 100644 --- a/deployment/roles/vitamui/tasks/ui.yml +++ b/deployment/roles/vitamui/tasks/ui.yml @@ -12,7 +12,7 @@ dest: "{{ vitamui_defaults.folder.root_path}}/conf/assets" owner: "{{ vitamui_defaults.users.vitamui }}" group: "{{ vitamui_defaults.users.group }}" - mode: "{{ vitamui_defaults.folder.folder_permission }}" + mode: "{{ vitamui_defaults.folder.conf_permission }}" with_fileglob: - ../files/ui/assets/* tags: diff --git a/deployment/roles/vitamui/templates/cas-server/application.yml.j2 b/deployment/roles/vitamui/templates/cas-server/application.yml.j2 index fe21e88734ae02134e3e68a33cb5ec2c4795f68e..6bbe7978b0cec22a9f19d6c1b5b437e80063e271 100644 --- a/deployment/roles/vitamui/templates/cas-server/application.yml.j2 +++ b/deployment/roles/vitamui/templates/cas-server/application.yml.j2 @@ -59,7 +59,7 @@ iam-client: cas.authn.accept.users: -cas.messageBundle.baseNames: classpath:overriden_messages,classpath:messages +cas.message-bundle.base-names: classpath:overriden_messages,classpath:messages {% if vitamui.cas_server.base_url is undefined %} @@ -76,7 +76,7 @@ cas.authn.pm.reset.crypto.enabled: true # # 4 (hours) * 60 (minutes) * 60 (seconds) #server.servlet.session.timeout: PT14400S -#cas.ticket.tgt.hardTimeout.timeToKillInSeconds: 14400 +#cas.ticket.tgt.hard-timeout.time-to-kill-in-seconds: 14400 {% if vitamui.cas_server.base_url is defined %} @@ -86,35 +86,36 @@ cas.server.prefix: {{ url_prefix }}/cas {% endif %} login.url: ${cas.server.prefix}/login -cas.serviceRegistry.mongo.clientUri: "mongodb://{{ mongodb.cas.user }}:{{ mongodb.cas.password }}@{{ mongodb.host }}:{{ mongodb.mongod_port | default(27017) }}/{{ mongodb.cas.db }}?replicaSet={{ mongod_replicaset_name }}&connectTimeoutMS={{ mongod_client_connect_timeout_ms }}" -cas.serviceRegistry.mongo.collection: services -cas.serviceRegistry.mongo.userId: {{ mongodb.cas.user }} -cas.serviceRegistry.mongo.password: {{ mongodb.cas.password }} +cas.service-registry.mongo.client-uri: "mongodb://{{ mongodb.cas.user }}:{{ mongodb.cas.password }}@{{ mongodb.host }}:{{ mongodb.mongod_port | default(27017) }}/{{ mongodb.cas.db }}?replicaSet={{ mongod_replicaset_name }}&connectTimeoutMS={{ mongod_client_connect_timeout_ms }}" +cas.service-registry.mongo.collection: services +cas.service-registry.mongo.user-id: {{ mongodb.cas.user }} +cas.service-registry.mongo.password: {{ mongodb.cas.password }} cas.authn.surrogate.separator: "," -cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +cas.authn.surrogate.sms.attribute-name: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS # 24 hours cache for login delegation -cas.ticket.tst.timeToKillInSeconds: 86400 +# Must be at least 24 hours as this cache is also used for password management and its 24-hour-creation email +cas.ticket.tst.time-to-kill-in-seconds: 86400 -cas.authn.pm.enabled: true +cas.authn.pm.core.enabled: true {% if vitamui_password_configurations.anssiPolicyPattern is defined and vitamui_password_configurations.password.profile == "anssi" %} -cas.authn.pm.policyPattern: '{{ vitamui_password_configurations.anssiPolicyPattern | replace("'","''") }}' +cas.authn.pm.core.policy-pattern: '{{ vitamui_password_configurations.anssiPolicyPattern | replace("'","''") }}' {% else %} -cas.authn.pm.policyPattern: '{{ vitamui_password_configurations.customPolicyPattern | replace("'","''") }}' +cas.authn.pm.core.policy-pattern: '{{ vitamui_password_configurations.customPolicyPattern | replace("'","''") }}' {% endif %} cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe cas.authn.pm.reset.mail.text: "Changez de mot de passe via le lien: %s" cas.authn.pm.reset.mail.from: {{ smtp.cas.sender }} -cas.authn.pm.reset.expirationMinutes: {{ smtp.cas.expiration }} -cas.authn.pm.reset.mail.attributeName: email -cas.authn.pm.reset.securityQuestionsEnabled: false -cas.authn.pm.reset.includeServerIpAddress: false -cas.authn.pm.reset.includeClientIpAddress: false -cas.authn.pm.autoLogin: true +cas.authn.pm.reset.expiration-minutes: {{ smtp.cas.expiration }} +cas.authn.pm.reset.mail.attribute-name: email +cas.authn.pm.reset.security-questions-enabled: false +cas.authn.pm.reset.include-server-ip-address: false +cas.authn.pm.reset.include-client-ip-address: false +cas.authn.pm.core.auto-login: true # Used to sign/encrypt the password-reset link # cas.authn.pm.reset.crypto.encryption.key: @@ -140,13 +141,13 @@ spring.mail.properties.mail.smtps.ssl.protocols: {{ smtp.smtps.ssl.protocols}} cas.authn.throttle.failure.threshold: 2 -cas.authn.throttle.failure.rangeSeconds: 3 +cas.authn.throttle.failure.range-seconds: 3 cas: logout: - followServiceRedirects: true - redirectParameter: next + follow-service-redirects: true + redirect-parameter: next management.endpoints.enabled-by-default: true @@ -157,8 +158,8 @@ management.metrics.export.prometheus.enabled: true {% if sms.enabled|lower == "true" %} # for SMS: -cas.smsProvider.twilio.accountId: {{ sms.account }} -cas.smsProvider.twilio.token: {{ sms.token }} +cas.sms-provider.twilio.account-id: {{ sms.account }} +cas.sms-provider.twilio.token: {{ sms.token }} mfa.sms.sender: "{{ sms.sender }}" {% endif %} @@ -173,7 +174,8 @@ ip.header: X-Real-IP # 8 hours in seconds -api.token.ttl: 28800 +# the old api.token.ttl property +cas.authn.oauth.access-token.max-time-to-live-in-seconds: 28800 server-identity: @@ -221,11 +223,11 @@ logging: {% if vitamui.cas_server.cors.enabled|lower == "true" %} # Cas CORS (necessary for mobile app) -cas.httpWebRequest.cors.enabled: true -cas.httpWebRequest.cors.allowCredentials: false -cas.httpWebRequest.cors.allowOrigins: ['*'] -cas.httpWebRequest.cors.allowMethods: ['*'] -cas.httpWebRequest.cors.allowHeaders: ['*'] +cas.http-web-request.cors.enabled: true +cas.http-web-request.cors.allow-credentials: false +cas.http-web-request.cors.allow-origins: ['*'] +cas.http-web-request.cors.allow-methods: ['*'] +cas.http-web-request.cors.allow-headers: ['*'] {% endif %} # Password configuration diff --git a/docs/DAT/README.md b/docs/DAT/README.md index e69f00a2b57296b3718b7622b3aeb2f1fbf699d3..a0723dfe19883386c51837ec00d590f69fe64b79 100644 --- a/docs/DAT/README.md +++ b/docs/DAT/README.md @@ -1,5 +1,8 @@ # DAT VITAMUI +** This document is deprecated.** +Check docs/fr/architecture instead + Vitamui DAT with markdown. Command to build PDF as output: diff --git a/docs/fr/architecture/README.md b/docs/fr/architecture/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ddbf10d545549750fdb7f146e03c7cd0a7f94b3a --- /dev/null +++ b/docs/fr/architecture/README.md @@ -0,0 +1,25 @@ +# Architecture documentation - usage + +## How to build the doc + +### prerequisite + +``` +sphinx +sphinx-rtd-theme +recommonmark +myst-parser +latexmk +texlive-latex-extra +texlive-latex-french +``` + +### Build html +```bash +make clean html +``` + +### Build pdf +```bash +make clean latexpdf +``` diff --git a/docs/fr/architecture/conf.py b/docs/fr/architecture/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..aadf9f3ea0b2ff8fa7988121c3ad17c1c83e253d --- /dev/null +++ b/docs/fr/architecture/conf.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +import sphinx_rtd_theme + +# -- Project information ----------------------------------------------------- + +project = u'Vitam-UI' +copyright = u'2021, Programme Vitam' +author = u'Programme Vitam' + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +#source_suffix = ['.rst'] +# source_suffix = '.rst' +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown' +} + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = u'fr' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} +html_theme_options = { + 'logo_only': True, + 'display_version': True, + 'prev_next_buttons_location': 'bottom', + 'style_external_links': True, + 'style_nav_header_background': 'white', + # Toc options + 'collapse_navigation': True, + 'sticky_navigation': False, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False +} +html_title = 'Vitam-UI documentation' +html_logo = 'images/Vitam_Logo-CMJN.png' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +#html_css_files = [ +# 'css/theme.css', +#] +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Vitam-UIdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Vitam-UI.tex', u'Vitam-UI Document d\'architecture', + u'Programme Vitam', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'vitam-ui', u'Vitam-UI Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Vitam-UI', u'Vitam-UI Documentation', + author, 'Vitam-UI', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- +extensions = [ +# 'recommonmark', + 'myst_parser' +] diff --git a/docs/fr/architecture/images/Vitam_Logo-CMJN.png b/docs/fr/architecture/images/Vitam_Logo-CMJN.png new file mode 100644 index 0000000000000000000000000000000000000000..a96a5b73bba27003ceb54f71e90cc75a464ee772 Binary files /dev/null and b/docs/fr/architecture/images/Vitam_Logo-CMJN.png differ diff --git a/docs/DAT/docs/dat_vitamui/images/dat_archi_cas.png b/docs/fr/architecture/images/dat_archi_cas.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_archi_cas.png rename to docs/fr/architecture/images/dat_archi_cas.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_cas_1.png b/docs/fr/architecture/images/dat_cas_1.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_cas_1.png rename to docs/fr/architecture/images/dat_cas_1.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_cas_2.png b/docs/fr/architecture/images/dat_cas_2.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_cas_2.png rename to docs/fr/architecture/images/dat_cas_2.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_cas_3.png b/docs/fr/architecture/images/dat_cas_3.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_cas_3.png rename to docs/fr/architecture/images/dat_cas_3.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_cas_4.png b/docs/fr/architecture/images/dat_cas_4.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_cas_4.png rename to docs/fr/architecture/images/dat_cas_4.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_chaine_deploiement.png b/docs/fr/architecture/images/dat_chaine_deploiement.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_chaine_deploiement.png rename to docs/fr/architecture/images/dat_chaine_deploiement.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_level.png b/docs/fr/architecture/images/dat_level.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_level.png rename to docs/fr/architecture/images/dat_level.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_pki_1.png b/docs/fr/architecture/images/dat_pki_1.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_pki_1.png rename to docs/fr/architecture/images/dat_pki_1.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_session_1.png b/docs/fr/architecture/images/dat_session_1.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_session_1.png rename to docs/fr/architecture/images/dat_session_1.png diff --git a/docs/DAT/docs/dat_vitamui/images/dat_zoning.png b/docs/fr/architecture/images/dat_zoning.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/dat_zoning.png rename to docs/fr/architecture/images/dat_zoning.png diff --git a/docs/DAT/docs/dat_vitamui/images/journalisation_architecture.png b/docs/fr/architecture/images/journalisation_architecture.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/journalisation_architecture.png rename to docs/fr/architecture/images/journalisation_architecture.png diff --git a/docs/DAT/docs/dat_vitamui/images/journalisation_transaction.png b/docs/fr/architecture/images/journalisation_transaction.png similarity index 100% rename from docs/DAT/docs/dat_vitamui/images/journalisation_transaction.png rename to docs/fr/architecture/images/journalisation_transaction.png diff --git a/docs/fr/architecture/index.rst b/docs/fr/architecture/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..3663db72e8717d1cda18f78e5f2dcc70d2fa83c1 --- /dev/null +++ b/docs/fr/architecture/index.rst @@ -0,0 +1,19 @@ +.. Vitam-UI documentation master file, created by + sphinx-quickstart on Wed Jan 19 14:39:39 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Documentation d'architecture de Vitam-UI +======================================== + +.. toctree:: + :maxdepth: 2 + :numbered: + :caption: Contents: + + sections/intro_objectifs + sections/orientations_techniques + sections/architecture + sections/implementation + sections/gestion + diff --git a/docs/fr/architecture/sections/architecture.md b/docs/fr/architecture/sections/architecture.md new file mode 100644 index 0000000000000000000000000000000000000000..8c8f2930217e3eb075d71055170eec7e5eb6a584 --- /dev/null +++ b/docs/fr/architecture/sections/architecture.md @@ -0,0 +1,1493 @@ +Architecture +============ + +Applications Web +---------------- + +Les applications Web constituent les IHM de la solution. Elles sont accessibles depuis le portail de la solution. L'authentification d'un utilisateur dans une application cliente se fait par l'intermédiaire de l'IAM CAS. Une application cliente est constituée de 2 parties. + +* Interface utilisateur Front (IHM WEB) qui donne accès aux fonctionnalités via un navigateur +* Interface utilisateur Back (Service BackOffice) qui gère la communication avec CAS et les accès aux API externes + +Une double authentification est nécessaire pour qu’un utilisateur puissent accéder aux API externes : + +* Le service UI Back de l’application cliente doit posséder un certificat reconnu par la solution +* L’utilisateur de l’application cliente doit être authentifié dans la solution (par CAS) et posséder un token valide + +Les applications de base : + +* portal : application portail donnant accès aux applications +* identity : application pour gérer les organisations, utilisateurs, profils, etc. + +Services externes +----------------- + +Les services externes exposent des API REST publiques accessibles en HTTPS. Ces services constituent une porte d'accès aux services internes et assurent principalement un rôle de sécurisation des ressources internes. + + La connexion d'une application cliente à un service externe nécessite le partage de certificats X509 client et serveur dans le cadre d'un processus d'authentification mutuel (Machine To Machine/M2M). Dans la solution VITAMUI, les certificats des clients sont associés à un contexte de sécurité stocké dans une collection MongoDb gérée par le service security_internal. D'autre part, les utilisateurs clients sont identifiés et authentifiés dans les services externes par le token fourni par CAS et transmis dans les headers des requêtes REST en HTTPS. + + Le service externe a pour responsabilité de sécuriser les accès en effectuant les différentes étapes de vérifications des droits (générale, tenant, rôles, groupes, etc.) et de déterminer les droits résultants du client à l'origine de la requête, en réalisant l'intersection des droits applicatifs, définis dans le contexte de sécurité, avec les droits issus des profils de l'utilisateur. Le service externe s'assure ensuite que le client possède bien les droits pour accéder à la ressource demandée. + +Les services externes s'auto-déclarent au démarrage dans l'annuaire de service Consul. + +Les services disposent d'API REST pour suivre leur état et leur activité. Ces API ne sont pas accessibles publiquement. + +* API Status pour connaitre la disponibilité du service (utilisé par Consul) +* API Health (basée sur SpringBoot) pour suivre l'activité + +Les services génèrent les logs techniques dans la solution de log centralisée basée sur ELK. + +### Service iam-external + +* Description : service externe pour la gestion des organisations, utilisateurs, profils, etc. +* Contraintes +* API swagger + +### Service cas-server + +* Description : service d’authentification nécessaire et accessible uniquement par l'IAM CAS +* Contraintes +* API swagger + +### Service referential-external + +* Description : service externe pour la gestion des référentiels de la solution logicielle VITAM. + +Le service de référentiel externe a pour responsabilité la réception, la sécurisation des resources internes de gestion des référentiels, et la communication sécurisée avec les couches internes. +Le service de référentiel externe est composé de plusieurs points d'APIs: + - API des contrats d'accès (/referential/accesscontract) + - API des contrats d'entrées (/referential/ingestcontract) + - API des contrats de gestion (/referential/managementcontract) + - API des services agents (/referential/agency) + - API des formats (/referential/fileformat) + - API des ontologies (/referential/ontology) + - API des profils d'archivages (/referential/profile) + - API des règles de gestion (/referential/profile) + - API des profils de sécurité (/referential/security-profile) + - API des contexts applicatifs (/referential/context) + - API des opérations permettant le lancement différents audits (cohérence, valeur probante ...). + +### Service ingest-external + +* Description : service externe pour la gestion des opérations d'entrées d'archives de la solution logicielle VITAM. + +Le service d'ingest externe a pour responsabilité la réception, la sécurisation des resources internes de versement, et la communication sécurisée avec les couches internes. +Le service d'ingest externe est composé de plusieurs points d'APIs: + - API de versement des archives permettant la consommation des flux d'archives (/v1/ingest/upload) + - API de visualisation des journaux d'opération des opérations d'entrées (API /v1/ingest) + - API de visualisation détaillé d'un journal d'une opération d'entrées (/v1/ingest/{id}) + - API permettant le téléchargement d'un rapport sous forme ODT d'une opération d'entrée (/v1/ingest/odtreport/{id}) + - API commune est utilisé pour le téléchargement du Manifest et de l'ATR (Archival Transfer Reply) d'une opération d'entrée. (Manifest: /logbooks/operations/{id}/download/manifest, ATR: /logbooks/operations/{id}/download/atr) + +### Service archive-search-external + +* Description : service externe pour la gestion d'accès et la recherche d'archives de la solution logicielle VITAM. + +Le service d'archive externe a pour responsabilité la réception, la sécurisation des resources internes, et la communication sécurisée avec les couches internes d'accès aux archives. +Le service d'archive externe est composé de plusieurs points d'APIs: + - API de recherche des archive par requetes (/archive-search/search) + - API de recherche des unités archivistiques (/archive-search/archiveunit/{id}) + - API de recherche des arbres de positionnement et plans de classement (/archive-search/filingholdingscheme) + - API de téléchargement des objets (/archive-search/downloadobjectfromunit/{id}) + - API d'export des résultats sous format csv (/export-csv-search) + +Services internes +----------------- + +Les services internes offrent des API REST accessibles en HTTPS uniquement depuis les services externes ou internes. Les API de ces services ne sont donc pas exposées publiquement. Les services internes implémentent les fonctionnalités de base de la solution ainsi que les fonctionnalités métiers. En fonction des besoins, les services internes peuvent être amenés à journaliser des évènements dans le logbook des opérations du socle VITAM. + +Les utilisateurs sont identifiés dans les services internes grâce au token transmis dans les headers des requêtes HTTPS. L'utililisation du protocole HTTPS permet de chiffrer les tokens et les informations sensibles qui sont transportées dans les requêtes. Les services internes peuvent éventuellement vérifier les droits d'accès de l'utilisateur avant d'accéder aux ressources. + +Les services internes s'auto-déclarent au démarrage dans l'annuaire de service Consul. + +Les services disposent d'API REST pour suivre leur état et leur activité. + +* API Status pour connaitre la disponibilité du service (utilisé par Consul) +* API Health (basée sur SpringBoot) pour suivre l'activité du service + +Les services génèrent les logs techniques dans la solution de log centralisée basée sur ELK. + +### Service iam-internal + +* Description : service d’administration des clients, des utilisateurs et des profils, portail +* Contraintes +* API swagger +* Modèle de données + +### Service security-internal + +* Description : service de gestion de la sécurité applicative +* Contraintes +* API swagger +* Modèle de données + +### Service referential-internal + +* Description : service interne pour la gestion des référentiels de la solution logicielle VITAM. + +Le service de référentiel interne reçoit les requtes du client référentiel externe, et communique avec VITAM via les clients Admin/Access pour la récupération des données. +Le service de référentiel interne est composé de plusieurs points d'APIs: + - API des contrats d'accès (/referential/accesscontract) + - API des contrats d'entrées (/referential/ingestcontract) + - API des contrats de gestion (/referential/managementcontract) + - API des services agents (/referential/agency) + - API des formats (/referential/fileformat) + - API des ontologies (/referential/ontology) + - API des profils d'archivages (/referential/profile) + - API des règles de gestion (/referential/profile) + - API des profils de sécurité (/referential/security-profile) + - API des contexts applicatifs (/referential/context) + - API des opérations permettant le lancement différents audits (cohérence, valeur probante ...). + + pour plus d'information: voir la documentation des [référentiels](https://www.programmevitam.fr/pages/documentation/pour_archiviste/) + +### Service ingest-internal + +* Description : service interne pour la gestion des opérations d'entrées d'archives de la solution logicielle VITAM. + +Le service d'ingest interne a pour responsabilité la réception, et la communication sécurisée avec les couches externes de VITAM. +Le service d'ingest interne est composé de plusieurs points d'APIs: + - API de versement des archives permettant la consommation des flux d'archives (/v1/ingest/upload) + - API de visualisation des journaux d'opération des opérations d'entrées (API /v1/ingest) + - API de visualisation détaillé d'un journal d'une opération d'entrées (/v1/ingest/{id}) + - API permettant le téléchargement d'un rapport sous forme ODT d'une opération d'entrée (/v1/ingest/odtreport/{id}) + - API commune est utilisé pour le téléchargement du Manifest et de l'ATR (Archival Transfer Reply) d'une opération d'entrée. (Manifest: /logbooks/operations/{id}/download/manifest, ATR: /logbooks/operations/{id}/download/atr) + +Ce service est configuré pour qu'il puisse communiquer avec la zone d'accès de la solution logicielle VITAM. +Pour aller plus loin: [1](https://www.programmevitam.fr/ressources/DocCourante/raml/externe/ingest.html), [2](https://www.programmevitam.fr/ressources/DocCourante/html/archi/archi-applicative/20-services-list.html#api-externes-ingest-external-et-access-external) + +### Service archive-search-internal + +* Description : service interne pour la gestion d'accès et la recherche d'archives de la solution logicielle VITAM. + +Le service d'archive interne a pour responsabilité la réception, et la communication sécurisée avec les couches externes VITAM. +Le service d'archive interne est composé de plusieurs points d'APIs: + - API de recherche des archive par requetes (/archive-search/search) + - API de recherche des unités archivistiques (/archive-search/archiveunit/{id}) + - API de recherche des arbres de positionnement et plans de classement (/archive-search/filingholdingscheme) + - API de téléchargement des objets (/archive-search/downloadobjectfromunit/{id}) + - API d'export des résultats sous format csv (/export-csv-search) + +Services d’infrastructure +------------------------- + +La solution utilise plusieurs services d'infrastructures : + +* l'annuaire de service. basé sur l'outil Consul, il permet de localiser les services actifs dans l'infrastructure +* le service de gestion des logs rsyslog. Il permet de collecter, gérer et de transporter les logs +* l'outil de centralisation et de recherche des logs ELK (Elasticsearch / Logstash / Kibana) + +Les services d'infrastructures sont basés et mutualisés avec VITAM. Vous pouvez donc vous référer aux documentations VITAM pour avoir un détail précis du fonctionnement de ces services : + +* [Doc VITAM : Chaîne de log - rsyslog / ELK ](http://www.programmevitam.fr/ressources/DocCourante/html/exploitation/composants/elasticsearch_log/_toc.html) +* [Doc VITAM : Annuaire de service consul](http://www.programmevitam.fr/ressources/DocCourante/html/exploitation/composants/consul/_toc.html) + + +Service d'archivage VITAM +------------------------- + +Le service d'archivage se base sur le socle logiciel VITAM a pour fonction de gérer l'archivage des documents. Il apporte une forte garantie de sécurité et de disponibilité pour les archives. + +Ses principales caractéristiques sont : + +* Fonctions d’archivage : versement, recherches, consultation, administration , structurations arborescentes, référentiels… +* Accès aux unités d’archives via un service de requêtage performant +* Garantie de la valeur probante par le respect des normes en vigueur, par la traçabilité des opérations et du cycle de vie des objets et leur journalisation sécurisée +* Sécurité et la robustesse : la gestion applicative du stockage permet une réplication des données, métadonnées, index et journaux sur plusieurs sites et plusieurs offres contrôlées. L’architecture interne du stockage assure la capacité de reconstruire le système à partir d’une seule offre, en une fois ou au fil de l’eau +* La possibilité d’une utilisation mutualisée grâce à la gestion multi-tenant des archives +* Offres de stockage multiple +* Capacité à absorber de fortes volumétries de données + +La documentation de la solution VITAM est disponible [ici](http://www.programmevitam.fr/). + +Service d'authentification +-------------------------- + +### Authentification des applications externes + +A l'initialisation de la connexion HTTPS d'une application cliente à un service API VITAMUI, un processus d'authentification mutuelle entre le client et le serveur basé sur des certificats x509 est mis en oeuvre. Le service VITAMUI contrôle le certificat applicatif x509 transmis par le client pour s'assurer de sa validité. En cas de certificat invalide, expiré ou absent du truststore du service VITAMUI, la connexion échoue. + + Il est fortement recommandé d'utiliser des certificats officiels pour toutes les authentifications publiques. + +### Authentification des utilisateurs externes + +Lorsque la connexion applicative a été réalisée avec succès, la solution VITAMUI récupère dans la base MongoDB le contexte de sécurité applicatif associé au certificat client. Le contexte de sécurité applicatif définit les autorisations d’accès aux différents services (rôles) et le périmètre d'accès (tenant) de l'application. Un même contexte peut être associé à plusieurs certificats. L’utilisateur se voit alors attribuer l’intersection des rôles et tenants du contexte de sécurité applicatif et de ses profils. + +La cinématique est la suivante : + +1. Initialisation de la connexion par l'application cliente +2. Vérification du certificat client transmis par l'application +3. Vérification du contexte de sécurité associé au certificat +4. Récupération des profils (rôles & tenants) de l'utilisateur +5. Intersection des rôles et tenants entre le conexte de sécurité et les profils +6. L'utilisateur peut accéder aux ressources autorisées + +Il est ainsi possible de limiter les risques d'élévations de privilèges en dissociant les contextes applicatifs de 2 instances d'une même application. + +Par exemple, dans une première instance de l'application exposée sur un réseau public et associé à un contexte applicatif possédant des droits limités, un administrateur ne pourra pas accéder à des fonctions d'admnistration. En revanche, une deuxième instance, bénéficiant d'un contexte applicatif adéquat, sur un réseau protégé et accessible à ce même administrateur permettra d'effectuer des opérations à haut privilège. + +### Service d'authentification centralisé CAS + +Dans VITAMUI, l'authentification des utilisateurs est réalisée au moyen du service CAS. CAS est un service d'authentification centralisé (SSO et fédération d’identité), développé depuis 2004 par une communauté open source, et destiné aux applications Web . + +CAS propose les fonctionnalités suivantes : + +* un protocole (CAS protocol) ouvert et documenté +* la prise en charge de moteur de stockage variés (LDAP, base de données, X.509, 2-facteur) +* la prise en charge de plusieurs protocoles (CAS, SAML, OAuth, OpenID) +* des bibliothèques de clients pour Java, .Net, PHP, Perl, Apache, uPortal, etc. +* l’intégration native avec uPortal, BlueSocket, TikiWiki, Mule, Liferay, Moodle, etc. + + + +Dans la solution VITAMUI, CAS porte uniquement le processus d'authentification (délégué ou non) avec les informations (tickets, cookies, etc.) nécessaires au bon fonctionnement de l'authentification. En revanche, toutes les données des utilisateurs (compte, profils, rôles, etc.) sont stockés dans une base MongoDB gérée par les services VITAMUI. Lors du processus d'authentification, CAS récupère les données des utilisateurs via des services REST dédiés et sécurisés dans VITAMUI. Il est important de noter que les crédentials d'accès à la solution, les données des utilisateurs ou des applications ne sont donc jamais stockés dans CAS. + +Ce choix simplifie l'exploitation de la solution car il n'est pas nécessaire de migrer les données lors de la mise à jour de CAS. + + + +La [documentation de CAS](https://www.apereo.org/projects/cas) est disponible sur internet. CAS est livré sous licence Apache 2.0. + +### Intégration CAS dans VITAMUI + +Les principes généraux de l'implémentation de CAS dans VITAMUI sont les suivants : + +* l'email de l'utilisateur assure l'indification de l'utilisateur dans le système + +* les applications VITAMUI (ie. Service Provider) raccordées au serveur CAS utilisent le protocole CAS. (Dans VITAMUI, la bibliothèque Spring Security fournit cette fonctionnalité) + +* les applications VITAMUI faisant office de services providers sont déclarées dans CAS + +* la délégation d’authentification du serveur CAS aux IDP des clients se fait en SAML 2.0 + +* les IDP SAML utilisés sont déclarés dans VITAMUI et sont stockés dans MongoDB + +* la fonction de révocation périodique de mot de passe est assurée par CAS + +* l’anti force brute est assurée par le serveur CAS (→ throttling) + +* la fonction de récupération de mot de passe et le contrôle de robustesse du mot de passe sont assurés par le module password management de CAS + +* l’authentification multi-facteur est assurée par SMS (Le fonctionnement du MFA : page de login CAS, étape supplémentaire est portée par le provider du deuxième facteur) est assurée par CAS + +* le service CAS dispose d'un certificat client pour être authentifié par VITAMUI + +* dans un environnement web clusterisé, le reverse proxy est configuré pour assurer l'affinité de session nécessaire à la conservation du cookie de session (JSESSIONID) dans l'application WEB + +Dans le cas d'un utilisateur n'utilisant pas le SSO : + +* le contrôle de robustesse du mot de passe est assuré par le service Identity de VITAMUI + +* le chiffrement des mots de passe est assuré par le service Identity de VITAMUI + +* le mot de passe est conservé chiffré dans le base MongoDB de VITAMUI + + + +### Authentification d'un utilisateur non authentifié + +Pour un utilisateur, non préalablement authentifiés, l'authentification dans CAS se fait en plusieurs étapes : + +1. L'utilisateur tente d'accéder à une page sécurisée et est alors redirigé var CAS + +2. une première page est affichée dans CAS pour saisir l’identifiant (unique) et le mot de passe de l’utilisateur + +3. selon le domaine email de l'utilisateur et les règles particulières à la délégation d’authentification, CAS délègue l’authentification ou authentifie lui-même l'utilisateur. + + * pas de délégation : une seconde page est affichée pour saisir le mot de passe et le serveur CAS vérifie les credentials auprès du service Identity de VITAMUI + + * délégation : l’utilisateur est redirigé pour authentification sur l'IDP de son organisation en SAML v2 + +4. CAS demande la création d'un token utilisateur via le service Identity de VITAMUI. Ce token assure l'identification de l'utilisateur dans les API external et internal de VITAMUI. + +4. le serveur CAS récupère les informations de l'utilisateur via le service Identity/CAS de VITAMUI + +5. l'application récupère le profil de l'utilisateur et son token API + +6. lors d'un apple à l'API VITAM, le token est transmis dans le header de la requête. + + + +### Authentification d'un utilisateur préalablement authentifié + +Si l'utilisateur est déjà authentifié auprès du CAS, aucune page de login ne s'affiche et l'utilisateur est redirigé vers l'application souhaitée, en étant authentifié dans cette application. Suivant les utilisateurs / applications demandées, une authentification multi-facteurs peut être jouée. + +### Délégation d'authentification + +La délégation d'authentification est prise en charge par CAS. Actuellement seul le protocol SAML v2 est supporté. + +Les étapes suivantes expliquent comment fonctionne la délégation d'authentification selon le protocole SAML v2 dans le cadre de VITAMUI. + +En amont de ce processus, l’ IDP (SSO) doit fournir à VITAMUI l'URL associée à son service d'authentification unique (SSO), ainsi que la clé publique qui lui sera nécessaire pour valider les réponses SAML. + +Le schéma ci-dessous illustre les étapes et le mécanisme de connexion d'un utilisateur à une application VITAMUI, via un service d'authentification unique basé sur le protocole SAML. La liste numérotée qui suit le diagramme revient en détail sur chacune des étapes. + +Connexion à VITAMUI via une délégation d'authentification en SAML v2 + + + +L'utilisateur tente d'accéder à une application VITAMUI hébergée + +1. VITAMUI génère une demande d'authentification SAML, qui est encodée et intégrée dans l'URL associée au service d'authentification unique (SSO) de l’IDP de l'organisation cliente. Le paramètre RelayState, qui contient l'URL encodée de l'application VITAMUI à laquelle tente d'accéder l'utilisateur, est également incorporé dans l'URL d'authentification unique. Il constitue un identificateur opaque qui sera par la suite renvoyé au navigateur de l'utilisateur sans modification ni vérification. + +2. VITAMUI envoie une URL de redirection au navigateur de l'utilisateur. Cette URL inclut la demande d'authentification SAML encodée qui doit être envoyée au service d'authentification unique de l'organisation cliente. + +3. L’IDP de l'organisation cliente décode la demande SAML et en extrait l'URL du service ACS (Assertion Consumer Service) de VITAMUI et de la destination de l'utilisateur (paramètre RelayState). Il authentifie ensuite l'utilisateur, soit en l'invitant à saisir ses identifiants de connexion, soit en vérifiant ses cookies de session. + +4. L’IDP de l'organisation cliente génère une réponse SAML contenant le nom de l'utilisateur authentifié. Conformément aux spécifications SAML 2.0, cette réponse contient les signatures numériques des clés DSA/RSA publiques et privées du de l'organisation cliente. + +5. L’IDP de l'organisation cliente encode la réponse SAML et le paramètre RelayState avant de les renvoyer au navigateur de l'utilisateur. Il fournit le mécanisme permettant au navigateur de transmettre ces informations au service ACS de VITAMUI. Par exemple, il peut incorporer la réponse SAML et l'URL de destination dans un formulaire, qui inclut un script JavaScript sur la page qui se charge alors d'envoyer automatiquement le formulaire à VITAMUI. + +6. Le service ACS de VITAMUI vérifie la réponse SAML à l'aide de la clé publique du de l'organisation cliente. Si la réponse est validée, l'utilisateur est redirigé vers l'URL de destination. + +L'utilisateur est redirigé vers l'URL de destination. Il est désormais connecté à VITAMUI. + +### Sécurisation de CAS + +En production, le serveur CAS sera composé de plusieurs noeuds. Il est nécessaire d'acyiver la sécurité et de configurer : +* une définition de services (dans MongoDB) propres aux URLs de production +* une configuration Hazelcast adéquate (stockage des sessions SSO). + +### Activation de la sécurité + +#### Configuration des propriétés de sécurité + +La configuration de CAS se trouve dans le fichier YAML applicatif (en développement : cas-server-application-dev.yml). Elle concerne d’abord les trois propriétés suivantes : +```yaml +cas.tgc.secure : cookie de session SSO en HTTPS +cas.tgc.crypto.enabled : cryptage / signature du cookie SSO +cas.webflow.crypto.enabled : cryptage / signature du webflow +``` +En production, il est absolument nécessaire que ces trois propriétés soient à true. + +Pour la propriété cas.tgc.crypto.enabled à true, il faut définir la clé de cryptage et de signature via les propriétés suivantes : + +```yaml +cas.tgc.crypto.encryption.key : clé de cryptage (ex. Jq-ZSJXTtrQ...) +cas.tgc.crypto.signing.key : clé de signature (ex. Qoc3V8oyK98a2Dr6...) +``` + +Pour la propriété cas.webflow.crypto.enabled à true, il faut définir la clé de cryptage et de signature via les propriétés suivantes : + +```yaml +cas.webflow.crypto.encryption.key : clé de cryptage +cas.webflow.crypto.signing.key : clé de signature +``` + +Si aucune clé n’est définie, le serveur CAS va les créer lui-même, ce qui ne fonctionnera pas car les clés générées seront différentes sur chaque noeud. + +En outre, pour la délégation d’authentification et la gestion du du mot de passe, il existe deux propriétés qui sont déjà à true, mais pour lesquelles aucune clé n’a été définie : + +```yaml +cas.authn.pac4j.cookie.crypto.enabled : chiffrement & signature pour + la délégation d’authentification + +cas.authn.pm.reset.crypto.enabled : chiffrement & signature pour + la gestion du mot de passe. +``` + +Pour la délégation d’authentification, il faut définir la clé de cryptage et de signature via les propriétés suivantes : + +```yaml +cas.authn.pac4j.cookie.crypto.encryption.key : clé de cryptage +cas.authn.pac4j.cookie.crypto.signing.key : clé de signature +``` + +Pour la gestion du mot de passe, il faut définir la clé de cryptage et de signature via les propriétés suivantes : + +```yaml +cas.authn.pm.reset.crypto.encryption.key : clé de cryptage +cas.authn.pm.reset.crypto.signing.key : clé de signature +``` + +#### Suppression des accès aux URLs d'auto-administration + +Les URLs d’auto-administration du CAS doivent être désactivées. La configuration suivante doit être appliquée : +```yaml +cas.adminPagesSecurity.ip: a +cas.monitor.endpoints.sensitive: true +cas.monitor.endpoints.enabled: false +endpoints.sensitive: true +endpoints.enabled: false +management.security.enabled: false +``` + +Cette dernière configuration est sans importance du moment que l’URL /status du serveur CAS n’est pas mappée en externe. + +### Définition des services supportés + +Il est nécessaire de fournir lors du déploiement de la solution VITAMUI, la liste des services autorisés à interagir avec CAS en tant que Service Provider. Cette liste permet à CAS de s'assuer que le service est connu avant d'effectuer le callback. La liste des services est stockée lors du déploiement dans la base MongoDB de VITAM UI est accessible uniqument par CAS. + +### Configuration Hazelcast + +Par défaut, les noeuds Hazelcast s’auto-découvrent et les tickets sont partitionnés entre tous les noeuds et chaque ticket a un backup. Il est néanmoins possible de configurer dans CAS des propriétés permettant d'affiner le réglage d’Hazelcast : + +```yaml +cas.ticket.registry.hazelcast.cluster.members: 123.456.789.000,123.456.789.001 +cas.ticket.registry.hazelcast.cluster.instanceName: localhost +cas.ticket.registry.hazelcast.cluster.port: 5701 +``` + +Ci-dessous sont listées des propriétés permettant une gestion avancée d'Hazelcast : + +```yaml +cas.ticket.registry.hazelcast.cluster.tcpipEnabled: true +cas.ticket.registry.hazelcast.cluster.partitionMemberGroupType: + HOST_AWARE|CUSTOM|PER_MEMBER|ZONE_AWARE|SPI +cas.ticket.registry.hazelcast.cluster.evictionPolicy: LRU +cas.ticket.registry.hazelcast.cluster.maxNoHeartbeatSeconds: 300 +cas.ticket.registry.hazelcast.cluster.loggingType: slf4j +cas.ticket.registry.hazelcast.cluster.portAutoIncrement: true +cas.ticket.registry.hazelcast.cluster.maxHeapSizePercentage: 85 +cas.ticket.registry.hazelcast.cluster.backupCount: 1 +cas.ticket.registry.hazelcast.cluster.asyncBackupCount: 0 +cas.ticket.registry.hazelcast.cluster.maxSizePolicy: USED_HEAP_PERCENTAGE +cas.ticket.registry.hazelcast.cluster.timeout: 5 +``` + +Multicast Discovery : +```yaml +cas.ticket.registry.hazelcast.cluster.multicastTrustedInterfaces: +cas.ticket.registry.hazelcast.cluster.multicastEnabled: false +cas.ticket.registry.hazelcast.cluster.multicastPort: +cas.ticket.registry.hazelcast.cluster.multicastGroup: +cas.ticket.registry.hazelcast.cluster.multicastTimeout: 2 +cas.ticket.registry.hazelcast.cluster.multicastTimeToLive: 32 +``` + +La documentation pour la génération des clés pour le cluster CAS est disponible [ici](https://dacurry-tns.github.io/deploying-apereo-cas/building_server_configure-server-properties.html#configure-ticket-granting-cookie-encryption). + +### Fonctionnalités + +Le serveur CAS VITAMUI est construit sur le serveur CAS Open Source v6.1.x via un mécanisme d'overlay Maven. + +Les beans Spring sont chargés via les classes `AppConfig` et `WebflowConfig` déclarées par le fichier `src/main/resources/META-INF/spring.factories`. + +Les propriétés spécifiques au client IAM sont mappées en Java via le bean `IamClientConfigurationProperties`. + +La configuration est située dans le répertoire `src/main/config` et dans les fichiers `src/main/resources/application.properties` et `src/main/resources/bootstrap.properties`. + +Une bannière custom est affichée au lancement (`CasEmbeddedContainerUtils`). + +Le serveur CAS VITAMUI contient les fonctionnalités suivantes : + + +#### Utilisation de MongoDB + +Les applications autorisées à s'authentifier sur le serveur CAS sont définies dans une base de données MongoDB. + +Cela est géré par la dépendance `cas-server-support-mongo-service-registry`. + + +#### Utilisation d'Hazelcast + +Les informations nécessaires durant les sessions SSO sont stockées dans Hazelcast. + +Cela est géré par la dépendance `cas-server-support-hazelcast-ticket-registry`. + + +#### Authentification login/mot de passe + +Le `UserAuthenticationHandler` vérifie les credentials de l'utilisateur auprès de l'API IAM et +le `UserPrincipalResolver` crée le profil utilisateur authentifié à partir des informations récupérées via l'API IAM. + +Après avoir saisi son identifiant (classe `DispatcherAction`): +- si l'utilisateur ou son subrogateur est inactif, il est envoyé vers une page dédiée (`casAccountDisabledView`) +- si aucun fournisseur d'identité n'est trouvé pour l'utilisateur, il est envoyé vers une page dédiée (`casAccountBadConfigurationView`). + + +#### Délégation d'authentification + +L'authentification peut être déléguée à un serveur SAML externe. + +Cela est géré par la dépendance `cas-server-support-pac4j-webflow`. + +Le flow d'authentification a été modifié (classe `CustomLoginWebflowConfigurer`) pour se dérouler en deux étapes : +- saisie de l'identifiant +- saisie du mot de passe (`src/main/resources/templates/casPwdView.html`) ou redirection vers le serveur SAML externe pour authentification. Cela est géré par l'action `DispatcherAction`. + +Cette délégation d'authentification peut être faite de manière transparente si le paramètre `idp` est présent (il est sauvé dans un cookie de session pour mémorisation). +Cela est gérée par la classe `CustomDelegatedClientAuthenticationAction`. + + +#### Subrogation + +Un utilisateur peut subroger un utilisateur authentifié ("il se fait passer pour lui"). + +Cela est géré par la dépendance `cas-server-support-surrogate-webflow`. + +La subrogation est gérée par CAS avec un identifiant contenant à la fois l'identifiant de l'utilisateur et le subrogateur séparé par une virgule. + +Pour permettre un affichage séparé des deux informations, elles sont découpées en avance dans les classes `CustomDelegatedClientAuthenticationAction` et `DispatcherAction`. + +Pour gérer correctement la subrogation lors d'une délégation d'authentification, le subrogé est sauvegardé en fin d'authentification (`DelegatedSurrogateAuthenticationPostProcessor`). + +Le droit de subroger est vérifié auprès de l'API IAM (`IamSurrogateAuthenticationService`). + +Le temps de session SSO est allongée dans le cas d'une subrogation générique (`DynamicTicketGrantingTicketFactory`). + + +#### Interface graphique customisée + +L'interface graphique du serveur CAS est adapté au look and feel VITAMUI. + +Les pages HTML modifiées sont dans le répertoire `src/main/resources/templates` et les ressources statiques (JS, CSS, images) sont dans le répertoire `src/main/resources/static`. + +Les messages customisés sont dans les fichiers `overriden_messages*.properties` (dans `src/main/resources`). + +Les bons logos à afficher sont calculés via les actions `CustomInitialFlowSetupAction` (login) et `GeneralTerminateSessionAction` (logout). + +Après une authentification réussie, une page "connexion sécurisée" est affichée avant de rediriger sur l'application demandée. Cela est gérée par l'action : `SelectRedirectAction`. + + +#### Double facteur SMS + +Dans certains cas, l'authentification nécessite un second facteur sous forme de token reçu par SMS à re-saisir dans l'IHM. + +Cela est géré par la dépendance `cas-server-support-simple-mfa`. + +Un webflow spécifique est géré dans `src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml` +pour gérer le cas où l'utilisateur n'a pas de téléphone (`casSmsMissingPhoneView`, classe `CustomSendTokenAction`), +le cas du code expiré (`casSmsCodeExpiredView`, classe `CheckMfaTokenAction`) et le fait que le token n'a pas de format CAS spécifique ("CASMFA-"). + + +#### Gestion du mot de passe + +Le serveur CAS permet également de réinitialiser ou de changer son mot de passe. + +Cela est géré par la dépendance `cas-server-support-pm-webflow`. + +Le changement de mot de passe est effectué auprès de l'API IAM grâce à la classe `IamPasswordManagementService`. + +Les emails envoyés lors de la réinitialisation du mot de passe sont internationalisés grâce aux classes `PmMessageToSend` et `I18NSendPasswordResetInstructionsAction`. +Ils sont aussi différents suivant le type d'évènement : réinitialisation standard ou création de compte. Tout comme le temps d'expiration (classe `PmTransientSessionTicketExpirationPolicyBuilder`). + +Une API REST dans CAS permet de déclencher la réinitialisation du mot de passe : `ResetPasswordController`. + +La classe `TriggerChangePasswordAction` permet de provoquer le changement de mot de l'utilisateur même s'il est déjà authentifié. + +La classe `CustomVerifyPasswordResetRequestAction` gère proprement les demandes de réinit de mot de passe expirées. + +La durée de vie des tickets transient est réglée à 1 jour (classe `HazelcastTicketRegistryTicketCatalogConfiguration`) pour gérer les demandes de réinit de mot de passe lors de la création d'un compte. + + +#### Support serveur OAuth + +Le serveur CAS se comporte comme un serveur OAuth pour permettre la cinématique "Resource Owner Password flow". + +Cela est géré par la dépendance `cas-server-support-oauth-webflow`. + +L'utilisateur est authentifié via ses credentials et des credentials applicatifs et les access tokens OAuth générés sont le token d'authentification VITAM de l'utilisateur (classe `CustomOAuth20DefaultAccessTokenFactory`). + + +#### Déconnexion + +Pour éviter tout problème avec des sessions applicatives persistantes, la déconnexion détruit toutes les sessions applicatives dans le cas où aucune session SSO n'est trouvée (classe `GeneralTerminateSessionAction`). + + +#### Throttling + +Le nombre de requêtes d'authentification accepté par le serveur CAS est limité. + +Cela est géré par la dépendance `cas-server-support-throttle`. + +Sessions applicatives +--------------------- + +### Liste des sessions + +Il existe 4 sessions définies dans la solution VITAMUI : + +* la session applicative Web (cookie JSESSIONID) +* la session des services API (token X-AUTH-TOKEN) +* la session applicative CAS (cookie JSESSIONID / Domaine CAS) +* la session de l'IDP SAML utilisé pour la délégation d'authentification + +### Séquence de création des sessions + +La séquence de création des sessions est liée à l'utisation du protocole CAS et à l'intégration des services API. +Dans le processus de connexion, la création des sessions s'effectue dans l'ordre suivant : + +1. création par l'application Web du cookie JSESSIONID +2. création de la session SAML (dans le cas d'une délégation d'authentification) +3. création dans CAS du cookie TGC +4. création par CAS dans l'API VITAMUI du token API + + + Schéma des sessions applicatives + + + + +### Session applicative Web + +La session applicative est portée par le cookie JSESSIONID créée dans l'application Web. Le cookie expire à l'issue du délai d'inactivité et sa durée de vie est réinitialisée à chaque utilisation. [A vérifier] + +Lorsque la session expire, le cookie est automatiquement recréé par l'application WEB et le client redirigé par un code HTTP 302 vers le service CAS. + +Si la session CAS (cookie TGC) a expiré, l’utilisateur doit se reloguer et les sessions CAS (TGC), services API (Token), et si nécessaire SAML, sont recréées. En revanche, si la session CAS est valide, l'utilisateur n’a pas besoin de se reloguer et est directement redirigé sur l’application Web. Dans ce dernier cas, la session des services est conservée et le token n'est pas recréé. + +### Session des services API + +La session des services API est porté par un token. Le token permet l'identification des utilisateurs dans les services API (external et internal) de VITAMUI. Le token expire à l'issue du délai d'inactivité et sa durée de vie est réinitialisée à chaque utilisation. + +Lors du processus d'authentification, le resolver de CAS extrait l’identité de l'utilisateur (de la réponse SAML en cas de délégation d'authentification) et appelle le service Identity de VITAMUI pour créer un token conservé dans la base mongoDB. + + Le token est fourni aux applications web, mais n'est pas visible dans le navigateur web du client car il est conservé dans la session applicative (JSESSIONID) de l'utilisateur. Dans chaque requête vers les services, le header X-Auth-Token est positionné avec la valeur du token. Avant d'accpter la requête, le service contrôle l'existence du header précédent et vérifie que le token est toujours valide. + + Lorsque le token a expiré, les services API génèrent une erreur 401 transmis aux applications web. Lors de la réception d'une erreur 401, l'application web invalide la session applicative (JSESSIONID) concernée, puis effectue une redirection vers le logout CAS (afin de détruire le TGC et la session SAML). L'utilisateur doit obligatoirement se reconnecter pour utiliser à nouveau l'application. + +### Session CAS + +La session CAS est portée par un cookie Ticket-Granting Cookie ou TGC. Le TGC est le cookie de session transmis par le serveur CAS au navigateur du client lors de la phase de login. Ce cookie ne peut être lu ou écrit que par le serveur CAS, sur canal sécurisé (HTTPS). Lors du processus d'authentification, le resolver de CAS extrait l’identité de l'utilisateur (de la réponse SAML en cas de délégation), crée le cookie TGC et un ticket dans l’URL puis stocke ces informations dans le cache HazelCast. + +[A vérifier] +En cas de délégation d'authentification, si la session CAS a expiré (TGC invalide) + +* l'utilisateur doit se reconnecter si la session SAML a expiré +* sinon CAS recrée automatiquement le TGC et le token + +Sans délégation d'authentification, l'utilisateur doit se reconnecter systématiquement pour que CAS puisse recréer le TGC et le token. + +### Session des IDP + +La session de l’IDP (Identiy Provider) est propre à chaque IDP SAML. Il existe néanmoins un délai maximum dans CAS pour accepter la délégation d'authentification d'un IDP SAML. + +L'utilisateur doit obligatoirement se reconnecter si la session SAML a expiré. + +### Expiration et cloture des sessions + +Il existe deux politiques d'expiration possibles : + +* expiration de session par délai d'inactivité : la session expire si aucune action n'est faite (par l'utilisateur) au bout du délai d'inactivité (session Token) +* expiration de session par délai maximum : la session expire au bout du délai maximum depuis la date de création, quelque soit les actions faites par l'utilisateur (Sessions Applicatives & CAS) + +A l’expiration de la session CAS, toutes les sessions applicatives sont supprimées. [Quid du token ?] Les sessions applicatives sont détruites via une redirection dans le navigateur. [A Préciser le fonctionnement via le navigateur vs certificats] + +Le logout d'une application web invalide la session applicative concernée, puis effectue une redirection vers le logout CAS afin de détruire la session CAS (destruction du TGC), la session API (destruction du token) et la session SAML. [à confirmer] + +A près un logout ou l'utilisateur doit obligatoirement se reconnecter pour utiliser à nouveau l'application. + +### Paramétrages des sessions + +Toutes ces valeurs sont paramétrables dans l’instance de la solution. + +Compte principal : [à confirmer] + +* la session applicative JSESSIONID : 15 minutes (délai d'inactivité) : +* la session du token : 165 minutes (délai maximum) : +* la session CAS TGC : 170 minutes (délai maximum) : +* délai maximum dans CAS pour accepter la délégation d’authentification : 14 jours (délai maximum) + +Dans le cas de la subrogation, on a : [à confirmer] + +* la session applicative JSESSIONID : 15 minutes (délai d'inactivité) : +* la session du token : 165 minutes (délai maximum) : +* la session CAS TGC : 170 minutes (délai maximum) : +* délai maximum dans CAS pour accepter la délégation d’authentification : 14 jours (délai maximum) + + +Profils et rôles +---------------- + +### Groupe de profils + +Un groupe de profils contient (entre autres) les informations suivantes : + +* liste de profils +* niveau + +Un groupe de profils est rattaché à un utilisateur, lui-même rattaché à une organisation. Un groupe de profil peut contenir des profils avec des tenants différents. Pour un tenant donné, un groupe de profil ne peut contenir qu'un seul profil d'une même APP. + +### Profils + +Le profil contient (entre autres) les informations suivantes : + +* tenant +* liste de rôles +* niveau +* APP + +Un profil contient un seul et unique tenant. + +L'APP permet d'autoriser l'affichage d'une application dans le portail. Le fait de pouvoir afficher l'application dans le portail ne préjuge pas des droits qui sont nécessaires au bon fonctionnement de l'application. + +Un profil est modifiable uniquement par un utilisateur possédant un rôle autorisant la modification de profil et qui possède un niveau supérieur à celui du niveau du profil concerné. + +Un profil ne peut être rattaché qu'à un groupe de profils de même niveau. + +Dans une instance VITAMUI partagée, il convient de limiter les droits des administrateurs d'une organisation afin qu’ils ne puissent pas réaliser certains actions sur des ressources sensibles. (ie. customer, idp, tenant, etc.). Les profils créés à l’initialisation d’une nouvelle organisation ne doivent donc jamais comporter certains rôles (gestion des organisations, idp, tenants, etc. ) afin d'interdire à l'administrateur d'une organisation d'utiliser ou de créer de nouveaux profils avec ces rôles pour réaliser des opérations multi-tenants. + +Généralement l'adminitrateur de l'instance possède tous les droits (et donc tous les rôles). + +### Rôles + +Le crôle constitue la granularité la plus fine dans le système de gestion des droits. Un rôle donne des droits d’accès à des endpoints (API) correspondants à des services. Un rôle peut être affecté à un ou plusieurs profils. Dans l'implémentation VITAMUI, l’accès à un endpoint est contrôlé par l’annotation @Secured. Il existe des rôles (dénommés sous-rôles) qui donnent accès à des fonctions protégées du service. Ces “sous-rôles†sont généralement contrôlés dans le corps de la méthode par le contexte de sécurité. + +``` + @Secured(ROLE_CREATE_XXX) + public MyDto create(final @Valid @RequestBody MyDto dto) { + if ( SecurityContext.hasRole(ROLE_CREATE_XXX_YYY) { + setProperty(...) + } + else { + return HTTP.403 ;. + } + } +``` + +Dans l'exemple ci-dessus : + +* ROLE_CREATE_XXX est un rôle qui donne accès au service create +* ROLE_CREATE_XXX_YYY est un sous-rôle, utilisé dans le corps de la méthode, qui donne accès à une fonctionnalité spécifique de la méthode. + +### Niveaux + +Dans une organisation, la gestion des utilisateurs, des profils et groupe de profils repose sur le principe de la filière unidirectionnelle d'autorité étendue. Elle donne autorité au manageur sur les ressources d'une entité. Plusieurs manageurs peuvent avoir autorité sur une même entité. Un manageur n’a jamais autorité sur l'entité à laquelle il appartient. Il existe cependant un manageur administrateur qui a autorité sur toutes les ressources de toutes les entités. + +Schéma de l’arbre de niveaux : + + + + + + +* Une entité dispose d'un niveau représenté par une chaine de caractère +* Une ressource est un objet (user, group, profile, etc.) appartenant à une entité +* Le manageur est un utilisateur qui a autorité sur des entités et leurs ressources associées + +Ex. niveau : "World.France.DSI.Infra" + +* World : entité racine - le niveau est vide (ou zéro). Le manageur World a autorité sur toutes les entités de l'arbre (dont lui-même) +* France : entité enfant de World - La manageur France a autorité sur les entités DSI et Infra +* DSI : entité enfant de France - La manageur DSI a autorité sur l'entité Infra +* Infra : entité enfant de DSI - La manageur Infra n'a autorité sur rien + +Un utilisateur : + +* manageur d'une ressource possède un niveau supérieur à celui de la ressource +* peut lister, modifier & supprimer une ressource dont il est le manageur +* peut créer une ressource dans une entité dont il est le manageur +* ne peut pas effectuer une action sur une ressource dont il n'est pas manageur +* ne peut pas effectuer des actions s’il ne dispose pas des rôles associés à ces actions +* ne peut pas affecter à un profil des rôles dont il ne dispose pas (cf. gestion des profils) + +Un utilisateur avec un niveau vide (administrateur) : + +* peut uniquement effectuer les actions associées aux rôles qu'il possède +* peut créer un profil ou un groupe de profils de niveau vide (admin) +* peut modifier ses ressources +* ne peut pas ajouter à un profil un rôle dont il ne dispose pas + +Un administrateur d'une organisation possède donc des droits limités aux rôles qui ont été affectés à l'initialisation du système. Il ne peut pas par exemple créer une nouvelle organisation, si ce rôle ne lui pas été donné à l'origine. D'autre part, les droits de l'administrateur restent également limités par les droits associés à ceux du contexte de sécurité de l'application qu'il utilise. + +* un profil ou un groupe de profils ne peuvent être supprimés que s'ils ne sont plus utilisés +* un profil avec un niveau ne peut être rattaché qu’à un groupe de un même niveau. + +### Matrice des droits + +Les tableaux ci-dessous indiquent les droits d'un utilisateur en fonction du niveau de la ressource cible. + +* Matrice des droits d'un utilisateur de niveau N pour réaliser des actions sur un utilisateur de niveau cible N+1, N, N-1 : + +|Niveau cible | N+1 | N | N-1 | +|---------------|:----------:|:---------:|:---------:| +|Créer | Non | Non | Oui | +|Modifier | Non | Non | Oui | +|Lire | Non | Oui (1) | Oui | +|Supprimer | Non | Non | Oui (2) | + +Oui(1) : oui mais uniquement s'il s'agit de lui-même +Oui(2) : en théorie, car il est n'est pas possible de supprimer un utilisateur + +* Matrice des droits d'un utilisateur de niveau N pour réaliser des actions sur un profil de niveau cible N+1, N, N-1 : + +|Niveau cible | N+1 | N | N-1 | +|---------------|:----------:|:---------:|:---------:| +|Créer | Non | Non | Oui | +|Modifier | Non | Non | Oui | +|Lire | Non | Oui (1) | Oui | +|Attribuer | Non | Non | Oui | +|Supprimer | Non | Non | Oui | + +Oui(1) : oui mais uniquement si le profil est présent dans son groupe de profils +Lors de la modification du niveau du profil. Il faut vérifier qu’il n’est associé à aucun groupe. +L'utilisateur ne peut affecter à un profil que les rôles et un tenant qu'il possède + +* Matrice des droits d'un utilisateur de niveau N pour réaliser des actions sur un groupe de profils de niveau cible N+1, N, N-1 : + +|Niveau cible | N+1 | N | N-1 | +|---------------|:----------:|:---------:|:---------:| +|Créer | Non | Non | Oui | +|Modifier | Non | Non | Oui | +|Lire | Non | Oui (1) | Oui | +|Attribuer | Non | Non | Oui | +|Supprimer | Non | Non | Oui | + +Oui(1) : oui mais uniquement s'il s'agit de son groupe +Lors de la modification du niveau d'un groupe. Il faut vérifier qu’il n’a pas de profils + +* Matrice des droits d'un administrateur de niveau racine (niveau vide) pour réaliser des actions sur une ressource de niveau cible N+1, N, N-1 : + +|Niveau cible | N+1 | N | N-1 | +|---------------|:--------:|:---------:|:---------:| +|Créer | - | Oui | Oui | +|Modifier | - | Oui | Oui | +|Lire | - | Oui | Oui | +|Attribuer | - | Oui | Oui | +|Supprimer | - | Oui | Oui | + +Un administrateur ne peut pas affecter à un profil des rôles qui ne sont pas autorisés dans son organisation. + + +### Sécurisation des ressources + +#### Vérification générale + +Le processus de sécurisation des ressources est systématique et identique quelque soit l’utilisateur appelant la ressource. Ce processus, implémenté dans Spring Security, est essentiel car il permet de s’assurer qu’un utilisateur ne sorte jamais de son tenant. Ce processus de sécurisation est réalisé sur les accès aux ressources des services externals. + +Les étapes du processus de sécurisation sont les suivantes : + +1. récupérer l’utilisateur associé au token utilisateur fourni dans le header +2. vérifier que l'organisation de l’utilisateur possède le tenant fourni dans le header +3. vérifier que l’utilisateur possède un profil avec le tenant fourni dans le header +4. trouver le contexte applicatif par rapport au certificat x509 fourni dans la requête +5. vérifier que le contexte applicatif autorise le tenant fourni dans le header +6. créer un contexte de sécurité utilisateur qui correspond au tenant fourni dans le header et à l’intersection des rôles des profils de l’utilisateur et ceux du contexte applicatif +7. vérifier que les rôles du contexte de sécurité de l’utilisateur autorisent l’utilisateur authentifié à appeler la ressource + +Si la ressource n'est pas accessible, une erreur 403 est retournée + +#### Vérification des sous-rôles + +Cette étape correspond à la vérification des sous-rôles dans le service appelé. Un sous-rôle donne accès à une fonction ou à un périmètre spécifique du service. + +Exemple : Un utilisateur RH a le droit de modifier un autre utilisateur sauf son email (qui est sécurisé). + +* L’utilisateur RH possède donc un rôle UPDATE_USER qui lui donne accès à l’API et au service de mise à jour globale des utilisateurs +* L’utilisateur RH ne possède pas le rôle UPDATE_USER_EMAIL qui permettrait de modifier l’email + +La vérification du rôle UPDATE_USER_EMAIL est réalisée dans le service de mise à jour de l’utilisateur. + +#### Vérification du tenant + +En règle générale, le tenant concerné par la requête est vérifié par le processus de vérification générale. Il existe néanmoins des cas où le tenant est fourni en paramètre ou dans le corps de la requête. + +Dans ce cas, les étapes de sécurisation sont les suivantes : + +* vérifier la validité du tenant dans le contexte de sécurité +* Si le tenant n’est pas valide, il faut éventuellement vérifier si l’utilisateur a le droit de réaliser une opération multi-tenant. Cette dernière vérification est implémentée grâce aux rôle et sous-rôles (cf. gestion des customer, des idp, des tenants, des profils, etc). +* Si le tenant n'est pas valide, une erreur 403 est retournée + +Cette implémentation permet ainsi de réaliser simplement des opérations multi-tenant en définissant des rôles appropriés. La solution VITAMUI fournit des services multi-tenant pour gérer les organisations, les fournisseurs d'identité, etc. Il est fondamental de limiter autant que possible l'utilisation de rôles muli-tenants. Il est en outre recommandé de borner l'usage des rôles multi-tenant à une zone protégée de l'infrastructure. + +L'ensemble des rôles autorisés dans une organisation sont définis à la création de cette organisation. + +Profils de paramétrage externes +------------------------------- + +### External Parameter Profile + +Un profil de paramétrage externe est une entité fictive, contient les informations suivantes : + +* le nom du profil (nom) +* la description du profil (description) +* le contrat d'accès associé (accessContract: voir external parameters) +* le statut du profil (enabled) + +Un profil de paramétrage externe permet d'associer un et unique profil à un contrat d'accès qui est lui meme lié à un paramétrage externe (ExternalParameters). + +### Profil + +voir [Profil et rôles](#profils-et-roles) + +### External Parameters + +TODO voir section concernée. + +### Illustration + +Donnée du profil + +```json +{ + "_id": "60d06c74663b6f71e8459eb0168d408ea49743f8bc4f80f21f3eeb266ec90cca", + "identifier": "216", + "name": "test profile", + "enabled": true, + "description": "test description profile", + "tenantIdentifier": 1, + "applicationName": "EXTERNAL_PARAM_PROFILE_APP", + "roles": [ + { + "name": "ROLE_CREATE_EXTERNAL_PARAM_PROFILE" + }, + { + "name": "ROLE_EDIT_EXTERNAL_PARAM_PROFILE" + }, + { + "name": "ROLE_SEARCH_EXTERNAL_PARAM_PROFILE" + } + ], + "level": "", + "readonly": false, + "externalParamId": "reference_identifier", + "customerId": "system_customer", + "_class": "profiles" +} +``` + +Donnée de l'external parameter + +```json +{ + "_id": "60d06c73663b6f71e8459eae3ce591e616a1428bb086acd40c5c517eb8ccfda7", + "identifier": "reference_identifier", + "name": "test profile", + "parameters": [ + { + "key": "PARAM_ACCESS_CONTRACT", + "value": "ContratTNR" + } + ], + "_class": "externalParameters" +} +``` + +Le profil de paramétrage externe provenant des deux données ci-dessus + +```json +{ + "id": "60d06c74663b6f71e8459eb0168d408ea49743f8bc4f80f21f3eeb266ec90cca", + "name": "test profile", + "description": "test description profile", + "accessContract": "ContratTNR", + "profileIdentifier": "216", + "idProfile": "60d06c74663b6f71e8459eb0168d408ea49743f8bc4f80f21f3eeb266ec90cca", + "externalParamIdentifier": "reference_identifier", + "idExternalParam": "60d06c73663b6f71e8459eae3ce591e616a1428bb086acd40c5c517eb8ccfda7", + "enabled": true, + "dateTime": "2021-06-21T12:52:34.430803Z" +} +``` + +### Événement lors de la mise à jour + +La mise à jour du profil de paramétrage externe peut générer jusqu'à trois événements de journalisations. + +Premier cas: + +* Modification des données liés aux données du profil + * Dans ce cas de figure, on émet un événement de journal externe de type `EXT_VITAMUI_UPDATE_PROFILE`. + * et un événement de modification du profil de paramétrage externe `EXT_VITAMUI_UPDATE_EXTERNAL_PARAM_PROFILE` + +Deuxième cas: + +* Modification des données liés à la donnée du paramétrage externe + * Dans ce cas de figure, on émet un événement de journal externe de type `EXT_VITAMUI_UPDATE_EXTERNAL_PARAM`. + * et un événement de modification du profil de paramétrage externe `EXT_VITAMUI_UPDATE_EXTERNAL_PARAM_PROFILE`. + + +Troisième cas: + +* Modification des données liés aux données du profil et du paramétrage externe, dans ce cas de figure, on émet 3 événements de journalisation : + * événement de type `EXT_VITAMUI_UPDATE_PROFILE`. + * événement de type `EXT_VITAMUI_UPDATE_EXTERNAL_PARAM`. + * et un événement de modification du profil de paramétrage externe `EXT_VITAMUI_UPDATE_EXTERNAL_PARAM_PROFILE`. + + +Exemple de mise à jour de la description du profil: + +```json +{ + "_id": "aecaaaaaaghohlrwaan3ial2fyt7xnaaaaaq", + "tenantIdentifier": 1, + "accessContractLogbookIdentifier": "AC-000002", + "evType": "EXT_VITAMUI_UPDATE_EXTERNAL_PARAM_PROFILE", + "evTypeProc": "EXTERNAL_LOGBOOK", + "outcome": "OK", + "outMessg": "Le profil paramètrage externe a été modifié", + "outDetail": "EXT_VITAMUI_UPDATE_EXTERNAL_PARAM_PROFILE.OK", + "evIdReq": "5043309c-c8d7-4bc9-bfcc-bd20852ce90e", + "evDateTime": "2021-06-21T10:40:10.164438Z", + "obId": "216", + "obIdReq": "externalparamprofile", + "evDetData": "{\"diff\":{\"-Description\":\"test description profile\",\"+Description\":\"test description profile updated\"}}", + "evIdAppSession": "EXTERNAL_PARAM_PROFILE_APP63597049221:5043309c-c8d7-4bc9-bfcc-bd20852ce90e:Contexte UI Identity:1:-:1", + "creationDate": 3960212756529822, + "status": "SUCCESS", + "_class": "events", + "synchronizedVitamDate": "2021-06-21T10:40:36.370328Z", + "vitamResponse": "{\"httpCode\":201,\"code\":\"\"}" +} +``` + +Journalisation +-------------- + +### Objectifs + +La journalisation des événements VITAMUI a pour objectifs : + +* Conservation de la valeur probante : être en capacité de prouver toute opération effectuée sur toute unité archivistique ou tout objet qui lui est associé. +* La sécurité d’un SAE doit être systémique, c’est-à -dire reposer sur un faisceau d’éléments redondants dont la modification simultanée et cohérente est impossible, ou plus exactement non réalisable en pratique. +* Les journaux constituent un élément central de cette sécurité systémique +Utilisation des journaux vitam NF Z42-013 + + + +### Événement +#### Vitam +* Un événement = Un événement Primaire (Primary) et ensemble de sous-événements secondaires (Secondary) + * Primary : événement initial + * les champs sont contrôlés par VITAM + * Marque le début de la transaction au sens VITAM + * L’heure de l’événement et mise par VITAM (cohérence des journeaux) + * Secondary : note un sous événement réalisé suite à l’action principale + * possède les mêmes champs que l’événement Master mais VITAM ne procède à aucun contrôle + * l’heure de l’êvénement est à l’appréciation du client + * Fin de la transaction : le dernier sous événement doit posséder le même champs “eventType†que l’événement Master pour finir la transaction. + +#### VITAMUI +* Primaire et Secondaire => Un event VITAMUI cf : fr.gouv.vitamui.commons.logbook.domain.event +* Un appel REST => Une ou plusieurs opération métier => ensemble d’events => le premier sera l'evénement primaire (Primary) et les suivants secondaires (Secondary) +* Stocker dans le tenant des éléments de preuves du client + + + +### Application dans VITAMUI + +#### Modèle + +|Propriétés | valeurs | +|-----------------|:-------------------------------------------------:| +|EventTypeProc |EXTERNAL_LOGBOOK | +|EventType | Nom du type d'événement (EXT_VITAMUI_CREATE_USER) | +|obIdReq | Nom de la collection Mongo (USERS) | +|obId |Identifiant métier de l’objet | +|evDetData |Contient les informations importantes (modification avant/après contenu du nouvelle objet) outcome : OK, KO (Pour le master -> OK, pour les sous-events le champ est libre) | +|evIdAppSession |applicationIdExt:requestId:applicationName:userIdentifier:superUserIdentifier:customerIdentifier | +|evIdReq |X-Request-Id | + +### Création +* L’ensemble des modifications de la base de données se font dans une unique transaction. +* Centralisation de la création des traces dans chaque module (IamLogbookService, ArchiveLogbookService, FlowLogbookService) (Responsable de la cohérence de la génération d’un event à partir d’un objet métier +* Chaque objet de notre modèle de données possède un converter associé (Capable de convertir un objet en json et qui sera mis dans le evDetData de l’event) + +### Sauvegarde +* Réalisation par les tâches asynchrones (Cf : SendEventToVitamTasks.java et DeleteSynchronizedEventsTasks.java) +* Les événements sont regroupés par rapport à leur X-Request-Id et triés par ordre chronologique croissant. +* Le premier événements du groupe devient le Primary et les autres des sous-events. +* Le premier est recopier a la fin des sous-events afin de fermer la “transaction au sens VITAM†+* Envoit vers vitam (La reponse vitam et la date d'envoi sont toujours stocké) : + * Succès -> Les events sont conservés X jours et sont marqué au status “SUCCESS†+ * Erreur -> Les events sont marqués au statut “ERROR†et un retry sera effectué dans X heure. + +Modèle de données +----------------- + +### Liste des bases + + iam + security + cas + archivesearch + +### Base IAM + +#### Collections + + applications + customers + events + groups + externalParameters + owners + profiles + providers + subrogations + tenants + tokens + users + + + * _Collection applications_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| :--------------------------- | :------------- | :---------------------------------------- | :--------------------------------------------------------------------- | +| _id | String | Clé Primaire | | +| identifier | String | minimum = 1, maximum = 100 | L'identifiant (unique) de l'application | +| url | String | minimum = 1, maximum = 100 | | +| serviceId | String | minimum = 1, maximum = 100 | Le meme serviceId que nous avons au niveau de la collection services | +| icon | String | minimum = 1, maximum = 50 | Logo de l'application | +| name | String | minimum = 1, maximum = 50 | Nom de l'application | +| category | String | minimum = 1, maximum = 12 | La catégorie de l'application | +| position | int | Non null | L'ordre d'affichage dans la liste des applications | +| hasCustomerList | boolean | default=false | | +| hasTenantList | boolean | default=false | Pour pouvoir changer le tenant au niveau de l'application | +| hasHighlight | boolean | default=false | | +| tooltip | String | minimum = 1, maximum = 100 | Un texte pour décrire l'application | +| target | String | maximum = 25 | | + + * _Collection customers_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| :--------------------------- | :-------------- | :---------------------------------------------------- | :------------------- | +| _id | String | Clé Primaire | | +| identifier | String | minimum = 1, maximum = 12 | | +| code | String | minimum = 6, maximum = 20 | | +| companyName | String | maximum = 250 | | +| language | String | Non null, valeurs = [FRENCH,ENGLISH] | | +| passwordRevocationDelay | Integer | Non null | exprimé en jour | +| otp | Enum | Non null, valeurs = [OPTIONAL,DISABLED,MANDATORY] | | +| emailDomains | List<_String_> | Non null, Non vide | | +| defaultEmailDomain | String | Non null | | +| address | Address | Non null | | +| name | String | maximum = 100 | | +| subrogeable | boolean | default=false | | +| readonly | boolean | default=false | | +| graphicIdentity | GraphicIdentity| | | +| gdprAlert | boolean | default=false | | +| gdprAlertDelay | int | minimum=1 | | + + * GraphicIdentity (Embarqué) + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| hasCustomGraphicIdentity | boolean | | | +| logoDataBase64 | String | | | +| logoHeaderBase64 | String | | Base64 encoded logo | +| portalTitle | String | | | +| portalMessage | String | maximum length = 500 chars) | | +| themeColors | Map<String, String> | | | + + * themeColors + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| vitamui-primary | String | hexadeciaml color like | | +| vitamui-secondary | String | hexadeciaml color like | | +| vitamui-tertiary | String | hexadeciaml color like | | +| vitamui-header-footer | String | hexadeciaml color like | | +| vitamui-background | String | hexadeciaml color like | | + + * _Collection tenants_ + +Le tenant correspond à un container (ie. espace de travail) logique. +Chaque tenant est unique dans le système et appartient à un seul et unique client. +Un client peut posséder plusieurs tenants. +Un client ne doit jamais pouvoir accéder au tenant d’un autre client. +Les tenants VITAMUI correspondent aux tenants VITAM. +Toutes les requêtes HTTP dans VITAMUI doivent renseigner le tenant dans le header. +Dans VITAMUI, le tenant permet de vérifier les autorisations applicatives (certificat et contexte) et utilisateurs (profils). + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| customerId | String | Non null, Clé Étrangère | | +| identifier | Integer | Non null | correspond au tenant vitam | +| ownerId | String | Non null, Clé Étrangère| | +| name | String | maximum = 100 | exprimé en jour | +| proof | Boolean | | identifie le tenant de preuve | +| readonly | Boolean | | | +| ingestContractHoldingIdentifier | String | Non null | contrat d’entrée pour l’arbre | +| accessContractHoldingIdentifier | String | Non null | contrat d’accès pour l’arbre | +| itemIngestContractIdentifier | String | Non null | contrat d’entrée pour les bordereaux | +| accessContractLogbookIdentifier | String | Non null | contrat d’accès pour le logbook | +| enabled | Boolean | Non null | | + + * _Collection owners_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| identifier | String | minimum = 1, maximum = 12 | | +| customerId | String | Clé Étrangère | Clé Étrangère | +| name | String | maximum = 100| | +| code | String | minimum = 6, maximum = 20 | | +| companyName | String | maximum = 250 | | +| address | Address | | embedded | +| readonly | Boolean | | | + + + * Address (Embarqué) + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| street | String | maximum = 250 | | +| zipCode | String | maximum = 10 | | +| city | String | maximum = 100 | | +| country | String | maximum = 50| | + + + * _Collection Identity Provider_ + + L’identity provider L’IDP est soit externe (Clients/Organisations externes) soit interne. + L’IDP interne est CAS lui même et les utilisateurs sont alors gérés uniquement dans l’annuaire CAS de VITAMUI. + + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| customerId | String | Clé Étrangère | | +| identifier | String | minimum = 1, maximum = 12 | | +| name | String | maximum = 100| | +| technicalName | String | | | +| internal | Boolean | default=true| | +| patterns | List<_String_> | minimum = 1| | +| enabled | Boolean | default=true| | +| keystoreBase64 | String | | | +| keystorePassword | String | | Mot de passe | +| privateKeyPassword | String | | Mot de passe | +| idpMetadata | String | | XML | +| spMetadata | String | | XML | +| maximumAuthenticationLifetime | Integer | | | +| readonly | Boolean | | | + + +* _Collection users_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| customerId | String | Clé Étrangère | | +| enabled | boolean | default = true | | +| status | Enum | default = ENABLED, BLOCKED, ANONYM, DISABLED | | +| type | Enum | NOMINATIVE, GENERIC | | +| password | String | maximum = 100 | Mot de passe | +| oldPasswords | List<_String_> | | | +| identifier | String | minimum = 1, maximum = 12 | | +| email | String | email, Unique | | +| firstname | String | maximum = 50 | | +| lastname | String | maximum = 50 | | +| language | String | Non null, valeurs = [FRENCH,ENGLISH] | | +| phone | String | phone number | | +| mobile | String | mobile phone number | | +| otp | Boolean | default = false | | +| groupId | String | Not null | | +| subrogeable | Boolean | | | +| lastConnection | OffsetDateTime | | | +| nbFailedAttempts | int | | | +| readonly | boolean | default=false | | +| level | String | Not null | | +| passwordExpirationDate | OffsetDateTime | | | +| address | Address | | | +| analytics | AnalyticsDto | | | + + + * AnalyticsDto (Embarqué) + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| applications | ApplicationAnalyticsDto | | | +| lastTenantIdentifier | Integer | | | + + * ApplicationAnalyticsDto (Embarqué) + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| applicationId | String | | | +| accessCounter | int | | | +| lastAccess | OffsetDateTime | | ex: YYYY-MM-ddTHH:mm:ss.ssssssZ | + + +* _Collection externalParameters_ + +La collection qui définit un contrat d'accès par défaut + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| identifier | String | minimum = 1, maximum = 12 | | +| name | String | minimum = 2, maximum = 100| | +| parameters | ParameterDto | Not Null | | + + * ParameterDto (Embarqué) + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| key | String | | exemple: PARAM_ACCESS_CONTRACT | +| value | String | | exemple: AC-000001 | + +* _Collection groups_ + +Le groupe de profil définit un ensemble de profils. +Un groupe de profil ne peut contenir qu’un seul profil par “app:tenantâ€. Par exemple : “profil(app1:tenant1), profil(app1:tenant2), profil(app2:tenant1)†est autorisé. + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| identifier | String | minimum = 1, maximum = 12 | | +| customerId | String | Non Null, Clé Étrangère | | +| name | String | maximum = 100 | | +| description | String | maximum = 250 | | +| profileIds | List<_String_> | clé étrangére | les profils | +| level | String | maximum = 250 | | +| readonly | Boolean | | | +| enabled | Boolean | | | + +* _Collection profiles_ + +Le profil définit les permissions (rôles) données à un utilisateur et l’accès à une application (applicationName), généralement une IHM qui regroupe un ensemble de fonctionnalités selon une logique métier et appelant des API backoffice. +Un profil appartient à une groupe (de profils). Il ne peut y avoir qu’un seule et unique profile par tenant, applicationName dans un groupe. + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| identifier | String | minimum = 1, maximum = 12 | | +| tenantIdentifier | Integer | | | +| name | String | maximum = 100 | | +| enabled | boolean | default=true | | +| description | String | maximum = 250 | +| applicationName | String | maximum = 250 | | +| roles | List<_Role_> | | rôle Spring | +| readonly | Boolean | | | +| level | String | maximum = 250 | | +| externalParamId | String | | | + +* _Collection subrogations_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| status | Enum | CREATED, ACCEPTED | | +| date | Date | | | +| surrogate | String | email, minimum = 4, maximum = 100 | celui qui est subrogé | +| superUser | String | email, minimum = 4, maximum = 100 | celui qui subroge | +| surrogateCustomerId | String | not null | | +| superUserCustomerId | String | not null | | + +* _Collection tokens_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | | | +| updatedDate | Date | not null | | +| refId | String | not null | | +| surrogation | Boolean | | | + +* _Collection events_ + +| Nom | Type | Contrainte(s) | Remarque(s) | +| -------- | -------- | ------ | ------ | +| _id | String | Clé Primaire | | +| tenantIdentifier | Integer | not null | | +| accessContractLogbookIdentifier | String | not null | | +| evParentId | String | | | +| evIdProc | String | | | +| evType | String | not null | | +| evTypeProc | Enum | EXTERNAL_LOGBOOK | +| outcome | Enum | UNKNOWN, STARTED, ALREADY_EXECUTED, OK, WARNING, KO, FATAL | | +| outMessg | String | not null | | +| outDetail | String | not null | | +| evIdReq | String | not null | | +| evDateTime | String | not null | | +| obId | String | not null | | +| obIdReq | String | not null | | +| evDetData | String | not null | | +| evIdAppSession | String | not null | | +| creationDate | Long | not null | | +| status | Enum | CREATED, SUCCESS, ERROR | | +| vitamResponse | String | | | +| synchronizedVitamDate | OffsetDateTime | | | + +Pour aller plus loin, le modèle de données Vitam concernant les journaux d'archives est accessible par [ici](http://www.programmevitam.fr/ressources/DocCourante/autres/fonctionnel/VITAM_Modele_de_donnees.pdf#%5B%7B%22num%22%3A45%2C%22gen%22%3A0%7D%2C%7B%22name%22%3A%22XYZ%22%7D%2C56.7%2C748.3%2C0%5D) + +### Base security + +* _Collection Context_ + +Le contexte applicatif permet d’attribuer à une application cliente selon son certificat X509 transmis lors de la connexion https les droits d’accès (rôles) à différents services. + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | _id | String | Clé Primaire | | + | fullAccess | Boolean | default = false | | + | name | String | not null | | + | tenants | List<_Integer_> | Not Null, List de Clé Étrangère | Liste des tenants autorisés | + | roleNames | List<_String_> | Not Null | Liste des rôles autorisés | + +* _Collection Certificate_ + +La collection certificat permet de stocker les certificats correspondant à un contexte. +Le certificat est transmis par l’application client lors de la connexion SSL. + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | _id | String | Clé Primaire | | + | contextId | String | Not Null | | + | serialNumber | String | Not Null | Numéro de série du certificat | + | subjectDN | String | Not Null | Identifiant unique (Distinguished Name) du certificat | + | issuerDN | String | Not Null | Identifiant unique (Distinguished Name) de l’autorité de certification | + | data | String | Not Null | Certificat en base64 | + +* _Collection CustomSequence_ + +La collection sequence permet de stocker les différentes séquences utilisés. + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | _id | String | Clé Primaire | | + | name | String | Not Null | Nom de la séquence | + | sequence | int | | Valeur courante | + +La liste des noms de séquences : + +- tenant_identifier +- user_identifier +- profile_identifier +- group_identifier +- provider_identifier +- customer_identifier +- owner_identifier + + +### Base Cas + +Cette base est initialisée à la création de l'environnement. Elle est uniquement utilisée par CAS en lecture seule. + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | _id | String | Clé Primaire | | + | serviceId | String | Not Null | url du service web | + | name | String | | nom du service | + | logoutType | String | | | + | logoutUrl | String | | url de logout | + | attributeReleasePolicy | | | Stratégie des attributs | + + +### Base archivesearch + +Cette base est utilisé pour stocker les critères de filtres de recherche des utilisateurs, Aujourd'hui, elle est utilisée uniquement par le service Archive-Search, en particulier l'application de consultation et de recherche d'archives. + + +#### Collections + + searchCriteriaHistories + + * _Collection searchCriteriaHistories_ + + + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | _id | String | Clé Primaire | | + | name | String | Not Null, minimum = 1, maximum = 150 | nom de la recherche sauvegardée | + | userId | String | Not Null | l'identifiant de l'utilisateur | + | date | Date | | date de la sauvegarde des critères de recherche | + | searchCriteriaList | List<_SearchCriteriasDto_> | | liste des critères de recherches sauvegardées incluant les critères d'arbres et plan | + + * SearchCriteriasDto (Embarqué) + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | nodes | List<_String_> | | liste des identifiants des noeuds d'arbre de positionnement/ plans de classemnt| + | criteriaList | List<_SearchCriteriaElementsDto_> | | liste des critères de recherches sauvegardées | + + + * SearchCriteriaElementsDto (Embarqué) + + | Nom | Type | Contrainte(s) | Remarque(s) | + | -------- | -------- | ------ | ------ | + | criteria | String | | le nom du critère de recherche (eg: Title, StartDate, #opi, #id ...) | + | values | List<_String_> | | liste des valeurs du critère de filtre | diff --git a/docs/fr/architecture/sections/gestion.md b/docs/fr/architecture/sections/gestion.md new file mode 100644 index 0000000000000000000000000000000000000000..35dc236fd3efe708683aa16de358af787137bdab --- /dev/null +++ b/docs/fr/architecture/sections/gestion.md @@ -0,0 +1,95 @@ +Gestion du système +================== + +Chaîne de déploiement +--------------------- + +* Les composants de la solution VITAM UI sont installés et configurés par un outil de déploiement automatique (ansible) dans des systèmes d’exploitation cibles (VM, container..). + +* La procédure d’installation et de configuration se fait à travers un ensemble de scripts dont le source code est stocké dans le repository GIT. + +* L’outil de déploiement (yum) utilise exclusivement les dépôts de packages pour installer les softs, afin de se décharger des étapes d’installation des dépendances et la gestion de conflit de fichiers. + + + Schéma du processus de déploiement + + + +### Packaging + +Les packages VITAMUI sont disponibles au format RPM (CentOS). + +Chaque package respecte les principes suivants : + +* Nom des packages : vitamui<id> du package +* Version du package : Numéro de “release†du projet +* Les dossiers (ainsi que les droits associés) compris dans les packages respectent les principes dictés dans la section dédiée aux utilisateurs, dossiers et droits. +* Les fichiers de configuration sont gérés par l’outil de déploiement de manière externe aux packages et ne sont pas inclus dans les packages. + +Les composants de la solution VITAMUI sont tous disponibles sous forme de packages natif aux distributions supportées (rpm pour CentOS 7). Ceci inclut notamment : + +* L’usage des pré-requis (au sens Require ou Depends) nativement inclus dans la distribution concernée +* L’arborescence des répertoires OS de la distribution concernée +* L’usage du système de démarrage systemd. + +Les packages ne contiennent pas de pré/post action d’arrêt/démarrage/redémarrage de services. La gestion de démarrage des services et leur démarrage (a minima initial) est de la responsabilité de l’outillage de déploiement. + +Les fichiers de configuration ne sont pas gérés dans les packages RPM. Par conséquent, ils n’apparaissent pas dans le résultat de commandes telles que rpm -ql. Les fichiers de configuration sont instanciés par l’outil de déploiement. Pour éviter la génération de fichier .rpmnew ou .rpmsave, il n’est pas utilisé la directive %config. + +Les limitations associés au format de packaging choisi sont : + +* L’instanciation d’une seule instance d’un même moteur par machine (il n’est ainsi pas possible d’installer 2 moteurs d’exécution sur le même OS) ; +* La redondance de certains contenus dans les packages (ex: les librairies Java sont embarquées dans les packages, et non tirées dans les dépendances de package) + +### Dépôts + +L’installation de VITAMUI s’appuie sur des dépôts Nexus et dans le Repository RPM; ou tout autre repository mis en oeuvr suite au build. Il est également possible de déployer VitamUI en générant des dépôts locaux sur les machines cibles. + +### Principes de déploiement + +Les principes généraux de déploiement sont les suivants : + +* Les packages d’installation (rpm) sont identiques pour tous les environnements. Seule leur configuration change. + +* La configuration des services est externalisée et gérée par l’outillage de déploiement. + +* Le déploiement est décrit intégralement dans un fichier de définition du déploiement. En dehors des pré-requis, le déploiement initial est automatisé en totalité (sauf exception). + +* Les services sont configurés par défaut pour permettre leur colocalisation (dans le sens de la colocalisation de deux instances de deux moteurs différents) (ex: dossiers d’installation / de fonctionnement différents, ports d’écoute différents, ...). + +* Le déploiement s’effectue à partir d’un point central. Les commandes passées sur chaque serveur à partir de ce point central utilisent le protocole SSH. + +Les service de déploiement fourni permet le déploiement de la solution VITAMUI. + +* Gestion des binaires d’installations (version, intégrité) +* Gestion des éléments de configuration spécifiques à chaque plate-forme +* Pilotage de l’installation des services sur les éléments d’infrastructure (VM/containers) de manière cohérente + +Données gérées : + +* Configuration technique du système VITAMUI +* Certificats x509 : le moteur de déploiement et de configuration doit posséder la référence des certificats techniques déployés sur la plate-forme (car il doit entre autres assurer la cohérence de ces certificats entre les différentes instances des composants VITAMUI déployés) + +## Cloisonnement + +TODO + +## Logs techniques + + La gestion des logs techniques dans VITAMUI est similaire à celle de VITAM. Pour une description complète du fonctionnement des logs et d'ELK, il est possible de se référer à la documentation VITAM. + +* [Doc VITAM : Chaîne de log - rsyslog / ELK ](http://www.programmevitam.fr/ressources/DocCourante/html/exploitation/composants/elasticsearch_log/_toc.html) + + +## Supervision + +TODO + +## Métriques + +TODO + +## PRA + +TODO + diff --git a/docs/fr/architecture/sections/implementation.md b/docs/fr/architecture/sections/implementation.md new file mode 100644 index 0000000000000000000000000000000000000000..c443630638eb50a65f4e60492d36edce7673147f --- /dev/null +++ b/docs/fr/architecture/sections/implementation.md @@ -0,0 +1,602 @@ +Implémentation +============== + +Technologies +------------ + +### Briques techniques + +La solution est développée principalement avec les briques technologies suivantes : + +* Java 1.8+ (Java 11) +* Angular 8 : framework front +* Spring Boot 2 : framework applicatif +* MongoDB : base de données NoSQL +* Swagger : documentation API + +### COTS + +Les composants suivant sont utilisés dans la solution : + +* CAS : gestionnaire d'authentification centralisé (IAM) +* VITAM : socle d'archivage développé par le programme VITAM +* MongoDB : base de données orientée documents +* Curator : maintenance des index d’elasticsearch +* ELK : agrégation et traitement des logs et dashboards et recherche des logs techniques +* Consul : annuaire de services + +Les solutions CAS et VITAM sont également développées en Java dans des technologies proches ou similaires. + +En fonction du choix de l'implémentation de la solution, il est possible de partager des dépendances logicielles avec la solution VITAM. + +## Services + +La solution est bâtie selon une architecture de type micro-services. Ces services communiquent entre eux en HTTPS via des API REST. + +* Les services externes exposés publiquement sont sécurisés par la mise en oeuvre d'un protocole M2M nécessitant l'utilisation de certificats X509 client et serveur reconnus mutuellement lors de la connexion. + +* Les services internes, ne sont jamais exposés publiquement. Ils sont accessibles uniquement en HTTPS par les services externes ou par d'autres services internes. + +* Les accès aux bases de données MongoDb ou aux socles techniques externes (ie. VITAM) se font uniquement via les services internes. + +* Les utilisateurs sont authentifiés via CAS et disposent d'un token, validé à chaque appel, qui les identifient durant toute la chaîne de traitement des requêtes. + +### Identification des services + +Il est primordial que chaque service de la solution puisse être identifié de manière unique sur le système. A cet effet, les services disposent des différents identifiants suivant : + +* ID de service (ou service_id) : c’est une chaîne de caractères qui nomme de manière unique un service. Cette chaîne de caractère doit respecter l’expression régulière suivante : [a-z][a-z-]*. Chaque cluster de service possède un ID unique de service. + +* ID d’instance (ou instance_id) : c’est l’ID d’un service instancié dans un environnement ; ainsi, pour un même service, il peut exister plusieurs instances de manière concurrente dans un environnement donné. Cet ID a la forme suivante : <service_id>-<instance_number>, avec <instance_number> respectant l’expression régulière suivante : [0-9]{2}. Chaque instance dans ce cluster possède un id d’instance (instance_id). + +* ID de package (ou package_id) : il est de la forme vitamui<service_id>. C’est le nom du package à déployer. + +### Communications inter-services + +Les services VITAMUI suivent les principes suivants lors d’un appel entre deux composants : + +1. Le composant amont effectue un appel (de type DNS) à l’annuaire de service en indiquant le service_id du service qu’il souhaite appeler + +2. L’annuaire de service lui retourne une liste ordonnée d’instance_id. C’est de la responsabilité de l’annuaire de service de trier cette liste dans l’ordre préférentiel d’appel (en fonction de l’état des différents services, et avec un algorithme d’équilibrage dont il a la charge) + +3. Le composant amont appelle la première instance présente dans la liste. En cas d’échec de cet appel, il recommence depuis le point 1. La communication vers une instance cible de type Service API utilise nécessairement le protocole sécurisé HTTPS. + +Ces principes ont pour but de garantir les trois points suivants : +* Les clients des services doivent être agnostiques de la topologie de déploiement, et notamment du nombre d’instances de chaque service dans chaque cluster. La connaissance de cette topologie est déléguée à l’annuaire de service. + +* Le choix de l’instance cible d’un appel doit être décorrélé de l’appel effectif afin d’optimiser les performances et la résilience. + +* La garantie de la confidentialité des informations transmises entre les services (hors COTS) + +Dans le cas des COTS, la gestion de l’équilibrage de charge et de la haute disponibilité doit être intégrée de manière native dans le COTS utilisé. D'autre part, la sécurisation de la transmission dépend du COTS. Dans le cas où le chiffrement des données transmises n'est pas assuré, il est alors recommandé d'isoler le COTS dans une zone réseau spécifique. + +### Cloisonnement des services + +Le cloisonnement applicatif permet de séparer les services de manière physique (subnet/port) et ainsi limiter la portée d’une attaque en cas d’intrusion dans une des zones. Ce cloisonnement applique le principe de défense en profondeur préconisé par l’ANSI. + +Chaque zone héberge des clusters de services. Un cluster doit être présent en entier dans une zone, et ne peut par conséquent pas être réparti dans deux zones différentes. Chaque noeud d’un cluster applicatif doit être installé sur un hôte (OS) distinct (la colocalisation de deux instances d’un même service n’étant pas supporté). La mise en oeuvre d’une infrastructure virtualisée impose de placer deux noeuds d’un même cluster applicatif sur deux serveurs physiques différents. + +Un exemple de découpage en zones applicative est fourni ci-dessous. Ce découpage repose sur une logique assez classique adapté à une infrastructure de type VmWare ESX. Pour une architecture reposant sur une technologie de type Docker, il serait envisageable de découper plus finement les zones jusqu'à envisager une zone pour chaque cluster de service. + +Dans cet exemple, il est prévu pour respecter les contraintes de flux inter-zones suivants : + +* les utilisateurs de la zone USERS communiquent avec les services de la zone IHM +* les administrateurs de la zone ADMIN communiquent avec les services de la zone IHM ADMIN +* les services de la zone IHM et IHM-ADMIN communiquent avec les services de la zone API-EXTERNAL +* les services de la zone API-EXTERNAL communiquent avec les services de la zone API-INTERNAL +* les services de la zone API-INTERNAL communiquent avec les services de la zone DATA +* les services de toutes les zones communiquent avec les services déployés dans la zone INFRA +* les exploitants techniques accédent aux services de la zone EXPLOITATION puis intervenir dans toutes les zones + +#### Les différentes zones: + ##### zone IHM: + La zone IHM se compose de plusieurs services: + - UI Identity + - UI Portal + - UI Referential + - UI Ingest + - UI Archive Search + ##### zone API-EXTERNAL: + La zone API-EXTERNAL se compose de plusieurs services: +- IAM EXTERNAL +- REFERENTIAL EXTERNAL +- INGEST EXTERNAL +- ARCHIVE SEARCH EXTERNAL + ##### zone API-INTERNAL: + La zone API-INTERNAL se compose de plusieurs services: + - IAM INTERNAL + - REFERENTIAL INTERNAL + - INGEST INTERNAL + - ARCHIVE SEARCH INTERNAL + ##### zone DATA: + La zone stockage: MongoDB + ##### zone INFRA: + Les services consul, kibana, elk etc.. + +Tous les serveurs cibles doivent avoir accès aux dépôts de binaires contenant les paquets des logiciels VITAMUI et des composants externes requis pour l’installation. Les autres éléments d’installation (playbook ansible, ...) doivent être disponibles sur la machine ansible orchestrant le déploiement de la solution dans la zone INFRA. + + Schéma de zoning : + + + + + +Intégration système +------------------- + +### Utilisateurs et groupes d’exécution + +La segmentation des droits utilisateurs permet de respecter les contraintes suivantes : + +* Assurer une séparation des utilisateurs humains du système et des utilisateurs système sous lesquels tournent les processus +* Séparer les droits des rôles d’exploitation différents suivants : + * Les administrateurs système (OS) ; + * Les administrateurs techniques des logiciels VITAMUI; + * Les administrateurs des bases de données VITAMUI + +Les utilisateurs et groupes décrits dans les paragraphes suivants doivent être ajoutés par les scripts d’installation de la solution VITAMUI. En outre, les règles de sudoer associées aux groupes vitamui*-admin doivent également être mis en place par les scripts d’installation. + +Les sudoers sont paramétrés en mode NOPASSWD, c’est à dire qu’aucun mot de passe n’est demandé à l’utilisateur faisant partie du groupe vitamui*-admin pour lancer les commandes d’arrêt relance des applicatifs vitamui. + +Les fichiers de règles sudoers des groupes vitamui-admin et vitamuidb-admin sont systématiquement écrasés à chaque installation des paquets (rpm) déclarant les utilisateurs VITAMUI. (Un backup de l’ancien fichier est cependant effectué). + +#### Utilisateurs + +Les utilisateurs suivant sont définis : + +* vitamui(UID : 4000) : user pour les services ne stockant pas les données +* vitamuidb (UID : 4001) : user pour les services stockant des données (Ex : MongoDB) + +Les processus VITAMUI tournent sous ces utilisateurs. Leurs logins sont désactivés. + +#### Groupes + +Les groupes suivant sont définis : + +* vitamui(GID : 4000) : groupe primaire des utilisateurs de service +* vitamui-admin (GID : 5000) : groupe d’utilisateurs ayant les droits “sudo†permettant le lancement des services VITAMUI +* vitamuidb-admin (GID : 5001) : groupe d’utilisateurs ayant les droits “sudo†permettant le lancement des services VITAMUI stockant de la donnée. + +### Arborescence de fichiers + +L’arborescence /vitamui héberge les fichiers propres aux différents services. Elle est normalisée selon le pattern suivant : /vitamui/<folder_type>/<service_id> où : + +Pour un service d’id service_id, les fichiers et dossiers impactés par VITAMUI sont les suivants. + +* service_id est l’id du service auquel appartient les fichiers +* folder-type est le type de fichiers contenu par le dossier : + * app : fichiers de ressources (non-jar) requis pour l’application (ex: .war) + * bin : binaires (le cas échéant) + * script : Répertoire des scripts d’exploitation du module (start/stop/status/backup) + * conf : Fichiers de configuration + * lib : Fichiers binaires (ex: jar) + * log : Logs du composant + * data : Données sauvegardes du composant + * tmp : Données temporaires produites par l’application + +Les dossiers /vitamui et /vitamui/<folder_type> ont les droits suivants : + +* Owner : root +* Group owner : root +* Droits : 0555 + +A l’intérieur de ces dossiers, les droits par défaut sont les suivants : + +* Fichiers standards : + * Owner : vitamui (ou vitamuidb) + * Group owner : vitamui + * Droits : 0640 + +* Fichiers exécutables et répertoires : + * Owner : vitamui (ou vitamuidb) + * Group owner : vitamui + * Droits : 0750 + +Cette arborescence ne doit pas contenir de caractère spécial. Les éléments du chemin (notamment le service_id) doivent respecter l’expression régulière suivante : [0-9A-Za-z-_]+ + +Le système de déploiement et de gestion de configuration de la solution est responsable de la bonne définition de cette arborescence (tant dans sa structure que dans les droits utilisateurs associés). + +### Intégration au service d'initialisation Systemd + +L’intégration est réalisée par l’utilisation du système d’initialisation systemd. La configuration se fait de la manière suivante : + +* /usr/lib/systemd/system/ : répertoire racine des définitions de units systemd de type “service†+* <service_id>.service : fichier de définition du service systemd associé au service VITAMUI + +Les COTS utilisent la même nomenclature de répertoires et utilisateurs que les services VITAMUI, à l’exception des fichiers binaires et bibliothèques qui utilisent les dossiers de l’installation du paquet natif. + +Sécurisation +------------ + +### Sécurisation des accès aux services externes + +Les services exposants publiquement des API REST implémentent les mesures de sécurité suivantes : + +* mise en place de filtres dans les applications IHM pour contrer les attaques de type CSRF et XSS + +* utilisation du protocole HTTPS. Par défaut, la configuration suivante est appliquée (Protocoles exclus : TLS 1.0, TLS 1.1, SSLv2, SSLv3 & Ciphers exclus : .*NULL.*, .*RC4.*, .*MD5.*, .*DES.*, .*DSS.*) + +* authentification par certificat X509 requise des applications externes (authentification M2M) basée sur une liste blanche de certificats valides + +* mise à jour des droits utilisateurs grâce aux contextes applicatifs, associés certificats clients, stockés dans la collections XXX de base MongoDb gérée par le service SECURITY INTERNAL. + +* un service batch contrôle régulièrement l'expéritaion des certificats stockés dans le truststore des services et dans le référentiel de certificats clients (MongoDb) géré par le service SECURITY INTERNAL. + +### Sécurisation des communications internes + +Les communications internes sont sécurisées par le protocole HTTPS. D’autre part, dans chaque requête, le header X-Auth-Token est positionné. Il contient le token initialisé par CAS à la connexion de l’utilisateur. + +A chaque requête le service VITAMUI internal procède aux contrôles suivants : + +* vérification de l'existence du header X-Auth-Token +* vérification de la validité (non expiré) du token extrait du header + +En cas d’échec, la requête est refusée et la connexion est fermée. + +### Sécurisation des accès aux bases de données + +Les bases de données de MongoDB sont sécurisées via un cloisonnement physique (réseau) et/ou logique (compte utilisateur) des différentes bases de données qui les constituent. + +### Sécurisation des secrets de déploiement + +Les secrets de l’intégralité de la solution VITAM déployée sont tous présents sur le serveur de déploiement ; par conséquent, ils doivent y être stockés de manière sécurisée, avec les principes suivants : + +* Les mot de passe et token utilisés par ansible doivent être stockés dans des fichiers d’inventaire chiffrés par ansible-vault ; +* Les clés privées des certificats doivent être protégées par des mot de passe complexes et doivent suivre la règle précédente. + +### Liste des secrets + +Les secrets nécessaires au bon déploiement de VITAMUI sont les suivants : + +* Certificat ou mot de passe de connexion SSH à un compte sudoers sur les serveurs cibles (pour le déploiement) + +* Certificats x509 serveur (comprenant la clé privée) pour les modules de la zone d’accès (services *-external), ainsi que les CA (finales et intermédiaires) et CRL associées. Ces certificats seront déployés dans des keystores java en tant qu’élément de configuration de ces services + +* Certificats x509 client pour les clients du SAE (ex: les applications métier, le service ihm-admin), ainsi que les CA (finales et intermédiaires) et CRL associées. Ces certificats seront déployés dans des keystores java en tant qu’élément de configuration de ces services + +Les secrets définis lors de l’installation de VITAM sont les suivants : + +* Mots de passe des keystores ; +* Mots de passe des administrateurs fonctionnels de l’application VITAMUI +* Mots de passe d’administration de base de données MongoDB ; +* Mots de passe des comptes d’accès aux bases de données MongoDB. + +Note. Les secrets de VITAMUI sont différents de ceux VITAM + +### Authentification du compte SSH + +Il existe plusieurs méthodes envisageables pour authentifier le compte utilisateur utilisé pour la connexion SSH : +* par clé SSH avec passphrase +* par login/mot de passe +* par clé SSH sans passphrase + +La méthode d’authentification retenue dépend de plusieurs paramètres : +* criticité des serveurs (services) +* zone de confiance +* technologie de déploiement + +Dans un contexte sensible, il est fortement recommandé d'utiliser un bastion logiciel (par ex. https://www.wallix.com/bastion-privileged-access-management/) pour authentifier et tracer les actions des administrateurs du système. + +### Authentification des hôtes + +Pour éviter les attaques de type MitM, le client SSH cherche à authentifier le serveur sur lequel il se connecte. Ceci se base généralement sur le stockage des clés publiques des serveurs auxquels il faut faire confiance (~/.ssh/known_hosts). + +Il existe différentes méthodes pour remplir ce fichier (vérification humaine à la première connexion, gestion centralisée, DNSSEC). La gestion du fichier known_hosts est un pré-requis pour le lancement d’ansible. + +### Elévation de privilèges + +Plusieurs solutions sont envisageables : + +* par sudo avec mot de passe + * Au lancement de la commande ansible, le mot de passe sera demandé par sudo +* par su + * Au lancement de la commande ansible, le mot de passe root sera demandé + * par sudo sans mot de passe + +Certificats et PKI +------------------ + +La PKI permet de gérer de manière robuste les certificats de la solution VITAMUI. Une PKI est une architecture de +confiance constituée d’un ensemble de systèmes fournissant des services permettant la gestion des cycles de vie des +certificats numériques : + +* émission de certificats à des entités préalablement authentifiées +* déploiement des certificats +* révocation des certificats +* établir, publier et respecter des pratiques de certification de confiance pour établir un espace de confiance + +### Principes de fonctionnement PKI de VITAMUI + +La PKI VITAMUI gère les certificats nécessaires à l'authentification des services VITAMUI et des entités extérieurs. La +logique de fonctionnement de la PKI VITAMUI est similaire à celle utilisée par la solution VITAM. + +Les principes de fonctionnement de la PKI sont les suivants : + +* Emission des certificats VITAMUI (les dates de création et de fin de validité des CA sont générées dans cette phase). +* Gestion du cycle de vie (révocation) des certificats +* Publication des certificats et des clés (.crt et .key) +* Déploiement : + * Génération des magasins de certificats VITAMUI (les certificats .crt et .key sont utilisés pour construire un + magasin de certificats qui contient des certificats .p12 et .jks) + * Déploiement dans VITAMUI des certificats .p12 et .jks par Ansible + + Schéma de la PKI : + + + +### Explication avancée du fonctionnement + +Le fonctionnement de la PKI de la solution *VitamUI* est basée à celle de Vitam - la logique d'architecture reste +identique. + +Lien des documentations existantes : +PKI +VITAM : <http://www.programmevitam.fr/ressources/DocCourante/html/installation/annexes/10-overview_certificats.html?highlight=pki> + +PKI VITAM suite : <https://www.programmevitam.fr/ressources/DocCourante/html/installation/annexes/15-certificates.html#> + +La PKI voit ses fichiers répartis à deux emplacements: + +- deployment/pki + + A cet emplacement se trouvent les scripts et fichiers de configuration associés à la génération des assets ( + certificats, clés privées ...) + + Fichier | Description | + ------------ | :----------- | + pki/ca | Répertoire dans lequel sont stockés les CA de chaque zone | + pki/config | Répertoire dans lequel sont stockées les configurations pour la génération des CA/certificats | + pki/config/scripts | Répertoire dans lequel sont stockées les scripts de génération de la PKI. | + +### Génération des certificats + +Revenons en détails sur les scripts de génération des différents éléments de la PKI: + +- generate_ca*.sh: + - Paramètre(s): + - ERASE [Facultatif]: Booléen indiquant si les CA et fichiers associés existants doivent être supprimés avant + génération - Valeur par défaut: **false** + - Description: + Permet de générer les certificats d'autorité mentionnées dans le script de génération. Attention, toute autorité + existante n'est pas regénérée, l'utilisation du paramètre **ERASE** sera recommandée lors de la première + génération de la PKI. + +- generate_certs*.sh + - Paramètre(s): + - ENVIRONNEMENT_FILE [Obligatoire]: Chemin vers le fichier d'environnement pour lequel les certificats vont être + générés + - ERASE [Facultatif]: Booléen indiquant si les certificats et fichiers associés existants doivent être supprimés + avant génération - Valeur par défaut: **false** + - Description: + Permet de générer les certificats (serveur, client) mentionnés dans le script de génération. Attention, tout + certificat existant n'est pas regénéré, l'utilisation du paramètre **ERASE** sera recommandée lors de la première + génération de la PKI. Deux types de fichiers seront modifiés lors de cette exécution: + - les fichiers de configuration des CA (serial, index.txt ...) + - les fichiers générés (`deployment/environment/certs`) + +Les scripts suffixés par **_dev** concernent le matériel SSL utilisé pour le lancement de l'application en local sur l' +environnement de developpement. L'ensemble des fichiers générés se trouveront dans l'arborescence **dev-deployment** du +projet. Il faudra par la suite copier les fichiers générés associés à chaque module dans le repertoire /resources/dev du +projet associé. + +- deployment/environment/certs + +A cet emplacement figure l'ensemble de la PKI de la solution. Par défaut, on retrouvera trois zones (une par autorité): + +- server: l'ensemble du certificats permettant la communication HTTPS entre les différentes applications de la solution +- client-vitam: certificats utilisés par l'application pour communiquer avec Vitam. Avec le script **generate_certs.sh** + fournis par la PKI, un certificat sera généré pour s'interfacer avec Vitam. +- client-external: certificats des clients autorisés à solliciter les API externes + +### Cas pratiques + +- Instaurer la communication entre la solution VitamUI <-> Vitam + + Quelques rappels: + - au sein de la solution VitamUI, vous avez: + + - un client Vitam Java (access-external, ingest-external) permettant de réaliser des requêtes aurpès de Vitam. + Ce client se base sur un fichier de configuration dans lesquels sont référencés un **keystore** (concenant le + certificat utilisé pour chiffrer la requête) et un **trustore** (contenant le(s) CA(s) utilisé(s) pour les + échanges avec les applications à l'extérieur de VitamUI) + - `deployment/environement/certs/client-vitam/ca`: certificat d'autorité intervenant dans la communication + VitamUI <-> Vitam. /!\ L'ensemble des CA présents dans ce répertoire seront embarqués dans le trustore + exploités par le client Vitam Java lors de l'exécution du script *generate_keystores.sh*. + + - `deployment/environement/certs/client-vitam/clients/vitamui`: certificat utilisé pour la communication + VitamUI <-> Vitam. /!\ Le certificat sera embarqué dans le keystore utilisé par le client Vitam Java lors de + l'exécution du script *generate_keystores.sh*. + + - au sein de la solution Vitam, vous avez: + + - au sein du modèle de données, un certificat est associé à un contexte de sécurité (restriction d'actions par + tenant à travers des contrats), lui-même associé à un profile de sécurité (permission sur les API externes). + Cette association s'effectue dans le fichier `environment/group_vars/all/postinstall_param.yml` + + - la structure de la PKI VitamUI étant identique à celle de Vitam, le comportement est le suivant: + + - tout CA utilisé par un client pour solliciter les API externes et nécessaire à la chaine de vérification + de son certificat doit se trouver dans le répertoire `envionment/certs/client-external/ca` + /!\ L'ensemble des CA présents dans ce répertoire seront embarqués dans le trustore exploités par les API + externes lors de l'exécution du script *generate_keystores.sh*. + + - le certificat d'un client accédant aux API externes doit figurer à + l'emplacement `envionment/certs/client-external/clients/external` + + De ce fait, vous devez synchroniser vos PKI et vos solutions pour assurer une bonne communication: + - VitamUI -> Vitam + - Copier le CA du certificat VitamUI `{vitamui_inventory_dir}/certs/client-vitam/ca/ca-*.crt` + dans `{vitam_inventory_dir}/certs/client-external/ca` + - Copier le certificat VitamUI `{vitamui_inventory_dir}/certs/client-vitam/clients/vitamui/vitamui.crt` + dans `{vitam_inventory_dir}/certs/client-external/clients/external` + - Mise à jour de la PKI Vitam: + - `./generate_stores.sh` + - `ansible-playbook ansible-vitam/vitam.yml ${ANSIBLE_OPTS} --tags update_vitam_certificates` + - Création du contexte VitamUI: + - Population du fichier postinstall_param.yml: + + ```yaml + vitam_additional_securityprofiles: + - name: vitamui-security-profile + identifier: vitamui-security-profile + hasFullAccess: true + permissions: "null" + contexts: + - name: vitamui-context + identifier: vitamui-context + status: ACTIVE + enable_control: false + # No control, idc about permissions, VitamUI will do it :) + permissions: "[ { \"tenant\": 0, \"AccessContracts\": [], \"IngestContracts\": [] }, { \"tenant\": 1, \"AccessContracts\": [], \"IngestContracts\": [] }]" + certificates: ['external/vitamui.crt'] + ``` + + - Exécution du playbook de mise à jour: + `ansible-playbook ansible-vitam-exploitation/add_contexts.yml` + + - Vitam -> VitamUI + - Copier le(s) CA(s) de Vitam `{vitam_inventory_dir}/certs/client-vitam/ca` + dans `{vitamui_inventory_dir}/certs/client-vitam/ca/` + - Mise à jour de la PKI VitamUI: + - `./generate_stores.sh` + - `ansible-playbook vitamui_apps.yml --tags update_vitamui_certificates` + +- Instaurer la communication entre la solution VitamUI <-> *Le monde extérieur* + + TODO (le process n'est pas encore industrialisé) + +### PKI de test + +VITAMUI propose de générer à partir d’une PKI de tests les autorités de certification root et intermédiaires pour les +clients et les serveurs. Cette PKI de test permet de connaître facilement l’ensemble des certificats nécessaires au bon +fonctionnement de la solution. Attention, la PKI de test ne doit être utilisée que pour faire des tests, et ne doit +surtout pas être utilisée en environnement de production. + +### Liste des certificats utilisés + +Le tableau ci-dessous détail l’ensemble du contenu des keystores et truststores par service. + +|Composants | Keystores |Truststores| +|------------|-------------|-----------| +|**ui-portal** | ui-portal.crt, ui-portal.key | ca-root.crt, ca-intermediate.crt | +|**ui-identity** | ui-identity.crt, ui-identity.key | ca-root.crt, ca-intermediate.crt | +|**ui-identity-admin** | ui-identity-admin.crt, ui-identity-admin.key | ca-root.crt, ca-intermediate.crt | +|**ui-referential** | ui-referential.crt, ui-referential.key | ca-root.crt, ca-intermediate.crt | +|**ui-ingest** | ui-ingest.crt, ui-ingest.key | ca-root.crt, ca-intermediate.crt | +|**ui-archive-search** | ui-archive-search.crt, ui-archive-search.key | ca-root.crt, ca-intermediate.crt | +|**cas-server** | cas-server.crt, cas-server.key | ca-root.crt, ca-intermediate.crt | +|**iam-external** | iam-external.crt, iam-external.key | ca-root.crt | +|**iam-internal** | iam-internal.crt, iam-internal.key | ca-root.crt | +|**referential-external** | referential-external.crt, referential-external.key | ca-root.crt | +|**referential-internal** | referential-internal.crt, referential-internal.key | ca-root.crt | +|**ingest-external** | ingest-external.crt, ingest-external.key | ca-root.crt | +|**ingest-internal** | ingest-internal.crt, ingest-internal.key | ca-root.crt | +|**archive-search-external** | archive-search-external.crt, archive-external.key | ca-root.crt | +|**archive-search-internal** | archive-search-internal.crt, archive-internal.key | ca-root.crt | +|**security-server** | security-server.crt, security-server | ca-root.crt | + +La liste des certificats utilisées par VITAM est décrite à cette +adresse : http://www.programmevitam.fr/ressources/DocCourante/html/archi/securite/20-certificates.html + +### Procédure d’ajout d’un certificat client externe + +Le certificat ou l’autorité de certification doit présent dans les truststores des APIs external VITAMUI. La procédure +d'ajout d’un certificat client externe aux truststores des services de VITAMUI est la suivante : + +* Déposer le(s) CA(s) du client dans le répertoire deployment/environment/certs/client-external/ca + +* Déposer le certificat du client dans le répertoire deployment/environment/certs/client-external/clients/external/ + +* Régénérer les keystores à l’aide du script deployment/generate_stores.sh + +* Exécuter le playbook pour redéployer les keystores sur la solution VITAMUI : + +```console +ansible-playbook vitamui_apps.yml -i environments/hosts --vault-password-file vault_pass.txt --tags update_vitamui_certificates +``` + +L’utilisation d’un certificat client sur les environnements VITAMUI nécessite également de vérifier que le certificat +soit présent dans la base de données VITAMUI et rattaché à un contexte de sécurité du client. + +Clusterisation +-------------- + +A compléter + +Détail des services +------------------- + +### Service 1 + +* Description +* Contraintes + +### Service 2 + +* Description +* Contraintes + + + +Détail des COTS +--------------- + +### CAS + +* Description +* Contraintes + +### Annuaire de services Consul + +* Description +* Contraintes + +La découverte des services est réalisée avec Consul via l’utilisation du protocole DNS. Le service DNS configuré lors du déploiement doit pouvoir résoudre les noms DNS associés à la fois aux service_id et aux instance_id. Tout hôte portant un service VITAMUI doit utiliser ce service DNS par défaut. L’installation et la configuration du service DNS applicatif sont intégrées à VITAMUI. + +La résilience est assurée par l’annuaire de service Consul. Il est partagé avec VITAM. +* Les services sont enregistrés au démarrage dans Consul +* Les clients utilisent Consul (mode DNS) pour localiser les services +* Consul effectue régulièrement des health checks sur les services enregistrés. Ces informations sont utilisées pour router les demandes des clients sur les services actifs + +La solution de DNS applicatif intégrée à VITAMUI et VITAM est présentée plus en détails dans la section dédiée à Consul dans la documentation VITAM. + +### Base NOSQL MongoDB + +* Description +* Contraintes + + + +Multi instanciation des micro services +-------------------------------------- + +### Multi instanciation + +Les services vitamui multi instanciable à ce jour sont: + - Service IAM Internal + - Service IAM External + - Service UI Identity + - Service Portal + - Service Referential Internal + - Service Referential External + - Service UI Referential + - Service Ingest Internal + - Service Ingest External + - Service UI Ingest + - Service Archive Search Internal + - Service Archive Search External + - Service UI Archive Search + - Service Mongod (en cours de mise à niveau/!\\) + +Un load balancer/reverse proxy (à défaut Consul) est installé et configuré pour la répartition de charge entre +différentes instances (cette configurtion est en cours de réalisation). + +La configuration de la mémoire des services est par défaut: +``` + Xms=512m et Xmx=512m +``` +cette configuration est modifiable, pour plus d'informations (cf: DEX). + +### Mono instanciation + +Le fameux service mono instanciable dans vitamui est le serveur CAS. diff --git a/docs/DAT/docs/dat_vitamui/introduction/intro_objectifs.md b/docs/fr/architecture/sections/intro_objectifs.md similarity index 100% rename from docs/DAT/docs/dat_vitamui/introduction/intro_objectifs.md rename to docs/fr/architecture/sections/intro_objectifs.md diff --git a/docs/DAT/docs/dat_vitamui/orientations/orientations_techniques.md b/docs/fr/architecture/sections/orientations_techniques.md similarity index 100% rename from docs/DAT/docs/dat_vitamui/orientations/orientations_techniques.md rename to docs/fr/architecture/sections/orientations_techniques.md diff --git a/docs/fr/exploitation/Makefile b/docs/fr/exploitation/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d4bb2cbb9eddb1bb1b4f366623044af8e4830919 --- /dev/null +++ b/docs/fr/exploitation/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/fr/exploitation/_static/css/theme.css b/docs/fr/exploitation/_static/css/theme.css new file mode 100644 index 0000000000000000000000000000000000000000..78bf83ded4be2beb82c7eafb61a352d8464e9f42 --- /dev/null +++ b/docs/fr/exploitation/_static/css/theme.css @@ -0,0 +1,5369 @@ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block +} +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1 +} +audio:not([controls]) { + display: none +} +[hidden] { + display: none +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100% +} +body { + margin: 0 +} +a:hover, +a:active { + outline: 0 +} +abbr[title] { + border-bottom: 1px dotted +} +b, +strong { + font-weight: bold +} +blockquote { + margin: 0 +} +dfn { + font-style: italic +} +ins { + background: #ff9; + color: #000; + text-decoration: none +} +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold +} +pre, +code, +.rst-content tt, +.rst-content code, +kbd, +samp { + font-family: monospace, serif; + _font-family: "courier new", monospace; + font-size: 1em +} +pre { + white-space: pre +} +q { + quotes: none +} +q:before, +q:after { + content: ""; + content: none +} +small { + font-size: 85% +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} +sup { + top: -0.5em +} +sub { + bottom: -0.25em +} +ul, +ol, +dl { + margin: 0; + padding: 0; + list-style: none; + list-style-image: none +} +li { + list-style: none +} +dd { + margin: 0 +} +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; + max-width: 100% +} +svg:not(:root) { + overflow: hidden +} +figure { + margin: 0 +} +form { + margin: 0 +} +fieldset { + border: 0; + margin: 0; + padding: 0 +} +label { + cursor: pointer +} +legend { + border: 0; + *margin-left: -7px; + padding: 0; + white-space: normal +} +button, +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} +button, +input { + line-height: normal +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + *overflow: visible +} +button[disabled], +input[disabled] { + cursor: default +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; + *width: 13px; + *height: 13px +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box +} +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} +textarea { + overflow: auto; + vertical-align: top; + resize: vertical +} +table { + border-collapse: collapse; + border-spacing: 0 +} +td { + vertical-align: top +} +.chromeframe { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0 +} +.ir { + display: block; + border: 0; + text-indent: -999em; + overflow: hidden; + background-color: transparent; + background-repeat: no-repeat; + text-align: left; + direction: ltr; + *line-height: 0 +} +.ir br { + display: none +} +.hidden { + display: none !important; + visibility: hidden +} +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px +} +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto +} +.invisible { + visibility: hidden +} +.relative { + position: relative +} +big, +small { + font-size: 100% +} +@media print { + html, + body, + section { + background: none !important + } + * { + box-shadow: none !important; + text-shadow: none !important; + filter: none !important; + -ms-filter: none !important + } + a, + a:visited { + text-decoration: underline + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: "" + } + pre, + blockquote { + page-break-inside: avoid + } + thead { + display: table-header-group + } + tr, + img { + page-break-inside: avoid + } + img { + max-width: 100% !important + } + @page { + margin: 0.5cm + } + p, + h2, + .rst-content .toctree-wrapper p.caption, + h3 { + orphans: 3; + widows: 3 + } + h2, + .rst-content .toctree-wrapper p.caption, + h3 { + page-break-after: avoid + } +} +.fa:before, +.wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before, +.rst-content .admonition-title:before, +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before, +.rst-content dl dt .headerlink:before, +.rst-content p.caption .headerlink:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before, +.icon:before, +.wy-dropdown .caret:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before, +.wy-alert, +.rst-content .note, +.rst-content .attention, +.rst-content .caution, +.rst-content .danger, +.rst-content .error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .warning, +.rst-content .seealso, +.rst-content .admonition-todo, +.btn, +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="week"], +input[type="number"], +input[type="search"], +input[type="tel"], +input[type="color"], +select, +textarea, +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a, +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a, +.wy-nav-top a { + -webkit-font-smoothing: antialiased +} +.clearfix { + *zoom: 1 +} +.clearfix:before, +.clearfix:after { + display: table; + content: "" +} +.clearfix:after { + clear: both +} +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@font-face { + font-family: 'FontAwesome'; + src: url("../fonts/fontawesome-webfont.eot"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff") format("woff"), url("../fonts/fontawesome-webfont.ttf") format("truetype"), url("../fonts/fontawesome-webfont.svg") format("svg"); + font-weight: normal; + font-style: normal +} +.fa, +.wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +.rst-content .admonition-title, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink, +.rst-content tt.download span:first-child, +.rst-content code.download span:first-child, +.icon { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} +.fa-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -15% +} +.fa-2x { + font-size: 2em +} +.fa-3x { + font-size: 3em +} +.fa-4x { + font-size: 4em +} +.fa-5x { + font-size: 5em +} +.fa-fw { + width: 1.28571em; + text-align: center +} +.fa-ul { + padding-left: 0; + margin-left: 2.14286em; + list-style-type: none +} +.fa-ul>li { + position: relative +} +.fa-li { + position: absolute; + left: -2.14286em; + width: 2.14286em; + top: 0.14286em; + text-align: center +} +.fa-li.fa-lg { + left: -1.85714em +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eee; + border-radius: .1em +} +.pull-right { + float: right +} +.pull-left { + float: left +} +.fa.pull-left, +.wy-menu-vertical li span.pull-left.toctree-expand, +.wy-menu-vertical li.on a span.pull-left.toctree-expand, +.wy-menu-vertical li.current>a span.pull-left.toctree-expand, +.rst-content .pull-left.admonition-title, +.rst-content h1 .pull-left.headerlink, +.rst-content h2 .pull-left.headerlink, +.rst-content h3 .pull-left.headerlink, +.rst-content h4 .pull-left.headerlink, +.rst-content h5 .pull-left.headerlink, +.rst-content h6 .pull-left.headerlink, +.rst-content dl dt .pull-left.headerlink, +.rst-content p.caption .pull-left.headerlink, +.rst-content tt.download span.pull-left:first-child, +.rst-content code.download span.pull-left:first-child, +.pull-left.icon { + margin-right: .3em +} +.fa.pull-right, +.wy-menu-vertical li span.pull-right.toctree-expand, +.wy-menu-vertical li.on a span.pull-right.toctree-expand, +.wy-menu-vertical li.current>a span.pull-right.toctree-expand, +.rst-content .pull-right.admonition-title, +.rst-content h1 .pull-right.headerlink, +.rst-content h2 .pull-right.headerlink, +.rst-content h3 .pull-right.headerlink, +.rst-content h4 .pull-right.headerlink, +.rst-content h5 .pull-right.headerlink, +.rst-content h6 .pull-right.headerlink, +.rst-content dl dt .pull-right.headerlink, +.rst-content p.caption .pull-right.headerlink, +.rst-content tt.download span.pull-right:first-child, +.rst-content code.download span.pull-right:first-child, +.pull-right.icon { + margin-left: .3em +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg) + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg) + } +} +.fa-rotate-90 { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg) +} +.fa-rotate-180 { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg) +} +.fa-rotate-270 { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg) +} +.fa-flip-horizontal { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=0); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1) +} +.fa-flip-vertical { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1) +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center +} +.fa-stack-1x { + line-height: inherit +} +.fa-stack-2x { + font-size: 2em +} +.fa-inverse { + color: #fff +} +.fa-glass:before { + content: "" +} +.fa-music:before { + content: "ï€" +} +.fa-search:before, +.icon-search:before { + content: "" +} +.fa-envelope-o:before { + content: "" +} +.fa-heart:before { + content: "" +} +.fa-star:before { + content: "" +} +.fa-star-o:before { + content: "" +} +.fa-user:before { + content: "" +} +.fa-film:before { + content: "" +} +.fa-th-large:before { + content: "" +} +.fa-th:before { + content: "" +} +.fa-th-list:before { + content: "" +} +.fa-check:before { + content: "" +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "ï€" +} +.fa-search-plus:before { + content: "" +} +.fa-search-minus:before { + content: "ï€" +} +.fa-power-off:before { + content: "" +} +.fa-signal:before { + content: "" +} +.fa-gear:before, +.fa-cog:before { + content: "" +} +.fa-trash-o:before { + content: "" +} +.fa-home:before, +.icon-home:before { + content: "" +} +.fa-file-o:before { + content: "" +} +.fa-clock-o:before { + content: "" +} +.fa-road:before { + content: "" +} +.fa-download:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before { + content: "" +} +.fa-arrow-circle-o-down:before { + content: "" +} +.fa-arrow-circle-o-up:before { + content: "" +} +.fa-inbox:before { + content: "" +} +.fa-play-circle-o:before { + content: "ï€" +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "" +} +.fa-refresh:before { + content: "" +} +.fa-list-alt:before { + content: "" +} +.fa-lock:before { + content: "" +} +.fa-flag:before { + content: "" +} +.fa-headphones:before { + content: "" +} +.fa-volume-off:before { + content: "" +} +.fa-volume-down:before { + content: "" +} +.fa-volume-up:before { + content: "" +} +.fa-qrcode:before { + content: "" +} +.fa-barcode:before { + content: "" +} +.fa-tag:before { + content: "" +} +.fa-tags:before { + content: "" +} +.fa-book:before, +.icon-book:before { + content: "ï€" +} +.fa-bookmark:before { + content: "" +} +.fa-print:before { + content: "" +} +.fa-camera:before { + content: "" +} +.fa-font:before { + content: "" +} +.fa-bold:before { + content: "" +} +.fa-italic:before { + content: "" +} +.fa-text-height:before { + content: "" +} +.fa-text-width:before { + content: "" +} +.fa-align-left:before { + content: "" +} +.fa-align-center:before { + content: "" +} +.fa-align-right:before { + content: "" +} +.fa-align-justify:before { + content: "" +} +.fa-list:before { + content: "" +} +.fa-dedent:before, +.fa-outdent:before { + content: "" +} +.fa-indent:before { + content: "" +} +.fa-video-camera:before { + content: "" +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "" +} +.fa-pencil:before { + content: "ï€" +} +.fa-map-marker:before { + content: "ï" +} +.fa-adjust:before { + content: "ï‚" +} +.fa-tint:before { + content: "ïƒ" +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "ï„" +} +.fa-share-square-o:before { + content: "ï…" +} +.fa-check-square-o:before { + content: "ï†" +} +.fa-arrows:before { + content: "ï‡" +} +.fa-step-backward:before { + content: "ïˆ" +} +.fa-fast-backward:before { + content: "ï‰" +} +.fa-backward:before { + content: "ïŠ" +} +.fa-play:before { + content: "ï‹" +} +.fa-pause:before { + content: "ïŒ" +} +.fa-stop:before { + content: "ï" +} +.fa-forward:before { + content: "ïŽ" +} +.fa-fast-forward:before { + content: "ï" +} +.fa-step-forward:before { + content: "ï‘" +} +.fa-eject:before { + content: "ï’" +} +.fa-chevron-left:before { + content: "ï“" +} +.fa-chevron-right:before { + content: "ï”" +} +.fa-plus-circle:before { + content: "ï•" +} +.fa-minus-circle:before { + content: "ï–" +} +.fa-times-circle:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before { + content: "ï—" +} +.fa-check-circle:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before { + content: "ï˜" +} +.fa-question-circle:before { + content: "ï™" +} +.fa-info-circle:before { + content: "ïš" +} +.fa-crosshairs:before { + content: "ï›" +} +.fa-times-circle-o:before { + content: "ïœ" +} +.fa-check-circle-o:before { + content: "ï" +} +.fa-ban:before { + content: "ïž" +} +.fa-arrow-left:before { + content: "ï " +} +.fa-arrow-right:before { + content: "ï¡" +} +.fa-arrow-up:before { + content: "ï¢" +} +.fa-arrow-down:before { + content: "ï£" +} +.fa-mail-forward:before, +.fa-share:before { + content: "ï¤" +} +.fa-expand:before { + content: "ï¥" +} +.fa-compress:before { + content: "ï¦" +} +.fa-plus:before { + content: "ï§" +} +.fa-minus:before { + content: "ï¨" +} +.fa-asterisk:before { + content: "ï©" +} +.fa-exclamation-circle:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before, +.rst-content .admonition-title:before { + content: "ïª" +} +.fa-gift:before { + content: "ï«" +} +.fa-leaf:before { + content: "ï¬" +} +.fa-fire:before, +.icon-fire:before { + content: "ï" +} +.fa-eye:before { + content: "ï®" +} +.fa-eye-slash:before { + content: "ï°" +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "ï±" +} +.fa-plane:before { + content: "ï²" +} +.fa-calendar:before { + content: "ï³" +} +.fa-random:before { + content: "ï´" +} +.fa-comment:before { + content: "ïµ" +} +.fa-magnet:before { + content: "ï¶" +} +.fa-chevron-up:before { + content: "ï·" +} +.fa-chevron-down:before { + content: "ï¸" +} +.fa-retweet:before { + content: "ï¹" +} +.fa-shopping-cart:before { + content: "ïº" +} +.fa-folder:before { + content: "ï»" +} +.fa-folder-open:before { + content: "ï¼" +} +.fa-arrows-v:before { + content: "ï½" +} +.fa-arrows-h:before { + content: "ï¾" +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "ï‚€" +} +.fa-twitter-square:before { + content: "ï‚" +} +.fa-facebook-square:before { + content: "ï‚‚" +} +.fa-camera-retro:before { + content: "" +} +.fa-key:before { + content: "ï‚„" +} +.fa-gears:before, +.fa-cogs:before { + content: "ï‚…" +} +.fa-comments:before { + content: "" +} +.fa-thumbs-o-up:before { + content: "" +} +.fa-thumbs-o-down:before { + content: "" +} +.fa-star-half:before { + content: "" +} +.fa-heart-o:before { + content: "" +} +.fa-sign-out:before { + content: "ï‚‹" +} +.fa-linkedin-square:before { + content: "" +} +.fa-thumb-tack:before { + content: "ï‚" +} +.fa-external-link:before { + content: "" +} +.fa-sign-in:before { + content: "ï‚" +} +.fa-trophy:before { + content: "ï‚‘" +} +.fa-github-square:before { + content: "ï‚’" +} +.fa-upload:before { + content: "ï‚“" +} +.fa-lemon-o:before { + content: "ï‚”" +} +.fa-phone:before { + content: "ï‚•" +} +.fa-square-o:before { + content: "ï‚–" +} +.fa-bookmark-o:before { + content: "ï‚—" +} +.fa-phone-square:before { + content: "" +} +.fa-twitter:before { + content: "ï‚™" +} +.fa-facebook:before { + content: "" +} +.fa-github:before, +.icon-github:before { + content: "ï‚›" +} +.fa-unlock:before { + content: "" +} +.fa-credit-card:before { + content: "ï‚" +} +.fa-rss:before { + content: "" +} +.fa-hdd-o:before { + content: "ï‚ " +} +.fa-bullhorn:before { + content: "ï‚¡" +} +.fa-bell:before { + content: "" +} +.fa-certificate:before { + content: "ï‚£" +} +.fa-hand-o-right:before { + content: "" +} +.fa-hand-o-left:before { + content: "ï‚¥" +} +.fa-hand-o-up:before { + content: "" +} +.fa-hand-o-down:before { + content: "ï‚§" +} +.fa-arrow-circle-left:before, +.icon-circle-arrow-left:before { + content: "" +} +.fa-arrow-circle-right:before, +.icon-circle-arrow-right:before { + content: "ï‚©" +} +.fa-arrow-circle-up:before { + content: "" +} +.fa-arrow-circle-down:before { + content: "ï‚«" +} +.fa-globe:before { + content: "" +} +.fa-wrench:before { + content: "ï‚" +} +.fa-tasks:before { + content: "ï‚®" +} +.fa-filter:before { + content: "ï‚°" +} +.fa-briefcase:before { + content: "" +} +.fa-arrows-alt:before { + content: "" +} +.fa-group:before, +.fa-users:before { + content: "" +} +.fa-chain:before, +.fa-link:before, +.icon-link:before { + content: "ïƒ" +} +.fa-cloud:before { + content: "" +} +.fa-flask:before { + content: "" +} +.fa-cut:before, +.fa-scissors:before { + content: "" +} +.fa-copy:before, +.fa-files-o:before { + content: "" +} +.fa-paperclip:before { + content: "" +} +.fa-save:before, +.fa-floppy-o:before { + content: "" +} +.fa-square:before { + content: "" +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "" +} +.fa-list-ul:before { + content: "" +} +.fa-list-ol:before { + content: "" +} +.fa-strikethrough:before { + content: "" +} +.fa-underline:before { + content: "ïƒ" +} +.fa-table:before { + content: "" +} +.fa-magic:before { + content: "ïƒ" +} +.fa-truck:before { + content: "" +} +.fa-pinterest:before { + content: "" +} +.fa-pinterest-square:before { + content: "" +} +.fa-google-plus-square:before { + content: "" +} +.fa-google-plus:before { + content: "" +} +.fa-money:before { + content: "" +} +.fa-caret-down:before, +.wy-dropdown .caret:before, +.icon-caret-down:before { + content: "" +} +.fa-caret-up:before { + content: "" +} +.fa-caret-left:before { + content: "" +} +.fa-caret-right:before { + content: "" +} +.fa-columns:before { + content: "" +} +.fa-unsorted:before, +.fa-sort:before { + content: "" +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "ïƒ" +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "" +} +.fa-envelope:before { + content: "ïƒ " +} +.fa-linkedin:before { + content: "" +} +.fa-rotate-left:before, +.fa-undo:before { + content: "" +} +.fa-legal:before, +.fa-gavel:before { + content: "" +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "" +} +.fa-comment-o:before { + content: "" +} +.fa-comments-o:before { + content: "" +} +.fa-flash:before, +.fa-bolt:before { + content: "" +} +.fa-sitemap:before { + content: "" +} +.fa-umbrella:before { + content: "" +} +.fa-paste:before, +.fa-clipboard:before { + content: "" +} +.fa-lightbulb-o:before { + content: "" +} +.fa-exchange:before { + content: "" +} +.fa-cloud-download:before { + content: "ïƒ" +} +.fa-cloud-upload:before { + content: "" +} +.fa-user-md:before { + content: "" +} +.fa-stethoscope:before { + content: "" +} +.fa-suitcase:before { + content: "" +} +.fa-bell-o:before { + content: "ï‚¢" +} +.fa-coffee:before { + content: "" +} +.fa-cutlery:before { + content: "" +} +.fa-file-text-o:before { + content: "" +} +.fa-building-o:before { + content: "" +} +.fa-hospital-o:before { + content: "" +} +.fa-ambulance:before { + content: "" +} +.fa-medkit:before { + content: "" +} +.fa-fighter-jet:before { + content: "" +} +.fa-beer:before { + content: "" +} +.fa-h-square:before { + content: "" +} +.fa-plus-square:before { + content: "" +} +.fa-angle-double-left:before { + content: "ï„€" +} +.fa-angle-double-right:before { + content: "ï„" +} +.fa-angle-double-up:before { + content: "ï„‚" +} +.fa-angle-double-down:before { + content: "" +} +.fa-angle-left:before { + content: "ï„„" +} +.fa-angle-right:before { + content: "ï„…" +} +.fa-angle-up:before { + content: "" +} +.fa-angle-down:before { + content: "" +} +.fa-desktop:before { + content: "" +} +.fa-laptop:before { + content: "" +} +.fa-tablet:before { + content: "" +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "ï„‹" +} +.fa-circle-o:before { + content: "" +} +.fa-quote-left:before { + content: "ï„" +} +.fa-quote-right:before { + content: "" +} +.fa-spinner:before { + content: "ï„" +} +.fa-circle:before { + content: "ï„‘" +} +.fa-mail-reply:before, +.fa-reply:before { + content: "ï„’" +} +.fa-github-alt:before { + content: "ï„“" +} +.fa-folder-o:before { + content: "ï„”" +} +.fa-folder-open-o:before { + content: "ï„•" +} +.fa-smile-o:before { + content: "" +} +.fa-frown-o:before { + content: "ï„™" +} +.fa-meh-o:before { + content: "" +} +.fa-gamepad:before { + content: "ï„›" +} +.fa-keyboard-o:before { + content: "" +} +.fa-flag-o:before { + content: "ï„" +} +.fa-flag-checkered:before { + content: "" +} +.fa-terminal:before { + content: "ï„ " +} +.fa-code:before { + content: "ï„¡" +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "ï„¢" +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "ï„£" +} +.fa-location-arrow:before { + content: "" +} +.fa-crop:before { + content: "ï„¥" +} +.fa-code-fork:before { + content: "" +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "ï„§" +} +.fa-question:before { + content: "" +} +.fa-info:before { + content: "ï„©" +} +.fa-exclamation:before { + content: "" +} +.fa-superscript:before { + content: "ï„«" +} +.fa-subscript:before { + content: "" +} +.fa-eraser:before { + content: "ï„" +} +.fa-puzzle-piece:before { + content: "ï„®" +} +.fa-microphone:before { + content: "ï„°" +} +.fa-microphone-slash:before { + content: "" +} +.fa-shield:before { + content: "" +} +.fa-calendar-o:before { + content: "" +} +.fa-fire-extinguisher:before { + content: "ï„´" +} +.fa-rocket:before { + content: "" +} +.fa-maxcdn:before { + content: "ï„¶" +} +.fa-chevron-circle-left:before { + content: "ï„·" +} +.fa-chevron-circle-right:before { + content: "" +} +.fa-chevron-circle-up:before { + content: "" +} +.fa-chevron-circle-down:before { + content: "" +} +.fa-html5:before { + content: "ï„»" +} +.fa-css3:before { + content: "" +} +.fa-anchor:before { + content: "" +} +.fa-unlock-alt:before { + content: "" +} +.fa-bullseye:before { + content: "ï…€" +} +.fa-ellipsis-h:before { + content: "ï…" +} +.fa-ellipsis-v:before { + content: "ï…‚" +} +.fa-rss-square:before { + content: "ï…ƒ" +} +.fa-play-circle:before { + content: "ï…„" +} +.fa-ticket:before { + content: "ï……" +} +.fa-minus-square:before { + content: "ï…†" +} +.fa-minus-square-o:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before { + content: "ï…‡" +} +.fa-level-up:before { + content: "ï…ˆ" +} +.fa-level-down:before { + content: "ï…‰" +} +.fa-check-square:before { + content: "ï…Š" +} +.fa-pencil-square:before { + content: "ï…‹" +} +.fa-external-link-square:before { + content: "ï…Œ" +} +.fa-share-square:before { + content: "ï…" +} +.fa-compass:before { + content: "ï…Ž" +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "ï…" +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "ï…‘" +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "ï…’" +} +.fa-euro:before, +.fa-eur:before { + content: "ï…“" +} +.fa-gbp:before { + content: "ï…”" +} +.fa-dollar:before, +.fa-usd:before { + content: "ï…•" +} +.fa-rupee:before, +.fa-inr:before { + content: "ï…–" +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "ï…—" +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "ï…˜" +} +.fa-won:before, +.fa-krw:before { + content: "ï…™" +} +.fa-bitcoin:before, +.fa-btc:before { + content: "ï…š" +} +.fa-file:before { + content: "ï…›" +} +.fa-file-text:before { + content: "ï…œ" +} +.fa-sort-alpha-asc:before { + content: "ï…" +} +.fa-sort-alpha-desc:before { + content: "ï…ž" +} +.fa-sort-amount-asc:before { + content: "ï… " +} +.fa-sort-amount-desc:before { + content: "ï…¡" +} +.fa-sort-numeric-asc:before { + content: "ï…¢" +} +.fa-sort-numeric-desc:before { + content: "ï…£" +} +.fa-thumbs-up:before { + content: "ï…¤" +} +.fa-thumbs-down:before { + content: "ï…¥" +} +.fa-youtube-square:before { + content: "ï…¦" +} +.fa-youtube:before { + content: "ï…§" +} +.fa-xing:before { + content: "ï…¨" +} +.fa-xing-square:before { + content: "ï…©" +} +.fa-youtube-play:before { + content: "ï…ª" +} +.fa-dropbox:before { + content: "ï…«" +} +.fa-stack-overflow:before { + content: "ï…¬" +} +.fa-instagram:before { + content: "ï…" +} +.fa-flickr:before { + content: "ï…®" +} +.fa-adn:before { + content: "ï…°" +} +.fa-bitbucket:before, +.icon-bitbucket:before { + content: "ï…±" +} +.fa-bitbucket-square:before { + content: "ï…²" +} +.fa-tumblr:before { + content: "ï…³" +} +.fa-tumblr-square:before { + content: "ï…´" +} +.fa-long-arrow-down:before { + content: "ï…µ" +} +.fa-long-arrow-up:before { + content: "ï…¶" +} +.fa-long-arrow-left:before { + content: "ï…·" +} +.fa-long-arrow-right:before { + content: "ï…¸" +} +.fa-apple:before { + content: "ï…¹" +} +.fa-windows:before { + content: "ï…º" +} +.fa-android:before { + content: "ï…»" +} +.fa-linux:before { + content: "ï…¼" +} +.fa-dribbble:before { + content: "ï…½" +} +.fa-skype:before { + content: "ï…¾" +} +.fa-foursquare:before { + content: "" +} +.fa-trello:before { + content: "ï†" +} +.fa-female:before { + content: "" +} +.fa-male:before { + content: "" +} +.fa-gittip:before { + content: "" +} +.fa-sun-o:before { + content: "" +} +.fa-moon-o:before { + content: "" +} +.fa-archive:before { + content: "" +} +.fa-bug:before { + content: "" +} +.fa-vk:before { + content: "" +} +.fa-weibo:before { + content: "" +} +.fa-renren:before { + content: "" +} +.fa-pagelines:before { + content: "" +} +.fa-stack-exchange:before { + content: "ï†" +} +.fa-arrow-circle-o-right:before { + content: "" +} +.fa-arrow-circle-o-left:before { + content: "ï†" +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "" +} +.fa-dot-circle-o:before { + content: "" +} +.fa-wheelchair:before { + content: "" +} +.fa-vimeo-square:before { + content: "" +} +.fa-turkish-lira:before, +.fa-try:before { + content: "" +} +.fa-plus-square-o:before, +.wy-menu-vertical li span.toctree-expand:before { + content: "" +} +.fa-space-shuttle:before { + content: "" +} +.fa-slack:before { + content: "" +} +.fa-envelope-square:before { + content: "" +} +.fa-wordpress:before { + content: "" +} +.fa-openid:before { + content: "" +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "" +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "ï†" +} +.fa-yahoo:before { + content: "" +} +.fa-google:before { + content: "ï† " +} +.fa-reddit:before { + content: "" +} +.fa-reddit-square:before { + content: "" +} +.fa-stumbleupon-circle:before { + content: "" +} +.fa-stumbleupon:before { + content: "" +} +.fa-delicious:before { + content: "" +} +.fa-digg:before { + content: "" +} +.fa-pied-piper:before { + content: "" +} +.fa-pied-piper-alt:before { + content: "" +} +.fa-drupal:before { + content: "" +} +.fa-joomla:before { + content: "" +} +.fa-language:before { + content: "" +} +.fa-fax:before { + content: "" +} +.fa-building:before { + content: "ï†" +} +.fa-child:before { + content: "" +} +.fa-paw:before { + content: "" +} +.fa-spoon:before { + content: "" +} +.fa-cube:before { + content: "" +} +.fa-cubes:before { + content: "" +} +.fa-behance:before { + content: "" +} +.fa-behance-square:before { + content: "" +} +.fa-steam:before { + content: "" +} +.fa-steam-square:before { + content: "" +} +.fa-recycle:before { + content: "" +} +.fa-automobile:before, +.fa-car:before { + content: "" +} +.fa-cab:before, +.fa-taxi:before { + content: "" +} +.fa-tree:before { + content: "" +} +.fa-spotify:before { + content: "" +} +.fa-deviantart:before { + content: "" +} +.fa-soundcloud:before { + content: "" +} +.fa-database:before { + content: "" +} +.fa-file-pdf-o:before { + content: "ï‡" +} +.fa-file-word-o:before { + content: "" +} +.fa-file-excel-o:before { + content: "" +} +.fa-file-powerpoint-o:before { + content: "" +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "" +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "" +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "" +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "" +} +.fa-file-code-o:before { + content: "" +} +.fa-vine:before { + content: "" +} +.fa-codepen:before { + content: "" +} +.fa-jsfiddle:before { + content: "" +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "ï‡" +} +.fa-circle-o-notch:before { + content: "" +} +.fa-ra:before, +.fa-rebel:before { + content: "ï‡" +} +.fa-ge:before, +.fa-empire:before { + content: "" +} +.fa-git-square:before { + content: "" +} +.fa-git:before { + content: "" +} +.fa-hacker-news:before { + content: "" +} +.fa-tencent-weibo:before { + content: "" +} +.fa-qq:before { + content: "" +} +.fa-wechat:before, +.fa-weixin:before { + content: "" +} +.fa-send:before, +.fa-paper-plane:before { + content: "" +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "" +} +.fa-history:before { + content: "" +} +.fa-circle-thin:before { + content: "" +} +.fa-header:before { + content: "" +} +.fa-paragraph:before { + content: "ï‡" +} +.fa-sliders:before { + content: "" +} +.fa-share-alt:before { + content: "ï‡ " +} +.fa-share-alt-square:before { + content: "" +} +.fa-bomb:before { + content: "" +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "" +} +.fa-tty:before { + content: "" +} +.fa-binoculars:before { + content: "" +} +.fa-plug:before { + content: "" +} +.fa-slideshare:before { + content: "" +} +.fa-twitch:before { + content: "" +} +.fa-yelp:before { + content: "" +} +.fa-newspaper-o:before { + content: "" +} +.fa-wifi:before { + content: "" +} +.fa-calculator:before { + content: "" +} +.fa-paypal:before { + content: "ï‡" +} +.fa-google-wallet:before { + content: "" +} +.fa-cc-visa:before { + content: "" +} +.fa-cc-mastercard:before { + content: "" +} +.fa-cc-discover:before { + content: "" +} +.fa-cc-amex:before { + content: "" +} +.fa-cc-paypal:before { + content: "" +} +.fa-cc-stripe:before { + content: "" +} +.fa-bell-slash:before { + content: "" +} +.fa-bell-slash-o:before { + content: "" +} +.fa-trash:before { + content: "" +} +.fa-copyright:before { + content: "" +} +.fa-at:before { + content: "" +} +.fa-eyedropper:before { + content: "" +} +.fa-paint-brush:before { + content: "" +} +.fa-birthday-cake:before { + content: "" +} +.fa-area-chart:before { + content: "" +} +.fa-pie-chart:before { + content: "" +} +.fa-line-chart:before { + content: "ïˆ" +} +.fa-lastfm:before { + content: "" +} +.fa-lastfm-square:before { + content: "" +} +.fa-toggle-off:before { + content: "" +} +.fa-toggle-on:before { + content: "" +} +.fa-bicycle:before { + content: "" +} +.fa-bus:before { + content: "" +} +.fa-ioxhost:before { + content: "" +} +.fa-angellist:before { + content: "" +} +.fa-cc:before { + content: "" +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "" +} +.fa-meanpath:before { + content: "" +} +.fa, +.wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +.rst-content .admonition-title, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink, +.rst-content tt.download span:first-child, +.rst-content code.download span:first-child, +.icon, +.wy-dropdown .caret, +.wy-inline-validate.wy-inline-validate-success .wy-input-context, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context, +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + font-family: inherit +} +.fa:before, +.wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before, +.rst-content .admonition-title:before, +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before, +.rst-content dl dt .headerlink:before, +.rst-content p.caption .headerlink:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before, +.icon:before, +.wy-dropdown .caret:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before { + font-family: "FontAwesome"; + display: inline-block; + font-style: normal; + font-weight: normal; + line-height: 1; + text-decoration: inherit +} +a .fa, +a .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li a span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +a .rst-content .admonition-title, +.rst-content a .admonition-title, +a .rst-content h1 .headerlink, +.rst-content h1 a .headerlink, +a .rst-content h2 .headerlink, +.rst-content h2 a .headerlink, +a .rst-content h3 .headerlink, +.rst-content h3 a .headerlink, +a .rst-content h4 .headerlink, +.rst-content h4 a .headerlink, +a .rst-content h5 .headerlink, +.rst-content h5 a .headerlink, +a .rst-content h6 .headerlink, +.rst-content h6 a .headerlink, +a .rst-content dl dt .headerlink, +.rst-content dl dt a .headerlink, +a .rst-content p.caption .headerlink, +.rst-content p.caption a .headerlink, +a .rst-content tt.download span:first-child, +.rst-content tt.download a span:first-child, +a .rst-content code.download span:first-child, +.rst-content code.download a span:first-child, +a .icon { + display: inline-block; + text-decoration: inherit +} +.btn .fa, +.btn .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .btn span.toctree-expand, +.btn .wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.on a .btn span.toctree-expand, +.btn .wy-menu-vertical li.current>a span.toctree-expand, +.wy-menu-vertical li.current>a .btn span.toctree-expand, +.btn .rst-content .admonition-title, +.rst-content .btn .admonition-title, +.btn .rst-content h1 .headerlink, +.rst-content h1 .btn .headerlink, +.btn .rst-content h2 .headerlink, +.rst-content h2 .btn .headerlink, +.btn .rst-content h3 .headerlink, +.rst-content h3 .btn .headerlink, +.btn .rst-content h4 .headerlink, +.rst-content h4 .btn .headerlink, +.btn .rst-content h5 .headerlink, +.rst-content h5 .btn .headerlink, +.btn .rst-content h6 .headerlink, +.rst-content h6 .btn .headerlink, +.btn .rst-content dl dt .headerlink, +.rst-content dl dt .btn .headerlink, +.btn .rst-content p.caption .headerlink, +.rst-content p.caption .btn .headerlink, +.btn .rst-content tt.download span:first-child, +.rst-content tt.download .btn span:first-child, +.btn .rst-content code.download span:first-child, +.rst-content code.download .btn span:first-child, +.btn .icon, +.nav .fa, +.nav .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .nav span.toctree-expand, +.nav .wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.on a .nav span.toctree-expand, +.nav .wy-menu-vertical li.current>a span.toctree-expand, +.wy-menu-vertical li.current>a .nav span.toctree-expand, +.nav .rst-content .admonition-title, +.rst-content .nav .admonition-title, +.nav .rst-content h1 .headerlink, +.rst-content h1 .nav .headerlink, +.nav .rst-content h2 .headerlink, +.rst-content h2 .nav .headerlink, +.nav .rst-content h3 .headerlink, +.rst-content h3 .nav .headerlink, +.nav .rst-content h4 .headerlink, +.rst-content h4 .nav .headerlink, +.nav .rst-content h5 .headerlink, +.rst-content h5 .nav .headerlink, +.nav .rst-content h6 .headerlink, +.rst-content h6 .nav .headerlink, +.nav .rst-content dl dt .headerlink, +.rst-content dl dt .nav .headerlink, +.nav .rst-content p.caption .headerlink, +.rst-content p.caption .nav .headerlink, +.nav .rst-content tt.download span:first-child, +.rst-content tt.download .nav span:first-child, +.nav .rst-content code.download span:first-child, +.rst-content code.download .nav span:first-child, +.nav .icon { + display: inline +} +.btn .fa.fa-large, +.btn .wy-menu-vertical li span.fa-large.toctree-expand, +.wy-menu-vertical li .btn span.fa-large.toctree-expand, +.btn .rst-content .fa-large.admonition-title, +.rst-content .btn .fa-large.admonition-title, +.btn .rst-content h1 .fa-large.headerlink, +.rst-content h1 .btn .fa-large.headerlink, +.btn .rst-content h2 .fa-large.headerlink, +.rst-content h2 .btn .fa-large.headerlink, +.btn .rst-content h3 .fa-large.headerlink, +.rst-content h3 .btn .fa-large.headerlink, +.btn .rst-content h4 .fa-large.headerlink, +.rst-content h4 .btn .fa-large.headerlink, +.btn .rst-content h5 .fa-large.headerlink, +.rst-content h5 .btn .fa-large.headerlink, +.btn .rst-content h6 .fa-large.headerlink, +.rst-content h6 .btn .fa-large.headerlink, +.btn .rst-content dl dt .fa-large.headerlink, +.rst-content dl dt .btn .fa-large.headerlink, +.btn .rst-content p.caption .fa-large.headerlink, +.rst-content p.caption .btn .fa-large.headerlink, +.btn .rst-content tt.download span.fa-large:first-child, +.rst-content tt.download .btn span.fa-large:first-child, +.btn .rst-content code.download span.fa-large:first-child, +.rst-content code.download .btn span.fa-large:first-child, +.btn .fa-large.icon, +.nav .fa.fa-large, +.nav .wy-menu-vertical li span.fa-large.toctree-expand, +.wy-menu-vertical li .nav span.fa-large.toctree-expand, +.nav .rst-content .fa-large.admonition-title, +.rst-content .nav .fa-large.admonition-title, +.nav .rst-content h1 .fa-large.headerlink, +.rst-content h1 .nav .fa-large.headerlink, +.nav .rst-content h2 .fa-large.headerlink, +.rst-content h2 .nav .fa-large.headerlink, +.nav .rst-content h3 .fa-large.headerlink, +.rst-content h3 .nav .fa-large.headerlink, +.nav .rst-content h4 .fa-large.headerlink, +.rst-content h4 .nav .fa-large.headerlink, +.nav .rst-content h5 .fa-large.headerlink, +.rst-content h5 .nav .fa-large.headerlink, +.nav .rst-content h6 .fa-large.headerlink, +.rst-content h6 .nav .fa-large.headerlink, +.nav .rst-content dl dt .fa-large.headerlink, +.rst-content dl dt .nav .fa-large.headerlink, +.nav .rst-content p.caption .fa-large.headerlink, +.rst-content p.caption .nav .fa-large.headerlink, +.nav .rst-content tt.download span.fa-large:first-child, +.rst-content tt.download .nav span.fa-large:first-child, +.nav .rst-content code.download span.fa-large:first-child, +.rst-content code.download .nav span.fa-large:first-child, +.nav .fa-large.icon { + line-height: 0.9em +} +.btn .fa.fa-spin, +.btn .wy-menu-vertical li span.fa-spin.toctree-expand, +.wy-menu-vertical li .btn span.fa-spin.toctree-expand, +.btn .rst-content .fa-spin.admonition-title, +.rst-content .btn .fa-spin.admonition-title, +.btn .rst-content h1 .fa-spin.headerlink, +.rst-content h1 .btn .fa-spin.headerlink, +.btn .rst-content h2 .fa-spin.headerlink, +.rst-content h2 .btn .fa-spin.headerlink, +.btn .rst-content h3 .fa-spin.headerlink, +.rst-content h3 .btn .fa-spin.headerlink, +.btn .rst-content h4 .fa-spin.headerlink, +.rst-content h4 .btn .fa-spin.headerlink, +.btn .rst-content h5 .fa-spin.headerlink, +.rst-content h5 .btn .fa-spin.headerlink, +.btn .rst-content h6 .fa-spin.headerlink, +.rst-content h6 .btn .fa-spin.headerlink, +.btn .rst-content dl dt .fa-spin.headerlink, +.rst-content dl dt .btn .fa-spin.headerlink, +.btn .rst-content p.caption .fa-spin.headerlink, +.rst-content p.caption .btn .fa-spin.headerlink, +.btn .rst-content tt.download span.fa-spin:first-child, +.rst-content tt.download .btn span.fa-spin:first-child, +.btn .rst-content code.download span.fa-spin:first-child, +.rst-content code.download .btn span.fa-spin:first-child, +.btn .fa-spin.icon, +.nav .fa.fa-spin, +.nav .wy-menu-vertical li span.fa-spin.toctree-expand, +.wy-menu-vertical li .nav span.fa-spin.toctree-expand, +.nav .rst-content .fa-spin.admonition-title, +.rst-content .nav .fa-spin.admonition-title, +.nav .rst-content h1 .fa-spin.headerlink, +.rst-content h1 .nav .fa-spin.headerlink, +.nav .rst-content h2 .fa-spin.headerlink, +.rst-content h2 .nav .fa-spin.headerlink, +.nav .rst-content h3 .fa-spin.headerlink, +.rst-content h3 .nav .fa-spin.headerlink, +.nav .rst-content h4 .fa-spin.headerlink, +.rst-content h4 .nav .fa-spin.headerlink, +.nav .rst-content h5 .fa-spin.headerlink, +.rst-content h5 .nav .fa-spin.headerlink, +.nav .rst-content h6 .fa-spin.headerlink, +.rst-content h6 .nav .fa-spin.headerlink, +.nav .rst-content dl dt .fa-spin.headerlink, +.rst-content dl dt .nav .fa-spin.headerlink, +.nav .rst-content p.caption .fa-spin.headerlink, +.rst-content p.caption .nav .fa-spin.headerlink, +.nav .rst-content tt.download span.fa-spin:first-child, +.rst-content tt.download .nav span.fa-spin:first-child, +.nav .rst-content code.download span.fa-spin:first-child, +.rst-content code.download .nav span.fa-spin:first-child, +.nav .fa-spin.icon { + display: inline-block +} +.btn.fa:before, +.wy-menu-vertical li span.btn.toctree-expand:before, +.rst-content .btn.admonition-title:before, +.rst-content h1 .btn.headerlink:before, +.rst-content h2 .btn.headerlink:before, +.rst-content h3 .btn.headerlink:before, +.rst-content h4 .btn.headerlink:before, +.rst-content h5 .btn.headerlink:before, +.rst-content h6 .btn.headerlink:before, +.rst-content dl dt .btn.headerlink:before, +.rst-content p.caption .btn.headerlink:before, +.rst-content tt.download span.btn:first-child:before, +.rst-content code.download span.btn:first-child:before, +.btn.icon:before { + opacity: 0.5; + -webkit-transition: opacity 0.05s ease-in; + -moz-transition: opacity 0.05s ease-in; + transition: opacity 0.05s ease-in +} +.btn.fa:hover:before, +.wy-menu-vertical li span.btn.toctree-expand:hover:before, +.rst-content .btn.admonition-title:hover:before, +.rst-content h1 .btn.headerlink:hover:before, +.rst-content h2 .btn.headerlink:hover:before, +.rst-content h3 .btn.headerlink:hover:before, +.rst-content h4 .btn.headerlink:hover:before, +.rst-content h5 .btn.headerlink:hover:before, +.rst-content h6 .btn.headerlink:hover:before, +.rst-content dl dt .btn.headerlink:hover:before, +.rst-content p.caption .btn.headerlink:hover:before, +.rst-content tt.download span.btn:first-child:hover:before, +.rst-content code.download span.btn:first-child:hover:before, +.btn.icon:hover:before { + opacity: 1 +} +.btn-mini .fa:before, +.btn-mini .wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li .btn-mini span.toctree-expand:before, +.btn-mini .rst-content .admonition-title:before, +.rst-content .btn-mini .admonition-title:before, +.btn-mini .rst-content h1 .headerlink:before, +.rst-content h1 .btn-mini .headerlink:before, +.btn-mini .rst-content h2 .headerlink:before, +.rst-content h2 .btn-mini .headerlink:before, +.btn-mini .rst-content h3 .headerlink:before, +.rst-content h3 .btn-mini .headerlink:before, +.btn-mini .rst-content h4 .headerlink:before, +.rst-content h4 .btn-mini .headerlink:before, +.btn-mini .rst-content h5 .headerlink:before, +.rst-content h5 .btn-mini .headerlink:before, +.btn-mini .rst-content h6 .headerlink:before, +.rst-content h6 .btn-mini .headerlink:before, +.btn-mini .rst-content dl dt .headerlink:before, +.rst-content dl dt .btn-mini .headerlink:before, +.btn-mini .rst-content p.caption .headerlink:before, +.rst-content p.caption .btn-mini .headerlink:before, +.btn-mini .rst-content tt.download span:first-child:before, +.rst-content tt.download .btn-mini span:first-child:before, +.btn-mini .rst-content code.download span:first-child:before, +.rst-content code.download .btn-mini span:first-child:before, +.btn-mini .icon:before { + font-size: 14px; + vertical-align: -15% +} +.wy-alert, +.rst-content .note, +.rst-content .attention, +.rst-content .caution, +.rst-content .danger, +.rst-content .error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .warning, +.rst-content .seealso, +.rst-content .admonition-todo { + padding: 12px; + line-height: 24px; + margin-bottom: 24px; + background: #e7f2fa +} +.wy-alert-title, +.rst-content .admonition-title { + color: #fff; + font-weight: bold; + display: block; + color: #fff; + background: #6ab0de; + margin: -12px; + padding: 6px 12px; + margin-bottom: 12px +} +.wy-alert.wy-alert-danger, +.rst-content .wy-alert-danger.note, +.rst-content .wy-alert-danger.attention, +.rst-content .wy-alert-danger.caution, +.rst-content .danger, +.rst-content .error, +.rst-content .wy-alert-danger.hint, +.rst-content .wy-alert-danger.important, +.rst-content .wy-alert-danger.tip, +.rst-content .wy-alert-danger.warning, +.rst-content .wy-alert-danger.seealso, +.rst-content .wy-alert-danger.admonition-todo { + background: #fdf3f2 +} +.wy-alert.wy-alert-danger .wy-alert-title, +.rst-content .wy-alert-danger.note .wy-alert-title, +.rst-content .wy-alert-danger.attention .wy-alert-title, +.rst-content .wy-alert-danger.caution .wy-alert-title, +.rst-content .danger .wy-alert-title, +.rst-content .error .wy-alert-title, +.rst-content .wy-alert-danger.hint .wy-alert-title, +.rst-content .wy-alert-danger.important .wy-alert-title, +.rst-content .wy-alert-danger.tip .wy-alert-title, +.rst-content .wy-alert-danger.warning .wy-alert-title, +.rst-content .wy-alert-danger.seealso .wy-alert-title, +.rst-content .wy-alert-danger.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-danger .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-danger .admonition-title, +.rst-content .wy-alert-danger.note .admonition-title, +.rst-content .wy-alert-danger.attention .admonition-title, +.rst-content .wy-alert-danger.caution .admonition-title, +.rst-content .danger .admonition-title, +.rst-content .error .admonition-title, +.rst-content .wy-alert-danger.hint .admonition-title, +.rst-content .wy-alert-danger.important .admonition-title, +.rst-content .wy-alert-danger.tip .admonition-title, +.rst-content .wy-alert-danger.warning .admonition-title, +.rst-content .wy-alert-danger.seealso .admonition-title, +.rst-content .wy-alert-danger.admonition-todo .admonition-title { + background: #f29f97 +} +.wy-alert.wy-alert-warning, +.rst-content .wy-alert-warning.note, +.rst-content .attention, +.rst-content .caution, +.rst-content .wy-alert-warning.danger, +.rst-content .wy-alert-warning.error, +.rst-content .wy-alert-warning.hint, +.rst-content .wy-alert-warning.important, +.rst-content .wy-alert-warning.tip, +.rst-content .warning, +.rst-content .wy-alert-warning.seealso, +.rst-content .admonition-todo { + background: #ffedcc +} +.wy-alert.wy-alert-warning .wy-alert-title, +.rst-content .wy-alert-warning.note .wy-alert-title, +.rst-content .attention .wy-alert-title, +.rst-content .caution .wy-alert-title, +.rst-content .wy-alert-warning.danger .wy-alert-title, +.rst-content .wy-alert-warning.error .wy-alert-title, +.rst-content .wy-alert-warning.hint .wy-alert-title, +.rst-content .wy-alert-warning.important .wy-alert-title, +.rst-content .wy-alert-warning.tip .wy-alert-title, +.rst-content .warning .wy-alert-title, +.rst-content .wy-alert-warning.seealso .wy-alert-title, +.rst-content .admonition-todo .wy-alert-title, +.wy-alert.wy-alert-warning .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-warning .admonition-title, +.rst-content .wy-alert-warning.note .admonition-title, +.rst-content .attention .admonition-title, +.rst-content .caution .admonition-title, +.rst-content .wy-alert-warning.danger .admonition-title, +.rst-content .wy-alert-warning.error .admonition-title, +.rst-content .wy-alert-warning.hint .admonition-title, +.rst-content .wy-alert-warning.important .admonition-title, +.rst-content .wy-alert-warning.tip .admonition-title, +.rst-content .warning .admonition-title, +.rst-content .wy-alert-warning.seealso .admonition-title, +.rst-content .admonition-todo .admonition-title { + background: #f0b37e +} +.wy-alert.wy-alert-info, +.rst-content .note, +.rst-content .wy-alert-info.attention, +.rst-content .wy-alert-info.caution, +.rst-content .wy-alert-info.danger, +.rst-content .wy-alert-info.error, +.rst-content .wy-alert-info.hint, +.rst-content .wy-alert-info.important, +.rst-content .wy-alert-info.tip, +.rst-content .wy-alert-info.warning, +.rst-content .seealso, +.rst-content .wy-alert-info.admonition-todo { + background: #e7d0fa +} +.wy-alert.wy-alert-info .wy-alert-title, +.rst-content .note .wy-alert-title, +.rst-content .wy-alert-info.attention .wy-alert-title, +.rst-content .wy-alert-info.caution .wy-alert-title, +.rst-content .wy-alert-info.danger .wy-alert-title, +.rst-content .wy-alert-info.error .wy-alert-title, +.rst-content .wy-alert-info.hint .wy-alert-title, +.rst-content .wy-alert-info.important .wy-alert-title, +.rst-content .wy-alert-info.tip .wy-alert-title, +.rst-content .wy-alert-info.warning .wy-alert-title, +.rst-content .seealso .wy-alert-title, +.rst-content .wy-alert-info.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-info .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-info .admonition-title, +.rst-content .note .admonition-title, +.rst-content .wy-alert-info.attention .admonition-title, +.rst-content .wy-alert-info.caution .admonition-title, +.rst-content .wy-alert-info.danger .admonition-title, +.rst-content .wy-alert-info.error .admonition-title, +.rst-content .wy-alert-info.hint .admonition-title, +.rst-content .wy-alert-info.important .admonition-title, +.rst-content .wy-alert-info.tip .admonition-title, +.rst-content .wy-alert-info.warning .admonition-title, +.rst-content .seealso .admonition-title, +.rst-content .wy-alert-info.admonition-todo .admonition-title { + background: #40008F +} +.wy-alert.wy-alert-success, +.rst-content .wy-alert-success.note, +.rst-content .wy-alert-success.attention, +.rst-content .wy-alert-success.caution, +.rst-content .wy-alert-success.danger, +.rst-content .wy-alert-success.error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .wy-alert-success.warning, +.rst-content .wy-alert-success.seealso, +.rst-content .wy-alert-success.admonition-todo { + background: #dbfaf4 +} +.wy-alert.wy-alert-success .wy-alert-title, +.rst-content .wy-alert-success.note .wy-alert-title, +.rst-content .wy-alert-success.attention .wy-alert-title, +.rst-content .wy-alert-success.caution .wy-alert-title, +.rst-content .wy-alert-success.danger .wy-alert-title, +.rst-content .wy-alert-success.error .wy-alert-title, +.rst-content .hint .wy-alert-title, +.rst-content .important .wy-alert-title, +.rst-content .tip .wy-alert-title, +.rst-content .wy-alert-success.warning .wy-alert-title, +.rst-content .wy-alert-success.seealso .wy-alert-title, +.rst-content .wy-alert-success.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-success .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-success .admonition-title, +.rst-content .wy-alert-success.note .admonition-title, +.rst-content .wy-alert-success.attention .admonition-title, +.rst-content .wy-alert-success.caution .admonition-title, +.rst-content .wy-alert-success.danger .admonition-title, +.rst-content .wy-alert-success.error .admonition-title, +.rst-content .hint .admonition-title, +.rst-content .important .admonition-title, +.rst-content .tip .admonition-title, +.rst-content .wy-alert-success.warning .admonition-title, +.rst-content .wy-alert-success.seealso .admonition-title, +.rst-content .wy-alert-success.admonition-todo .admonition-title { + background: #1abc9c +} +.wy-alert.wy-alert-neutral, +.rst-content .wy-alert-neutral.note, +.rst-content .wy-alert-neutral.attention, +.rst-content .wy-alert-neutral.caution, +.rst-content .wy-alert-neutral.danger, +.rst-content .wy-alert-neutral.error, +.rst-content .wy-alert-neutral.hint, +.rst-content .wy-alert-neutral.important, +.rst-content .wy-alert-neutral.tip, +.rst-content .wy-alert-neutral.warning, +.rst-content .wy-alert-neutral.seealso, +.rst-content .wy-alert-neutral.admonition-todo { + background: #f3f6f6 +} +.wy-alert.wy-alert-neutral .wy-alert-title, +.rst-content .wy-alert-neutral.note .wy-alert-title, +.rst-content .wy-alert-neutral.attention .wy-alert-title, +.rst-content .wy-alert-neutral.caution .wy-alert-title, +.rst-content .wy-alert-neutral.danger .wy-alert-title, +.rst-content .wy-alert-neutral.error .wy-alert-title, +.rst-content .wy-alert-neutral.hint .wy-alert-title, +.rst-content .wy-alert-neutral.important .wy-alert-title, +.rst-content .wy-alert-neutral.tip .wy-alert-title, +.rst-content .wy-alert-neutral.warning .wy-alert-title, +.rst-content .wy-alert-neutral.seealso .wy-alert-title, +.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-neutral .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-neutral .admonition-title, +.rst-content .wy-alert-neutral.note .admonition-title, +.rst-content .wy-alert-neutral.attention .admonition-title, +.rst-content .wy-alert-neutral.caution .admonition-title, +.rst-content .wy-alert-neutral.danger .admonition-title, +.rst-content .wy-alert-neutral.error .admonition-title, +.rst-content .wy-alert-neutral.hint .admonition-title, +.rst-content .wy-alert-neutral.important .admonition-title, +.rst-content .wy-alert-neutral.tip .admonition-title, +.rst-content .wy-alert-neutral.warning .admonition-title, +.rst-content .wy-alert-neutral.seealso .admonition-title, +.rst-content .wy-alert-neutral.admonition-todo .admonition-title { + color: #40008F; + background: #e1e4e5 +} +.wy-alert.wy-alert-neutral a, +.rst-content .wy-alert-neutral.note a, +.rst-content .wy-alert-neutral.attention a, +.rst-content .wy-alert-neutral.caution a, +.rst-content .wy-alert-neutral.danger a, +.rst-content .wy-alert-neutral.error a, +.rst-content .wy-alert-neutral.hint a, +.rst-content .wy-alert-neutral.important a, +.rst-content .wy-alert-neutral.tip a, +.rst-content .wy-alert-neutral.warning a, +.rst-content .wy-alert-neutral.seealso a, +.rst-content .wy-alert-neutral.admonition-todo a { + color: #40008f +} +.wy-alert p:last-child, +.rst-content .note p:last-child, +.rst-content .attention p:last-child, +.rst-content .caution p:last-child, +.rst-content .danger p:last-child, +.rst-content .error p:last-child, +.rst-content .hint p:last-child, +.rst-content .important p:last-child, +.rst-content .tip p:last-child, +.rst-content .warning p:last-child, +.rst-content .seealso p:last-child, +.rst-content .admonition-todo p:last-child { + margin-bottom: 0 +} +.wy-tray-container { + position: fixed; + bottom: 0px; + left: 0; + z-index: 600 +} +.wy-tray-container li { + display: block; + width: 300px; + background: transparent; + color: #fff; + text-align: center; + box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.1); + padding: 0 24px; + min-width: 20%; + opacity: 0; + height: 0; + line-height: 56px; + overflow: hidden; + -webkit-transition: all 0.3s ease-in; + -moz-transition: all 0.3s ease-in; + transition: all 0.3s ease-in +} +.wy-tray-container li.wy-tray-item-success { + background: #27AE60 +} +.wy-tray-container li.wy-tray-item-info { + background: #40008f +} +.wy-tray-container li.wy-tray-item-warning { + background: #E67E22 +} +.wy-tray-container li.wy-tray-item-danger { + background: #E74C3C +} +.wy-tray-container li.on { + opacity: 1; + height: 56px +} +@media screen and (max-width: 768px) { + .wy-tray-container { + bottom: auto; + top: 0; + width: 100% + } + .wy-tray-container li { + width: 100% + } +} +button { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + cursor: pointer; + line-height: normal; + -webkit-appearance: button; + *overflow: visible +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} +button[disabled] { + cursor: default +} +.btn { + display: inline-block; + border-radius: 2px; + line-height: normal; + white-space: nowrap; + text-align: center; + cursor: pointer; + font-size: 100%; + padding: 6px 12px 8px 12px; + color: #fff; + border: 1px solid rgba(0, 0, 0, 0.1); + background-color: #27AE60; + text-decoration: none; + font-weight: normal; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + box-shadow: 0px 1px 2px -1px rgba(255, 255, 255, 0.5) inset, 0px -2px 0px 0px rgba(0, 0, 0, 0.1) inset; + outline-none: false; + vertical-align: middle; + *display: inline; + zoom: 1; + -webkit-user-drag: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-transition: all 0.1s linear; + -moz-transition: all 0.1s linear; + transition: all 0.1s linear +} +.btn-hover { + background: #2e8ece; + color: #fff +} +.btn:hover { + background: #2cc36b; + color: #fff +} +.btn:focus { + background: #2cc36b; + outline: 0 +} +.btn:active { + box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.05) inset, 0px 2px 0px 0px rgba(0, 0, 0, 0.1) inset; + padding: 8px 12px 6px 12px +} +.btn:visited { + color: #fff +} +.btn:disabled { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} +.btn-disabled { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} +.btn-disabled:hover, +.btn-disabled:focus, +.btn-disabled:active { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} +.btn::-moz-focus-inner { + padding: 0; + border: 0 +} +.btn-small { + font-size: 80% +} +.btn-info { + background-color: #40008f !important +} +.btn-info:hover { + background-color: #2e8ece !important +} +.btn-neutral { + background-color: #f3f6f6 !important; + color: #40008F !important +} +.btn-neutral:hover { + background-color: #e5ebeb !important; + color: #40008F +} +.btn-neutral:visited { + color: #40008F !important +} +.btn-success { + background-color: #27AE60 !important +} +.btn-success:hover { + background-color: #295 !important +} +.btn-danger { + background-color: #E74C3C !important +} +.btn-danger:hover { + background-color: #ea6153 !important +} +.btn-warning { + background-color: #E67E22 !important +} +.btn-warning:hover { + background-color: #e98b39 !important +} +.btn-invert { + background-color: #222 +} +.btn-invert:hover { + background-color: #2f2f2f !important +} +.btn-link { + background-color: transparent !important; + color: #40008f; + box-shadow: none; + border-color: transparent !important +} +.btn-link:hover { + background-color: transparent !important; + color: #409ad5 !important; + box-shadow: none +} +.btn-link:active { + background-color: transparent !important; + color: #409ad5 !important; + box-shadow: none +} +.btn-link:visited { + color: #9B59B6 +} +.wy-btn-group .btn, +.wy-control .btn { + vertical-align: middle +} +.wy-btn-group { + margin-bottom: 24px; + *zoom: 1 +} +.wy-btn-group:before, +.wy-btn-group:after { + display: table; + content: "" +} +.wy-btn-group:after { + clear: both +} +.wy-dropdown { + position: relative; + display: inline-block +} +.wy-dropdown-active .wy-dropdown-menu { + display: block +} +.wy-dropdown-menu { + position: absolute; + left: 0; + display: none; + float: left; + top: 100%; + min-width: 100%; + background: white; + z-index: 100; + border: solid 1px #cfd7dd; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); + padding: 12px +} +.wy-dropdown-menu>dd>a { + display: block; + clear: both; + color: #40008F; + white-space: nowrap; + font-size: 90%; + padding: 0 12px; + cursor: pointer +} +.wy-dropdown-menu>dd>a:hover { + background: #40008f; + color: #fff +} +.wy-dropdown-menu>dd.divider { + border-top: solid 1px #cfd7dd; + margin: 6px 0 +} +.wy-dropdown-menu>dd.search { + padding-bottom: 12px +} +.wy-dropdown-menu>dd.search input[type="search"] { + width: 100% +} +.wy-dropdown-menu>dd.call-to-action { + background: #e3e3e3; + text-transform: uppercase; + font-weight: 500; + font-size: 80% +} +.wy-dropdown-menu>dd.call-to-action:hover { + background: #e3e3e3 +} +.wy-dropdown-menu>dd.call-to-action .btn { + color: #fff +} +.wy-dropdown.wy-dropdown-up .wy-dropdown-menu { + bottom: 100%; + top: auto; + left: auto; + right: 0 +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { + background: white; + margin-top: 2px +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a { + padding: 6px 12px +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { + background: white; + color: #fff +} +.wy-dropdown.wy-dropdown-left .wy-dropdown-menu { + right: 0; + left: auto; + text-align: right +} +.wy-dropdown-arrow:before { + content: " "; + border-bottom: 5px solid #f5f5f5; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + position: absolute; + display: block; + top: -4px; + left: 50%; + margin-left: -3px +} +.wy-dropdown-arrow.wy-dropdown-arrow-left:before { + left: 11px +} +.wy-form-stacked select { + display: block +} +.wy-form-aligned input, +.wy-form-aligned textarea, +.wy-form-aligned select, +.wy-form-aligned .wy-help-inline, +.wy-form-aligned label { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} +.wy-form-aligned .wy-control-group>label { + display: inline-block; + vertical-align: middle; + width: 10em; + margin: 6px 12px 0 0; + float: left +} +.wy-form-aligned .wy-control { + float: left +} +.wy-form-aligned .wy-control label { + display: block +} +.wy-form-aligned .wy-control select { + margin-top: 6px +} +fieldset { + border: 0; + margin: 0; + padding: 0 +} +legend { + display: block; + width: 100%; + border: 0; + padding: 0; + white-space: normal; + margin-bottom: 24px; + font-size: 150%; + *margin-left: -7px +} +label { + display: block; + margin: 0 0 0.3125em 0; + color: #333; + font-size: 90% +} +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} +.wy-control-group { + margin-bottom: 24px; + *zoom: 1; + max-width: 68em; + margin-left: auto; + margin-right: auto; + *zoom: 1 +} +.wy-control-group:before, +.wy-control-group:after { + display: table; + content: "" +} +.wy-control-group:after { + clear: both +} +.wy-control-group:before, +.wy-control-group:after { + display: table; + content: "" +} +.wy-control-group:after { + clear: both +} +.wy-control-group.wy-control-group-required>label:after { + content: " *"; + color: #E74C3C +} +.wy-control-group .wy-form-full, +.wy-control-group .wy-form-halves, +.wy-control-group .wy-form-thirds { + padding-bottom: 12px +} +.wy-control-group .wy-form-full select, +.wy-control-group .wy-form-halves select, +.wy-control-group .wy-form-thirds select { + width: 100% +} +.wy-control-group .wy-form-full input[type="text"], +.wy-control-group .wy-form-full input[type="password"], +.wy-control-group .wy-form-full input[type="email"], +.wy-control-group .wy-form-full input[type="url"], +.wy-control-group .wy-form-full input[type="date"], +.wy-control-group .wy-form-full input[type="month"], +.wy-control-group .wy-form-full input[type="time"], +.wy-control-group .wy-form-full input[type="datetime"], +.wy-control-group .wy-form-full input[type="datetime-local"], +.wy-control-group .wy-form-full input[type="week"], +.wy-control-group .wy-form-full input[type="number"], +.wy-control-group .wy-form-full input[type="search"], +.wy-control-group .wy-form-full input[type="tel"], +.wy-control-group .wy-form-full input[type="color"], +.wy-control-group .wy-form-halves input[type="text"], +.wy-control-group .wy-form-halves input[type="password"], +.wy-control-group .wy-form-halves input[type="email"], +.wy-control-group .wy-form-halves input[type="url"], +.wy-control-group .wy-form-halves input[type="date"], +.wy-control-group .wy-form-halves input[type="month"], +.wy-control-group .wy-form-halves input[type="time"], +.wy-control-group .wy-form-halves input[type="datetime"], +.wy-control-group .wy-form-halves input[type="datetime-local"], +.wy-control-group .wy-form-halves input[type="week"], +.wy-control-group .wy-form-halves input[type="number"], +.wy-control-group .wy-form-halves input[type="search"], +.wy-control-group .wy-form-halves input[type="tel"], +.wy-control-group .wy-form-halves input[type="color"], +.wy-control-group .wy-form-thirds input[type="text"], +.wy-control-group .wy-form-thirds input[type="password"], +.wy-control-group .wy-form-thirds input[type="email"], +.wy-control-group .wy-form-thirds input[type="url"], +.wy-control-group .wy-form-thirds input[type="date"], +.wy-control-group .wy-form-thirds input[type="month"], +.wy-control-group .wy-form-thirds input[type="time"], +.wy-control-group .wy-form-thirds input[type="datetime"], +.wy-control-group .wy-form-thirds input[type="datetime-local"], +.wy-control-group .wy-form-thirds input[type="week"], +.wy-control-group .wy-form-thirds input[type="number"], +.wy-control-group .wy-form-thirds input[type="search"], +.wy-control-group .wy-form-thirds input[type="tel"], +.wy-control-group .wy-form-thirds input[type="color"] { + width: 100% +} +.wy-control-group .wy-form-full { + float: left; + display: block; + margin-right: 2.35765%; + width: 100%; + margin-right: 0 +} +.wy-control-group .wy-form-full:last-child { + margin-right: 0 +} +.wy-control-group .wy-form-halves { + float: left; + display: block; + margin-right: 2.35765%; + width: 48.82117% +} +.wy-control-group .wy-form-halves:last-child { + margin-right: 0 +} +.wy-control-group .wy-form-halves:nth-of-type(2n) { + margin-right: 0 +} +.wy-control-group .wy-form-halves:nth-of-type(2n+1) { + clear: left +} +.wy-control-group .wy-form-thirds { + float: left; + display: block; + margin-right: 2.35765%; + width: 31.76157% +} +.wy-control-group .wy-form-thirds:last-child { + margin-right: 0 +} +.wy-control-group .wy-form-thirds:nth-of-type(3n) { + margin-right: 0 +} +.wy-control-group .wy-form-thirds:nth-of-type(3n+1) { + clear: left +} +.wy-control-group.wy-control-group-no-input .wy-control { + margin: 6px 0 0 0; + font-size: 90% +} +.wy-control-no-input { + display: inline-block; + margin: 6px 0 0 0; + font-size: 90% +} +.wy-control-group.fluid-input input[type="text"], +.wy-control-group.fluid-input input[type="password"], +.wy-control-group.fluid-input input[type="email"], +.wy-control-group.fluid-input input[type="url"], +.wy-control-group.fluid-input input[type="date"], +.wy-control-group.fluid-input input[type="month"], +.wy-control-group.fluid-input input[type="time"], +.wy-control-group.fluid-input input[type="datetime"], +.wy-control-group.fluid-input input[type="datetime-local"], +.wy-control-group.fluid-input input[type="week"], +.wy-control-group.fluid-input input[type="number"], +.wy-control-group.fluid-input input[type="search"], +.wy-control-group.fluid-input input[type="tel"], +.wy-control-group.fluid-input input[type="color"] { + width: 100% +} +.wy-form-message-inline { + display: inline-block; + padding-left: 0.3em; + color: #666; + vertical-align: middle; + font-size: 90% +} +.wy-form-message { + display: block; + color: #999; + font-size: 70%; + margin-top: 0.3125em; + font-style: italic +} +.wy-form-message p { + font-size: inherit; + font-style: italic; + margin-bottom: 6px +} +.wy-form-message p:last-child { + margin-bottom: 0 +} +input { + line-height: normal +} +input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + *overflow: visible +} +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="week"], +input[type="number"], +input[type="search"], +input[type="tel"], +input[type="color"] { + -webkit-appearance: none; + padding: 6px; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 0; + -webkit-transition: border 0.3s linear; + -moz-transition: border 0.3s linear; + transition: border 0.3s linear +} +input[type="datetime-local"] { + padding: 0.34375em 0.625em +} +input[disabled] { + cursor: default +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + margin-right: 0.3125em; + *height: 13px; + *width: 13px +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none +} +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus { + outline: 0; + outline: thin dotted \9; + border-color: #333 +} +input.no-focus:focus { + border-color: #ccc !important +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 1px auto #129FEA +} +input[type="text"][disabled], +input[type="password"][disabled], +input[type="email"][disabled], +input[type="url"][disabled], +input[type="date"][disabled], +input[type="month"][disabled], +input[type="time"][disabled], +input[type="datetime"][disabled], +input[type="datetime-local"][disabled], +input[type="week"][disabled], +input[type="number"][disabled], +input[type="search"][disabled], +input[type="tel"][disabled], +input[type="color"][disabled] { + cursor: not-allowed; + background-color: #fafafa +} +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #E74C3C; + border: 1px solid #E74C3C +} +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #E74C3C +} +input[type="file"]:focus:invalid:focus, +input[type="radio"]:focus:invalid:focus, +input[type="checkbox"]:focus:invalid:focus { + outline-color: #E74C3C +} +input.wy-input-large { + padding: 12px; + font-size: 100% +} +textarea { + overflow: auto; + vertical-align: top; + width: 100%; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif +} +select, +textarea { + padding: 0.5em 0.625em; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + box-shadow: inset 0 1px 3px #ddd; + -webkit-transition: border 0.3s linear; + -moz-transition: border 0.3s linear; + transition: border 0.3s linear +} +select { + border: 1px solid #ccc; + background-color: #fff +} +select[multiple] { + height: auto +} +select:focus, +textarea:focus { + outline: 0 +} +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #fafafa +} +input[type="radio"][disabled], +input[type="checkbox"][disabled] { + cursor: not-allowed +} +.wy-checkbox, +.wy-radio { + margin: 6px 0; + color: #40008F; + display: block +} +.wy-checkbox input, +.wy-radio input { + vertical-align: baseline +} +.wy-form-message-inline { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} +.wy-input-prefix, +.wy-input-suffix { + white-space: nowrap; + padding: 6px +} +.wy-input-prefix .wy-input-context, +.wy-input-suffix .wy-input-context { + line-height: 27px; + padding: 0 8px; + display: inline-block; + font-size: 80%; + background-color: #f3f6f6; + border: solid 1px #ccc; + color: #999 +} +.wy-input-suffix .wy-input-context { + border-left: 0 +} +.wy-input-prefix .wy-input-context { + border-right: 0 +} +.wy-switch { + width: 36px; + height: 12px; + margin: 12px 0; + position: relative; + border-radius: 4px; + background: #ccc; + cursor: pointer; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out +} +.wy-switch:before { + position: absolute; + content: ""; + display: block; + width: 18px; + height: 18px; + border-radius: 4px; + background: #999; + left: -3px; + top: -3px; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out +} +.wy-switch:after { + content: "false"; + position: absolute; + left: 48px; + display: block; + font-size: 12px; + color: #ccc +} +.wy-switch.active { + background: #1e8449 +} +.wy-switch.active:before { + left: 24px; + background: #27AE60 +} +.wy-switch.active:after { + content: "true" +} +.wy-switch.disabled, +.wy-switch.active.disabled { + cursor: not-allowed +} +.wy-control-group.wy-control-group-error .wy-form-message, +.wy-control-group.wy-control-group-error>label { + color: #E74C3C +} +.wy-control-group.wy-control-group-error input[type="text"], +.wy-control-group.wy-control-group-error input[type="password"], +.wy-control-group.wy-control-group-error input[type="email"], +.wy-control-group.wy-control-group-error input[type="url"], +.wy-control-group.wy-control-group-error input[type="date"], +.wy-control-group.wy-control-group-error input[type="month"], +.wy-control-group.wy-control-group-error input[type="time"], +.wy-control-group.wy-control-group-error input[type="datetime"], +.wy-control-group.wy-control-group-error input[type="datetime-local"], +.wy-control-group.wy-control-group-error input[type="week"], +.wy-control-group.wy-control-group-error input[type="number"], +.wy-control-group.wy-control-group-error input[type="search"], +.wy-control-group.wy-control-group-error input[type="tel"], +.wy-control-group.wy-control-group-error input[type="color"] { + border: solid 1px #E74C3C +} +.wy-control-group.wy-control-group-error textarea { + border: solid 1px #E74C3C +} +.wy-inline-validate { + white-space: nowrap +} +.wy-inline-validate .wy-input-context { + padding: 0.5em 0.625em; + display: inline-block; + font-size: 80% +} +.wy-inline-validate.wy-inline-validate-success .wy-input-context { + color: #27AE60 +} +.wy-inline-validate.wy-inline-validate-danger .wy-input-context { + color: #E74C3C +} +.wy-inline-validate.wy-inline-validate-warning .wy-input-context { + color: #E67E22 +} +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + color: #40008f +} +.rotate-90 { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg) +} +.rotate-180 { + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg) +} +.rotate-270 { + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg) +} +.mirror { + -webkit-transform: scaleX(-1); + -moz-transform: scaleX(-1); + -ms-transform: scaleX(-1); + -o-transform: scaleX(-1); + transform: scaleX(-1) +} +.mirror.rotate-90 { + -webkit-transform: scaleX(-1) rotate(90deg); + -moz-transform: scaleX(-1) rotate(90deg); + -ms-transform: scaleX(-1) rotate(90deg); + -o-transform: scaleX(-1) rotate(90deg); + transform: scaleX(-1) rotate(90deg) +} +.mirror.rotate-180 { + -webkit-transform: scaleX(-1) rotate(180deg); + -moz-transform: scaleX(-1) rotate(180deg); + -ms-transform: scaleX(-1) rotate(180deg); + -o-transform: scaleX(-1) rotate(180deg); + transform: scaleX(-1) rotate(180deg) +} +.mirror.rotate-270 { + -webkit-transform: scaleX(-1) rotate(270deg); + -moz-transform: scaleX(-1) rotate(270deg); + -ms-transform: scaleX(-1) rotate(270deg); + -o-transform: scaleX(-1) rotate(270deg); + transform: scaleX(-1) rotate(270deg) +} +@media only screen and (max-width: 480px) { + .wy-form button[type="submit"] { + margin: 0.7em 0 0 + } + .wy-form input[type="text"], + .wy-form input[type="password"], + .wy-form input[type="email"], + .wy-form input[type="url"], + .wy-form input[type="date"], + .wy-form input[type="month"], + .wy-form input[type="time"], + .wy-form input[type="datetime"], + .wy-form input[type="datetime-local"], + .wy-form input[type="week"], + .wy-form input[type="number"], + .wy-form input[type="search"], + .wy-form input[type="tel"], + .wy-form input[type="color"] { + margin-bottom: 0.3em; + display: block + } + .wy-form label { + margin-bottom: 0.3em; + display: block + } + .wy-form input[type="password"], + .wy-form input[type="email"], + .wy-form input[type="url"], + .wy-form input[type="date"], + .wy-form input[type="month"], + .wy-form input[type="time"], + .wy-form input[type="datetime"], + .wy-form input[type="datetime-local"], + .wy-form input[type="week"], + .wy-form input[type="number"], + .wy-form input[type="search"], + .wy-form input[type="tel"], + .wy-form input[type="color"] { + margin-bottom: 0 + } + .wy-form-aligned .wy-control-group label { + margin-bottom: 0.3em; + text-align: left; + display: block; + width: 100% + } + .wy-form-aligned .wy-control { + margin: 1.5em 0 0 0 + } + .wy-form .wy-help-inline, + .wy-form-message-inline, + .wy-form-message { + display: block; + font-size: 80%; + padding: 6px 0 + } +} +@media screen and (max-width: 768px) { + .tablet-hide { + display: none + } +} +@media screen and (max-width: 480px) { + .mobile-hide { + display: none + } +} +.float-left { + float: left +} +.float-right { + float: right +} +.full-width { + width: 100% +} +.wy-table, +.rst-content table.docutils, +.rst-content table.field-list { + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + margin-bottom: 24px +} +.wy-table caption, +.rst-content table.docutils caption, +.rst-content table.field-list caption { + color: #000; + font: italic 85%/1 arial, sans-serif; + padding: 1em 0; + text-align: center +} +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td, +.wy-table th, +.rst-content table.docutils th, +.rst-content table.field-list th { + font-size: 90%; + margin: 0; + overflow: visible; + padding: 8px 16px +} +.wy-table td:first-child, +.rst-content table.docutils td:first-child, +.rst-content table.field-list td:first-child, +.wy-table th:first-child, +.rst-content table.docutils th:first-child, +.rst-content table.field-list th:first-child { + border-left-width: 0 +} +.wy-table thead, +.rst-content table.docutils thead, +.rst-content table.field-list thead { + color: #000; + text-align: left; + vertical-align: bottom; + white-space: nowrap +} +.wy-table thead th, +.rst-content table.docutils thead th, +.rst-content table.field-list thead th { + font-weight: bold; + border-bottom: solid 2px #e1e4e5 +} +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td { + background-color: transparent; + vertical-align: middle +} +.wy-table td p, +.rst-content table.docutils td p, +.rst-content table.field-list td p { + line-height: 18px +} +.wy-table td p:last-child, +.rst-content table.docutils td p:last-child, +.rst-content table.field-list td p:last-child { + margin-bottom: 0 +} +.wy-table .wy-table-cell-min, +.rst-content table.docutils .wy-table-cell-min, +.rst-content table.field-list .wy-table-cell-min { + width: 1%; + padding-right: 0 +} +.wy-table .wy-table-cell-min input[type=checkbox], +.rst-content table.docutils .wy-table-cell-min input[type=checkbox], +.rst-content table.field-list .wy-table-cell-min input[type=checkbox], +.wy-table .wy-table-cell-min input[type=checkbox], +.rst-content table.docutils .wy-table-cell-min input[type=checkbox], +.rst-content table.field-list .wy-table-cell-min input[type=checkbox] { + margin: 0 +} +.wy-table-secondary { + color: gray; + font-size: 90% +} +.wy-table-tertiary { + color: gray; + font-size: 80% +} +.wy-table-odd td, +.wy-table-striped tr:nth-child(2n-1) td, +.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #f3f6f6 +} +.wy-table-backed { + background-color: #f3f6f6 +} +.wy-table-bordered-all, +.rst-content table.docutils { + border: 1px solid #e1e4e5 +} +.wy-table-bordered-all td, +.rst-content table.docutils td { + border-bottom: 1px solid #e1e4e5; + border-left: 1px solid #e1e4e5 +} +.wy-table-bordered-all tbody>tr:last-child td, +.rst-content table.docutils tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-bordered { + border: 1px solid #e1e4e5 +} +.wy-table-bordered-rows td { + border-bottom: 1px solid #e1e4e5 +} +.wy-table-bordered-rows tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-horizontal tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-horizontal td, +.wy-table-horizontal th { + border-width: 0 0 1px 0; + border-bottom: 1px solid #e1e4e5 +} +.wy-table-horizontal tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: auto +} +.wy-table-responsive table { + margin-bottom: 0 !important +} +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: nowrap +} +a { + color: #40008f; + text-decoration: none; + cursor: pointer +} +a:hover { + color: #7000dF +} +a:visited { + color: #7000dF +} +html { + height: 100%; + overflow-x: hidden +} +body { + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-weight: normal; + color: #303030; + min-height: 100%; + overflow-x: hidden; + background: white +} +.wy-text-left { + text-align: left +} +.wy-text-center { + text-align: center +} +.wy-text-right { + text-align: right +} +.wy-text-large { + font-size: 120% +} +.wy-text-normal { + font-size: 100% +} +.wy-text-small, +small { + font-size: 80% +} +.wy-text-strike { + text-decoration: line-through +} +.wy-text-warning { + color: #E67E22 !important +} +a.wy-text-warning:hover { + color: #eb9950 !important +} +.wy-text-info { + color: #40008f !important +} +a.wy-text-info:hover { + color: #409ad5 !important +} +.wy-text-success { + color: #27AE60 !important +} +a.wy-text-success:hover { + color: #36d278 !important +} +.wy-text-danger { + color: #E74C3C !important +} +a.wy-text-danger:hover { + color: #ed7669 !important +} +.wy-text-neutral { + color: #40008F !important +} +a.wy-text-neutral:hover { + color: #595959 !important +} +h1, +h2, +.rst-content .toctree-wrapper p.caption, +h3, +h4, +h5, +h6, +legend { + margin-top: 0; + font-weight: 700; + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif +} +p { + line-height: 24px; + margin: 0; + font-size: 16px; + margin-bottom: 24px +} +h1 { + font-size: 175% +} +h2, +.rst-content .toctree-wrapper p.caption { + font-size: 150% +} +h3 { + font-size: 125% +} +h4 { + font-size: 115% +} +h5 { + font-size: 110% +} +h6 { + font-size: 100% +} +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #e1e4e5; + margin: 24px 0; + padding: 0 +} +code, +.rst-content tt, +.rst-content code { + white-space: nowrap; + max-width: 100%; + background: #fff; + border: solid 1px #e1e4e5; + font-size: 75%; + padding: 0 5px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + color: #E74C3C; + overflow-x: auto +} +code.code-large, +.rst-content tt.code-large { + font-size: 90% +} +.wy-plain-list-disc, +.rst-content .section ul, +.rst-content .toctree-wrapper ul, +article ul { + list-style: disc; + line-height: 24px; + margin-bottom: 24px +} +.wy-plain-list-disc li, +.rst-content .section ul li, +.rst-content .toctree-wrapper ul li, +article ul li { + list-style: disc; + margin-left: 24px +} +.wy-plain-list-disc li p:last-child, +.rst-content .section ul li p:last-child, +.rst-content .toctree-wrapper ul li p:last-child, +article ul li p:last-child { + margin-bottom: 0 +} +.wy-plain-list-disc li ul, +.rst-content .section ul li ul, +.rst-content .toctree-wrapper ul li ul, +article ul li ul { + margin-bottom: 0 +} +.wy-plain-list-disc li li, +.rst-content .section ul li li, +.rst-content .toctree-wrapper ul li li, +article ul li li { + list-style: circle +} +.wy-plain-list-disc li li li, +.rst-content .section ul li li li, +.rst-content .toctree-wrapper ul li li li, +article ul li li li { + list-style: square +} +.wy-plain-list-disc li ol li, +.rst-content .section ul li ol li, +.rst-content .toctree-wrapper ul li ol li, +article ul li ol li { + list-style: decimal +} +.wy-plain-list-decimal, +.rst-content .section ol, +.rst-content ol.arabic, +article ol { + list-style: decimal; + line-height: 24px; + margin-bottom: 24px +} +.wy-plain-list-decimal li, +.rst-content .section ol li, +.rst-content ol.arabic li, +article ol li { + list-style: decimal; + margin-left: 24px +} +.wy-plain-list-decimal li p:last-child, +.rst-content .section ol li p:last-child, +.rst-content ol.arabic li p:last-child, +article ol li p:last-child { + margin-bottom: 0 +} +.wy-plain-list-decimal li ul, +.rst-content .section ol li ul, +.rst-content ol.arabic li ul, +article ol li ul { + margin-bottom: 0 +} +.wy-plain-list-decimal li ul li, +.rst-content .section ol li ul li, +.rst-content ol.arabic li ul li, +article ol li ul li { + list-style: disc +} +.codeblock-example { + border: 1px solid #e1e4e5; + border-bottom: none; + padding: 24px; + padding-top: 48px; + font-weight: 500; + background: #fff; + position: relative +} +.codeblock-example:after { + content: "Example"; + position: absolute; + top: 0px; + left: 0px; + background: #9B59B6; + color: #fff; + padding: 6px 12px +} +.codeblock-example.prettyprint-example-only { + border: 1px solid #e1e4e5; + margin-bottom: 24px +} +.codeblock, +pre.literal-block, +.rst-content .literal-block, +.rst-content pre.literal-block, +div[class^='highlight'] { + border: 1px solid #e1e4e5; + padding: 0px; + overflow-x: auto; + background: #fff; + margin: 1px 0 24px 0 +} +.codeblock div[class^='highlight'], +pre.literal-block div[class^='highlight'], +.rst-content .literal-block div[class^='highlight'], +div[class^='highlight'] div[class^='highlight'] { + border: none; + background: none; + margin: 0 +} +div[class^='highlight'] td.code { + width: 100% +} +.linenodiv pre { + border-right: solid 1px #e6e9ea; + margin: 0; + padding: 12px 12px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.5; + color: #d9d9d9 +} +div[class^='highlight'] pre { + white-space: pre; + margin: 0; + padding: 12px 12px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.5; + display: block; + overflow: auto; + color: #40008F +} +@media print { + .codeblock, + pre.literal-block, + .rst-content .literal-block, + .rst-content pre.literal-block, + div[class^='highlight'], + div[class^='highlight'] pre { + white-space: pre-wrap + } +} +.hll { + background-color: #ffc; + margin: 0 -12px; + padding: 0 12px; + display: block +} +.c { + color: #998; + font-style: italic +} +.err { + color: #a61717; + background-color: #e3d2d2 +} +.k { + font-weight: bold +} +.o { + font-weight: bold +} +.cm { + color: #998; + font-style: italic +} +.cp { + color: #999; + font-weight: bold +} +.c1 { + color: #998; + font-style: italic +} +.cs { + color: #999; + font-weight: bold; + font-style: italic +} +.gd { + color: #000; + background-color: #fdd +} +.gd .x { + color: #000; + background-color: #faa +} +.ge { + font-style: italic +} +.gr { + color: #a00 +} +.gh { + color: #999 +} +.gi { + color: #000; + background-color: #dfd +} +.gi .x { + color: #000; + background-color: #afa +} +.go { + color: #888 +} +.gp { + color: #555 +} +.gs { + font-weight: bold +} +.gu { + color: purple; + font-weight: bold +} +.gt { + color: #a00 +} +.kc { + font-weight: bold +} +.kd { + font-weight: bold +} +.kn { + font-weight: bold +} +.kp { + font-weight: bold +} +.kr { + font-weight: bold +} +.kt { + color: #458; + font-weight: bold +} +.m { + color: #099 +} +.s { + color: #d14 +} +.n { + color: #333 +} +.na { + color: teal +} +.nb { + color: #0086b3 +} +.nc { + color: #458; + font-weight: bold +} +.no { + color: teal +} +.ni { + color: purple +} +.ne { + color: #900; + font-weight: bold +} +.nf { + color: #900; + font-weight: bold +} +.nn { + color: #555 +} +.nt { + color: navy +} +.nv { + color: teal +} +.ow { + font-weight: bold +} +.w { + color: #bbb +} +.mf { + color: #099 +} +.mh { + color: #099 +} +.mi { + color: #099 +} +.mo { + color: #099 +} +.sb { + color: #d14 +} +.sc { + color: #d14 +} +.sd { + color: #d14 +} +.s2 { + color: #d14 +} +.se { + color: #d14 +} +.sh { + color: #d14 +} +.si { + color: #d14 +} +.sx { + color: #d14 +} +.sr { + color: #009926 +} +.s1 { + color: #d14 +} +.ss { + color: #990073 +} +.bp { + color: #999 +} +.vc { + color: teal +} +.vg { + color: teal +} +.vi { + color: teal +} +.il { + color: #099 +} +.gc { + color: #999; + background-color: #EAF2F5 +} +.wy-breadcrumbs li { + display: inline-block +} +.wy-breadcrumbs li.wy-breadcrumbs-aside { + float: right +} +.wy-breadcrumbs li a { + display: inline-block; + padding: 5px +} +.wy-breadcrumbs li a:first-child { + padding-left: 0 +} +.wy-breadcrumbs li code, +.wy-breadcrumbs li .rst-content tt, +.rst-content .wy-breadcrumbs li tt { + padding: 5px; + border: none; + background: none +} +.wy-breadcrumbs li code.literal, +.wy-breadcrumbs li .rst-content tt.literal, +.rst-content .wy-breadcrumbs li tt.literal { + color: #40008F +} +.wy-breadcrumbs-extra { + margin-bottom: 0; + color: #40008f; + font-size: 80%; + display: inline-block +} +@media screen and (max-width: 480px) { + .wy-breadcrumbs-extra { + display: none + } + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none + } +} +@media print { + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none + } +} +.wy-affix { + position: fixed; + top: 1.618em +} +.wy-menu a:hover { + text-decoration: none +} +.wy-menu-horiz { + *zoom: 1 +} +.wy-menu-horiz:before, +.wy-menu-horiz:after { + display: table; + content: "" +} +.wy-menu-horiz:after { + clear: both +} +.wy-menu-horiz ul, +.wy-menu-horiz li { + display: inline-block +} +.wy-menu-horiz li:hover { + background: rgba(255, 255, 255, 0.1) +} +.wy-menu-horiz li.divide-left { + border-left: solid 1px #40008F +} +.wy-menu-horiz li.divide-right { + border-right: solid 1px #40008F +} +.wy-menu-horiz a { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 16px +} +.wy-menu-vertical { + width: 300px +} +.wy-menu-vertical header, +.wy-menu-vertical p.caption { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 1.618em; + margin-bottom: 0; + display: block; + font-weight: bold; + text-transform: uppercase; + font-size: 80%; + color: #555; + white-space: nowrap +} +.wy-menu-vertical ul { + margin-bottom: 0 +} +.wy-menu-vertical li.divide-top { + border-top: solid 1px #40008F +} +.wy-menu-vertical li.divide-bottom { + border-bottom: solid 1px #40008F +} +.wy-menu-vertical li.current { + background: #e3e3e3 +} +.wy-menu-vertical li.current a { + color: gray; + border-right: solid 1px white; + padding: 0.4045em 2.427em +} +.wy-menu-vertical li.current a:hover { + background: white +} +.wy-menu-vertical li code, +.wy-menu-vertical li .rst-content tt, +.rst-content .wy-menu-vertical li tt { + border: none; + background: inherit; + color: inherit; + padding-left: 0; + padding-right: 0 +} +.wy-menu-vertical li span.toctree-expand { + display: block; + float: left; + margin-left: -1.2em; + font-size: 0.8em; + line-height: 1.6em; + color: #4d4d4d +} +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a { + color: #40008F; + padding: 0.4045em 1.618em; + font-weight: bold; + position: relative; + background: white; + border: none; + border-bottom: solid 1px white; + border-top: solid 1px white; + padding-left: 1.618em -4px +} +.wy-menu-vertical li.on a:hover, +.wy-menu-vertical li.current>a:hover { + background: white +} +.wy-menu-vertical li.on a:hover span.toctree-expand, +.wy-menu-vertical li.current>a:hover span.toctree-expand { + color: gray +} +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand { + display: block; + font-size: 0.8em; + line-height: 1.6em; + color: #333 +} +.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul { + display: none +} +.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul { + display: block +} +.wy-menu-vertical li.toctree-l2.current>a { + background: white; + padding: 0.4045em 2.427em +} +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a { + display: block; + background: white; + padding: 0.4045em 4.045em +} +.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { + color: gray +} +.wy-menu-vertical li.toctree-l2 span.toctree-expand { + color: #a3a3a3 +} +.wy-menu-vertical li.toctree-l3 { + font-size: 0.9em +} +.wy-menu-vertical li.toctree-l3.current>a { + background: #bdbdbd; + padding: 0.4045em 4.045em +} +.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a { + display: block; + background: #bdbdbd; + padding: 0.4045em 5.663em; + border-top: none; + border-bottom: none +} +.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { + color: gray +} +.wy-menu-vertical li.toctree-l3 span.toctree-expand { + color: #969696 +} +.wy-menu-vertical li.toctree-l4 { + font-size: 0.9em +} +.wy-menu-vertical li.current ul { + display: block +} +.wy-menu-vertical li ul { + margin-bottom: 0; + display: none +} +.wy-menu-vertical .local-toc li ul { + display: block +} +.wy-menu-vertical li ul li a { + margin-bottom: 0; + color: #40008f; + font-weight: normal +} +.wy-menu-vertical a { + display: inline-block; + line-height: 18px; + padding: 0.4045em 1.618em; + display: block; + position: relative; + font-size: 90%; + color: #40008f +} +.wy-menu-vertical a:hover { + background-color: #e3e3e3; + cursor: pointer +} +.wy-menu-vertical a:hover span.toctree-expand { + color: #40008f +} +.wy-menu-vertical a:active { + background-color: white; + cursor: pointer; + color: #fff +} +.wy-menu-vertical a:active span.toctree-expand { + color: #fff +} +.wy-side-nav-search { + display: block; + width: 300px; + padding: 0.809em; + margin-bottom: 0.809em; + z-index: 200; + background-color: white; + text-align: center; + padding: 0.809em; + display: block; + color: #40008f; + margin-bottom: 0.809em +} +.wy-side-nav-search input[type=text] { + width: 100%; + border-radius: 50px; + padding: 6px 12px; + border-color: #40008f +} +.wy-side-nav-search img { + display: block; + margin: auto auto 0.809em auto; + height: 45px; + width: 45px; + background-color: white; + padding: 5px; + border-radius: 100% +} +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a { + color: #40008f; + font-size: 100%; + font-weight: bold; + display: inline-block; + padding: 4px 6px; + margin-bottom: 0.809em +} +.wy-side-nav-search>a:hover, +.wy-side-nav-search .wy-dropdown>a:hover { + background: white/*rgba(255, 255, 255, 0.1)*/ +} +.wy-side-nav-search>a img.logo, +.wy-side-nav-search .wy-dropdown>a img.logo { + display: block; + margin: 0 auto; + height: auto; + width: 200px; + border-radius: 0; + background: transparent +} +.wy-side-nav-search>a.icon img.logo, +.wy-side-nav-search .wy-dropdown>a.icon img.logo { + margin-top: 0.85em +} +.wy-side-nav-search>div.version { + margin-top: -0.4045em; + margin-bottom: 0.809em; + font-weight: normal; + color: white /*rgba(255, 255, 255, 0.3)*/ +} +.wy-nav .wy-menu-vertical header { + color: #40008f +} +.wy-nav .wy-menu-vertical a { + color: #40008f +} +.wy-nav .wy-menu-vertical a:hover { + background-color: #40008f; + color: #fff +} +[data-menu-wrap] { + -webkit-transition: all 0.2s ease-in; + -moz-transition: all 0.2s ease-in; + transition: all 0.2s ease-in; + position: absolute; + opacity: 1; + width: 100%; + opacity: 0 +} +[data-menu-wrap].move-center { + left: 0; + right: auto; + opacity: 1 +} +[data-menu-wrap].move-left { + right: auto; + left: -100%; + opacity: 0 +} +[data-menu-wrap].move-right { + right: -100%; + left: auto; + opacity: 0 +} +.wy-body-for-nav { + background: left repeat-y white; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC); + background-size: 300px 1px +} +.wy-grid-for-nav { + position: absolute; + width: 100%; + height: 100% +} +.wy-nav-side { + position: fixed; + top: 0; + bottom: 0; + left: 0; + padding-bottom: 2em; + width: 300px; + overflow-x: hidden; + overflow-y: hidden; + min-height: 100%; + background: white; + z-index: 200 +} +.wy-side-scroll { + width: 320px; + position: relative; + overflow-x: hidden; + overflow-y: scroll; + height: 100% +} +.wy-nav-top { + display: none; + background: white; + color: #fff; + padding: 0.4045em 0.809em; + position: relative; + line-height: 50px; + text-align: center; + font-size: 100%; + *zoom: 1 +} +.wy-nav-top:before, +.wy-nav-top:after { + display: table; + content: "" +} +.wy-nav-top:after { + clear: both +} +.wy-nav-top a { + color: #fff; + font-weight: bold +} +.wy-nav-top img { + margin-right: 12px; + height: 45px; + width: 45px; + background-color:white; + padding: 5px; + border-radius: 100% +} +.wy-nav-top i { + font-size: 30px; + float: left; + cursor: pointer +} +.wy-nav-content-wrap { + margin-left: 300px; + background: white; + min-height: 100% +} +.wy-nav-content { + padding: 1.618em 3.236em; + height: 100%; + max-width: 800px; + margin: auto +} +.wy-body-mask { + position: fixed; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.2); + display: none; + z-index: 499 +} +.wy-body-mask.on { + display: block +} +footer { + color: #999 +} +footer p { + margin-bottom: 12px +} +footer span.commit code, +footer span.commit .rst-content tt, +.rst-content footer span.commit tt { + padding: 0px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 1em; + background: none; + border: none; + color: #999 +} +.rst-footer-buttons { + *zoom: 1 +} +.rst-footer-buttons:before, +.rst-footer-buttons:after { + display: table; + content: "" +} +.rst-footer-buttons:after { + clear: both +} +#search-results .search li { + margin-bottom: 24px; + border-bottom: solid 1px #e1e4e5; + padding-bottom: 24px +} +#search-results .search li:first-child { + border-top: solid 1px #e1e4e5; + padding-top: 24px +} +#search-results .search li a { + font-size: 120%; + margin-bottom: 12px; + display: inline-block +} +#search-results .context { + color: gray; + font-size: 90% +} +@media screen and (max-width: 768px) { + .wy-body-for-nav { + background: white + } + .wy-nav-top { + display: block + } + .wy-nav-side { + left: -300px + } + .wy-nav-side.shift { + width: 85%; + left: 0 + } + .wy-side-scroll { + width: auto + } + .wy-side-nav-search { + width: auto + } + .wy-menu.wy-menu-vertical { + width: auto + } + .wy-nav-content-wrap { + margin-left: 0 + } + .wy-nav-content-wrap .wy-nav-content { + padding: 1.618em + } + .wy-nav-content-wrap.shift { + position: fixed; + min-width: 100%; + left: 85%; + top: 0; + height: 100%; + overflow: hidden + } +} +@media screen and (min-width: 1400px) { + .wy-nav-content-wrap { + background: white/*rgba(0, 0, 0, 0.05)*/ + } + .wy-nav-content { + margin: 0; + background: white + } +} +@media print { + .rst-versions, + footer, + .wy-nav-side { + display: none + } + .wy-nav-content-wrap { + margin-left: 0 + } +} +.rst-versions { + position: fixed; + bottom: 0; + left: 0; + width: 300px; + color: white; + background: #1f1d1d; + border-top: solid 10px white; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + z-index: 400 +} +.rst-versions a { + color: #40008f; + text-decoration: none +} +.rst-versions .rst-badge-small { + display: none +} +.rst-versions .rst-current-version { + padding: 12px; + background-color: #272525; + display: block; + text-align: right; + font-size: 90%; + cursor: pointer; + color: #27AE60; + *zoom: 1 +} +.rst-versions .rst-current-version:before, +.rst-versions .rst-current-version:after { + display: table; + content: "" +} +.rst-versions .rst-current-version:after { + clear: both +} +.rst-versions .rst-current-version .fa, +.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand, +.rst-versions .rst-current-version .rst-content .admonition-title, +.rst-content .rst-versions .rst-current-version .admonition-title, +.rst-versions .rst-current-version .rst-content h1 .headerlink, +.rst-content h1 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h2 .headerlink, +.rst-content h2 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h3 .headerlink, +.rst-content h3 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h4 .headerlink, +.rst-content h4 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h5 .headerlink, +.rst-content h5 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h6 .headerlink, +.rst-content h6 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content dl dt .headerlink, +.rst-content dl dt .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content p.caption .headerlink, +.rst-content p.caption .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content tt.download span:first-child, +.rst-content tt.download .rst-versions .rst-current-version span:first-child, +.rst-versions .rst-current-version .rst-content code.download span:first-child, +.rst-content code.download .rst-versions .rst-current-version span:first-child, +.rst-versions .rst-current-version .icon { + color: white +} +.rst-versions .rst-current-version .fa-book, +.rst-versions .rst-current-version .icon-book { + float: left +} +.rst-versions .rst-current-version .icon-book { + float: left +} +.rst-versions .rst-current-version.rst-out-of-date { + background-color: #E74C3C; + color: #fff +} +.rst-versions .rst-current-version.rst-active-old-version { + background-color: #F1C40F; + color: #000 +} +.rst-versions.shift-up .rst-other-versions { + display: block +} +.rst-versions .rst-other-versions { + font-size: 90%; + padding: 12px; + color: gray; + display: none +} +.rst-versions .rst-other-versions hr { + display: block; + height: 1px; + border: 0; + margin: 20px 0; + padding: 0; + border-top: solid 1px #413d3d +} +.rst-versions .rst-other-versions dd { + display: inline-block; + margin: 0 +} +.rst-versions .rst-other-versions dd a { + display: inline-block; + padding: 6px; + color: white +} +.rst-versions.rst-badge { + width: auto; + bottom: 20px; + right: 20px; + left: auto; + border: none; + max-width: 300px +} +.rst-versions.rst-badge .icon-book { + float: none +} +.rst-versions.rst-badge .fa-book, +.rst-versions.rst-badge .icon-book { + float: none +} +.rst-versions.rst-badge.shift-up .rst-current-version { + text-align: right +} +.rst-versions.rst-badge.shift-up .rst-current-version .fa-book, +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} +.rst-versions.rst-badge .rst-current-version { + width: auto; + height: 30px; + line-height: 30px; + padding: 0 6px; + display: block; + text-align: center +} +@media screen and (max-width: 768px) { + .rst-versions { + width: 85%; + display: none + } + .rst-versions.shift { + display: block + } + img { + width: 100%; + height: auto + } +} +.rst-content img { + max-width: 100%; + height: auto !important +} +.rst-content div.figure { + margin-bottom: 24px +} +.rst-content div.figure p.caption { + font-style: italic +} +.rst-content div.figure.align-center { + text-align: center +} +.rst-content .section>img, +.rst-content .section>a>img { + margin-bottom: 24px +} +.rst-content blockquote { + margin-left: 24px; + line-height: 24px; + margin-bottom: 24px +} +.rst-content .note .last, +.rst-content .attention .last, +.rst-content .caution .last, +.rst-content .danger .last, +.rst-content .error .last, +.rst-content .hint .last, +.rst-content .important .last, +.rst-content .tip .last, +.rst-content .warning .last, +.rst-content .seealso .last, +.rst-content .admonition-todo .last { + margin-bottom: 0 +} +.rst-content .admonition-title:before { + margin-right: 4px +} +.rst-content .admonition table { + border-color: rgba(0, 0, 0, 0.1) +} +.rst-content .admonition table td, +.rst-content .admonition table th { + background: transparent !important; + border-color: rgba(0, 0, 0, 0.1) !important +} +.rst-content .section ol.loweralpha, +.rst-content .section ol.loweralpha li { + list-style: lower-alpha +} +.rst-content .section ol.upperalpha, +.rst-content .section ol.upperalpha li { + list-style: upper-alpha +} +.rst-content .section ol p, +.rst-content .section ul p { + margin-bottom: 12px +} +.rst-content .line-block { + margin-left: 24px +} +.rst-content .topic-title { + font-weight: bold; + margin-bottom: 12px +} +.rst-content .toc-backref { + color: #40008F +} +.rst-content .align-right { + float: right; + margin: 0px 0px 24px 24px +} +.rst-content .align-left { + float: left; + margin: 0px 24px 24px 0px +} +.rst-content .align-center { + margin: auto; + display: block +} +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content .toctree-wrapper p.caption .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink { + display: none; + visibility: hidden; + font-size: 14px +} +.rst-content h1 .headerlink:after, +.rst-content h2 .headerlink:after, +.rst-content .toctree-wrapper p.caption .headerlink:after, +.rst-content h3 .headerlink:after, +.rst-content h4 .headerlink:after, +.rst-content h5 .headerlink:after, +.rst-content h6 .headerlink:after, +.rst-content dl dt .headerlink:after, +.rst-content p.caption .headerlink:after { + visibility: visible; + content: "ïƒ"; + font-family: FontAwesome; + display: inline-block +} +.rst-content h1:hover .headerlink, +.rst-content h2:hover .headerlink, +.rst-content .toctree-wrapper p.caption:hover .headerlink, +.rst-content h3:hover .headerlink, +.rst-content h4:hover .headerlink, +.rst-content h5:hover .headerlink, +.rst-content h6:hover .headerlink, +.rst-content dl dt:hover .headerlink, +.rst-content p.caption:hover .headerlink { + display: inline-block +} +.rst-content .sidebar { + float: right; + width: 40%; + display: block; + margin: 0 0 24px 24px; + padding: 24px; + background: #f3f6f6; + border: solid 1px #e1e4e5 +} +.rst-content .sidebar p, +.rst-content .sidebar ul, +.rst-content .sidebar dl { + font-size: 90% +} +.rst-content .sidebar .last { + margin-bottom: 0 +} +.rst-content .sidebar .sidebar-title { + display: block; + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif; + font-weight: bold; + background: #e1e4e5; + padding: 6px 12px; + margin: -24px; + margin-bottom: 24px; + font-size: 100% +} +.rst-content .highlighted { + background: #F1C40F; + display: inline-block; + font-weight: bold; + padding: 0 6px +} +.rst-content .footnote-reference, +.rst-content .citation-reference { + vertical-align: super; + font-size: 90% +} +.rst-content table.docutils.citation, +.rst-content table.docutils.footnote { + background: none; + border: none; + color: #999 +} +.rst-content table.docutils.citation td, +.rst-content table.docutils.citation tr, +.rst-content table.docutils.footnote td, +.rst-content table.docutils.footnote tr { + border: none; + background-color: transparent !important; + white-space: normal +} +.rst-content table.docutils.citation td.label, +.rst-content table.docutils.footnote td.label { + padding-left: 0; + padding-right: 0; + vertical-align: top +} +.rst-content table.docutils.citation tt, +.rst-content table.docutils.citation code, +.rst-content table.docutils.footnote tt, +.rst-content table.docutils.footnote code { + color: #555 +} +.rst-content table.field-list { + border: none +} +.rst-content table.field-list td { + border: none; + padding-top: 5px +} +.rst-content table.field-list td>strong { + display: inline-block; + margin-top: 3px +} +.rst-content table.field-list .field-name { + padding-right: 10px; + text-align: left; + white-space: nowrap +} +.rst-content table.field-list .field-body { + text-align: left; + padding-left: 0 +} +.rst-content tt, +.rst-content tt, +.rst-content code { + color: #000; + padding: 2px 5px +} +.rst-content tt big, +.rst-content tt em, +.rst-content tt big, +.rst-content code big, +.rst-content tt em, +.rst-content code em { + font-size: 100% !important; + line-height: normal +} +.rst-content tt.literal, +.rst-content tt.literal, +.rst-content code.literal { + color: #E74C3C +} +.rst-content tt.xref, +a .rst-content tt, +.rst-content tt.xref, +.rst-content code.xref, +a .rst-content tt, +a .rst-content code { + font-weight: bold; + color: #40008F +} +.rst-content a tt, +.rst-content a tt, +.rst-content a code { + color: #40008f +} +.rst-content dl { + margin-bottom: 24px +} +.rst-content dl dt { + font-weight: bold +} +.rst-content dl p, +.rst-content dl table, +.rst-content dl ul, +.rst-content dl ol { + margin-bottom: 12px !important +} +.rst-content dl dd { + margin: 0 0 12px 24px +} +.rst-content dl:not(.docutils) { + margin-bottom: 24px +} +.rst-content dl:not(.docutils) dt { + display: inline-block; + margin: 6px 0; + font-size: 90%; + line-height: normal; + background: #e7f2fa; + color: #40008f; + border-top: solid 3px #6ab0de; + padding: 6px; + position: relative +} +.rst-content dl:not(.docutils) dt:before { + color: #6ab0de +} +.rst-content dl:not(.docutils) dt .headerlink { + color: #40008F; + font-size: 100% !important +} +.rst-content dl:not(.docutils) dl dt { + margin-bottom: 6px; + border: none; + border-left: solid 3px #ccc; + background: #f0f0f0; + color: #555 +} +.rst-content dl:not(.docutils) dl dt .headerlink { + color: #40008F; + font-size: 100% !important +} +.rst-content dl:not(.docutils) dt:first-child { + margin-top: 0 +} +.rst-content dl:not(.docutils) tt, +.rst-content dl:not(.docutils) tt, +.rst-content dl:not(.docutils) code { + font-weight: bold +} +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) tt.descclassname, +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) code.descname, +.rst-content dl:not(.docutils) tt.descclassname, +.rst-content dl:not(.docutils) code.descclassname { + background-color: transparent; + border: none; + padding: 0; + font-size: 100% !important +} +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) code.descname { + font-weight: bold +} +.rst-content dl:not(.docutils) .optional { + display: inline-block; + padding: 0 4px; + color: #000; + font-weight: bold +} +.rst-content dl:not(.docutils) .property { + display: inline-block; + padding-right: 8px +} +.rst-content .viewcode-link, +.rst-content .viewcode-back { + display: inline-block; + color: #27AE60; + font-size: 80%; + padding-left: 24px +} +.rst-content .viewcode-back { + display: block; + float: right +} +.rst-content p.rubric { + margin-bottom: 12px; + font-weight: bold +} +.rst-content tt.download, +.rst-content code.download { + background: inherit; + padding: inherit; + font-family: inherit; + font-size: inherit; + color: inherit; + border: inherit; + white-space: inherit +} +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before { + margin-right: 4px +} +@media screen and (max-width: 480px) { + .rst-content .sidebar { + width: 100% + } +} +span[id*='MathJax-Span'] { + color: #40008F +} +.math { + text-align: center +} +@font-face { + font-family: "Inconsolata"; + font-style: normal; + font-weight: 400; + src: local("Inconsolata"), local("Inconsolata-Regular"), url(../fonts/Inconsolata-Regular.ttf) format("truetype") +} +@font-face { + font-family: "Inconsolata"; + font-style: normal; + font-weight: 700; + src: local("Inconsolata Bold"), local("Inconsolata-Bold"), url(../fonts/Inconsolata-Bold.ttf) format("truetype") +} +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 400; + src: local("Lato Regular"), local("Lato-Regular"), url(../fonts/Lato-Regular.ttf) format("truetype") +} +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 700; + src: local("Lato Bold"), local("Lato-Bold"), url(../fonts/Lato-Bold.ttf) format("truetype") +} +@font-face { + font-family: "Roboto Slab"; + font-style: normal; + font-weight: 400; + src: local("Roboto Slab Regular"), local("RobotoSlab-Regular"), url(../fonts/RobotoSlab-Regular.ttf) format("truetype") +} +@font-face { + font-family: "Roboto Slab"; + font-style: normal; + font-weight: 700; + src: local("Roboto Slab Bold"), local("RobotoSlab-Bold"), url(../fonts/RobotoSlab-Bold.ttf) format("truetype") +} +/*# sourceMappingURL=theme.css.map */ \ No newline at end of file diff --git a/docs/fr/exploitation/_toc.rst b/docs/fr/exploitation/_toc.rst deleted file mode 100644 index c68ae48d6fd3543cbb8879261cc66020edc39e10..0000000000000000000000000000000000000000 --- a/docs/fr/exploitation/_toc.rst +++ /dev/null @@ -1,11 +0,0 @@ -Dossier d'exploitation de VITAM UI -######################################### - -Cette section décrit le dossier d'exploitation de la solution logicielle :term:`VITAMUI`. - -.. toctree:: - :maxdepth: 4 - :glob: - - */*/* - diff --git a/docs/fr/exploitation/conf.py b/docs/fr/exploitation/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..5f59d1431e9e228ffc36153879c4b551c428d017 --- /dev/null +++ b/docs/fr/exploitation/conf.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +import sphinx_rtd_theme + +# -- Project information ----------------------------------------------------- + +project = u'Vitam-UI' +copyright = u'2021, Programme Vitam' +author = u'Programme Vitam' + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +#source_suffix = ['.rst'] +# source_suffix = '.rst' +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown' +} + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = u'fr' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} +html_theme_options = { + 'logo_only': True, + 'display_version': True, + 'prev_next_buttons_location': 'bottom', + 'style_external_links': True, + 'style_nav_header_background': 'white', + # Toc options + 'collapse_navigation': True, + 'sticky_navigation': False, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False +} +html_title = 'Vitam-UI documentation' +html_logo = 'images/Vitam_Logo-CMJN.png' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +#html_css_files = [ +# 'css/theme.css', +#] +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Vitam-UIdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Vitam-UI.tex', u'Vitam-UI Documentation', + u'Programme Vitam', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'vitam-ui', u'Vitam-UI Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Vitam-UI', u'Vitam-UI Documentation', + author, 'Vitam-UI', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- +extensions = [ +# 'recommonmark', + 'myst_parser' +] diff --git a/docs/fr/exploitation/images/Vitam_Logo-CMJN.png b/docs/fr/exploitation/images/Vitam_Logo-CMJN.png new file mode 100644 index 0000000000000000000000000000000000000000..a96a5b73bba27003ceb54f71e90cc75a464ee772 Binary files /dev/null and b/docs/fr/exploitation/images/Vitam_Logo-CMJN.png differ diff --git a/docs/fr/exploitation/index.md b/docs/fr/exploitation/index.md deleted file mode 100644 index 3d64c22c9b5cd2cc9ffddde936669dc4702fec64..0000000000000000000000000000000000000000 --- a/docs/fr/exploitation/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Index du Dossier d'Exploitation - -1. [Introduction](sections/introduction.md) -2. [Procédures d'exploitation](sections/procedure.md) -3. [Annexes](sections/annexes.md) diff --git a/docs/fr/exploitation/index.rst b/docs/fr/exploitation/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..eca730de64e3271dc38762609c3c4c392ec2b85a --- /dev/null +++ b/docs/fr/exploitation/index.rst @@ -0,0 +1,17 @@ +.. Vitam-UI documentation master file, created by + sphinx-quickstart on Thu Jan 6 10:10:08 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Documentation d'installation de Vitam-UI +======================================== + +.. toctree:: + :maxdepth: 2 + :numbered: + :caption: Contents: + + sections/introduction + sections/procedure + sections/annexes + diff --git a/docs/fr/exploitation/sections/procedure.md b/docs/fr/exploitation/sections/procedure.md index c579599c60acaeeec4bc3b87e4791b7ac536ebd3..b122fc91218a996fdd3cd14d1d369e26e758a15c 100644 --- a/docs/fr/exploitation/sections/procedure.md +++ b/docs/fr/exploitation/sections/procedure.md @@ -37,7 +37,7 @@ La base de données Mongodb utilisée par Vitam-UI peut être sauvegardée de 2 Au préalable il est nécessaire de valoriser correctement les variables suivantes contenues dans le fichier `environments/group_vars/all/mongodb_vars.yml`: -~~~yml +~~~yaml mongo_dump_folder: /backup/mongod/ mongo_backup_reinstall: - db: "iam" @@ -49,7 +49,7 @@ mongo_backup_reinstall: La structure `mongo_backup_reinstall` va permettre de sauvegarder la base entière si elle est renseignée de la sorte: -~~~yml +~~~yaml mongo_backup_reinstall: - db: "admin" collections: [] @@ -57,7 +57,7 @@ mongo_backup_reinstall: Et les collections uniquement lorsque mentionné comme suit: -~~~yml +~~~yaml mongo_backup_reinstall: - db: "iam" collections: ["customers","externalParameters","groups","owners","profiles","sequences","subrogations","tenants","users","providers"] @@ -65,7 +65,7 @@ mongo_backup_reinstall: Le résultat du backup pourra se retrouver dans -~~~yml +~~~yaml mongo_dump_folder: /backup/mongod/ ~~~ @@ -180,8 +180,8 @@ L'exploitant peut changer quelques contraintes de la complexité du mot de passe #### Exemple de changement de la taille du mot de passe dans la section `password:`, l'attribut `length` est par défaut à 12, le changement de la taille du mot de passe revient à changer cet attribut `length`, cette valeur sera automatiquement portée par l'attribut -`cas.authn.pm.policyPattern:` qui contient l'expression `${password.length}$`. -l'attribut `cas.authn.pm.policyPattern:` contient l'expression régulière de validation du mot de passe et qui se trouve dans le meme fichier de configuration. +`cas.authn.pm.core.policy-pattern:` qui contient l'expression `${password.length}$`. +l'attribut `cas.authn.pm.core.policy-pattern:` contient l'expression régulière de validation du mot de passe et qui se trouve dans le meme fichier de configuration. > Cette valeur ne devra pas etre inférieur à la valeur par défaut, avec le profil anssi. @@ -219,7 +219,7 @@ fichier `/vitamui/conf/iam-internal/application.yml`, dans la section `password` Les messages peuvent etre personnalisés par l'exploitant dans le bloc, par langue -~~~yml +~~~yaml constraints: defaults: fr: @@ -261,8 +261,8 @@ constraints: #### Exemple de changement de la taille du mot de passe pour le profil custom -dans la section `password:`, l'attribut `length` est par défaut à 12, le changement de la taille du mot de passe revient à changer cet attribut `length`, cette valeur sera automatiquement portée par l'attribut `cas.authn.pm.policyPattern:` qui contient l'expression `${password.length}$` , à la fin de l'expression régulière. -L'attribut `cas.authn.pm.policyPattern:` contient l'expression régulière globale de validation du mot de passe et qui se trouve dans le meme fichier de configuration. +dans la section `password:`, l'attribut `length` est par défaut à 12, le changement de la taille du mot de passe revient à changer cet attribut `length`, cette valeur sera automatiquement portée par l'attribut `cas.authn.pm.core.policy-pattern:` qui contient l'expression `${password.length}$` , à la fin de l'expression régulière. +L'attribut `cas.authn.pm.core.policy-pattern:` contient l'expression régulière globale de validation du mot de passe et qui se trouve dans le meme fichier de configuration. > Cette valeur est par défaut égal à 8 pour le profil custom. @@ -280,7 +280,7 @@ La création d'oranisations est un worflow nécessitant l'execution de plusieurs * Dans ui-identity-admin -~~~yml +~~~yaml ui-identity: iam-external-client: connect-time-out: 30 @@ -290,7 +290,7 @@ ui-identity: * Dans iam-external -~~~yml +~~~yaml iam-external: iam-internal-client: connect-time-out: 30 diff --git a/docs/fr/installation/Makefile b/docs/fr/installation/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..298ea9e213e8c4c11f0431077510d4e325733c65 --- /dev/null +++ b/docs/fr/installation/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/fr/installation/_static/css/theme.css b/docs/fr/installation/_static/css/theme.css new file mode 100644 index 0000000000000000000000000000000000000000..78bf83ded4be2beb82c7eafb61a352d8464e9f42 --- /dev/null +++ b/docs/fr/installation/_static/css/theme.css @@ -0,0 +1,5369 @@ +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block +} +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1 +} +audio:not([controls]) { + display: none +} +[hidden] { + display: none +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100% +} +body { + margin: 0 +} +a:hover, +a:active { + outline: 0 +} +abbr[title] { + border-bottom: 1px dotted +} +b, +strong { + font-weight: bold +} +blockquote { + margin: 0 +} +dfn { + font-style: italic +} +ins { + background: #ff9; + color: #000; + text-decoration: none +} +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold +} +pre, +code, +.rst-content tt, +.rst-content code, +kbd, +samp { + font-family: monospace, serif; + _font-family: "courier new", monospace; + font-size: 1em +} +pre { + white-space: pre +} +q { + quotes: none +} +q:before, +q:after { + content: ""; + content: none +} +small { + font-size: 85% +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} +sup { + top: -0.5em +} +sub { + bottom: -0.25em +} +ul, +ol, +dl { + margin: 0; + padding: 0; + list-style: none; + list-style-image: none +} +li { + list-style: none +} +dd { + margin: 0 +} +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; + max-width: 100% +} +svg:not(:root) { + overflow: hidden +} +figure { + margin: 0 +} +form { + margin: 0 +} +fieldset { + border: 0; + margin: 0; + padding: 0 +} +label { + cursor: pointer +} +legend { + border: 0; + *margin-left: -7px; + padding: 0; + white-space: normal +} +button, +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} +button, +input { + line-height: normal +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + *overflow: visible +} +button[disabled], +input[disabled] { + cursor: default +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + padding: 0; + *width: 13px; + *height: 13px +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box +} +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} +textarea { + overflow: auto; + vertical-align: top; + resize: vertical +} +table { + border-collapse: collapse; + border-spacing: 0 +} +td { + vertical-align: top +} +.chromeframe { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0 +} +.ir { + display: block; + border: 0; + text-indent: -999em; + overflow: hidden; + background-color: transparent; + background-repeat: no-repeat; + text-align: left; + direction: ltr; + *line-height: 0 +} +.ir br { + display: none +} +.hidden { + display: none !important; + visibility: hidden +} +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px +} +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto +} +.invisible { + visibility: hidden +} +.relative { + position: relative +} +big, +small { + font-size: 100% +} +@media print { + html, + body, + section { + background: none !important + } + * { + box-shadow: none !important; + text-shadow: none !important; + filter: none !important; + -ms-filter: none !important + } + a, + a:visited { + text-decoration: underline + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: "" + } + pre, + blockquote { + page-break-inside: avoid + } + thead { + display: table-header-group + } + tr, + img { + page-break-inside: avoid + } + img { + max-width: 100% !important + } + @page { + margin: 0.5cm + } + p, + h2, + .rst-content .toctree-wrapper p.caption, + h3 { + orphans: 3; + widows: 3 + } + h2, + .rst-content .toctree-wrapper p.caption, + h3 { + page-break-after: avoid + } +} +.fa:before, +.wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before, +.rst-content .admonition-title:before, +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before, +.rst-content dl dt .headerlink:before, +.rst-content p.caption .headerlink:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before, +.icon:before, +.wy-dropdown .caret:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before, +.wy-alert, +.rst-content .note, +.rst-content .attention, +.rst-content .caution, +.rst-content .danger, +.rst-content .error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .warning, +.rst-content .seealso, +.rst-content .admonition-todo, +.btn, +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="week"], +input[type="number"], +input[type="search"], +input[type="tel"], +input[type="color"], +select, +textarea, +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a, +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a, +.wy-nav-top a { + -webkit-font-smoothing: antialiased +} +.clearfix { + *zoom: 1 +} +.clearfix:before, +.clearfix:after { + display: table; + content: "" +} +.clearfix:after { + clear: both +} +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@font-face { + font-family: 'FontAwesome'; + src: url("../fonts/fontawesome-webfont.eot"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff") format("woff"), url("../fonts/fontawesome-webfont.ttf") format("truetype"), url("../fonts/fontawesome-webfont.svg") format("svg"); + font-weight: normal; + font-style: normal +} +.fa, +.wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +.rst-content .admonition-title, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink, +.rst-content tt.download span:first-child, +.rst-content code.download span:first-child, +.icon { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale +} +.fa-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -15% +} +.fa-2x { + font-size: 2em +} +.fa-3x { + font-size: 3em +} +.fa-4x { + font-size: 4em +} +.fa-5x { + font-size: 5em +} +.fa-fw { + width: 1.28571em; + text-align: center +} +.fa-ul { + padding-left: 0; + margin-left: 2.14286em; + list-style-type: none +} +.fa-ul>li { + position: relative +} +.fa-li { + position: absolute; + left: -2.14286em; + width: 2.14286em; + top: 0.14286em; + text-align: center +} +.fa-li.fa-lg { + left: -1.85714em +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eee; + border-radius: .1em +} +.pull-right { + float: right +} +.pull-left { + float: left +} +.fa.pull-left, +.wy-menu-vertical li span.pull-left.toctree-expand, +.wy-menu-vertical li.on a span.pull-left.toctree-expand, +.wy-menu-vertical li.current>a span.pull-left.toctree-expand, +.rst-content .pull-left.admonition-title, +.rst-content h1 .pull-left.headerlink, +.rst-content h2 .pull-left.headerlink, +.rst-content h3 .pull-left.headerlink, +.rst-content h4 .pull-left.headerlink, +.rst-content h5 .pull-left.headerlink, +.rst-content h6 .pull-left.headerlink, +.rst-content dl dt .pull-left.headerlink, +.rst-content p.caption .pull-left.headerlink, +.rst-content tt.download span.pull-left:first-child, +.rst-content code.download span.pull-left:first-child, +.pull-left.icon { + margin-right: .3em +} +.fa.pull-right, +.wy-menu-vertical li span.pull-right.toctree-expand, +.wy-menu-vertical li.on a span.pull-right.toctree-expand, +.wy-menu-vertical li.current>a span.pull-right.toctree-expand, +.rst-content .pull-right.admonition-title, +.rst-content h1 .pull-right.headerlink, +.rst-content h2 .pull-right.headerlink, +.rst-content h3 .pull-right.headerlink, +.rst-content h4 .pull-right.headerlink, +.rst-content h5 .pull-right.headerlink, +.rst-content h6 .pull-right.headerlink, +.rst-content dl dt .pull-right.headerlink, +.rst-content p.caption .pull-right.headerlink, +.rst-content tt.download span.pull-right:first-child, +.rst-content code.download span.pull-right:first-child, +.pull-right.icon { + margin-left: .3em +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg) + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg) + } +} +.fa-rotate-90 { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg) +} +.fa-rotate-180 { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg) +} +.fa-rotate-270 { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg) +} +.fa-flip-horizontal { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=0); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1) +} +.fa-flip-vertical { + filter: progid: DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1) +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center +} +.fa-stack-1x { + line-height: inherit +} +.fa-stack-2x { + font-size: 2em +} +.fa-inverse { + color: #fff +} +.fa-glass:before { + content: "" +} +.fa-music:before { + content: "ï€" +} +.fa-search:before, +.icon-search:before { + content: "" +} +.fa-envelope-o:before { + content: "" +} +.fa-heart:before { + content: "" +} +.fa-star:before { + content: "" +} +.fa-star-o:before { + content: "" +} +.fa-user:before { + content: "" +} +.fa-film:before { + content: "" +} +.fa-th-large:before { + content: "" +} +.fa-th:before { + content: "" +} +.fa-th-list:before { + content: "" +} +.fa-check:before { + content: "" +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "ï€" +} +.fa-search-plus:before { + content: "" +} +.fa-search-minus:before { + content: "ï€" +} +.fa-power-off:before { + content: "" +} +.fa-signal:before { + content: "" +} +.fa-gear:before, +.fa-cog:before { + content: "" +} +.fa-trash-o:before { + content: "" +} +.fa-home:before, +.icon-home:before { + content: "" +} +.fa-file-o:before { + content: "" +} +.fa-clock-o:before { + content: "" +} +.fa-road:before { + content: "" +} +.fa-download:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before { + content: "" +} +.fa-arrow-circle-o-down:before { + content: "" +} +.fa-arrow-circle-o-up:before { + content: "" +} +.fa-inbox:before { + content: "" +} +.fa-play-circle-o:before { + content: "ï€" +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "" +} +.fa-refresh:before { + content: "" +} +.fa-list-alt:before { + content: "" +} +.fa-lock:before { + content: "" +} +.fa-flag:before { + content: "" +} +.fa-headphones:before { + content: "" +} +.fa-volume-off:before { + content: "" +} +.fa-volume-down:before { + content: "" +} +.fa-volume-up:before { + content: "" +} +.fa-qrcode:before { + content: "" +} +.fa-barcode:before { + content: "" +} +.fa-tag:before { + content: "" +} +.fa-tags:before { + content: "" +} +.fa-book:before, +.icon-book:before { + content: "ï€" +} +.fa-bookmark:before { + content: "" +} +.fa-print:before { + content: "" +} +.fa-camera:before { + content: "" +} +.fa-font:before { + content: "" +} +.fa-bold:before { + content: "" +} +.fa-italic:before { + content: "" +} +.fa-text-height:before { + content: "" +} +.fa-text-width:before { + content: "" +} +.fa-align-left:before { + content: "" +} +.fa-align-center:before { + content: "" +} +.fa-align-right:before { + content: "" +} +.fa-align-justify:before { + content: "" +} +.fa-list:before { + content: "" +} +.fa-dedent:before, +.fa-outdent:before { + content: "" +} +.fa-indent:before { + content: "" +} +.fa-video-camera:before { + content: "" +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "" +} +.fa-pencil:before { + content: "ï€" +} +.fa-map-marker:before { + content: "ï" +} +.fa-adjust:before { + content: "ï‚" +} +.fa-tint:before { + content: "ïƒ" +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "ï„" +} +.fa-share-square-o:before { + content: "ï…" +} +.fa-check-square-o:before { + content: "ï†" +} +.fa-arrows:before { + content: "ï‡" +} +.fa-step-backward:before { + content: "ïˆ" +} +.fa-fast-backward:before { + content: "ï‰" +} +.fa-backward:before { + content: "ïŠ" +} +.fa-play:before { + content: "ï‹" +} +.fa-pause:before { + content: "ïŒ" +} +.fa-stop:before { + content: "ï" +} +.fa-forward:before { + content: "ïŽ" +} +.fa-fast-forward:before { + content: "ï" +} +.fa-step-forward:before { + content: "ï‘" +} +.fa-eject:before { + content: "ï’" +} +.fa-chevron-left:before { + content: "ï“" +} +.fa-chevron-right:before { + content: "ï”" +} +.fa-plus-circle:before { + content: "ï•" +} +.fa-minus-circle:before { + content: "ï–" +} +.fa-times-circle:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before { + content: "ï—" +} +.fa-check-circle:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before { + content: "ï˜" +} +.fa-question-circle:before { + content: "ï™" +} +.fa-info-circle:before { + content: "ïš" +} +.fa-crosshairs:before { + content: "ï›" +} +.fa-times-circle-o:before { + content: "ïœ" +} +.fa-check-circle-o:before { + content: "ï" +} +.fa-ban:before { + content: "ïž" +} +.fa-arrow-left:before { + content: "ï " +} +.fa-arrow-right:before { + content: "ï¡" +} +.fa-arrow-up:before { + content: "ï¢" +} +.fa-arrow-down:before { + content: "ï£" +} +.fa-mail-forward:before, +.fa-share:before { + content: "ï¤" +} +.fa-expand:before { + content: "ï¥" +} +.fa-compress:before { + content: "ï¦" +} +.fa-plus:before { + content: "ï§" +} +.fa-minus:before { + content: "ï¨" +} +.fa-asterisk:before { + content: "ï©" +} +.fa-exclamation-circle:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before, +.rst-content .admonition-title:before { + content: "ïª" +} +.fa-gift:before { + content: "ï«" +} +.fa-leaf:before { + content: "ï¬" +} +.fa-fire:before, +.icon-fire:before { + content: "ï" +} +.fa-eye:before { + content: "ï®" +} +.fa-eye-slash:before { + content: "ï°" +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "ï±" +} +.fa-plane:before { + content: "ï²" +} +.fa-calendar:before { + content: "ï³" +} +.fa-random:before { + content: "ï´" +} +.fa-comment:before { + content: "ïµ" +} +.fa-magnet:before { + content: "ï¶" +} +.fa-chevron-up:before { + content: "ï·" +} +.fa-chevron-down:before { + content: "ï¸" +} +.fa-retweet:before { + content: "ï¹" +} +.fa-shopping-cart:before { + content: "ïº" +} +.fa-folder:before { + content: "ï»" +} +.fa-folder-open:before { + content: "ï¼" +} +.fa-arrows-v:before { + content: "ï½" +} +.fa-arrows-h:before { + content: "ï¾" +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "ï‚€" +} +.fa-twitter-square:before { + content: "ï‚" +} +.fa-facebook-square:before { + content: "ï‚‚" +} +.fa-camera-retro:before { + content: "" +} +.fa-key:before { + content: "ï‚„" +} +.fa-gears:before, +.fa-cogs:before { + content: "ï‚…" +} +.fa-comments:before { + content: "" +} +.fa-thumbs-o-up:before { + content: "" +} +.fa-thumbs-o-down:before { + content: "" +} +.fa-star-half:before { + content: "" +} +.fa-heart-o:before { + content: "" +} +.fa-sign-out:before { + content: "ï‚‹" +} +.fa-linkedin-square:before { + content: "" +} +.fa-thumb-tack:before { + content: "ï‚" +} +.fa-external-link:before { + content: "" +} +.fa-sign-in:before { + content: "ï‚" +} +.fa-trophy:before { + content: "ï‚‘" +} +.fa-github-square:before { + content: "ï‚’" +} +.fa-upload:before { + content: "ï‚“" +} +.fa-lemon-o:before { + content: "ï‚”" +} +.fa-phone:before { + content: "ï‚•" +} +.fa-square-o:before { + content: "ï‚–" +} +.fa-bookmark-o:before { + content: "ï‚—" +} +.fa-phone-square:before { + content: "" +} +.fa-twitter:before { + content: "ï‚™" +} +.fa-facebook:before { + content: "" +} +.fa-github:before, +.icon-github:before { + content: "ï‚›" +} +.fa-unlock:before { + content: "" +} +.fa-credit-card:before { + content: "ï‚" +} +.fa-rss:before { + content: "" +} +.fa-hdd-o:before { + content: "ï‚ " +} +.fa-bullhorn:before { + content: "ï‚¡" +} +.fa-bell:before { + content: "" +} +.fa-certificate:before { + content: "ï‚£" +} +.fa-hand-o-right:before { + content: "" +} +.fa-hand-o-left:before { + content: "ï‚¥" +} +.fa-hand-o-up:before { + content: "" +} +.fa-hand-o-down:before { + content: "ï‚§" +} +.fa-arrow-circle-left:before, +.icon-circle-arrow-left:before { + content: "" +} +.fa-arrow-circle-right:before, +.icon-circle-arrow-right:before { + content: "ï‚©" +} +.fa-arrow-circle-up:before { + content: "" +} +.fa-arrow-circle-down:before { + content: "ï‚«" +} +.fa-globe:before { + content: "" +} +.fa-wrench:before { + content: "ï‚" +} +.fa-tasks:before { + content: "ï‚®" +} +.fa-filter:before { + content: "ï‚°" +} +.fa-briefcase:before { + content: "" +} +.fa-arrows-alt:before { + content: "" +} +.fa-group:before, +.fa-users:before { + content: "" +} +.fa-chain:before, +.fa-link:before, +.icon-link:before { + content: "ïƒ" +} +.fa-cloud:before { + content: "" +} +.fa-flask:before { + content: "" +} +.fa-cut:before, +.fa-scissors:before { + content: "" +} +.fa-copy:before, +.fa-files-o:before { + content: "" +} +.fa-paperclip:before { + content: "" +} +.fa-save:before, +.fa-floppy-o:before { + content: "" +} +.fa-square:before { + content: "" +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "" +} +.fa-list-ul:before { + content: "" +} +.fa-list-ol:before { + content: "" +} +.fa-strikethrough:before { + content: "" +} +.fa-underline:before { + content: "ïƒ" +} +.fa-table:before { + content: "" +} +.fa-magic:before { + content: "ïƒ" +} +.fa-truck:before { + content: "" +} +.fa-pinterest:before { + content: "" +} +.fa-pinterest-square:before { + content: "" +} +.fa-google-plus-square:before { + content: "" +} +.fa-google-plus:before { + content: "" +} +.fa-money:before { + content: "" +} +.fa-caret-down:before, +.wy-dropdown .caret:before, +.icon-caret-down:before { + content: "" +} +.fa-caret-up:before { + content: "" +} +.fa-caret-left:before { + content: "" +} +.fa-caret-right:before { + content: "" +} +.fa-columns:before { + content: "" +} +.fa-unsorted:before, +.fa-sort:before { + content: "" +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "ïƒ" +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "" +} +.fa-envelope:before { + content: "ïƒ " +} +.fa-linkedin:before { + content: "" +} +.fa-rotate-left:before, +.fa-undo:before { + content: "" +} +.fa-legal:before, +.fa-gavel:before { + content: "" +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "" +} +.fa-comment-o:before { + content: "" +} +.fa-comments-o:before { + content: "" +} +.fa-flash:before, +.fa-bolt:before { + content: "" +} +.fa-sitemap:before { + content: "" +} +.fa-umbrella:before { + content: "" +} +.fa-paste:before, +.fa-clipboard:before { + content: "" +} +.fa-lightbulb-o:before { + content: "" +} +.fa-exchange:before { + content: "" +} +.fa-cloud-download:before { + content: "ïƒ" +} +.fa-cloud-upload:before { + content: "" +} +.fa-user-md:before { + content: "" +} +.fa-stethoscope:before { + content: "" +} +.fa-suitcase:before { + content: "" +} +.fa-bell-o:before { + content: "ï‚¢" +} +.fa-coffee:before { + content: "" +} +.fa-cutlery:before { + content: "" +} +.fa-file-text-o:before { + content: "" +} +.fa-building-o:before { + content: "" +} +.fa-hospital-o:before { + content: "" +} +.fa-ambulance:before { + content: "" +} +.fa-medkit:before { + content: "" +} +.fa-fighter-jet:before { + content: "" +} +.fa-beer:before { + content: "" +} +.fa-h-square:before { + content: "" +} +.fa-plus-square:before { + content: "" +} +.fa-angle-double-left:before { + content: "ï„€" +} +.fa-angle-double-right:before { + content: "ï„" +} +.fa-angle-double-up:before { + content: "ï„‚" +} +.fa-angle-double-down:before { + content: "" +} +.fa-angle-left:before { + content: "ï„„" +} +.fa-angle-right:before { + content: "ï„…" +} +.fa-angle-up:before { + content: "" +} +.fa-angle-down:before { + content: "" +} +.fa-desktop:before { + content: "" +} +.fa-laptop:before { + content: "" +} +.fa-tablet:before { + content: "" +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "ï„‹" +} +.fa-circle-o:before { + content: "" +} +.fa-quote-left:before { + content: "ï„" +} +.fa-quote-right:before { + content: "" +} +.fa-spinner:before { + content: "ï„" +} +.fa-circle:before { + content: "ï„‘" +} +.fa-mail-reply:before, +.fa-reply:before { + content: "ï„’" +} +.fa-github-alt:before { + content: "ï„“" +} +.fa-folder-o:before { + content: "ï„”" +} +.fa-folder-open-o:before { + content: "ï„•" +} +.fa-smile-o:before { + content: "" +} +.fa-frown-o:before { + content: "ï„™" +} +.fa-meh-o:before { + content: "" +} +.fa-gamepad:before { + content: "ï„›" +} +.fa-keyboard-o:before { + content: "" +} +.fa-flag-o:before { + content: "ï„" +} +.fa-flag-checkered:before { + content: "" +} +.fa-terminal:before { + content: "ï„ " +} +.fa-code:before { + content: "ï„¡" +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "ï„¢" +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "ï„£" +} +.fa-location-arrow:before { + content: "" +} +.fa-crop:before { + content: "ï„¥" +} +.fa-code-fork:before { + content: "" +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "ï„§" +} +.fa-question:before { + content: "" +} +.fa-info:before { + content: "ï„©" +} +.fa-exclamation:before { + content: "" +} +.fa-superscript:before { + content: "ï„«" +} +.fa-subscript:before { + content: "" +} +.fa-eraser:before { + content: "ï„" +} +.fa-puzzle-piece:before { + content: "ï„®" +} +.fa-microphone:before { + content: "ï„°" +} +.fa-microphone-slash:before { + content: "" +} +.fa-shield:before { + content: "" +} +.fa-calendar-o:before { + content: "" +} +.fa-fire-extinguisher:before { + content: "ï„´" +} +.fa-rocket:before { + content: "" +} +.fa-maxcdn:before { + content: "ï„¶" +} +.fa-chevron-circle-left:before { + content: "ï„·" +} +.fa-chevron-circle-right:before { + content: "" +} +.fa-chevron-circle-up:before { + content: "" +} +.fa-chevron-circle-down:before { + content: "" +} +.fa-html5:before { + content: "ï„»" +} +.fa-css3:before { + content: "" +} +.fa-anchor:before { + content: "" +} +.fa-unlock-alt:before { + content: "" +} +.fa-bullseye:before { + content: "ï…€" +} +.fa-ellipsis-h:before { + content: "ï…" +} +.fa-ellipsis-v:before { + content: "ï…‚" +} +.fa-rss-square:before { + content: "ï…ƒ" +} +.fa-play-circle:before { + content: "ï…„" +} +.fa-ticket:before { + content: "ï……" +} +.fa-minus-square:before { + content: "ï…†" +} +.fa-minus-square-o:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before { + content: "ï…‡" +} +.fa-level-up:before { + content: "ï…ˆ" +} +.fa-level-down:before { + content: "ï…‰" +} +.fa-check-square:before { + content: "ï…Š" +} +.fa-pencil-square:before { + content: "ï…‹" +} +.fa-external-link-square:before { + content: "ï…Œ" +} +.fa-share-square:before { + content: "ï…" +} +.fa-compass:before { + content: "ï…Ž" +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "ï…" +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "ï…‘" +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "ï…’" +} +.fa-euro:before, +.fa-eur:before { + content: "ï…“" +} +.fa-gbp:before { + content: "ï…”" +} +.fa-dollar:before, +.fa-usd:before { + content: "ï…•" +} +.fa-rupee:before, +.fa-inr:before { + content: "ï…–" +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "ï…—" +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "ï…˜" +} +.fa-won:before, +.fa-krw:before { + content: "ï…™" +} +.fa-bitcoin:before, +.fa-btc:before { + content: "ï…š" +} +.fa-file:before { + content: "ï…›" +} +.fa-file-text:before { + content: "ï…œ" +} +.fa-sort-alpha-asc:before { + content: "ï…" +} +.fa-sort-alpha-desc:before { + content: "ï…ž" +} +.fa-sort-amount-asc:before { + content: "ï… " +} +.fa-sort-amount-desc:before { + content: "ï…¡" +} +.fa-sort-numeric-asc:before { + content: "ï…¢" +} +.fa-sort-numeric-desc:before { + content: "ï…£" +} +.fa-thumbs-up:before { + content: "ï…¤" +} +.fa-thumbs-down:before { + content: "ï…¥" +} +.fa-youtube-square:before { + content: "ï…¦" +} +.fa-youtube:before { + content: "ï…§" +} +.fa-xing:before { + content: "ï…¨" +} +.fa-xing-square:before { + content: "ï…©" +} +.fa-youtube-play:before { + content: "ï…ª" +} +.fa-dropbox:before { + content: "ï…«" +} +.fa-stack-overflow:before { + content: "ï…¬" +} +.fa-instagram:before { + content: "ï…" +} +.fa-flickr:before { + content: "ï…®" +} +.fa-adn:before { + content: "ï…°" +} +.fa-bitbucket:before, +.icon-bitbucket:before { + content: "ï…±" +} +.fa-bitbucket-square:before { + content: "ï…²" +} +.fa-tumblr:before { + content: "ï…³" +} +.fa-tumblr-square:before { + content: "ï…´" +} +.fa-long-arrow-down:before { + content: "ï…µ" +} +.fa-long-arrow-up:before { + content: "ï…¶" +} +.fa-long-arrow-left:before { + content: "ï…·" +} +.fa-long-arrow-right:before { + content: "ï…¸" +} +.fa-apple:before { + content: "ï…¹" +} +.fa-windows:before { + content: "ï…º" +} +.fa-android:before { + content: "ï…»" +} +.fa-linux:before { + content: "ï…¼" +} +.fa-dribbble:before { + content: "ï…½" +} +.fa-skype:before { + content: "ï…¾" +} +.fa-foursquare:before { + content: "" +} +.fa-trello:before { + content: "ï†" +} +.fa-female:before { + content: "" +} +.fa-male:before { + content: "" +} +.fa-gittip:before { + content: "" +} +.fa-sun-o:before { + content: "" +} +.fa-moon-o:before { + content: "" +} +.fa-archive:before { + content: "" +} +.fa-bug:before { + content: "" +} +.fa-vk:before { + content: "" +} +.fa-weibo:before { + content: "" +} +.fa-renren:before { + content: "" +} +.fa-pagelines:before { + content: "" +} +.fa-stack-exchange:before { + content: "ï†" +} +.fa-arrow-circle-o-right:before { + content: "" +} +.fa-arrow-circle-o-left:before { + content: "ï†" +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "" +} +.fa-dot-circle-o:before { + content: "" +} +.fa-wheelchair:before { + content: "" +} +.fa-vimeo-square:before { + content: "" +} +.fa-turkish-lira:before, +.fa-try:before { + content: "" +} +.fa-plus-square-o:before, +.wy-menu-vertical li span.toctree-expand:before { + content: "" +} +.fa-space-shuttle:before { + content: "" +} +.fa-slack:before { + content: "" +} +.fa-envelope-square:before { + content: "" +} +.fa-wordpress:before { + content: "" +} +.fa-openid:before { + content: "" +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "" +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "ï†" +} +.fa-yahoo:before { + content: "" +} +.fa-google:before { + content: "ï† " +} +.fa-reddit:before { + content: "" +} +.fa-reddit-square:before { + content: "" +} +.fa-stumbleupon-circle:before { + content: "" +} +.fa-stumbleupon:before { + content: "" +} +.fa-delicious:before { + content: "" +} +.fa-digg:before { + content: "" +} +.fa-pied-piper:before { + content: "" +} +.fa-pied-piper-alt:before { + content: "" +} +.fa-drupal:before { + content: "" +} +.fa-joomla:before { + content: "" +} +.fa-language:before { + content: "" +} +.fa-fax:before { + content: "" +} +.fa-building:before { + content: "ï†" +} +.fa-child:before { + content: "" +} +.fa-paw:before { + content: "" +} +.fa-spoon:before { + content: "" +} +.fa-cube:before { + content: "" +} +.fa-cubes:before { + content: "" +} +.fa-behance:before { + content: "" +} +.fa-behance-square:before { + content: "" +} +.fa-steam:before { + content: "" +} +.fa-steam-square:before { + content: "" +} +.fa-recycle:before { + content: "" +} +.fa-automobile:before, +.fa-car:before { + content: "" +} +.fa-cab:before, +.fa-taxi:before { + content: "" +} +.fa-tree:before { + content: "" +} +.fa-spotify:before { + content: "" +} +.fa-deviantart:before { + content: "" +} +.fa-soundcloud:before { + content: "" +} +.fa-database:before { + content: "" +} +.fa-file-pdf-o:before { + content: "ï‡" +} +.fa-file-word-o:before { + content: "" +} +.fa-file-excel-o:before { + content: "" +} +.fa-file-powerpoint-o:before { + content: "" +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "" +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "" +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "" +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "" +} +.fa-file-code-o:before { + content: "" +} +.fa-vine:before { + content: "" +} +.fa-codepen:before { + content: "" +} +.fa-jsfiddle:before { + content: "" +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "ï‡" +} +.fa-circle-o-notch:before { + content: "" +} +.fa-ra:before, +.fa-rebel:before { + content: "ï‡" +} +.fa-ge:before, +.fa-empire:before { + content: "" +} +.fa-git-square:before { + content: "" +} +.fa-git:before { + content: "" +} +.fa-hacker-news:before { + content: "" +} +.fa-tencent-weibo:before { + content: "" +} +.fa-qq:before { + content: "" +} +.fa-wechat:before, +.fa-weixin:before { + content: "" +} +.fa-send:before, +.fa-paper-plane:before { + content: "" +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "" +} +.fa-history:before { + content: "" +} +.fa-circle-thin:before { + content: "" +} +.fa-header:before { + content: "" +} +.fa-paragraph:before { + content: "ï‡" +} +.fa-sliders:before { + content: "" +} +.fa-share-alt:before { + content: "ï‡ " +} +.fa-share-alt-square:before { + content: "" +} +.fa-bomb:before { + content: "" +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "" +} +.fa-tty:before { + content: "" +} +.fa-binoculars:before { + content: "" +} +.fa-plug:before { + content: "" +} +.fa-slideshare:before { + content: "" +} +.fa-twitch:before { + content: "" +} +.fa-yelp:before { + content: "" +} +.fa-newspaper-o:before { + content: "" +} +.fa-wifi:before { + content: "" +} +.fa-calculator:before { + content: "" +} +.fa-paypal:before { + content: "ï‡" +} +.fa-google-wallet:before { + content: "" +} +.fa-cc-visa:before { + content: "" +} +.fa-cc-mastercard:before { + content: "" +} +.fa-cc-discover:before { + content: "" +} +.fa-cc-amex:before { + content: "" +} +.fa-cc-paypal:before { + content: "" +} +.fa-cc-stripe:before { + content: "" +} +.fa-bell-slash:before { + content: "" +} +.fa-bell-slash-o:before { + content: "" +} +.fa-trash:before { + content: "" +} +.fa-copyright:before { + content: "" +} +.fa-at:before { + content: "" +} +.fa-eyedropper:before { + content: "" +} +.fa-paint-brush:before { + content: "" +} +.fa-birthday-cake:before { + content: "" +} +.fa-area-chart:before { + content: "" +} +.fa-pie-chart:before { + content: "" +} +.fa-line-chart:before { + content: "ïˆ" +} +.fa-lastfm:before { + content: "" +} +.fa-lastfm-square:before { + content: "" +} +.fa-toggle-off:before { + content: "" +} +.fa-toggle-on:before { + content: "" +} +.fa-bicycle:before { + content: "" +} +.fa-bus:before { + content: "" +} +.fa-ioxhost:before { + content: "" +} +.fa-angellist:before { + content: "" +} +.fa-cc:before { + content: "" +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "" +} +.fa-meanpath:before { + content: "" +} +.fa, +.wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +.rst-content .admonition-title, +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink, +.rst-content tt.download span:first-child, +.rst-content code.download span:first-child, +.icon, +.wy-dropdown .caret, +.wy-inline-validate.wy-inline-validate-success .wy-input-context, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context, +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + font-family: inherit +} +.fa:before, +.wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li.on a span.toctree-expand:before, +.wy-menu-vertical li.current>a span.toctree-expand:before, +.rst-content .admonition-title:before, +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before, +.rst-content dl dt .headerlink:before, +.rst-content p.caption .headerlink:before, +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before, +.icon:before, +.wy-dropdown .caret:before, +.wy-inline-validate.wy-inline-validate-success .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before, +.wy-inline-validate.wy-inline-validate-info .wy-input-context:before { + font-family: "FontAwesome"; + display: inline-block; + font-style: normal; + font-weight: normal; + line-height: 1; + text-decoration: inherit +} +a .fa, +a .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li a span.toctree-expand, +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand, +a .rst-content .admonition-title, +.rst-content a .admonition-title, +a .rst-content h1 .headerlink, +.rst-content h1 a .headerlink, +a .rst-content h2 .headerlink, +.rst-content h2 a .headerlink, +a .rst-content h3 .headerlink, +.rst-content h3 a .headerlink, +a .rst-content h4 .headerlink, +.rst-content h4 a .headerlink, +a .rst-content h5 .headerlink, +.rst-content h5 a .headerlink, +a .rst-content h6 .headerlink, +.rst-content h6 a .headerlink, +a .rst-content dl dt .headerlink, +.rst-content dl dt a .headerlink, +a .rst-content p.caption .headerlink, +.rst-content p.caption a .headerlink, +a .rst-content tt.download span:first-child, +.rst-content tt.download a span:first-child, +a .rst-content code.download span:first-child, +.rst-content code.download a span:first-child, +a .icon { + display: inline-block; + text-decoration: inherit +} +.btn .fa, +.btn .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .btn span.toctree-expand, +.btn .wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.on a .btn span.toctree-expand, +.btn .wy-menu-vertical li.current>a span.toctree-expand, +.wy-menu-vertical li.current>a .btn span.toctree-expand, +.btn .rst-content .admonition-title, +.rst-content .btn .admonition-title, +.btn .rst-content h1 .headerlink, +.rst-content h1 .btn .headerlink, +.btn .rst-content h2 .headerlink, +.rst-content h2 .btn .headerlink, +.btn .rst-content h3 .headerlink, +.rst-content h3 .btn .headerlink, +.btn .rst-content h4 .headerlink, +.rst-content h4 .btn .headerlink, +.btn .rst-content h5 .headerlink, +.rst-content h5 .btn .headerlink, +.btn .rst-content h6 .headerlink, +.rst-content h6 .btn .headerlink, +.btn .rst-content dl dt .headerlink, +.rst-content dl dt .btn .headerlink, +.btn .rst-content p.caption .headerlink, +.rst-content p.caption .btn .headerlink, +.btn .rst-content tt.download span:first-child, +.rst-content tt.download .btn span:first-child, +.btn .rst-content code.download span:first-child, +.rst-content code.download .btn span:first-child, +.btn .icon, +.nav .fa, +.nav .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .nav span.toctree-expand, +.nav .wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.on a .nav span.toctree-expand, +.nav .wy-menu-vertical li.current>a span.toctree-expand, +.wy-menu-vertical li.current>a .nav span.toctree-expand, +.nav .rst-content .admonition-title, +.rst-content .nav .admonition-title, +.nav .rst-content h1 .headerlink, +.rst-content h1 .nav .headerlink, +.nav .rst-content h2 .headerlink, +.rst-content h2 .nav .headerlink, +.nav .rst-content h3 .headerlink, +.rst-content h3 .nav .headerlink, +.nav .rst-content h4 .headerlink, +.rst-content h4 .nav .headerlink, +.nav .rst-content h5 .headerlink, +.rst-content h5 .nav .headerlink, +.nav .rst-content h6 .headerlink, +.rst-content h6 .nav .headerlink, +.nav .rst-content dl dt .headerlink, +.rst-content dl dt .nav .headerlink, +.nav .rst-content p.caption .headerlink, +.rst-content p.caption .nav .headerlink, +.nav .rst-content tt.download span:first-child, +.rst-content tt.download .nav span:first-child, +.nav .rst-content code.download span:first-child, +.rst-content code.download .nav span:first-child, +.nav .icon { + display: inline +} +.btn .fa.fa-large, +.btn .wy-menu-vertical li span.fa-large.toctree-expand, +.wy-menu-vertical li .btn span.fa-large.toctree-expand, +.btn .rst-content .fa-large.admonition-title, +.rst-content .btn .fa-large.admonition-title, +.btn .rst-content h1 .fa-large.headerlink, +.rst-content h1 .btn .fa-large.headerlink, +.btn .rst-content h2 .fa-large.headerlink, +.rst-content h2 .btn .fa-large.headerlink, +.btn .rst-content h3 .fa-large.headerlink, +.rst-content h3 .btn .fa-large.headerlink, +.btn .rst-content h4 .fa-large.headerlink, +.rst-content h4 .btn .fa-large.headerlink, +.btn .rst-content h5 .fa-large.headerlink, +.rst-content h5 .btn .fa-large.headerlink, +.btn .rst-content h6 .fa-large.headerlink, +.rst-content h6 .btn .fa-large.headerlink, +.btn .rst-content dl dt .fa-large.headerlink, +.rst-content dl dt .btn .fa-large.headerlink, +.btn .rst-content p.caption .fa-large.headerlink, +.rst-content p.caption .btn .fa-large.headerlink, +.btn .rst-content tt.download span.fa-large:first-child, +.rst-content tt.download .btn span.fa-large:first-child, +.btn .rst-content code.download span.fa-large:first-child, +.rst-content code.download .btn span.fa-large:first-child, +.btn .fa-large.icon, +.nav .fa.fa-large, +.nav .wy-menu-vertical li span.fa-large.toctree-expand, +.wy-menu-vertical li .nav span.fa-large.toctree-expand, +.nav .rst-content .fa-large.admonition-title, +.rst-content .nav .fa-large.admonition-title, +.nav .rst-content h1 .fa-large.headerlink, +.rst-content h1 .nav .fa-large.headerlink, +.nav .rst-content h2 .fa-large.headerlink, +.rst-content h2 .nav .fa-large.headerlink, +.nav .rst-content h3 .fa-large.headerlink, +.rst-content h3 .nav .fa-large.headerlink, +.nav .rst-content h4 .fa-large.headerlink, +.rst-content h4 .nav .fa-large.headerlink, +.nav .rst-content h5 .fa-large.headerlink, +.rst-content h5 .nav .fa-large.headerlink, +.nav .rst-content h6 .fa-large.headerlink, +.rst-content h6 .nav .fa-large.headerlink, +.nav .rst-content dl dt .fa-large.headerlink, +.rst-content dl dt .nav .fa-large.headerlink, +.nav .rst-content p.caption .fa-large.headerlink, +.rst-content p.caption .nav .fa-large.headerlink, +.nav .rst-content tt.download span.fa-large:first-child, +.rst-content tt.download .nav span.fa-large:first-child, +.nav .rst-content code.download span.fa-large:first-child, +.rst-content code.download .nav span.fa-large:first-child, +.nav .fa-large.icon { + line-height: 0.9em +} +.btn .fa.fa-spin, +.btn .wy-menu-vertical li span.fa-spin.toctree-expand, +.wy-menu-vertical li .btn span.fa-spin.toctree-expand, +.btn .rst-content .fa-spin.admonition-title, +.rst-content .btn .fa-spin.admonition-title, +.btn .rst-content h1 .fa-spin.headerlink, +.rst-content h1 .btn .fa-spin.headerlink, +.btn .rst-content h2 .fa-spin.headerlink, +.rst-content h2 .btn .fa-spin.headerlink, +.btn .rst-content h3 .fa-spin.headerlink, +.rst-content h3 .btn .fa-spin.headerlink, +.btn .rst-content h4 .fa-spin.headerlink, +.rst-content h4 .btn .fa-spin.headerlink, +.btn .rst-content h5 .fa-spin.headerlink, +.rst-content h5 .btn .fa-spin.headerlink, +.btn .rst-content h6 .fa-spin.headerlink, +.rst-content h6 .btn .fa-spin.headerlink, +.btn .rst-content dl dt .fa-spin.headerlink, +.rst-content dl dt .btn .fa-spin.headerlink, +.btn .rst-content p.caption .fa-spin.headerlink, +.rst-content p.caption .btn .fa-spin.headerlink, +.btn .rst-content tt.download span.fa-spin:first-child, +.rst-content tt.download .btn span.fa-spin:first-child, +.btn .rst-content code.download span.fa-spin:first-child, +.rst-content code.download .btn span.fa-spin:first-child, +.btn .fa-spin.icon, +.nav .fa.fa-spin, +.nav .wy-menu-vertical li span.fa-spin.toctree-expand, +.wy-menu-vertical li .nav span.fa-spin.toctree-expand, +.nav .rst-content .fa-spin.admonition-title, +.rst-content .nav .fa-spin.admonition-title, +.nav .rst-content h1 .fa-spin.headerlink, +.rst-content h1 .nav .fa-spin.headerlink, +.nav .rst-content h2 .fa-spin.headerlink, +.rst-content h2 .nav .fa-spin.headerlink, +.nav .rst-content h3 .fa-spin.headerlink, +.rst-content h3 .nav .fa-spin.headerlink, +.nav .rst-content h4 .fa-spin.headerlink, +.rst-content h4 .nav .fa-spin.headerlink, +.nav .rst-content h5 .fa-spin.headerlink, +.rst-content h5 .nav .fa-spin.headerlink, +.nav .rst-content h6 .fa-spin.headerlink, +.rst-content h6 .nav .fa-spin.headerlink, +.nav .rst-content dl dt .fa-spin.headerlink, +.rst-content dl dt .nav .fa-spin.headerlink, +.nav .rst-content p.caption .fa-spin.headerlink, +.rst-content p.caption .nav .fa-spin.headerlink, +.nav .rst-content tt.download span.fa-spin:first-child, +.rst-content tt.download .nav span.fa-spin:first-child, +.nav .rst-content code.download span.fa-spin:first-child, +.rst-content code.download .nav span.fa-spin:first-child, +.nav .fa-spin.icon { + display: inline-block +} +.btn.fa:before, +.wy-menu-vertical li span.btn.toctree-expand:before, +.rst-content .btn.admonition-title:before, +.rst-content h1 .btn.headerlink:before, +.rst-content h2 .btn.headerlink:before, +.rst-content h3 .btn.headerlink:before, +.rst-content h4 .btn.headerlink:before, +.rst-content h5 .btn.headerlink:before, +.rst-content h6 .btn.headerlink:before, +.rst-content dl dt .btn.headerlink:before, +.rst-content p.caption .btn.headerlink:before, +.rst-content tt.download span.btn:first-child:before, +.rst-content code.download span.btn:first-child:before, +.btn.icon:before { + opacity: 0.5; + -webkit-transition: opacity 0.05s ease-in; + -moz-transition: opacity 0.05s ease-in; + transition: opacity 0.05s ease-in +} +.btn.fa:hover:before, +.wy-menu-vertical li span.btn.toctree-expand:hover:before, +.rst-content .btn.admonition-title:hover:before, +.rst-content h1 .btn.headerlink:hover:before, +.rst-content h2 .btn.headerlink:hover:before, +.rst-content h3 .btn.headerlink:hover:before, +.rst-content h4 .btn.headerlink:hover:before, +.rst-content h5 .btn.headerlink:hover:before, +.rst-content h6 .btn.headerlink:hover:before, +.rst-content dl dt .btn.headerlink:hover:before, +.rst-content p.caption .btn.headerlink:hover:before, +.rst-content tt.download span.btn:first-child:hover:before, +.rst-content code.download span.btn:first-child:hover:before, +.btn.icon:hover:before { + opacity: 1 +} +.btn-mini .fa:before, +.btn-mini .wy-menu-vertical li span.toctree-expand:before, +.wy-menu-vertical li .btn-mini span.toctree-expand:before, +.btn-mini .rst-content .admonition-title:before, +.rst-content .btn-mini .admonition-title:before, +.btn-mini .rst-content h1 .headerlink:before, +.rst-content h1 .btn-mini .headerlink:before, +.btn-mini .rst-content h2 .headerlink:before, +.rst-content h2 .btn-mini .headerlink:before, +.btn-mini .rst-content h3 .headerlink:before, +.rst-content h3 .btn-mini .headerlink:before, +.btn-mini .rst-content h4 .headerlink:before, +.rst-content h4 .btn-mini .headerlink:before, +.btn-mini .rst-content h5 .headerlink:before, +.rst-content h5 .btn-mini .headerlink:before, +.btn-mini .rst-content h6 .headerlink:before, +.rst-content h6 .btn-mini .headerlink:before, +.btn-mini .rst-content dl dt .headerlink:before, +.rst-content dl dt .btn-mini .headerlink:before, +.btn-mini .rst-content p.caption .headerlink:before, +.rst-content p.caption .btn-mini .headerlink:before, +.btn-mini .rst-content tt.download span:first-child:before, +.rst-content tt.download .btn-mini span:first-child:before, +.btn-mini .rst-content code.download span:first-child:before, +.rst-content code.download .btn-mini span:first-child:before, +.btn-mini .icon:before { + font-size: 14px; + vertical-align: -15% +} +.wy-alert, +.rst-content .note, +.rst-content .attention, +.rst-content .caution, +.rst-content .danger, +.rst-content .error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .warning, +.rst-content .seealso, +.rst-content .admonition-todo { + padding: 12px; + line-height: 24px; + margin-bottom: 24px; + background: #e7f2fa +} +.wy-alert-title, +.rst-content .admonition-title { + color: #fff; + font-weight: bold; + display: block; + color: #fff; + background: #6ab0de; + margin: -12px; + padding: 6px 12px; + margin-bottom: 12px +} +.wy-alert.wy-alert-danger, +.rst-content .wy-alert-danger.note, +.rst-content .wy-alert-danger.attention, +.rst-content .wy-alert-danger.caution, +.rst-content .danger, +.rst-content .error, +.rst-content .wy-alert-danger.hint, +.rst-content .wy-alert-danger.important, +.rst-content .wy-alert-danger.tip, +.rst-content .wy-alert-danger.warning, +.rst-content .wy-alert-danger.seealso, +.rst-content .wy-alert-danger.admonition-todo { + background: #fdf3f2 +} +.wy-alert.wy-alert-danger .wy-alert-title, +.rst-content .wy-alert-danger.note .wy-alert-title, +.rst-content .wy-alert-danger.attention .wy-alert-title, +.rst-content .wy-alert-danger.caution .wy-alert-title, +.rst-content .danger .wy-alert-title, +.rst-content .error .wy-alert-title, +.rst-content .wy-alert-danger.hint .wy-alert-title, +.rst-content .wy-alert-danger.important .wy-alert-title, +.rst-content .wy-alert-danger.tip .wy-alert-title, +.rst-content .wy-alert-danger.warning .wy-alert-title, +.rst-content .wy-alert-danger.seealso .wy-alert-title, +.rst-content .wy-alert-danger.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-danger .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-danger .admonition-title, +.rst-content .wy-alert-danger.note .admonition-title, +.rst-content .wy-alert-danger.attention .admonition-title, +.rst-content .wy-alert-danger.caution .admonition-title, +.rst-content .danger .admonition-title, +.rst-content .error .admonition-title, +.rst-content .wy-alert-danger.hint .admonition-title, +.rst-content .wy-alert-danger.important .admonition-title, +.rst-content .wy-alert-danger.tip .admonition-title, +.rst-content .wy-alert-danger.warning .admonition-title, +.rst-content .wy-alert-danger.seealso .admonition-title, +.rst-content .wy-alert-danger.admonition-todo .admonition-title { + background: #f29f97 +} +.wy-alert.wy-alert-warning, +.rst-content .wy-alert-warning.note, +.rst-content .attention, +.rst-content .caution, +.rst-content .wy-alert-warning.danger, +.rst-content .wy-alert-warning.error, +.rst-content .wy-alert-warning.hint, +.rst-content .wy-alert-warning.important, +.rst-content .wy-alert-warning.tip, +.rst-content .warning, +.rst-content .wy-alert-warning.seealso, +.rst-content .admonition-todo { + background: #ffedcc +} +.wy-alert.wy-alert-warning .wy-alert-title, +.rst-content .wy-alert-warning.note .wy-alert-title, +.rst-content .attention .wy-alert-title, +.rst-content .caution .wy-alert-title, +.rst-content .wy-alert-warning.danger .wy-alert-title, +.rst-content .wy-alert-warning.error .wy-alert-title, +.rst-content .wy-alert-warning.hint .wy-alert-title, +.rst-content .wy-alert-warning.important .wy-alert-title, +.rst-content .wy-alert-warning.tip .wy-alert-title, +.rst-content .warning .wy-alert-title, +.rst-content .wy-alert-warning.seealso .wy-alert-title, +.rst-content .admonition-todo .wy-alert-title, +.wy-alert.wy-alert-warning .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-warning .admonition-title, +.rst-content .wy-alert-warning.note .admonition-title, +.rst-content .attention .admonition-title, +.rst-content .caution .admonition-title, +.rst-content .wy-alert-warning.danger .admonition-title, +.rst-content .wy-alert-warning.error .admonition-title, +.rst-content .wy-alert-warning.hint .admonition-title, +.rst-content .wy-alert-warning.important .admonition-title, +.rst-content .wy-alert-warning.tip .admonition-title, +.rst-content .warning .admonition-title, +.rst-content .wy-alert-warning.seealso .admonition-title, +.rst-content .admonition-todo .admonition-title { + background: #f0b37e +} +.wy-alert.wy-alert-info, +.rst-content .note, +.rst-content .wy-alert-info.attention, +.rst-content .wy-alert-info.caution, +.rst-content .wy-alert-info.danger, +.rst-content .wy-alert-info.error, +.rst-content .wy-alert-info.hint, +.rst-content .wy-alert-info.important, +.rst-content .wy-alert-info.tip, +.rst-content .wy-alert-info.warning, +.rst-content .seealso, +.rst-content .wy-alert-info.admonition-todo { + background: #e7d0fa +} +.wy-alert.wy-alert-info .wy-alert-title, +.rst-content .note .wy-alert-title, +.rst-content .wy-alert-info.attention .wy-alert-title, +.rst-content .wy-alert-info.caution .wy-alert-title, +.rst-content .wy-alert-info.danger .wy-alert-title, +.rst-content .wy-alert-info.error .wy-alert-title, +.rst-content .wy-alert-info.hint .wy-alert-title, +.rst-content .wy-alert-info.important .wy-alert-title, +.rst-content .wy-alert-info.tip .wy-alert-title, +.rst-content .wy-alert-info.warning .wy-alert-title, +.rst-content .seealso .wy-alert-title, +.rst-content .wy-alert-info.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-info .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-info .admonition-title, +.rst-content .note .admonition-title, +.rst-content .wy-alert-info.attention .admonition-title, +.rst-content .wy-alert-info.caution .admonition-title, +.rst-content .wy-alert-info.danger .admonition-title, +.rst-content .wy-alert-info.error .admonition-title, +.rst-content .wy-alert-info.hint .admonition-title, +.rst-content .wy-alert-info.important .admonition-title, +.rst-content .wy-alert-info.tip .admonition-title, +.rst-content .wy-alert-info.warning .admonition-title, +.rst-content .seealso .admonition-title, +.rst-content .wy-alert-info.admonition-todo .admonition-title { + background: #40008F +} +.wy-alert.wy-alert-success, +.rst-content .wy-alert-success.note, +.rst-content .wy-alert-success.attention, +.rst-content .wy-alert-success.caution, +.rst-content .wy-alert-success.danger, +.rst-content .wy-alert-success.error, +.rst-content .hint, +.rst-content .important, +.rst-content .tip, +.rst-content .wy-alert-success.warning, +.rst-content .wy-alert-success.seealso, +.rst-content .wy-alert-success.admonition-todo { + background: #dbfaf4 +} +.wy-alert.wy-alert-success .wy-alert-title, +.rst-content .wy-alert-success.note .wy-alert-title, +.rst-content .wy-alert-success.attention .wy-alert-title, +.rst-content .wy-alert-success.caution .wy-alert-title, +.rst-content .wy-alert-success.danger .wy-alert-title, +.rst-content .wy-alert-success.error .wy-alert-title, +.rst-content .hint .wy-alert-title, +.rst-content .important .wy-alert-title, +.rst-content .tip .wy-alert-title, +.rst-content .wy-alert-success.warning .wy-alert-title, +.rst-content .wy-alert-success.seealso .wy-alert-title, +.rst-content .wy-alert-success.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-success .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-success .admonition-title, +.rst-content .wy-alert-success.note .admonition-title, +.rst-content .wy-alert-success.attention .admonition-title, +.rst-content .wy-alert-success.caution .admonition-title, +.rst-content .wy-alert-success.danger .admonition-title, +.rst-content .wy-alert-success.error .admonition-title, +.rst-content .hint .admonition-title, +.rst-content .important .admonition-title, +.rst-content .tip .admonition-title, +.rst-content .wy-alert-success.warning .admonition-title, +.rst-content .wy-alert-success.seealso .admonition-title, +.rst-content .wy-alert-success.admonition-todo .admonition-title { + background: #1abc9c +} +.wy-alert.wy-alert-neutral, +.rst-content .wy-alert-neutral.note, +.rst-content .wy-alert-neutral.attention, +.rst-content .wy-alert-neutral.caution, +.rst-content .wy-alert-neutral.danger, +.rst-content .wy-alert-neutral.error, +.rst-content .wy-alert-neutral.hint, +.rst-content .wy-alert-neutral.important, +.rst-content .wy-alert-neutral.tip, +.rst-content .wy-alert-neutral.warning, +.rst-content .wy-alert-neutral.seealso, +.rst-content .wy-alert-neutral.admonition-todo { + background: #f3f6f6 +} +.wy-alert.wy-alert-neutral .wy-alert-title, +.rst-content .wy-alert-neutral.note .wy-alert-title, +.rst-content .wy-alert-neutral.attention .wy-alert-title, +.rst-content .wy-alert-neutral.caution .wy-alert-title, +.rst-content .wy-alert-neutral.danger .wy-alert-title, +.rst-content .wy-alert-neutral.error .wy-alert-title, +.rst-content .wy-alert-neutral.hint .wy-alert-title, +.rst-content .wy-alert-neutral.important .wy-alert-title, +.rst-content .wy-alert-neutral.tip .wy-alert-title, +.rst-content .wy-alert-neutral.warning .wy-alert-title, +.rst-content .wy-alert-neutral.seealso .wy-alert-title, +.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, +.wy-alert.wy-alert-neutral .rst-content .admonition-title, +.rst-content .wy-alert.wy-alert-neutral .admonition-title, +.rst-content .wy-alert-neutral.note .admonition-title, +.rst-content .wy-alert-neutral.attention .admonition-title, +.rst-content .wy-alert-neutral.caution .admonition-title, +.rst-content .wy-alert-neutral.danger .admonition-title, +.rst-content .wy-alert-neutral.error .admonition-title, +.rst-content .wy-alert-neutral.hint .admonition-title, +.rst-content .wy-alert-neutral.important .admonition-title, +.rst-content .wy-alert-neutral.tip .admonition-title, +.rst-content .wy-alert-neutral.warning .admonition-title, +.rst-content .wy-alert-neutral.seealso .admonition-title, +.rst-content .wy-alert-neutral.admonition-todo .admonition-title { + color: #40008F; + background: #e1e4e5 +} +.wy-alert.wy-alert-neutral a, +.rst-content .wy-alert-neutral.note a, +.rst-content .wy-alert-neutral.attention a, +.rst-content .wy-alert-neutral.caution a, +.rst-content .wy-alert-neutral.danger a, +.rst-content .wy-alert-neutral.error a, +.rst-content .wy-alert-neutral.hint a, +.rst-content .wy-alert-neutral.important a, +.rst-content .wy-alert-neutral.tip a, +.rst-content .wy-alert-neutral.warning a, +.rst-content .wy-alert-neutral.seealso a, +.rst-content .wy-alert-neutral.admonition-todo a { + color: #40008f +} +.wy-alert p:last-child, +.rst-content .note p:last-child, +.rst-content .attention p:last-child, +.rst-content .caution p:last-child, +.rst-content .danger p:last-child, +.rst-content .error p:last-child, +.rst-content .hint p:last-child, +.rst-content .important p:last-child, +.rst-content .tip p:last-child, +.rst-content .warning p:last-child, +.rst-content .seealso p:last-child, +.rst-content .admonition-todo p:last-child { + margin-bottom: 0 +} +.wy-tray-container { + position: fixed; + bottom: 0px; + left: 0; + z-index: 600 +} +.wy-tray-container li { + display: block; + width: 300px; + background: transparent; + color: #fff; + text-align: center; + box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.1); + padding: 0 24px; + min-width: 20%; + opacity: 0; + height: 0; + line-height: 56px; + overflow: hidden; + -webkit-transition: all 0.3s ease-in; + -moz-transition: all 0.3s ease-in; + transition: all 0.3s ease-in +} +.wy-tray-container li.wy-tray-item-success { + background: #27AE60 +} +.wy-tray-container li.wy-tray-item-info { + background: #40008f +} +.wy-tray-container li.wy-tray-item-warning { + background: #E67E22 +} +.wy-tray-container li.wy-tray-item-danger { + background: #E74C3C +} +.wy-tray-container li.on { + opacity: 1; + height: 56px +} +@media screen and (max-width: 768px) { + .wy-tray-container { + bottom: auto; + top: 0; + width: 100% + } + .wy-tray-container li { + width: 100% + } +} +button { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + cursor: pointer; + line-height: normal; + -webkit-appearance: button; + *overflow: visible +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0 +} +button[disabled] { + cursor: default +} +.btn { + display: inline-block; + border-radius: 2px; + line-height: normal; + white-space: nowrap; + text-align: center; + cursor: pointer; + font-size: 100%; + padding: 6px 12px 8px 12px; + color: #fff; + border: 1px solid rgba(0, 0, 0, 0.1); + background-color: #27AE60; + text-decoration: none; + font-weight: normal; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + box-shadow: 0px 1px 2px -1px rgba(255, 255, 255, 0.5) inset, 0px -2px 0px 0px rgba(0, 0, 0, 0.1) inset; + outline-none: false; + vertical-align: middle; + *display: inline; + zoom: 1; + -webkit-user-drag: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-transition: all 0.1s linear; + -moz-transition: all 0.1s linear; + transition: all 0.1s linear +} +.btn-hover { + background: #2e8ece; + color: #fff +} +.btn:hover { + background: #2cc36b; + color: #fff +} +.btn:focus { + background: #2cc36b; + outline: 0 +} +.btn:active { + box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.05) inset, 0px 2px 0px 0px rgba(0, 0, 0, 0.1) inset; + padding: 8px 12px 6px 12px +} +.btn:visited { + color: #fff +} +.btn:disabled { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} +.btn-disabled { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} +.btn-disabled:hover, +.btn-disabled:focus, +.btn-disabled:active { + background-image: none; + filter: progid: DXImageTransform.Microsoft.gradient(enabled false); + filter: alpha(opacity=40); + opacity: 0.4; + cursor: not-allowed; + box-shadow: none +} +.btn::-moz-focus-inner { + padding: 0; + border: 0 +} +.btn-small { + font-size: 80% +} +.btn-info { + background-color: #40008f !important +} +.btn-info:hover { + background-color: #2e8ece !important +} +.btn-neutral { + background-color: #f3f6f6 !important; + color: #40008F !important +} +.btn-neutral:hover { + background-color: #e5ebeb !important; + color: #40008F +} +.btn-neutral:visited { + color: #40008F !important +} +.btn-success { + background-color: #27AE60 !important +} +.btn-success:hover { + background-color: #295 !important +} +.btn-danger { + background-color: #E74C3C !important +} +.btn-danger:hover { + background-color: #ea6153 !important +} +.btn-warning { + background-color: #E67E22 !important +} +.btn-warning:hover { + background-color: #e98b39 !important +} +.btn-invert { + background-color: #222 +} +.btn-invert:hover { + background-color: #2f2f2f !important +} +.btn-link { + background-color: transparent !important; + color: #40008f; + box-shadow: none; + border-color: transparent !important +} +.btn-link:hover { + background-color: transparent !important; + color: #409ad5 !important; + box-shadow: none +} +.btn-link:active { + background-color: transparent !important; + color: #409ad5 !important; + box-shadow: none +} +.btn-link:visited { + color: #9B59B6 +} +.wy-btn-group .btn, +.wy-control .btn { + vertical-align: middle +} +.wy-btn-group { + margin-bottom: 24px; + *zoom: 1 +} +.wy-btn-group:before, +.wy-btn-group:after { + display: table; + content: "" +} +.wy-btn-group:after { + clear: both +} +.wy-dropdown { + position: relative; + display: inline-block +} +.wy-dropdown-active .wy-dropdown-menu { + display: block +} +.wy-dropdown-menu { + position: absolute; + left: 0; + display: none; + float: left; + top: 100%; + min-width: 100%; + background: white; + z-index: 100; + border: solid 1px #cfd7dd; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); + padding: 12px +} +.wy-dropdown-menu>dd>a { + display: block; + clear: both; + color: #40008F; + white-space: nowrap; + font-size: 90%; + padding: 0 12px; + cursor: pointer +} +.wy-dropdown-menu>dd>a:hover { + background: #40008f; + color: #fff +} +.wy-dropdown-menu>dd.divider { + border-top: solid 1px #cfd7dd; + margin: 6px 0 +} +.wy-dropdown-menu>dd.search { + padding-bottom: 12px +} +.wy-dropdown-menu>dd.search input[type="search"] { + width: 100% +} +.wy-dropdown-menu>dd.call-to-action { + background: #e3e3e3; + text-transform: uppercase; + font-weight: 500; + font-size: 80% +} +.wy-dropdown-menu>dd.call-to-action:hover { + background: #e3e3e3 +} +.wy-dropdown-menu>dd.call-to-action .btn { + color: #fff +} +.wy-dropdown.wy-dropdown-up .wy-dropdown-menu { + bottom: 100%; + top: auto; + left: auto; + right: 0 +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { + background: white; + margin-top: 2px +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a { + padding: 6px 12px +} +.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { + background: white; + color: #fff +} +.wy-dropdown.wy-dropdown-left .wy-dropdown-menu { + right: 0; + left: auto; + text-align: right +} +.wy-dropdown-arrow:before { + content: " "; + border-bottom: 5px solid #f5f5f5; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + position: absolute; + display: block; + top: -4px; + left: 50%; + margin-left: -3px +} +.wy-dropdown-arrow.wy-dropdown-arrow-left:before { + left: 11px +} +.wy-form-stacked select { + display: block +} +.wy-form-aligned input, +.wy-form-aligned textarea, +.wy-form-aligned select, +.wy-form-aligned .wy-help-inline, +.wy-form-aligned label { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} +.wy-form-aligned .wy-control-group>label { + display: inline-block; + vertical-align: middle; + width: 10em; + margin: 6px 12px 0 0; + float: left +} +.wy-form-aligned .wy-control { + float: left +} +.wy-form-aligned .wy-control label { + display: block +} +.wy-form-aligned .wy-control select { + margin-top: 6px +} +fieldset { + border: 0; + margin: 0; + padding: 0 +} +legend { + display: block; + width: 100%; + border: 0; + padding: 0; + white-space: normal; + margin-bottom: 24px; + font-size: 150%; + *margin-left: -7px +} +label { + display: block; + margin: 0 0 0.3125em 0; + color: #333; + font-size: 90% +} +input, +select, +textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle +} +.wy-control-group { + margin-bottom: 24px; + *zoom: 1; + max-width: 68em; + margin-left: auto; + margin-right: auto; + *zoom: 1 +} +.wy-control-group:before, +.wy-control-group:after { + display: table; + content: "" +} +.wy-control-group:after { + clear: both +} +.wy-control-group:before, +.wy-control-group:after { + display: table; + content: "" +} +.wy-control-group:after { + clear: both +} +.wy-control-group.wy-control-group-required>label:after { + content: " *"; + color: #E74C3C +} +.wy-control-group .wy-form-full, +.wy-control-group .wy-form-halves, +.wy-control-group .wy-form-thirds { + padding-bottom: 12px +} +.wy-control-group .wy-form-full select, +.wy-control-group .wy-form-halves select, +.wy-control-group .wy-form-thirds select { + width: 100% +} +.wy-control-group .wy-form-full input[type="text"], +.wy-control-group .wy-form-full input[type="password"], +.wy-control-group .wy-form-full input[type="email"], +.wy-control-group .wy-form-full input[type="url"], +.wy-control-group .wy-form-full input[type="date"], +.wy-control-group .wy-form-full input[type="month"], +.wy-control-group .wy-form-full input[type="time"], +.wy-control-group .wy-form-full input[type="datetime"], +.wy-control-group .wy-form-full input[type="datetime-local"], +.wy-control-group .wy-form-full input[type="week"], +.wy-control-group .wy-form-full input[type="number"], +.wy-control-group .wy-form-full input[type="search"], +.wy-control-group .wy-form-full input[type="tel"], +.wy-control-group .wy-form-full input[type="color"], +.wy-control-group .wy-form-halves input[type="text"], +.wy-control-group .wy-form-halves input[type="password"], +.wy-control-group .wy-form-halves input[type="email"], +.wy-control-group .wy-form-halves input[type="url"], +.wy-control-group .wy-form-halves input[type="date"], +.wy-control-group .wy-form-halves input[type="month"], +.wy-control-group .wy-form-halves input[type="time"], +.wy-control-group .wy-form-halves input[type="datetime"], +.wy-control-group .wy-form-halves input[type="datetime-local"], +.wy-control-group .wy-form-halves input[type="week"], +.wy-control-group .wy-form-halves input[type="number"], +.wy-control-group .wy-form-halves input[type="search"], +.wy-control-group .wy-form-halves input[type="tel"], +.wy-control-group .wy-form-halves input[type="color"], +.wy-control-group .wy-form-thirds input[type="text"], +.wy-control-group .wy-form-thirds input[type="password"], +.wy-control-group .wy-form-thirds input[type="email"], +.wy-control-group .wy-form-thirds input[type="url"], +.wy-control-group .wy-form-thirds input[type="date"], +.wy-control-group .wy-form-thirds input[type="month"], +.wy-control-group .wy-form-thirds input[type="time"], +.wy-control-group .wy-form-thirds input[type="datetime"], +.wy-control-group .wy-form-thirds input[type="datetime-local"], +.wy-control-group .wy-form-thirds input[type="week"], +.wy-control-group .wy-form-thirds input[type="number"], +.wy-control-group .wy-form-thirds input[type="search"], +.wy-control-group .wy-form-thirds input[type="tel"], +.wy-control-group .wy-form-thirds input[type="color"] { + width: 100% +} +.wy-control-group .wy-form-full { + float: left; + display: block; + margin-right: 2.35765%; + width: 100%; + margin-right: 0 +} +.wy-control-group .wy-form-full:last-child { + margin-right: 0 +} +.wy-control-group .wy-form-halves { + float: left; + display: block; + margin-right: 2.35765%; + width: 48.82117% +} +.wy-control-group .wy-form-halves:last-child { + margin-right: 0 +} +.wy-control-group .wy-form-halves:nth-of-type(2n) { + margin-right: 0 +} +.wy-control-group .wy-form-halves:nth-of-type(2n+1) { + clear: left +} +.wy-control-group .wy-form-thirds { + float: left; + display: block; + margin-right: 2.35765%; + width: 31.76157% +} +.wy-control-group .wy-form-thirds:last-child { + margin-right: 0 +} +.wy-control-group .wy-form-thirds:nth-of-type(3n) { + margin-right: 0 +} +.wy-control-group .wy-form-thirds:nth-of-type(3n+1) { + clear: left +} +.wy-control-group.wy-control-group-no-input .wy-control { + margin: 6px 0 0 0; + font-size: 90% +} +.wy-control-no-input { + display: inline-block; + margin: 6px 0 0 0; + font-size: 90% +} +.wy-control-group.fluid-input input[type="text"], +.wy-control-group.fluid-input input[type="password"], +.wy-control-group.fluid-input input[type="email"], +.wy-control-group.fluid-input input[type="url"], +.wy-control-group.fluid-input input[type="date"], +.wy-control-group.fluid-input input[type="month"], +.wy-control-group.fluid-input input[type="time"], +.wy-control-group.fluid-input input[type="datetime"], +.wy-control-group.fluid-input input[type="datetime-local"], +.wy-control-group.fluid-input input[type="week"], +.wy-control-group.fluid-input input[type="number"], +.wy-control-group.fluid-input input[type="search"], +.wy-control-group.fluid-input input[type="tel"], +.wy-control-group.fluid-input input[type="color"] { + width: 100% +} +.wy-form-message-inline { + display: inline-block; + padding-left: 0.3em; + color: #666; + vertical-align: middle; + font-size: 90% +} +.wy-form-message { + display: block; + color: #999; + font-size: 70%; + margin-top: 0.3125em; + font-style: italic +} +.wy-form-message p { + font-size: inherit; + font-style: italic; + margin-bottom: 6px +} +.wy-form-message p:last-child { + margin-bottom: 0 +} +input { + line-height: normal +} +input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + *overflow: visible +} +input[type="text"], +input[type="password"], +input[type="email"], +input[type="url"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="datetime"], +input[type="datetime-local"], +input[type="week"], +input[type="number"], +input[type="search"], +input[type="tel"], +input[type="color"] { + -webkit-appearance: none; + padding: 6px; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + box-shadow: inset 0 1px 3px #ddd; + border-radius: 0; + -webkit-transition: border 0.3s linear; + -moz-transition: border 0.3s linear; + transition: border 0.3s linear +} +input[type="datetime-local"] { + padding: 0.34375em 0.625em +} +input[disabled] { + cursor: default +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; + margin-right: 0.3125em; + *height: 13px; + *width: 13px +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none +} +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus { + outline: 0; + outline: thin dotted \9; + border-color: #333 +} +input.no-focus:focus { + border-color: #ccc !important +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 1px auto #129FEA +} +input[type="text"][disabled], +input[type="password"][disabled], +input[type="email"][disabled], +input[type="url"][disabled], +input[type="date"][disabled], +input[type="month"][disabled], +input[type="time"][disabled], +input[type="datetime"][disabled], +input[type="datetime-local"][disabled], +input[type="week"][disabled], +input[type="number"][disabled], +input[type="search"][disabled], +input[type="tel"][disabled], +input[type="color"][disabled] { + cursor: not-allowed; + background-color: #fafafa +} +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #E74C3C; + border: 1px solid #E74C3C +} +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #E74C3C +} +input[type="file"]:focus:invalid:focus, +input[type="radio"]:focus:invalid:focus, +input[type="checkbox"]:focus:invalid:focus { + outline-color: #E74C3C +} +input.wy-input-large { + padding: 12px; + font-size: 100% +} +textarea { + overflow: auto; + vertical-align: top; + width: 100%; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif +} +select, +textarea { + padding: 0.5em 0.625em; + display: inline-block; + border: 1px solid #ccc; + font-size: 80%; + box-shadow: inset 0 1px 3px #ddd; + -webkit-transition: border 0.3s linear; + -moz-transition: border 0.3s linear; + transition: border 0.3s linear +} +select { + border: 1px solid #ccc; + background-color: #fff +} +select[multiple] { + height: auto +} +select:focus, +textarea:focus { + outline: 0 +} +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #fafafa +} +input[type="radio"][disabled], +input[type="checkbox"][disabled] { + cursor: not-allowed +} +.wy-checkbox, +.wy-radio { + margin: 6px 0; + color: #40008F; + display: block +} +.wy-checkbox input, +.wy-radio input { + vertical-align: baseline +} +.wy-form-message-inline { + display: inline-block; + *display: inline; + *zoom: 1; + vertical-align: middle +} +.wy-input-prefix, +.wy-input-suffix { + white-space: nowrap; + padding: 6px +} +.wy-input-prefix .wy-input-context, +.wy-input-suffix .wy-input-context { + line-height: 27px; + padding: 0 8px; + display: inline-block; + font-size: 80%; + background-color: #f3f6f6; + border: solid 1px #ccc; + color: #999 +} +.wy-input-suffix .wy-input-context { + border-left: 0 +} +.wy-input-prefix .wy-input-context { + border-right: 0 +} +.wy-switch { + width: 36px; + height: 12px; + margin: 12px 0; + position: relative; + border-radius: 4px; + background: #ccc; + cursor: pointer; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out +} +.wy-switch:before { + position: absolute; + content: ""; + display: block; + width: 18px; + height: 18px; + border-radius: 4px; + background: #999; + left: -3px; + top: -3px; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out +} +.wy-switch:after { + content: "false"; + position: absolute; + left: 48px; + display: block; + font-size: 12px; + color: #ccc +} +.wy-switch.active { + background: #1e8449 +} +.wy-switch.active:before { + left: 24px; + background: #27AE60 +} +.wy-switch.active:after { + content: "true" +} +.wy-switch.disabled, +.wy-switch.active.disabled { + cursor: not-allowed +} +.wy-control-group.wy-control-group-error .wy-form-message, +.wy-control-group.wy-control-group-error>label { + color: #E74C3C +} +.wy-control-group.wy-control-group-error input[type="text"], +.wy-control-group.wy-control-group-error input[type="password"], +.wy-control-group.wy-control-group-error input[type="email"], +.wy-control-group.wy-control-group-error input[type="url"], +.wy-control-group.wy-control-group-error input[type="date"], +.wy-control-group.wy-control-group-error input[type="month"], +.wy-control-group.wy-control-group-error input[type="time"], +.wy-control-group.wy-control-group-error input[type="datetime"], +.wy-control-group.wy-control-group-error input[type="datetime-local"], +.wy-control-group.wy-control-group-error input[type="week"], +.wy-control-group.wy-control-group-error input[type="number"], +.wy-control-group.wy-control-group-error input[type="search"], +.wy-control-group.wy-control-group-error input[type="tel"], +.wy-control-group.wy-control-group-error input[type="color"] { + border: solid 1px #E74C3C +} +.wy-control-group.wy-control-group-error textarea { + border: solid 1px #E74C3C +} +.wy-inline-validate { + white-space: nowrap +} +.wy-inline-validate .wy-input-context { + padding: 0.5em 0.625em; + display: inline-block; + font-size: 80% +} +.wy-inline-validate.wy-inline-validate-success .wy-input-context { + color: #27AE60 +} +.wy-inline-validate.wy-inline-validate-danger .wy-input-context { + color: #E74C3C +} +.wy-inline-validate.wy-inline-validate-warning .wy-input-context { + color: #E67E22 +} +.wy-inline-validate.wy-inline-validate-info .wy-input-context { + color: #40008f +} +.rotate-90 { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg) +} +.rotate-180 { + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg) +} +.rotate-270 { + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg) +} +.mirror { + -webkit-transform: scaleX(-1); + -moz-transform: scaleX(-1); + -ms-transform: scaleX(-1); + -o-transform: scaleX(-1); + transform: scaleX(-1) +} +.mirror.rotate-90 { + -webkit-transform: scaleX(-1) rotate(90deg); + -moz-transform: scaleX(-1) rotate(90deg); + -ms-transform: scaleX(-1) rotate(90deg); + -o-transform: scaleX(-1) rotate(90deg); + transform: scaleX(-1) rotate(90deg) +} +.mirror.rotate-180 { + -webkit-transform: scaleX(-1) rotate(180deg); + -moz-transform: scaleX(-1) rotate(180deg); + -ms-transform: scaleX(-1) rotate(180deg); + -o-transform: scaleX(-1) rotate(180deg); + transform: scaleX(-1) rotate(180deg) +} +.mirror.rotate-270 { + -webkit-transform: scaleX(-1) rotate(270deg); + -moz-transform: scaleX(-1) rotate(270deg); + -ms-transform: scaleX(-1) rotate(270deg); + -o-transform: scaleX(-1) rotate(270deg); + transform: scaleX(-1) rotate(270deg) +} +@media only screen and (max-width: 480px) { + .wy-form button[type="submit"] { + margin: 0.7em 0 0 + } + .wy-form input[type="text"], + .wy-form input[type="password"], + .wy-form input[type="email"], + .wy-form input[type="url"], + .wy-form input[type="date"], + .wy-form input[type="month"], + .wy-form input[type="time"], + .wy-form input[type="datetime"], + .wy-form input[type="datetime-local"], + .wy-form input[type="week"], + .wy-form input[type="number"], + .wy-form input[type="search"], + .wy-form input[type="tel"], + .wy-form input[type="color"] { + margin-bottom: 0.3em; + display: block + } + .wy-form label { + margin-bottom: 0.3em; + display: block + } + .wy-form input[type="password"], + .wy-form input[type="email"], + .wy-form input[type="url"], + .wy-form input[type="date"], + .wy-form input[type="month"], + .wy-form input[type="time"], + .wy-form input[type="datetime"], + .wy-form input[type="datetime-local"], + .wy-form input[type="week"], + .wy-form input[type="number"], + .wy-form input[type="search"], + .wy-form input[type="tel"], + .wy-form input[type="color"] { + margin-bottom: 0 + } + .wy-form-aligned .wy-control-group label { + margin-bottom: 0.3em; + text-align: left; + display: block; + width: 100% + } + .wy-form-aligned .wy-control { + margin: 1.5em 0 0 0 + } + .wy-form .wy-help-inline, + .wy-form-message-inline, + .wy-form-message { + display: block; + font-size: 80%; + padding: 6px 0 + } +} +@media screen and (max-width: 768px) { + .tablet-hide { + display: none + } +} +@media screen and (max-width: 480px) { + .mobile-hide { + display: none + } +} +.float-left { + float: left +} +.float-right { + float: right +} +.full-width { + width: 100% +} +.wy-table, +.rst-content table.docutils, +.rst-content table.field-list { + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + margin-bottom: 24px +} +.wy-table caption, +.rst-content table.docutils caption, +.rst-content table.field-list caption { + color: #000; + font: italic 85%/1 arial, sans-serif; + padding: 1em 0; + text-align: center +} +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td, +.wy-table th, +.rst-content table.docutils th, +.rst-content table.field-list th { + font-size: 90%; + margin: 0; + overflow: visible; + padding: 8px 16px +} +.wy-table td:first-child, +.rst-content table.docutils td:first-child, +.rst-content table.field-list td:first-child, +.wy-table th:first-child, +.rst-content table.docutils th:first-child, +.rst-content table.field-list th:first-child { + border-left-width: 0 +} +.wy-table thead, +.rst-content table.docutils thead, +.rst-content table.field-list thead { + color: #000; + text-align: left; + vertical-align: bottom; + white-space: nowrap +} +.wy-table thead th, +.rst-content table.docutils thead th, +.rst-content table.field-list thead th { + font-weight: bold; + border-bottom: solid 2px #e1e4e5 +} +.wy-table td, +.rst-content table.docutils td, +.rst-content table.field-list td { + background-color: transparent; + vertical-align: middle +} +.wy-table td p, +.rst-content table.docutils td p, +.rst-content table.field-list td p { + line-height: 18px +} +.wy-table td p:last-child, +.rst-content table.docutils td p:last-child, +.rst-content table.field-list td p:last-child { + margin-bottom: 0 +} +.wy-table .wy-table-cell-min, +.rst-content table.docutils .wy-table-cell-min, +.rst-content table.field-list .wy-table-cell-min { + width: 1%; + padding-right: 0 +} +.wy-table .wy-table-cell-min input[type=checkbox], +.rst-content table.docutils .wy-table-cell-min input[type=checkbox], +.rst-content table.field-list .wy-table-cell-min input[type=checkbox], +.wy-table .wy-table-cell-min input[type=checkbox], +.rst-content table.docutils .wy-table-cell-min input[type=checkbox], +.rst-content table.field-list .wy-table-cell-min input[type=checkbox] { + margin: 0 +} +.wy-table-secondary { + color: gray; + font-size: 90% +} +.wy-table-tertiary { + color: gray; + font-size: 80% +} +.wy-table-odd td, +.wy-table-striped tr:nth-child(2n-1) td, +.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background-color: #f3f6f6 +} +.wy-table-backed { + background-color: #f3f6f6 +} +.wy-table-bordered-all, +.rst-content table.docutils { + border: 1px solid #e1e4e5 +} +.wy-table-bordered-all td, +.rst-content table.docutils td { + border-bottom: 1px solid #e1e4e5; + border-left: 1px solid #e1e4e5 +} +.wy-table-bordered-all tbody>tr:last-child td, +.rst-content table.docutils tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-bordered { + border: 1px solid #e1e4e5 +} +.wy-table-bordered-rows td { + border-bottom: 1px solid #e1e4e5 +} +.wy-table-bordered-rows tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-horizontal tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-horizontal td, +.wy-table-horizontal th { + border-width: 0 0 1px 0; + border-bottom: 1px solid #e1e4e5 +} +.wy-table-horizontal tbody>tr:last-child td { + border-bottom-width: 0 +} +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: auto +} +.wy-table-responsive table { + margin-bottom: 0 !important +} +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: nowrap +} +a { + color: #40008f; + text-decoration: none; + cursor: pointer +} +a:hover { + color: #7000dF +} +a:visited { + color: #7000dF +} +html { + height: 100%; + overflow-x: hidden +} +body { + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + font-weight: normal; + color: #303030; + min-height: 100%; + overflow-x: hidden; + background: white +} +.wy-text-left { + text-align: left +} +.wy-text-center { + text-align: center +} +.wy-text-right { + text-align: right +} +.wy-text-large { + font-size: 120% +} +.wy-text-normal { + font-size: 100% +} +.wy-text-small, +small { + font-size: 80% +} +.wy-text-strike { + text-decoration: line-through +} +.wy-text-warning { + color: #E67E22 !important +} +a.wy-text-warning:hover { + color: #eb9950 !important +} +.wy-text-info { + color: #40008f !important +} +a.wy-text-info:hover { + color: #409ad5 !important +} +.wy-text-success { + color: #27AE60 !important +} +a.wy-text-success:hover { + color: #36d278 !important +} +.wy-text-danger { + color: #E74C3C !important +} +a.wy-text-danger:hover { + color: #ed7669 !important +} +.wy-text-neutral { + color: #40008F !important +} +a.wy-text-neutral:hover { + color: #595959 !important +} +h1, +h2, +.rst-content .toctree-wrapper p.caption, +h3, +h4, +h5, +h6, +legend { + margin-top: 0; + font-weight: 700; + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif +} +p { + line-height: 24px; + margin: 0; + font-size: 16px; + margin-bottom: 24px +} +h1 { + font-size: 175% +} +h2, +.rst-content .toctree-wrapper p.caption { + font-size: 150% +} +h3 { + font-size: 125% +} +h4 { + font-size: 115% +} +h5 { + font-size: 110% +} +h6 { + font-size: 100% +} +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #e1e4e5; + margin: 24px 0; + padding: 0 +} +code, +.rst-content tt, +.rst-content code { + white-space: nowrap; + max-width: 100%; + background: #fff; + border: solid 1px #e1e4e5; + font-size: 75%; + padding: 0 5px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + color: #E74C3C; + overflow-x: auto +} +code.code-large, +.rst-content tt.code-large { + font-size: 90% +} +.wy-plain-list-disc, +.rst-content .section ul, +.rst-content .toctree-wrapper ul, +article ul { + list-style: disc; + line-height: 24px; + margin-bottom: 24px +} +.wy-plain-list-disc li, +.rst-content .section ul li, +.rst-content .toctree-wrapper ul li, +article ul li { + list-style: disc; + margin-left: 24px +} +.wy-plain-list-disc li p:last-child, +.rst-content .section ul li p:last-child, +.rst-content .toctree-wrapper ul li p:last-child, +article ul li p:last-child { + margin-bottom: 0 +} +.wy-plain-list-disc li ul, +.rst-content .section ul li ul, +.rst-content .toctree-wrapper ul li ul, +article ul li ul { + margin-bottom: 0 +} +.wy-plain-list-disc li li, +.rst-content .section ul li li, +.rst-content .toctree-wrapper ul li li, +article ul li li { + list-style: circle +} +.wy-plain-list-disc li li li, +.rst-content .section ul li li li, +.rst-content .toctree-wrapper ul li li li, +article ul li li li { + list-style: square +} +.wy-plain-list-disc li ol li, +.rst-content .section ul li ol li, +.rst-content .toctree-wrapper ul li ol li, +article ul li ol li { + list-style: decimal +} +.wy-plain-list-decimal, +.rst-content .section ol, +.rst-content ol.arabic, +article ol { + list-style: decimal; + line-height: 24px; + margin-bottom: 24px +} +.wy-plain-list-decimal li, +.rst-content .section ol li, +.rst-content ol.arabic li, +article ol li { + list-style: decimal; + margin-left: 24px +} +.wy-plain-list-decimal li p:last-child, +.rst-content .section ol li p:last-child, +.rst-content ol.arabic li p:last-child, +article ol li p:last-child { + margin-bottom: 0 +} +.wy-plain-list-decimal li ul, +.rst-content .section ol li ul, +.rst-content ol.arabic li ul, +article ol li ul { + margin-bottom: 0 +} +.wy-plain-list-decimal li ul li, +.rst-content .section ol li ul li, +.rst-content ol.arabic li ul li, +article ol li ul li { + list-style: disc +} +.codeblock-example { + border: 1px solid #e1e4e5; + border-bottom: none; + padding: 24px; + padding-top: 48px; + font-weight: 500; + background: #fff; + position: relative +} +.codeblock-example:after { + content: "Example"; + position: absolute; + top: 0px; + left: 0px; + background: #9B59B6; + color: #fff; + padding: 6px 12px +} +.codeblock-example.prettyprint-example-only { + border: 1px solid #e1e4e5; + margin-bottom: 24px +} +.codeblock, +pre.literal-block, +.rst-content .literal-block, +.rst-content pre.literal-block, +div[class^='highlight'] { + border: 1px solid #e1e4e5; + padding: 0px; + overflow-x: auto; + background: #fff; + margin: 1px 0 24px 0 +} +.codeblock div[class^='highlight'], +pre.literal-block div[class^='highlight'], +.rst-content .literal-block div[class^='highlight'], +div[class^='highlight'] div[class^='highlight'] { + border: none; + background: none; + margin: 0 +} +div[class^='highlight'] td.code { + width: 100% +} +.linenodiv pre { + border-right: solid 1px #e6e9ea; + margin: 0; + padding: 12px 12px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.5; + color: #d9d9d9 +} +div[class^='highlight'] pre { + white-space: pre; + margin: 0; + padding: 12px 12px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 12px; + line-height: 1.5; + display: block; + overflow: auto; + color: #40008F +} +@media print { + .codeblock, + pre.literal-block, + .rst-content .literal-block, + .rst-content pre.literal-block, + div[class^='highlight'], + div[class^='highlight'] pre { + white-space: pre-wrap + } +} +.hll { + background-color: #ffc; + margin: 0 -12px; + padding: 0 12px; + display: block +} +.c { + color: #998; + font-style: italic +} +.err { + color: #a61717; + background-color: #e3d2d2 +} +.k { + font-weight: bold +} +.o { + font-weight: bold +} +.cm { + color: #998; + font-style: italic +} +.cp { + color: #999; + font-weight: bold +} +.c1 { + color: #998; + font-style: italic +} +.cs { + color: #999; + font-weight: bold; + font-style: italic +} +.gd { + color: #000; + background-color: #fdd +} +.gd .x { + color: #000; + background-color: #faa +} +.ge { + font-style: italic +} +.gr { + color: #a00 +} +.gh { + color: #999 +} +.gi { + color: #000; + background-color: #dfd +} +.gi .x { + color: #000; + background-color: #afa +} +.go { + color: #888 +} +.gp { + color: #555 +} +.gs { + font-weight: bold +} +.gu { + color: purple; + font-weight: bold +} +.gt { + color: #a00 +} +.kc { + font-weight: bold +} +.kd { + font-weight: bold +} +.kn { + font-weight: bold +} +.kp { + font-weight: bold +} +.kr { + font-weight: bold +} +.kt { + color: #458; + font-weight: bold +} +.m { + color: #099 +} +.s { + color: #d14 +} +.n { + color: #333 +} +.na { + color: teal +} +.nb { + color: #0086b3 +} +.nc { + color: #458; + font-weight: bold +} +.no { + color: teal +} +.ni { + color: purple +} +.ne { + color: #900; + font-weight: bold +} +.nf { + color: #900; + font-weight: bold +} +.nn { + color: #555 +} +.nt { + color: navy +} +.nv { + color: teal +} +.ow { + font-weight: bold +} +.w { + color: #bbb +} +.mf { + color: #099 +} +.mh { + color: #099 +} +.mi { + color: #099 +} +.mo { + color: #099 +} +.sb { + color: #d14 +} +.sc { + color: #d14 +} +.sd { + color: #d14 +} +.s2 { + color: #d14 +} +.se { + color: #d14 +} +.sh { + color: #d14 +} +.si { + color: #d14 +} +.sx { + color: #d14 +} +.sr { + color: #009926 +} +.s1 { + color: #d14 +} +.ss { + color: #990073 +} +.bp { + color: #999 +} +.vc { + color: teal +} +.vg { + color: teal +} +.vi { + color: teal +} +.il { + color: #099 +} +.gc { + color: #999; + background-color: #EAF2F5 +} +.wy-breadcrumbs li { + display: inline-block +} +.wy-breadcrumbs li.wy-breadcrumbs-aside { + float: right +} +.wy-breadcrumbs li a { + display: inline-block; + padding: 5px +} +.wy-breadcrumbs li a:first-child { + padding-left: 0 +} +.wy-breadcrumbs li code, +.wy-breadcrumbs li .rst-content tt, +.rst-content .wy-breadcrumbs li tt { + padding: 5px; + border: none; + background: none +} +.wy-breadcrumbs li code.literal, +.wy-breadcrumbs li .rst-content tt.literal, +.rst-content .wy-breadcrumbs li tt.literal { + color: #40008F +} +.wy-breadcrumbs-extra { + margin-bottom: 0; + color: #40008f; + font-size: 80%; + display: inline-block +} +@media screen and (max-width: 480px) { + .wy-breadcrumbs-extra { + display: none + } + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none + } +} +@media print { + .wy-breadcrumbs li.wy-breadcrumbs-aside { + display: none + } +} +.wy-affix { + position: fixed; + top: 1.618em +} +.wy-menu a:hover { + text-decoration: none +} +.wy-menu-horiz { + *zoom: 1 +} +.wy-menu-horiz:before, +.wy-menu-horiz:after { + display: table; + content: "" +} +.wy-menu-horiz:after { + clear: both +} +.wy-menu-horiz ul, +.wy-menu-horiz li { + display: inline-block +} +.wy-menu-horiz li:hover { + background: rgba(255, 255, 255, 0.1) +} +.wy-menu-horiz li.divide-left { + border-left: solid 1px #40008F +} +.wy-menu-horiz li.divide-right { + border-right: solid 1px #40008F +} +.wy-menu-horiz a { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 16px +} +.wy-menu-vertical { + width: 300px +} +.wy-menu-vertical header, +.wy-menu-vertical p.caption { + height: 32px; + display: inline-block; + line-height: 32px; + padding: 0 1.618em; + margin-bottom: 0; + display: block; + font-weight: bold; + text-transform: uppercase; + font-size: 80%; + color: #555; + white-space: nowrap +} +.wy-menu-vertical ul { + margin-bottom: 0 +} +.wy-menu-vertical li.divide-top { + border-top: solid 1px #40008F +} +.wy-menu-vertical li.divide-bottom { + border-bottom: solid 1px #40008F +} +.wy-menu-vertical li.current { + background: #e3e3e3 +} +.wy-menu-vertical li.current a { + color: gray; + border-right: solid 1px white; + padding: 0.4045em 2.427em +} +.wy-menu-vertical li.current a:hover { + background: white +} +.wy-menu-vertical li code, +.wy-menu-vertical li .rst-content tt, +.rst-content .wy-menu-vertical li tt { + border: none; + background: inherit; + color: inherit; + padding-left: 0; + padding-right: 0 +} +.wy-menu-vertical li span.toctree-expand { + display: block; + float: left; + margin-left: -1.2em; + font-size: 0.8em; + line-height: 1.6em; + color: #4d4d4d +} +.wy-menu-vertical li.on a, +.wy-menu-vertical li.current>a { + color: #40008F; + padding: 0.4045em 1.618em; + font-weight: bold; + position: relative; + background: white; + border: none; + border-bottom: solid 1px white; + border-top: solid 1px white; + padding-left: 1.618em -4px +} +.wy-menu-vertical li.on a:hover, +.wy-menu-vertical li.current>a:hover { + background: white +} +.wy-menu-vertical li.on a:hover span.toctree-expand, +.wy-menu-vertical li.current>a:hover span.toctree-expand { + color: gray +} +.wy-menu-vertical li.on a span.toctree-expand, +.wy-menu-vertical li.current>a span.toctree-expand { + display: block; + font-size: 0.8em; + line-height: 1.6em; + color: #333 +} +.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul { + display: none +} +.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul, +.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul { + display: block +} +.wy-menu-vertical li.toctree-l2.current>a { + background: white; + padding: 0.4045em 2.427em +} +.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a { + display: block; + background: white; + padding: 0.4045em 4.045em +} +.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { + color: gray +} +.wy-menu-vertical li.toctree-l2 span.toctree-expand { + color: #a3a3a3 +} +.wy-menu-vertical li.toctree-l3 { + font-size: 0.9em +} +.wy-menu-vertical li.toctree-l3.current>a { + background: #bdbdbd; + padding: 0.4045em 4.045em +} +.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a { + display: block; + background: #bdbdbd; + padding: 0.4045em 5.663em; + border-top: none; + border-bottom: none +} +.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { + color: gray +} +.wy-menu-vertical li.toctree-l3 span.toctree-expand { + color: #969696 +} +.wy-menu-vertical li.toctree-l4 { + font-size: 0.9em +} +.wy-menu-vertical li.current ul { + display: block +} +.wy-menu-vertical li ul { + margin-bottom: 0; + display: none +} +.wy-menu-vertical .local-toc li ul { + display: block +} +.wy-menu-vertical li ul li a { + margin-bottom: 0; + color: #40008f; + font-weight: normal +} +.wy-menu-vertical a { + display: inline-block; + line-height: 18px; + padding: 0.4045em 1.618em; + display: block; + position: relative; + font-size: 90%; + color: #40008f +} +.wy-menu-vertical a:hover { + background-color: #e3e3e3; + cursor: pointer +} +.wy-menu-vertical a:hover span.toctree-expand { + color: #40008f +} +.wy-menu-vertical a:active { + background-color: white; + cursor: pointer; + color: #fff +} +.wy-menu-vertical a:active span.toctree-expand { + color: #fff +} +.wy-side-nav-search { + display: block; + width: 300px; + padding: 0.809em; + margin-bottom: 0.809em; + z-index: 200; + background-color: white; + text-align: center; + padding: 0.809em; + display: block; + color: #40008f; + margin-bottom: 0.809em +} +.wy-side-nav-search input[type=text] { + width: 100%; + border-radius: 50px; + padding: 6px 12px; + border-color: #40008f +} +.wy-side-nav-search img { + display: block; + margin: auto auto 0.809em auto; + height: 45px; + width: 45px; + background-color: white; + padding: 5px; + border-radius: 100% +} +.wy-side-nav-search>a, +.wy-side-nav-search .wy-dropdown>a { + color: #40008f; + font-size: 100%; + font-weight: bold; + display: inline-block; + padding: 4px 6px; + margin-bottom: 0.809em +} +.wy-side-nav-search>a:hover, +.wy-side-nav-search .wy-dropdown>a:hover { + background: white/*rgba(255, 255, 255, 0.1)*/ +} +.wy-side-nav-search>a img.logo, +.wy-side-nav-search .wy-dropdown>a img.logo { + display: block; + margin: 0 auto; + height: auto; + width: 200px; + border-radius: 0; + background: transparent +} +.wy-side-nav-search>a.icon img.logo, +.wy-side-nav-search .wy-dropdown>a.icon img.logo { + margin-top: 0.85em +} +.wy-side-nav-search>div.version { + margin-top: -0.4045em; + margin-bottom: 0.809em; + font-weight: normal; + color: white /*rgba(255, 255, 255, 0.3)*/ +} +.wy-nav .wy-menu-vertical header { + color: #40008f +} +.wy-nav .wy-menu-vertical a { + color: #40008f +} +.wy-nav .wy-menu-vertical a:hover { + background-color: #40008f; + color: #fff +} +[data-menu-wrap] { + -webkit-transition: all 0.2s ease-in; + -moz-transition: all 0.2s ease-in; + transition: all 0.2s ease-in; + position: absolute; + opacity: 1; + width: 100%; + opacity: 0 +} +[data-menu-wrap].move-center { + left: 0; + right: auto; + opacity: 1 +} +[data-menu-wrap].move-left { + right: auto; + left: -100%; + opacity: 0 +} +[data-menu-wrap].move-right { + right: -100%; + left: auto; + opacity: 0 +} +.wy-body-for-nav { + background: left repeat-y white; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC); + background-size: 300px 1px +} +.wy-grid-for-nav { + position: absolute; + width: 100%; + height: 100% +} +.wy-nav-side { + position: fixed; + top: 0; + bottom: 0; + left: 0; + padding-bottom: 2em; + width: 300px; + overflow-x: hidden; + overflow-y: hidden; + min-height: 100%; + background: white; + z-index: 200 +} +.wy-side-scroll { + width: 320px; + position: relative; + overflow-x: hidden; + overflow-y: scroll; + height: 100% +} +.wy-nav-top { + display: none; + background: white; + color: #fff; + padding: 0.4045em 0.809em; + position: relative; + line-height: 50px; + text-align: center; + font-size: 100%; + *zoom: 1 +} +.wy-nav-top:before, +.wy-nav-top:after { + display: table; + content: "" +} +.wy-nav-top:after { + clear: both +} +.wy-nav-top a { + color: #fff; + font-weight: bold +} +.wy-nav-top img { + margin-right: 12px; + height: 45px; + width: 45px; + background-color:white; + padding: 5px; + border-radius: 100% +} +.wy-nav-top i { + font-size: 30px; + float: left; + cursor: pointer +} +.wy-nav-content-wrap { + margin-left: 300px; + background: white; + min-height: 100% +} +.wy-nav-content { + padding: 1.618em 3.236em; + height: 100%; + max-width: 800px; + margin: auto +} +.wy-body-mask { + position: fixed; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.2); + display: none; + z-index: 499 +} +.wy-body-mask.on { + display: block +} +footer { + color: #999 +} +footer p { + margin-bottom: 12px +} +footer span.commit code, +footer span.commit .rst-content tt, +.rst-content footer span.commit tt { + padding: 0px; + font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; + font-size: 1em; + background: none; + border: none; + color: #999 +} +.rst-footer-buttons { + *zoom: 1 +} +.rst-footer-buttons:before, +.rst-footer-buttons:after { + display: table; + content: "" +} +.rst-footer-buttons:after { + clear: both +} +#search-results .search li { + margin-bottom: 24px; + border-bottom: solid 1px #e1e4e5; + padding-bottom: 24px +} +#search-results .search li:first-child { + border-top: solid 1px #e1e4e5; + padding-top: 24px +} +#search-results .search li a { + font-size: 120%; + margin-bottom: 12px; + display: inline-block +} +#search-results .context { + color: gray; + font-size: 90% +} +@media screen and (max-width: 768px) { + .wy-body-for-nav { + background: white + } + .wy-nav-top { + display: block + } + .wy-nav-side { + left: -300px + } + .wy-nav-side.shift { + width: 85%; + left: 0 + } + .wy-side-scroll { + width: auto + } + .wy-side-nav-search { + width: auto + } + .wy-menu.wy-menu-vertical { + width: auto + } + .wy-nav-content-wrap { + margin-left: 0 + } + .wy-nav-content-wrap .wy-nav-content { + padding: 1.618em + } + .wy-nav-content-wrap.shift { + position: fixed; + min-width: 100%; + left: 85%; + top: 0; + height: 100%; + overflow: hidden + } +} +@media screen and (min-width: 1400px) { + .wy-nav-content-wrap { + background: white/*rgba(0, 0, 0, 0.05)*/ + } + .wy-nav-content { + margin: 0; + background: white + } +} +@media print { + .rst-versions, + footer, + .wy-nav-side { + display: none + } + .wy-nav-content-wrap { + margin-left: 0 + } +} +.rst-versions { + position: fixed; + bottom: 0; + left: 0; + width: 300px; + color: white; + background: #1f1d1d; + border-top: solid 10px white; + font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif; + z-index: 400 +} +.rst-versions a { + color: #40008f; + text-decoration: none +} +.rst-versions .rst-badge-small { + display: none +} +.rst-versions .rst-current-version { + padding: 12px; + background-color: #272525; + display: block; + text-align: right; + font-size: 90%; + cursor: pointer; + color: #27AE60; + *zoom: 1 +} +.rst-versions .rst-current-version:before, +.rst-versions .rst-current-version:after { + display: table; + content: "" +} +.rst-versions .rst-current-version:after { + clear: both +} +.rst-versions .rst-current-version .fa, +.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, +.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand, +.rst-versions .rst-current-version .rst-content .admonition-title, +.rst-content .rst-versions .rst-current-version .admonition-title, +.rst-versions .rst-current-version .rst-content h1 .headerlink, +.rst-content h1 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h2 .headerlink, +.rst-content h2 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h3 .headerlink, +.rst-content h3 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h4 .headerlink, +.rst-content h4 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h5 .headerlink, +.rst-content h5 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content h6 .headerlink, +.rst-content h6 .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content dl dt .headerlink, +.rst-content dl dt .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content p.caption .headerlink, +.rst-content p.caption .rst-versions .rst-current-version .headerlink, +.rst-versions .rst-current-version .rst-content tt.download span:first-child, +.rst-content tt.download .rst-versions .rst-current-version span:first-child, +.rst-versions .rst-current-version .rst-content code.download span:first-child, +.rst-content code.download .rst-versions .rst-current-version span:first-child, +.rst-versions .rst-current-version .icon { + color: white +} +.rst-versions .rst-current-version .fa-book, +.rst-versions .rst-current-version .icon-book { + float: left +} +.rst-versions .rst-current-version .icon-book { + float: left +} +.rst-versions .rst-current-version.rst-out-of-date { + background-color: #E74C3C; + color: #fff +} +.rst-versions .rst-current-version.rst-active-old-version { + background-color: #F1C40F; + color: #000 +} +.rst-versions.shift-up .rst-other-versions { + display: block +} +.rst-versions .rst-other-versions { + font-size: 90%; + padding: 12px; + color: gray; + display: none +} +.rst-versions .rst-other-versions hr { + display: block; + height: 1px; + border: 0; + margin: 20px 0; + padding: 0; + border-top: solid 1px #413d3d +} +.rst-versions .rst-other-versions dd { + display: inline-block; + margin: 0 +} +.rst-versions .rst-other-versions dd a { + display: inline-block; + padding: 6px; + color: white +} +.rst-versions.rst-badge { + width: auto; + bottom: 20px; + right: 20px; + left: auto; + border: none; + max-width: 300px +} +.rst-versions.rst-badge .icon-book { + float: none +} +.rst-versions.rst-badge .fa-book, +.rst-versions.rst-badge .icon-book { + float: none +} +.rst-versions.rst-badge.shift-up .rst-current-version { + text-align: right +} +.rst-versions.rst-badge.shift-up .rst-current-version .fa-book, +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} +.rst-versions.rst-badge.shift-up .rst-current-version .icon-book { + float: left +} +.rst-versions.rst-badge .rst-current-version { + width: auto; + height: 30px; + line-height: 30px; + padding: 0 6px; + display: block; + text-align: center +} +@media screen and (max-width: 768px) { + .rst-versions { + width: 85%; + display: none + } + .rst-versions.shift { + display: block + } + img { + width: 100%; + height: auto + } +} +.rst-content img { + max-width: 100%; + height: auto !important +} +.rst-content div.figure { + margin-bottom: 24px +} +.rst-content div.figure p.caption { + font-style: italic +} +.rst-content div.figure.align-center { + text-align: center +} +.rst-content .section>img, +.rst-content .section>a>img { + margin-bottom: 24px +} +.rst-content blockquote { + margin-left: 24px; + line-height: 24px; + margin-bottom: 24px +} +.rst-content .note .last, +.rst-content .attention .last, +.rst-content .caution .last, +.rst-content .danger .last, +.rst-content .error .last, +.rst-content .hint .last, +.rst-content .important .last, +.rst-content .tip .last, +.rst-content .warning .last, +.rst-content .seealso .last, +.rst-content .admonition-todo .last { + margin-bottom: 0 +} +.rst-content .admonition-title:before { + margin-right: 4px +} +.rst-content .admonition table { + border-color: rgba(0, 0, 0, 0.1) +} +.rst-content .admonition table td, +.rst-content .admonition table th { + background: transparent !important; + border-color: rgba(0, 0, 0, 0.1) !important +} +.rst-content .section ol.loweralpha, +.rst-content .section ol.loweralpha li { + list-style: lower-alpha +} +.rst-content .section ol.upperalpha, +.rst-content .section ol.upperalpha li { + list-style: upper-alpha +} +.rst-content .section ol p, +.rst-content .section ul p { + margin-bottom: 12px +} +.rst-content .line-block { + margin-left: 24px +} +.rst-content .topic-title { + font-weight: bold; + margin-bottom: 12px +} +.rst-content .toc-backref { + color: #40008F +} +.rst-content .align-right { + float: right; + margin: 0px 0px 24px 24px +} +.rst-content .align-left { + float: left; + margin: 0px 24px 24px 0px +} +.rst-content .align-center { + margin: auto; + display: block +} +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content .toctree-wrapper p.caption .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink, +.rst-content dl dt .headerlink, +.rst-content p.caption .headerlink { + display: none; + visibility: hidden; + font-size: 14px +} +.rst-content h1 .headerlink:after, +.rst-content h2 .headerlink:after, +.rst-content .toctree-wrapper p.caption .headerlink:after, +.rst-content h3 .headerlink:after, +.rst-content h4 .headerlink:after, +.rst-content h5 .headerlink:after, +.rst-content h6 .headerlink:after, +.rst-content dl dt .headerlink:after, +.rst-content p.caption .headerlink:after { + visibility: visible; + content: "ïƒ"; + font-family: FontAwesome; + display: inline-block +} +.rst-content h1:hover .headerlink, +.rst-content h2:hover .headerlink, +.rst-content .toctree-wrapper p.caption:hover .headerlink, +.rst-content h3:hover .headerlink, +.rst-content h4:hover .headerlink, +.rst-content h5:hover .headerlink, +.rst-content h6:hover .headerlink, +.rst-content dl dt:hover .headerlink, +.rst-content p.caption:hover .headerlink { + display: inline-block +} +.rst-content .sidebar { + float: right; + width: 40%; + display: block; + margin: 0 0 24px 24px; + padding: 24px; + background: #f3f6f6; + border: solid 1px #e1e4e5 +} +.rst-content .sidebar p, +.rst-content .sidebar ul, +.rst-content .sidebar dl { + font-size: 90% +} +.rst-content .sidebar .last { + margin-bottom: 0 +} +.rst-content .sidebar .sidebar-title { + display: block; + font-family: "Roboto Slab", "ff-tisa-web-pro", "Georgia", Arial, sans-serif; + font-weight: bold; + background: #e1e4e5; + padding: 6px 12px; + margin: -24px; + margin-bottom: 24px; + font-size: 100% +} +.rst-content .highlighted { + background: #F1C40F; + display: inline-block; + font-weight: bold; + padding: 0 6px +} +.rst-content .footnote-reference, +.rst-content .citation-reference { + vertical-align: super; + font-size: 90% +} +.rst-content table.docutils.citation, +.rst-content table.docutils.footnote { + background: none; + border: none; + color: #999 +} +.rst-content table.docutils.citation td, +.rst-content table.docutils.citation tr, +.rst-content table.docutils.footnote td, +.rst-content table.docutils.footnote tr { + border: none; + background-color: transparent !important; + white-space: normal +} +.rst-content table.docutils.citation td.label, +.rst-content table.docutils.footnote td.label { + padding-left: 0; + padding-right: 0; + vertical-align: top +} +.rst-content table.docutils.citation tt, +.rst-content table.docutils.citation code, +.rst-content table.docutils.footnote tt, +.rst-content table.docutils.footnote code { + color: #555 +} +.rst-content table.field-list { + border: none +} +.rst-content table.field-list td { + border: none; + padding-top: 5px +} +.rst-content table.field-list td>strong { + display: inline-block; + margin-top: 3px +} +.rst-content table.field-list .field-name { + padding-right: 10px; + text-align: left; + white-space: nowrap +} +.rst-content table.field-list .field-body { + text-align: left; + padding-left: 0 +} +.rst-content tt, +.rst-content tt, +.rst-content code { + color: #000; + padding: 2px 5px +} +.rst-content tt big, +.rst-content tt em, +.rst-content tt big, +.rst-content code big, +.rst-content tt em, +.rst-content code em { + font-size: 100% !important; + line-height: normal +} +.rst-content tt.literal, +.rst-content tt.literal, +.rst-content code.literal { + color: #E74C3C +} +.rst-content tt.xref, +a .rst-content tt, +.rst-content tt.xref, +.rst-content code.xref, +a .rst-content tt, +a .rst-content code { + font-weight: bold; + color: #40008F +} +.rst-content a tt, +.rst-content a tt, +.rst-content a code { + color: #40008f +} +.rst-content dl { + margin-bottom: 24px +} +.rst-content dl dt { + font-weight: bold +} +.rst-content dl p, +.rst-content dl table, +.rst-content dl ul, +.rst-content dl ol { + margin-bottom: 12px !important +} +.rst-content dl dd { + margin: 0 0 12px 24px +} +.rst-content dl:not(.docutils) { + margin-bottom: 24px +} +.rst-content dl:not(.docutils) dt { + display: inline-block; + margin: 6px 0; + font-size: 90%; + line-height: normal; + background: #e7f2fa; + color: #40008f; + border-top: solid 3px #6ab0de; + padding: 6px; + position: relative +} +.rst-content dl:not(.docutils) dt:before { + color: #6ab0de +} +.rst-content dl:not(.docutils) dt .headerlink { + color: #40008F; + font-size: 100% !important +} +.rst-content dl:not(.docutils) dl dt { + margin-bottom: 6px; + border: none; + border-left: solid 3px #ccc; + background: #f0f0f0; + color: #555 +} +.rst-content dl:not(.docutils) dl dt .headerlink { + color: #40008F; + font-size: 100% !important +} +.rst-content dl:not(.docutils) dt:first-child { + margin-top: 0 +} +.rst-content dl:not(.docutils) tt, +.rst-content dl:not(.docutils) tt, +.rst-content dl:not(.docutils) code { + font-weight: bold +} +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) tt.descclassname, +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) code.descname, +.rst-content dl:not(.docutils) tt.descclassname, +.rst-content dl:not(.docutils) code.descclassname { + background-color: transparent; + border: none; + padding: 0; + font-size: 100% !important +} +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) tt.descname, +.rst-content dl:not(.docutils) code.descname { + font-weight: bold +} +.rst-content dl:not(.docutils) .optional { + display: inline-block; + padding: 0 4px; + color: #000; + font-weight: bold +} +.rst-content dl:not(.docutils) .property { + display: inline-block; + padding-right: 8px +} +.rst-content .viewcode-link, +.rst-content .viewcode-back { + display: inline-block; + color: #27AE60; + font-size: 80%; + padding-left: 24px +} +.rst-content .viewcode-back { + display: block; + float: right +} +.rst-content p.rubric { + margin-bottom: 12px; + font-weight: bold +} +.rst-content tt.download, +.rst-content code.download { + background: inherit; + padding: inherit; + font-family: inherit; + font-size: inherit; + color: inherit; + border: inherit; + white-space: inherit +} +.rst-content tt.download span:first-child:before, +.rst-content code.download span:first-child:before { + margin-right: 4px +} +@media screen and (max-width: 480px) { + .rst-content .sidebar { + width: 100% + } +} +span[id*='MathJax-Span'] { + color: #40008F +} +.math { + text-align: center +} +@font-face { + font-family: "Inconsolata"; + font-style: normal; + font-weight: 400; + src: local("Inconsolata"), local("Inconsolata-Regular"), url(../fonts/Inconsolata-Regular.ttf) format("truetype") +} +@font-face { + font-family: "Inconsolata"; + font-style: normal; + font-weight: 700; + src: local("Inconsolata Bold"), local("Inconsolata-Bold"), url(../fonts/Inconsolata-Bold.ttf) format("truetype") +} +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 400; + src: local("Lato Regular"), local("Lato-Regular"), url(../fonts/Lato-Regular.ttf) format("truetype") +} +@font-face { + font-family: "Lato"; + font-style: normal; + font-weight: 700; + src: local("Lato Bold"), local("Lato-Bold"), url(../fonts/Lato-Bold.ttf) format("truetype") +} +@font-face { + font-family: "Roboto Slab"; + font-style: normal; + font-weight: 400; + src: local("Roboto Slab Regular"), local("RobotoSlab-Regular"), url(../fonts/RobotoSlab-Regular.ttf) format("truetype") +} +@font-face { + font-family: "Roboto Slab"; + font-style: normal; + font-weight: 700; + src: local("Roboto Slab Bold"), local("RobotoSlab-Bold"), url(../fonts/RobotoSlab-Bold.ttf) format("truetype") +} +/*# sourceMappingURL=theme.css.map */ \ No newline at end of file diff --git a/docs/fr/installation/_toc.rst b/docs/fr/installation/_toc.rst deleted file mode 100644 index af15f5e0e6ce79e4fe8200b4dd2de27d3449fb63..0000000000000000000000000000000000000000 --- a/docs/fr/installation/_toc.rst +++ /dev/null @@ -1,11 +0,0 @@ -Dossier d'installation de VITAM UI -######################################### - -Cette section décrit le dossier d'installation de la solution logicielle :term:`VITAMUI`. - -.. toctree:: - :maxdepth: 4 - :glob: - - */*/* - diff --git a/docs/fr/installation/conf.py b/docs/fr/installation/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..5f59d1431e9e228ffc36153879c4b551c428d017 --- /dev/null +++ b/docs/fr/installation/conf.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +import sphinx_rtd_theme + +# -- Project information ----------------------------------------------------- + +project = u'Vitam-UI' +copyright = u'2021, Programme Vitam' +author = u'Programme Vitam' + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_rtd_theme' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +#source_suffix = ['.rst'] +# source_suffix = '.rst' +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown' +} + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = u'fr' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} +html_theme_options = { + 'logo_only': True, + 'display_version': True, + 'prev_next_buttons_location': 'bottom', + 'style_external_links': True, + 'style_nav_header_background': 'white', + # Toc options + 'collapse_navigation': True, + 'sticky_navigation': False, + 'navigation_depth': 4, + 'includehidden': True, + 'titles_only': False +} +html_title = 'Vitam-UI documentation' +html_logo = 'images/Vitam_Logo-CMJN.png' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +#html_css_files = [ +# 'css/theme.css', +#] +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Vitam-UIdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Vitam-UI.tex', u'Vitam-UI Documentation', + u'Programme Vitam', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'vitam-ui', u'Vitam-UI Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Vitam-UI', u'Vitam-UI Documentation', + author, 'Vitam-UI', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- +extensions = [ +# 'recommonmark', + 'myst_parser' +] diff --git a/docs/fr/installation/images/Vitam_Logo-CMJN.png b/docs/fr/installation/images/Vitam_Logo-CMJN.png new file mode 100644 index 0000000000000000000000000000000000000000..a96a5b73bba27003ceb54f71e90cc75a464ee772 Binary files /dev/null and b/docs/fr/installation/images/Vitam_Logo-CMJN.png differ diff --git a/docs/fr/installation/index.md b/docs/fr/installation/index.md deleted file mode 100644 index 2f3b15d35f665dd7bd87140b44d625de884c121a..0000000000000000000000000000000000000000 --- a/docs/fr/installation/index.md +++ /dev/null @@ -1,6 +0,0 @@ -### Index du Dossier d'Installation - -1. [Introduction](sections/introduction.md) -2. [Prérequis](sections/prerequis.md) -3. [Procédure de déploiement de VitamUI](sections/procedure.md) -4. [Annexes](sections/annexes.md) \ No newline at end of file diff --git a/docs/fr/installation/index.rst b/docs/fr/installation/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..9d6fafdef72e54b61b374e9a7446b9fb9ae93bd5 --- /dev/null +++ b/docs/fr/installation/index.rst @@ -0,0 +1,18 @@ +.. Vitam-UI documentation master file, created by + sphinx-quickstart on Thu Jan 6 10:10:08 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Documentation d'installation de Vitam-UI +======================================== + +.. toctree:: + :maxdepth: 2 + :numbered: + :caption: Contents: + + sections/introduction + sections/prerequis + sections/procedure + sections/annexes + diff --git a/docs/fr/installation/sections/annexes.md b/docs/fr/installation/sections/annexes.md index 4b48c9625c2ed892598c72c8ffa46af40d220465..46fa9a96317a40cb2148fff6f7308f7191f04fad 100644 --- a/docs/fr/installation/sections/annexes.md +++ b/docs/fr/installation/sections/annexes.md @@ -63,7 +63,7 @@ Dans ce cas précis il y a une incohérence/mismatch sur les certificats des ser Les identifiants sont les suivants: -~~~yml +~~~yaml user admin: login: admin@change-it.fr mdp : Change-it0! diff --git a/docs/fr/installation/sections/password_configurations.md b/docs/fr/installation/sections/password_configurations.md index 9a85ca1635b760375f833f9072e7ab1e8de2957b..bfa3d83696ddedd8f3c728abd50d7935b1d1f597 100644 --- a/docs/fr/installation/sections/password_configurations.md +++ b/docs/fr/installation/sections/password_configurations.md @@ -12,7 +12,7 @@ Ce profil est nommé `anssi`, l'exploitant peut le changer en choisissant le pro Exemple de profil custom pouvant être surchargé dans le fichier `environments/vitamui_extra_vars.yml`. -```yml +```yaml # Custom password configuration vitamui_password_configurations: customPolicyPattern: '^(?=.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`])(?=.*[a-z])(?=.*[A-Z])(?=.*[\d])[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{${password.length},}$' diff --git a/docs/fr/installation/sections/procedure.md b/docs/fr/installation/sections/procedure.md index b8d23236d7abec01ca81be2f38bf5897443d9fbe..02bc1d02f32417202fc2305928bc7e9078ae7eaa 100644 --- a/docs/fr/installation/sections/procedure.md +++ b/docs/fr/installation/sections/procedure.md @@ -179,7 +179,6 @@ ansible-playbook --vault-password-file vault_pass.txt -i environments/<hostfile_ > > Attention ! Dans le cas d'un déploiement avec 2 interfaces, il subsiste un bug avec consul et la résolution DNS de mongo dans Vitam-UI. Un ticket de support est en cours de résolution pour résoudre ce problème rapidement. ---- --- ## Gestion des certificats @@ -263,7 +262,6 @@ Par exemple: > Attention ! Après cette étape, il sera nécessaire de regénérer les stores de la zone Vitam, suite à l'ajout des certificats de Vitam-UI, et de reconfigurer Vitam en utilisant le `--tags update_vitam_certificates`. > Voir Le chapitre : [Reconfiguration de Vitam](#reconfiguration-de-vitam) -​ ### Génération des stores (dans Vitam-UI) @@ -290,7 +288,7 @@ Se placer dans le répertoire `deployment/` des sources Vitam et exécuter la co ### MAJ des certificats (dans Vitam) ~~~sh -ansible-playbook --vault-password-file vault_pass.txt -i environments/<hostfile_vitamui> ansible-vitam/vitam.yml --tags update_vitam_certificates +ansible-playbook --vault-password-file vault_pass.txt -i environments/<hostfile_vitam> ansible-vitam/vitam.yml --tags update_vitam_certificates ~~~ ### Ajout du contexte Vitam-UI @@ -321,7 +319,7 @@ Le certificat client `vitamui.crt` doit être présent au niveau du répertoire Puis lancer la commande suivante pour ajouter ce nouveau contexte : ~~~sh -ansible-playbook --vault-password-file vault_pass.txt -i environments/<hostfile_vitamui> ansible-vitam-exploitation/add_contexts.yml +ansible-playbook --vault-password-file vault_pass.txt -i environments/<hostfile_vitam> ansible-vitam-exploitation/add_contexts.yml ~~~ Ce playbook prend en paramétre le contenu du fichier `postinstall_param.yml`. Il réalise la création du security-profile, du contexte et l'ajout en base de données du certificat. diff --git a/pom.xml b/pom.xml index 3463ec0c4dc1fa3e3f639f79b35ee90d727f1adc..f3be975175a141395462ca7f77c3bdd9b61f0e80 100644 --- a/pom.xml +++ b/pom.xml @@ -67,13 +67,13 @@ <maven.compiler.target>11</maven.compiler.target> <!-- Dependencies version --> - <apache.commons.codec.version>1.14</apache.commons.codec.version> + <apache.commons.codec.version>1.15</apache.commons.codec.version> <apache.pdfbox.version>2.0.24</apache.pdfbox.version> <apache.pdfbox.xmpbox.version>2.0.16</apache.pdfbox.xmpbox.version> <asciidoctorj.pdf.version>1.5.3</asciidoctorj.pdf.version> <assertj.version>3.15.0</assertj.version> <bouncycastle.version>1.68</bouncycastle.version> - <cas.version>6.1.7.2</cas.version> + <cas.version>6.4.4.2</cas.version> <commons.beanutils.version>1.9.4</commons.beanutils.version> <commons.collections.version>3.2.2</commons.collections.version> <commons.collections4.version>4.4</commons.collections4.version> @@ -92,7 +92,7 @@ <http.client.version>4.5.13</http.client.version> <http.core.version>4.4.14</http.core.version> <glassfish.jaxb.version>2.3.2</glassfish.jaxb.version> - <jackson.version>2.12.3</jackson.version> + <jackson.version>2.12.4</jackson.version> <jaeger.tracing.version>3.2.2</jaeger.tracing.version> <jakarta.xml.binding.version>2.3.2</jakarta.xml.binding.version> <javax.el.version.version>3.0.1-b06</javax.el.version.version> @@ -112,12 +112,12 @@ <jruby.complete.version>9.2.13.0</jruby.complete.version> <jsonassert.version>1.5.0</jsonassert.version> <logback.version>1.2.3</logback.version> - <lombok.version>1.18.12</lombok.version> - <micrometer.version>1.6.5</micrometer.version> + <lombok.version>1.18.20</lombok.version> + <micrometer.version>1.7.3</micrometer.version> <mapstruct.version>1.3.0.Final</mapstruct.version> - <mockito.version>3.11.2</mockito.version> + <mockito.version>3.12.1</mockito.version> <nio.multipart.parser.version>1.1.0</nio.multipart.parser.version> - <pac4j.version>4.0.0</pac4j.version> + <pac4j.version>5.1.4</pac4j.version> <poi.version>4.1.2</poi.version> <org.odftoolkit.version>0.8.7</org.odftoolkit.version> <org.apache.odftoolkit.version>0.8.2-incubating</org.apache.odftoolkit.version> @@ -131,7 +131,7 @@ <slf4j.version>1.7.30</slf4j.version> <spring.boot.version>2.5.6</spring.boot.version> <spring.version>5.3.12</spring.version> - <spring.cloud.consul.version>3.0.2</spring.cloud.consul.version> + <spring.cloud.consul.version>3.0.3</spring.cloud.consul.version> <spring.security.version>5.5.3</spring.security.version> <swagger.version>3.0.0</swagger.version> <trang.version>20181222</trang.version> @@ -548,7 +548,12 @@ <!-- Security --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> + <version>${pac4j.version}</version> + </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-oidc</artifactId> <version>${pac4j.version}</version> </dependency> <dependency> diff --git a/ui/ui-frontend-common/src/app/modules/components/search-bar-with-sibling-button/search-bar-with-sibling-button.component.ts b/ui/ui-frontend-common/src/app/modules/components/search-bar-with-sibling-button/search-bar-with-sibling-button.component.ts index 7ab2691b694c0cab06d18e87c78cf25549bc7587..4751bd6d189d510fe564a0daabdfa1ff442d7cd3 100644 --- a/ui/ui-frontend-common/src/app/modules/components/search-bar-with-sibling-button/search-bar-with-sibling-button.component.ts +++ b/ui/ui-frontend-common/src/app/modules/components/search-bar-with-sibling-button/search-bar-with-sibling-button.component.ts @@ -67,8 +67,7 @@ export class SearchBarWithSiblingButtonComponent { @HostListener('keydown.escape') reset() { this.searchValue = null; - this.clear.emit(); - this.search.emit(this.searchValue); + this.clear.emit(this.searchValue); } public onFocus() { diff --git a/ui/ui-frontend-common/src/app/modules/models/access-register/accession-register-detail.ts b/ui/ui-frontend-common/src/app/modules/models/access-register/accession-register-detail.ts index 4be681ab59307c6919953c0698a46f0d454ae87c..0be8e4634708a048b7c8d2369f5dce25e50a7e34 100644 --- a/ui/ui-frontend-common/src/app/modules/models/access-register/accession-register-detail.ts +++ b/ui/ui-frontend-common/src/app/modules/models/access-register/accession-register-detail.ts @@ -1,22 +1,20 @@ +import {Id} from '../id.interface'; import { AccessionRegisterStatus } from './accession-register-status'; import { RegisterValueDetailModel } from './register-value-detail-model'; import { RegisterValueEventModel } from './register-value-event-model'; -import {Id} from "../id.interface"; export interface AccessionRegisterDetail extends Id { tenant: number; version: number; originatingAgency: string; submissionAgency: string; - archivalAgreement: string; - archivalProfile?: string; originatingAgencyLabel: string; + archivalAgreement: string; startDate: string; endDate: string; lastUpdate: string; opi: string; opc: string; - operationType: string; acquisitionInformation: string; events: RegisterValueEventModel[]; status: AccessionRegisterStatus; @@ -25,5 +23,9 @@ export interface AccessionRegisterDetail extends Id { totalObjects: RegisterValueDetailModel; totalUnits: RegisterValueDetailModel; operationsIds: string[]; + archivalProfile?: string; + operationType: string; legalStatus?: string; + obIdIn: string; + comment: string[]; } diff --git a/ui/ui-frontend/package.json b/ui/ui-frontend/package.json index d7bb0a8ead9d5dd701d9c93fff8383f405271c7d..0173df31b5a7a2cd9eaa54fb7b93d1a9636f063e 100644 --- a/ui/ui-frontend/package.json +++ b/ui/ui-frontend/package.json @@ -8,7 +8,7 @@ }, "scripts": { "ng": "ng", - "ng-high-memory": "node --max_old_space_size=4000 ./node_modules/@angular/cli/bin/ng", + "ng-high-memory": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng", "start": "ng serve --proxy-config proxy.conf.json --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", "start:en": "ng serve --proxy-config proxy.conf.json --configuration=en --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", "start:demo": "ng serve demo --proxy-config proxy.conf.json --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", @@ -24,7 +24,7 @@ "start:archive-search": "ng serve archive-search --proxy-config proxy.conf.json --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", "build": "ng build --prod --i18n-locale fr --build-optimizer=false --optimization=false", "postinstall": "ngcc", - "build:prod": "export NODE_OPTIONS=--max_old_space_size=4096; ng build --prod --output-path ../../../target/www", + "build:prod": "export NODE_OPTIONS=--max_old_space_size=8192; ng build --prod --output-path ../../../target/www", "build:dev": "ng build --prod --i18n-locale fr --build-optimizer=false --optimization=false", "build:fr": "ng build --prod --i18n-locale fr --output-path ../../../target/www/fr", "build:en": "ng build --prod --i18n-file src/locale/messages.en.xlf --i18n-format xlf --i18n-locale en --output-path ../../../target/www/en", @@ -129,7 +129,8 @@ "utf-8-validate": "^5.0.2", "uuid": "^7.0.2", "web-animations-js": "^2.3.2", - "zone.js": "~0.10.3" + "zone.js": "~0.10.3", + "colors": "1.4.0" }, "devDependencies": { "@angular-builders/custom-webpack": "^8.4.1", @@ -169,5 +170,8 @@ "tslint": "~5.11.0", "typescript": "~4.0.5", "webpack-bundle-analyzer": "^3.8.0" + }, + "overrides": { + "colors": "1.4.0" } } diff --git a/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.scss b/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.scss index aad2a6b306a26fe8f6cb1fdbe51046e7d9a0144d..d24eb45cc0bd2f6760f3c4fe72b8d6321db72ebf 100644 --- a/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.scss +++ b/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.scss @@ -16,3 +16,7 @@ .margin-btn { margin-right: 140px; } + +::ng-deep.mat-menu-panel { + max-width: fit-content !important; +} diff --git a/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.ts b/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.ts index 6f978927f708a9e09c88c6bbdb39b99aa21fea25..2d0c3b256c7fdd4751af35748a265d362a803053 100644 --- a/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.ts +++ b/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/archive-search.component.ts @@ -74,6 +74,7 @@ const DESCRIPTION_MAX_TEXT = 60; const PAGE_SIZE = 10; const FILTER_DEBOUNCE_TIME_MS = 400; const MAX_ELIMINATION_ANALYSIS_THRESHOLD = 10000; +const ELIMINATION_TECHNICAL_ID = 'ELIMINATION_TECHNICAL_ID'; @Component({ selector: 'app-archive-search', @@ -405,6 +406,15 @@ export class ArchiveSearchComponent implements OnInit, OnChanges, OnDestroy { removeCriteriaByCategory(category: string) { if (this.searchCriterias && this.searchCriterias.size > 0) { + if (category === SearchCriteriaTypeEnum.APPRAISAL_RULE) { + this.searchCriterias.forEach((criteriaValues, key) => { + if (key === ELIMINATION_TECHNICAL_ID) { + criteriaValues.values.forEach((value) => { + this.removeCriteria(key, value.value, true); + }); + } + }); + } this.searchCriterias.forEach((val, key) => { if (SearchCriteriaTypeEnum[val.category] === category) { val.values.forEach((value) => { diff --git a/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/search-criteria-list/search-criteria-list.component.html b/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/search-criteria-list/search-criteria-list.component.html index 1a3353dc93335d47b3e73b49b11b3762008b1206..154c4f34e160a92b2c9535c013db4754dea20f34 100644 --- a/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/search-criteria-list/search-criteria-list.component.html +++ b/ui/ui-frontend/projects/archive-search/src/app/archive/archive-search/search-criteria-list/search-criteria-list.component.html @@ -1,23 +1,29 @@ <div class="list-div"> <ng-container *ngFor="let criteria of searchCriteriaHistory"> - <button mat-menu-item (click)="storedSearchCriteriaHistory.emit(criteria)" matTooltip="{{ criteria.name }}" - matTooltipClass="vitamui-tooltip" [matTooltipShowDelay]="300"> - <div class="grid-container"> - <div class="criteria-name-date"> - <span class="criteria-name">{{ criteria.name | truncate:30 }}</span> - <span class="criteria-date">{{ criteria.savingDate | date: 'dd/MM/yyyy' }}</span> - </div> - <div> - <i class="material-icons btn-delete" (click)="deleteSearchCriteriaHistory(criteria)">delete</i> - </div> - </div> - </button> + <button + mat-menu-item + (click)="storedSearchCriteriaHistory.emit(criteria)" + matTooltip="{{ criteria.name }}" + matTooltipClass="vitamui-tooltip" + [matTooltipShowDelay]="300" + > + <div class="grid-container"> + <div class="criteria-name-date"> + <span class="criteria-name">{{ criteria.name | truncate: 30 }}</span> - + <span class="criteria-date">{{ criteria.savingDate | date: 'dd/MM/yyyy' }}</span> + </div> + <div> + <i class="material-icons btn-delete" (click)="deleteSearchCriteriaHistory(criteria)">delete</i> + </div> + </div> + </button> </ng-container> - <button mat-menu-item *ngIf="searchCriteriaHistory !== undefined && searchCriteriaHistory.length === 0"> - {{'ARCHIVE_SEARCH.SEARCH_CRITERIA_SAVER.NO_RESULTS' | translate }} - </button> + <button mat-menu-item *ngIf="searchCriteriaHistory !== undefined && searchCriteriaHistory.length === 0"> + {{ 'ARCHIVE_SEARCH.SEARCH_CRITERIA_SAVER.NO_RESULTS' | translate }} + </button> - <div class="center-spinner" *ngIf="pending"> - <mat-spinner diameter="50" color="accent"></mat-spinner> - </div> -</div> \ No newline at end of file + <div class="center-spinner" *ngIf="pending"> + <mat-spinner diameter="50" color="accent"></mat-spinner> + </div> +</div> diff --git a/ui/ui-frontend/projects/archive-search/src/assets/i18n/en.json b/ui/ui-frontend/projects/archive-search/src/assets/i18n/en.json index 3cb26ff43b80081e7111cfb7ee432e02dfd4e1b3..dc7054c1993fb0398bed073a53abff7004705b2c 100644 --- a/ui/ui-frontend/projects/archive-search/src/assets/i18n/en.json +++ b/ui/ui-frontend/projects/archive-search/src/assets/i18n/en.json @@ -51,6 +51,7 @@ "HIDE_SEARCH_CRITERIA": "hide the search filters", "SHOW_MORE_RESULTS": "Show more results...", "NO_MORE_RESULTS": "No more results", + "REMOVE_SEARCH_CRITERIA_BY_CATEGORY": "Delete rule criterias", "SEARCH_CRITERIA_FILTER": { "TITLE": "Search filters", "DUA_TITLE": "Administrative useful life", diff --git a/ui/ui-frontend/projects/archive-search/src/assets/i18n/fr.json b/ui/ui-frontend/projects/archive-search/src/assets/i18n/fr.json index 3d91115f28de8011ebf98db728a90fcc971e2c85..fb6b95fbbf4c14dc727a6112a15f967aae30ec13 100644 --- a/ui/ui-frontend/projects/archive-search/src/assets/i18n/fr.json +++ b/ui/ui-frontend/projects/archive-search/src/assets/i18n/fr.json @@ -51,7 +51,7 @@ "SHOW_SEARCH_CRITERIA": "Afficher les filtres de recherche", "HIDE_SEARCH_CRITERIA": "Masquer les filtres de recherche", "SHOW_MORE_RESULTS": "Afficher plus de résultats...", - "REMOVE_SEARCH_CRITERIA_BY_CATEGORY": "Supprimer les filtres de la règles", + "REMOVE_SEARCH_CRITERIA_BY_CATEGORY": "Supprimer les filtres de la règle", "NO_MORE_RESULTS": "Fin des résultats", "SEARCH_CRITERIA_FILTER": { "TITLE": "Filtre de recherche", diff --git a/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-information-tab/access-contract-information-tab.component.ts b/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-information-tab/access-contract-information-tab.component.ts index 2d08243b09d9604fd976d7e53afbc6816e32c83b..b9b2d4fcd3373b2c175bdbb23ecbcd5d1f699395 100644 --- a/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-information-tab/access-contract-information-tab.component.ts +++ b/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-information-tab/access-contract-information-tab.component.ts @@ -34,16 +34,15 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -import {Component, EventEmitter, Input, Output} from '@angular/core'; -import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; -import {AccessContract} from 'projects/vitamui-library/src/public-api'; -import {Observable, of} from 'rxjs'; -import {catchError, filter, map, switchMap} from 'rxjs/operators'; -import {diff, Option} from 'ui-frontend-common'; -import {extend, isEmpty} from 'underscore'; - -import {AccessContractCreateValidators} from '../../access-contract-create/access-contract-create.validators'; -import {AccessContractService} from '../../access-contract.service'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AccessContract } from 'projects/vitamui-library/src/public-api'; +import { Observable, of } from 'rxjs'; +import { catchError, filter, map, switchMap } from 'rxjs/operators'; +import { diff, Option } from 'ui-frontend-common'; +import { extend, isEmpty } from 'underscore'; +import { AccessContractCreateValidators } from '../../access-contract-create/access-contract-create.validators'; +import { AccessContractService } from '../../access-contract.service'; @Component({ selector: 'app-access-contract-information-tab', @@ -51,7 +50,6 @@ import {AccessContractService} from '../../access-contract.service'; styleUrls: ['./access-contract-information-tab.component.scss'] }) export class AccessContractInformationTabComponent { - @Input() set accessContract(accessContract: AccessContract) { this._accessContract = accessContract; @@ -77,10 +75,10 @@ export class AccessContractInformationTabComponent { @Input() set readOnly(readOnly: boolean) { if (readOnly && this.form.enabled) { - this.form.disable({emitEvent: false}); + this.form.disable({ emitEvent: false }); } else if (this.form.disabled) { - this.form.enable({emitEvent: false}); - this.form.get('identifier').disable({emitEvent: false}); + this.form.enable({ emitEvent: false }); + this.form.get('identifier').disable({ emitEvent: false }); } } @@ -100,11 +98,11 @@ export class AccessContractInformationTabComponent { }); this.statusControl.valueChanges.subscribe((value) => { - this.form.controls.status.setValue(value = (value === false) ? 'INACTIVE' : 'ACTIVE'); + this.form.controls.status.setValue((value = value === false ? 'INACTIVE' : 'ACTIVE')); }); this.accessLogControl.valueChanges.subscribe((value) => { - this.form.controls.accessLog.setValue(value = (value === false) ? 'INACTIVE' : 'ACTIVE'); + this.form.controls.accessLog.setValue((value = value === false ? 'INACTIVE' : 'ACTIVE')); }); this.ruleFilter.valueChanges.subscribe((val) => { @@ -131,19 +129,20 @@ export class AccessContractInformationTabComponent { // FIXME: Get list from common var ? rules: Option[] = [ - {key: 'StorageRule', label: 'Durée d\'utilité courante', info: ''}, - {key: 'ReuseRule', label: 'Durée de réutilisation', info: ''}, - {key: 'ClassificationRule', label: 'Durée de classification', info: ''}, - {key: 'DisseminationRule', label: 'Délai de diffusion', info: ''}, - {key: 'AccessRule', label: 'Durée d\'utilité administrative', info: ''}, - {key: 'AppraisalRule', label: 'Délai de communicabilité', info: ''} + { key: 'StorageRule', label: "Durée d'utilité courante", info: '' }, + { key: 'ReuseRule', label: 'Durée de réutilisation', info: '' }, + { key: 'ClassificationRule', label: 'Durée de classification', info: '' }, + { key: 'DisseminationRule', label: 'Délai de diffusion', info: '' }, + { key: 'AccessRule', label: "Durée d'utilité administrative", info: '' }, + { key: 'AppraisalRule', label: 'Délai de communicabilité', info: '' } ]; previousValue = (): AccessContract => { return this._accessContract; - } + }; unchanged(): boolean { - const unchanged = JSON.stringify(diff(this.form.getRawValue(), this.previousValue())) === '{}' && + const unchanged = + JSON.stringify(diff(this.form.getRawValue(), this.previousValue())) === '{}' && (this.statusControl.value ? 'ACTIVE' : 'INACTIVE') === this.previousValue().status && (this.accessLogControl.value ? 'ACTIVE' : 'INACTIVE') === this.previousValue().accessLog; @@ -153,31 +152,35 @@ export class AccessContractInformationTabComponent { } isInvalid(): boolean { - return this.form.get('name').invalid || this.form.get('name').pending || - this.form.get('description').invalid || this.form.get('description').pending || - this.form.get('status').invalid || this.form.get('status').pending || - this.form.get('accessLog').invalid || this.form.get('accessLog').pending || - (this.ruleFilter.value === false && (this.form.get('ruleCategoryToFilter').invalid || this.form.get('ruleCategoryToFilter').pending)); + return ( + this.form.get('name').invalid || + this.form.get('name').pending || + this.form.get('description').invalid || + this.form.get('description').pending || + this.form.get('status').invalid || + this.form.get('status').pending || + this.form.get('accessLog').invalid || + this.form.get('accessLog').pending || + (this.ruleFilter.value === false && (this.form.get('ruleCategoryToFilter').invalid || this.form.get('ruleCategoryToFilter').pending)) + ); } prepareSubmit(): Observable<AccessContract> { return of(diff(this.form.getRawValue(), this.previousValue())).pipe( filter((formData) => !isEmpty(formData)), - map((formData) => extend({id: this.previousValue().id, identifier: this.previousValue().identifier}, formData)), - switchMap((formData: { id: string, [key: string]: any }) => { - // Update the activation and deactivation dates if the contract status has changed before sending the data + map((formData) => extend({ id: this.previousValue().id, identifier: this.previousValue().identifier }, formData)), + switchMap((formData: { id: string; [key: string]: any }) => { if (formData.status) { if (formData.status === 'ACTIVE') { formData.activationDate = new Date(); - formData.deactivationDate = ''; } else { formData.status = 'INACTIVE'; - formData.activationDate = ''; formData.deactivationDate = new Date(); } } - return this.accessContractService.patch(formData).pipe(catchError(() => of(null))) - })); + return this.accessContractService.patch(formData).pipe(catchError(() => of(null))); + }) + ); } onSubmit() { @@ -185,22 +188,23 @@ export class AccessContractInformationTabComponent { if (this.isInvalid()) { return; } - this.prepareSubmit().subscribe(() => { - this.accessContractService.get(this._accessContract.identifier).subscribe( - response => { + this.prepareSubmit().subscribe( + () => { + this.accessContractService.get(this._accessContract.identifier).subscribe((response) => { this.submited = false; this.accessContract = response; this.resetForm(this.accessContract); - } - ); - }, () => { - this.submited = false; - }); + }); + }, + () => { + this.submited = false; + } + ); } resetForm(accessContract: AccessContract) { this.statusControl.setValue(accessContract.status === 'ACTIVE'); this.accessLogControl.setValue(accessContract.accessLog === 'ACTIVE'); - this.form.reset(accessContract, {emitEvent: false}); + this.form.reset(accessContract, { emitEvent: false }); } } diff --git a/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-preview.component.ts b/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-preview.component.ts index 6943a30c310aacf8967f85d7f00b0ac5a1d4bbfa..b5c248e22c3f571615db301c10cb2a85ba78b31c 100644 --- a/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-preview.component.ts +++ b/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract-preview/access-contract-preview.component.ts @@ -34,17 +34,15 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -import {Component, EventEmitter, HostListener, Input, Output, ViewChild, AfterViewInit} from '@angular/core'; -import {MatDialog} from '@angular/material/dialog'; -import {MatTab, MatTabGroup, MatTabHeader} from '@angular/material/tabs'; -import {AccessContract, ConfirmActionComponent} from 'projects/vitamui-library/src/public-api'; -import {Observable} from 'rxjs'; -import {AccessContractService} from '../access-contract.service'; -import {AccessContractInformationTabComponent} from './access-contract-information-tab/access-contract-information-tab.component'; -import { - AccessContractUsageAndServicesTabComponent -} from './access-contract-usage-and-services-tab/access-contract-usage-and-services-tab.component'; -import {AccessContractWriteAccessTabComponent} from './access-contract-write-access-tab/access-contract-write-access-tab.component'; +import { AfterViewInit, Component, EventEmitter, HostListener, Input, Output, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTab, MatTabGroup, MatTabHeader } from '@angular/material/tabs'; +import { AccessContract, ConfirmActionComponent } from 'projects/vitamui-library/src/public-api'; +import { Observable } from 'rxjs'; +import { AccessContractService } from '../access-contract.service'; +import { AccessContractInformationTabComponent } from './access-contract-information-tab/access-contract-information-tab.component'; +import { AccessContractUsageAndServicesTabComponent } from './access-contract-usage-and-services-tab/access-contract-usage-and-services-tab.component'; +import { AccessContractWriteAccessTabComponent } from './access-contract-write-access-tab/access-contract-write-access-tab.component'; @Component({ selector: 'app-access-contract-preview', @@ -60,14 +58,14 @@ export class AccessContractPreviewComponent implements AfterViewInit { // tab indexes: info = 0; usage = 1; write = 2; node = 3; history = 4; tabUpdated: boolean[] = [false, false, false, false, false]; - @ViewChild('tabs', {static: false}) tabs: MatTabGroup; + @ViewChild('tabs', { static: false }) tabs: MatTabGroup; - tabLinks: Array<AccessContractInformationTabComponent | - AccessContractUsageAndServicesTabComponent | - AccessContractWriteAccessTabComponent> = []; - @ViewChild('infoTab', {static: false}) infoTab: AccessContractInformationTabComponent; - @ViewChild('usageTab', {static: false}) usageTab: AccessContractUsageAndServicesTabComponent; - @ViewChild('writeTab', {static: false}) writeTab: AccessContractWriteAccessTabComponent; + tabLinks: Array< + AccessContractInformationTabComponent | AccessContractUsageAndServicesTabComponent | AccessContractWriteAccessTabComponent + > = []; + @ViewChild('infoTab', { static: false }) infoTab: AccessContractInformationTabComponent; + @ViewChild('usageTab', { static: false }) usageTab: AccessContractUsageAndServicesTabComponent; + @ViewChild('writeTab', { static: false }) writeTab: AccessContractWriteAccessTabComponent; @HostListener('window:beforeunload', ['$event']) beforeunloadHandler(event: any) { @@ -85,14 +83,14 @@ export class AccessContractPreviewComponent implements AfterViewInit { this.tabLinks[2] = this.writeTab; } - constructor(private matDialog: MatDialog, private accessContractService: AccessContractService) { - } + constructor(private matDialog: MatDialog, private accessContractService: AccessContractService) {} filterEvents(event: any): boolean { - return event.outDetail && ( - event.outDetail.includes('STP_UPDATE_ACCESS_CONTRACT') || - event.outDetail.includes('STP_IMPORT_ACCESS_CONTRACT') || - event.outDetail.includes('STP_BACKUP_ACCESS_CONTRACT') + return ( + event.outDetail && + (event.outDetail.includes('STP_UPDATE_ACCESS_CONTRACT') || + event.outDetail.includes('STP_IMPORT_ACCESS_CONTRACT') || + event.outDetail.includes('STP_BACKUP_ACCESS_CONTRACT')) ); } @@ -105,11 +103,9 @@ export class AccessContractPreviewComponent implements AfterViewInit { const submitAccessContractUpdate: Observable<AccessContract> = this.tabLinks[this.tabs.selectedIndex].prepareSubmit(); submitAccessContractUpdate.subscribe(() => { - this.accessContractService.get(this.accessContract.identifier).subscribe( - response => { - this.accessContract = response; - } - ); + this.accessContractService.get(this.accessContract.identifier).subscribe((response) => { + this.accessContract = response; + }); }); } else { this.tabLinks[this.tabs.selectedIndex].resetForm(this.accessContract); @@ -126,7 +122,7 @@ export class AccessContractPreviewComponent implements AfterViewInit { } async confirmAction(): Promise<boolean> { - const dialog = this.matDialog.open(ConfirmActionComponent, {panelClass: 'vitamui-confirm-dialog'}); + const dialog = this.matDialog.open(ConfirmActionComponent, { panelClass: 'vitamui-confirm-dialog' }); dialog.componentInstance.dialogType = 'changeTab'; return await dialog.afterClosed().toPromise(); } @@ -137,5 +133,4 @@ export class AccessContractPreviewComponent implements AfterViewInit { } this.previewClose.emit(); } - } diff --git a/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract.service.ts b/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract.service.ts index 8dad618e60bf9149125e01221fb034116fc12a39..50113bf9ad5cb4e909c38c891f265017352228e5 100644 --- a/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract.service.ts +++ b/ui/ui-frontend/projects/referential/src/app/access-contract/access-contract.service.ts @@ -34,27 +34,23 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {MatSnackBar} from '@angular/material/snack-bar'; -import {AccessContract} from 'projects/vitamui-library/src/public-api'; -import {Observable, Subject} from 'rxjs'; -import {tap} from 'rxjs/operators'; -import {SearchService} from 'ui-frontend-common'; - -import {AccessContractApiService} from '../core/api/access-contract-api.service'; -import {VitamUISnackBarComponent} from '../shared/vitamui-snack-bar'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { AccessContract } from 'projects/vitamui-library/src/public-api'; +import { Observable, Subject } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { SearchService } from 'ui-frontend-common'; +import { AccessContractApiService } from '../core/api/access-contract-api.service'; +import { VitamUISnackBarComponent } from '../shared/vitamui-snack-bar'; @Injectable({ providedIn: 'root' }) export class AccessContractService extends SearchService<AccessContract> { - updated = new Subject<AccessContract>(); - constructor(private accessContractApi: AccessContractApiService, - private snackBar: MatSnackBar, - http: HttpClient) { + constructor(private accessContractApi: AccessContractApiService, private snackBar: MatSnackBar, http: HttpClient) { super(http, accessContractApi, 'ALL'); } @@ -75,11 +71,11 @@ export class AccessContractService extends SearchService<AccessContract> { } exists(name: string): Observable<boolean> { - const accessContract = {name, identifier: name} as AccessContract; + const accessContract = { name, identifier: name } as AccessContract; return this.accessContractApi.check(accessContract, this.headers); } - existsProperties(properties: { name?: string, identifier?: string }): Observable<any> { + existsProperties(properties: { name?: string; identifier?: string }): Observable<any> { const existContract: any = {}; if (properties.name) { existContract.name = properties.name; @@ -92,50 +88,48 @@ export class AccessContractService extends SearchService<AccessContract> { return this.accessContractApi.check(context, this.headers); } - patch(data: { id: string, [key: string]: any }): Observable<AccessContract> { - return this.accessContractApi.patch(data) - .pipe( - tap((response) => this.updated.next(response)), - tap( - (response) => { - this.snackBar.openFromComponent(VitamUISnackBarComponent, { - panelClass: 'vitamui-snack-bar', - duration: 10000, - data: {type: 'accessContractUpdate', name: response.name} - }); - }, - (error) => { - this.snackBar.open(error.error.message, null, { - panelClass: 'vitamui-snack-bar', - duration: 10000 - }); - } - ) - ); + patch(data: { id: string; [key: string]: any }): Observable<AccessContract> { + return this.accessContractApi.patch(data).pipe( + tap((response) => this.updated.next(response)), + tap( + (response) => { + this.snackBar.openFromComponent(VitamUISnackBarComponent, { + panelClass: 'vitamui-snack-bar', + duration: 10000, + data: { type: 'accessContractUpdate', name: response.name } + }); + }, + (error) => { + this.snackBar.open(error.error.message, null, { + panelClass: 'vitamui-snack-bar', + duration: 10000 + }); + } + ) + ); } create(accessContract: AccessContract) { - return this.accessContractApi.create(accessContract) - .pipe( - tap( - (response: AccessContract) => { - this.snackBar.openFromComponent(VitamUISnackBarComponent, { - panelClass: 'vitamui-snack-bar', - data: {type: 'accessContractCreate', name: response.name}, - duration: 10000 - }); - }, - (error) => { - this.snackBar.open(error.error.message, null, { - panelClass: 'vitamui-snack-bar', - duration: 10000 - }); - } - ) - ); + return this.accessContractApi.create(accessContract).pipe( + tap( + (response: AccessContract) => { + this.snackBar.openFromComponent(VitamUISnackBarComponent, { + panelClass: 'vitamui-snack-bar', + data: { type: 'accessContractCreate', name: response.name }, + duration: 10000 + }); + }, + (error) => { + this.snackBar.open(error.error.message, null, { + panelClass: 'vitamui-snack-bar', + duration: 10000 + }); + } + ) + ); } setTenantId(tenantIdentifier: number) { - this.headers = new HttpHeaders({'X-Tenant-Id': tenantIdentifier.toString()}); + this.headers = new HttpHeaders({ 'X-Tenant-Id': tenantIdentifier.toString() }); } } diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.html b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.html index e2cf5f2f088abe70cc1a6f5e4c98db1dbaaa4429..bbb36057dd7dcf200361b3aac43cb82c87fd1bf6 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.html +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.html @@ -75,9 +75,9 @@ </div> </div> <div class="row py-1"> - <div class="col-2"> - <strong class="mr-5">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_ELIMINATIONS' | translate }}</strong> - <i class="vitamui-icon vitamui-icon-ic24-limination"></i> + <div class="col-2 register-title"> + <strong>{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_ELIMINATIONS' | translate }}</strong> + <i class="vitamui-icon vitamui-icon-ic24-limination register-icon"></i> </div> <div class="col-3"> <mat-radio-group aria-label="{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_ELIMINATIONS' | translate }}" id="eliminations" formControlName="elimination"> @@ -88,25 +88,12 @@ </div> </div> <div class="row py-1"> - <div class="col-2"> - <strong class="mr-5">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_TRANSFERTS' | translate }}</strong> - <i class="vitamui-icon vitamui-icon-ic24-user-transfer"></i> + <div class="col-2 register-title"> + <strong>{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_TRANSFERTS' | translate }}</strong> + <i class="vitamui-icon vitamui-icon-ic24-user-transfer register-icon"></i> </div> <div class="col-3"> - <mat-radio-group aria-label="{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_TRANSFERTS' | translate }}" id="transferts" formControlName="transfer"> - <mat-radio-button class="mr-2" value="true">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATIONS_YES_RADIO' | translate }}</mat-radio-button> - <mat-radio-button class="mr-2" value="false">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATIONS_NO_RADIO' | translate }}</mat-radio-button> - <mat-radio-button value="all">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATIONS_ALL_RADIO' | translate }}</mat-radio-button> - </mat-radio-group> - </div> - </div> - <div class="row py-1"> - <div class="col-2"> - <strong class="mr-5 text-right">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_PRESERVATIONS' | translate }}</strong> - <i class="vitamui-icon vitamui-icon-ic24-prservation"></i> - </div> - <div class="col-3"> - <mat-radio-group aria-label="{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_PRESERVATIONS' | translate }}" id="preservation" formControlName="preservation"> + <mat-radio-group aria-label="{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATION_TRANSFERTS' | translate }}" id="transferts" formControlName="transfer_reply"> <mat-radio-button class="mr-2" value="true">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATIONS_YES_RADIO' | translate }}</mat-radio-button> <mat-radio-button class="mr-2" value="false">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATIONS_NO_RADIO' | translate }}</mat-radio-button> <mat-radio-button value="all">{{ 'ACCESSION_REGISTER.ADVANCED_SEARCH.OPERATIONS_ALL_RADIO' | translate }}</mat-radio-button> diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.scss b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.scss index 56ea2f40a866f3e2016db48cf91e066009e3a4bc..e6f1476b21320b207b055447bf2fa3b4052c57fa 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.scss +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.scss @@ -9,3 +9,16 @@ label, mat-label { border: none !important; } } + +.register-icon { + display: inline-block; + font-size: 20px; + font-weight: 500; +} + +.register-title { + display: flex; + justify-content: space-evenly; + align-items: center; + line-height: 20px; +} diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.ts b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.ts index e09d560ef731895193b623d2d1abec46cc61d9f7..2f473d1003729e08602b64c2efc02a01a33ea8a9 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.ts +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-advanced-search/accession-register-advanced-search.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { AfterViewChecked, ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; import { tap } from 'rxjs/operators'; @@ -10,7 +10,7 @@ import { AccessionRegistersService } from '../accession-register.service'; templateUrl: './accession-register-advanced-search.component.html', styleUrls: ['./accession-register-advanced-search.component.scss'], }) -export class AccessionRegisterAdvancedSearchComponent implements OnInit, OnDestroy { +export class AccessionRegisterAdvancedSearchComponent implements OnInit, OnDestroy, AfterViewChecked { @Output() showAdvancedSearchPanel = new EventEmitter<boolean>(); advancedSearchForm: FormGroup; @@ -22,7 +22,15 @@ export class AccessionRegisterAdvancedSearchComponent implements OnInit, OnDestr valuesChangedSub: Subscription; resetSub: Subscription; - constructor(private formBuilder: FormBuilder, private accessionRegistersService: AccessionRegistersService) {} + constructor( + private formBuilder: FormBuilder, + private accessionRegistersService: AccessionRegistersService, + private cdr: ChangeDetectorRef + ) {} + + ngAfterViewChecked(): void { + this.cdr.detectChanges(); + } ngOnInit(): void { this.acquisitionInformations = this.accessionRegistersService.getAcquisitionInformations(); @@ -42,8 +50,7 @@ export class AccessionRegisterAdvancedSearchComponent implements OnInit, OnDestr this.advancedSearchForm.reset({ acquisitionInformations: this.acquisitionInformations, elimination: 'all', - transfer: 'all', - preservation: 'all', + transfer_reply: 'all', }); } }) @@ -58,8 +65,7 @@ export class AccessionRegisterAdvancedSearchComponent implements OnInit, OnDestr OjectUtils.arrayNotUndefined(values.archivalProfiles) || values.acquisitionInformations.length !== this.acquisitionInformations.length || values.elimination !== 'all' || - values.transfer !== 'all' || - values.preservation !== 'all'; + values.transfer_reply !== 'all'; this.accessionRegistersService.setAdvancedFormHaveChanged(haveChanged); this.accessionRegistersService.setGlobalSearchButtonEvent(false); @@ -72,8 +78,7 @@ export class AccessionRegisterAdvancedSearchComponent implements OnInit, OnDestr archivalProfiles: [[], []], acquisitionInformations: this.acquisitionInformationsControl, elimination: ['all', []], - transfer: ['all', []], - preservation: ['all', []], + transfer_reply: ['all', []], }); } diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.html b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.html index f3c890c155769cd538503c26efdff6cc8ee5055b..05e87e127bacf5e1651151a5207ee82a201604de 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.html +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.html @@ -41,12 +41,6 @@ </div> <div class="col-1"> <span>{{ 'ACCESSION_REGISTER.LIST.ACQUISITION_INFORMATION' | translate }}</span> - <vitamui-common-order-by-button - orderByKey="AcquisitionInformation" - [(orderBy)]="orderBy" - [(direction)]="direction" - (orderChange)="emitOrderChange($event)" - ></vitamui-common-order-by-button> </div> <div class="col-1"> <span>{{ 'ACCESSION_REGISTER.LIST.TOTAL_UNITS' | translate }}</span> diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.ts b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.ts index 61726c2d4bbf1299b745bbae6073e75db5ba660e..72118e4504437010dca1b3700fc7e3bedea86c0b 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.ts +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register-list/accession-register-list.component.ts @@ -56,7 +56,7 @@ export class AccessionRegisterListComponent extends InfiniteScrollTable<Accessio filterDebounceTimeMs = 400; direction = Direction.DESCENDANT; - orderBy = 'StartDate'; + orderBy = 'EndDate'; private filterChange = new BehaviorSubject<{ [key: string]: any[] }>({}); private searchChange = new BehaviorSubject<string>(null); @@ -139,12 +139,8 @@ export class AccessionRegisterListComponent extends InfiniteScrollTable<Accessio query.elimination = avancedSearchData.elimination; } - if (OjectUtils.valueNotUndefined(avancedSearchData.transfer)) { - query.transfer = avancedSearchData.transfer; - } - - if (OjectUtils.valueNotUndefined(avancedSearchData.preservation)) { - query.preservation = avancedSearchData.preservation; + if (OjectUtils.valueNotUndefined(avancedSearchData.transfer_reply)) { + query.transfer_reply = avancedSearchData.transfer_reply; } } @@ -161,7 +157,7 @@ export class AccessionRegisterListComponent extends InfiniteScrollTable<Accessio } addCriteriaFromSearch(query: any) { - if (this.entryToSearch !== undefined && this.entryToSearch.length > 0) { + if (this.entryToSearch !== undefined && this.entryToSearch !== null && this.entryToSearch.length > 0) { this.searchKeys.forEach((key) => { query[key] = this.entryToSearch; }); diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.html b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.html index f50764dda27f3376c9f4ce9c28878294b60e9586..693e16fedda2aab3181c533bb358bbc81e34f016 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.html +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.html @@ -7,13 +7,16 @@ <search-bar-with-sibling-button buttonLabel="{{ 'ACCESSION_REGISTER.SEARCH_BAR_BUTTON_LABEL' | translate }}" placeholder="{{ 'ACCESSION_REGISTER.SEARCH_BAR_PLACEHOLDER' | translate }}" - (search)="onSearchSubmit($event)" + (search)="onSearchSubmit()" + (searchChanged)="onSearchTextChanged($event)" + (clear)="onSearchTextChanged($event)" > <div class="d-flex div-btn-delete ml-3"> <button class="mat-boutton"> <button (click)="resetAdvancedSearch()" type="button" + title="{{ 'ACCESSION_REGISTER.DELETE_BUTTON_HOVER_MESSAGE' | translate }}" class="circle editable-field-cancel clickable"> <i class="material-icons primary-save-icon">delete</i> </button> diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.ts b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.ts index 5caa01c0976a1109c4d117f600e0f25a82115ef6..d9f5f8b898ed5253dafa41855b1a62b36deddd9a 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.ts +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.component.ts @@ -64,8 +64,11 @@ export class AccessionRegisterComponent extends SidenavPage<AccessionRegisterDet super.ngOnDestroy(); } - onSearchSubmit(search: string) { + onSearchTextChanged(search: string) { this.search = search; + } + + onSearchSubmit() { this.accessionRegistersService.setGlobalSearchButtonEvent(true); } diff --git a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.service.ts b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.service.ts index 31e40b6032634b39a7046e814c3bbeaad0377f4f..59c16151f2b8f8113c4eb6c19a4008657f5ac662 100644 --- a/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.service.ts +++ b/ui/ui-frontend/projects/referential/src/app/accession-register/accession-register.service.ts @@ -37,8 +37,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import {BehaviorSubject, EMPTY, Observable, of, Subject} from 'rxjs'; -import {catchError, exhaustMap, map, withLatestFrom} from 'rxjs/operators'; +import { BehaviorSubject, EMPTY, Observable, of, Subject } from 'rxjs'; +import { catchError, exhaustMap, map, withLatestFrom } from 'rxjs/operators'; import { AccessionRegisterDetail, AccessionRegisterStats, diff --git a/ui/ui-frontend/projects/referential/src/assets/i18n/en.json b/ui/ui-frontend/projects/referential/src/assets/i18n/en.json index 261a108b66bea7e91a1163d2907503cf201b7a71..bea90ad746296f56343c821cd4ef0a64045f6bed 100644 --- a/ui/ui-frontend/projects/referential/src/assets/i18n/en.json +++ b/ui/ui-frontend/projects/referential/src/assets/i18n/en.json @@ -100,17 +100,18 @@ "SEARCH_BAR_BUTTON_LABEL": "Search", "SEARCH_BAR_PLACEHOLDER": "Search for a producer service or an entry identifier", "ADVANCED_SEARCH_FILTERS_ACTIVATED": "Some advanced search filters are activated", + "DELETE_BUTTON_HOVER_MESSAGE": "Remove all advanced search filters", "LIST": { "NB_ENTRIES": "{{nb}} entries", - "DATE": "Pick-up date", - "OPERATION_IDENTIFIER": "Operation identifier", + "DATE": "Date", + "OPERATION_IDENTIFIER": "Technical operation identifier", "ORIGINATING_AGENCY": "Originating Agency", "ARCHIVAL_AGREEMENT": "Archival Agreement", "ACQUISITION_INFORMATION": "Acquisition Information", "TOTAL_UNITS": "Total initials units", "TOTAL_OBJECTS_GROUPS": "Total initials obj groups", "TOTAL_OBJECTS": "Total initials obj", - "OBJECT_SIZE_INGESTED": "Size", + "OBJECT_SIZE_INGESTED": "Initial Size", "STATUS": "Status", "NO_RESULT": "No result", "LOAD_MORE_RESULTS": "Load more results..." @@ -122,7 +123,7 @@ }, "FACETS": { "SHOW_SEARCH_FILTERS": "Show search filters", - "DATE_SEARCH_FILTER_LABEL": "Filter by operation date between", + "DATE_SEARCH_FILTER_LABEL": "Filter by pick-up date between", "TOTAL_OPERATION_ENTRIES": "All operations", "TOTAL_UNITS": "All units", "TOTAL_OBJECTS_GROUP": "All Objects Groups", diff --git a/ui/ui-frontend/projects/referential/src/assets/i18n/fr.json b/ui/ui-frontend/projects/referential/src/assets/i18n/fr.json index cf3a23489ee7b3ac8aecf7e39432a4d9f506d061..76dd58a29a546d5db0fc4c44c17c22611b90a610 100644 --- a/ui/ui-frontend/projects/referential/src/assets/i18n/fr.json +++ b/ui/ui-frontend/projects/referential/src/assets/i18n/fr.json @@ -99,18 +99,19 @@ "ACCESSION_REGISTER": "Registre des fonds", "SEARCH_BAR_BUTTON_LABEL": "Lancer une recherche", "SEARCH_BAR_PLACEHOLDER": "Rechercher par service producteur ou par identifiant de l'entrée", - "ADVANCED_SEARCH_FILTERS_ACTIVATED": "Des filtres de recherche avancée sont activées", + "ADVANCED_SEARCH_FILTERS_ACTIVATED": "Des filtres de recherche avancée sont activés", + "DELETE_BUTTON_HOVER_MESSAGE": "Supprimer tous les filtres de recherche avancée", "LIST": { "NB_ENTRIES": "{{nb}} entrées", - "DATE": "Date de prise en charge", - "OPERATION_IDENTIFIER": "Identifiant de l'entrée", + "DATE": "Date", + "OPERATION_IDENTIFIER": "Identifiant technique du versement", "ORIGINATING_AGENCY": "Service producteur", "ARCHIVAL_AGREEMENT": "Contrat d'entrée", "ACQUISITION_INFORMATION": "Modalité d'entrée", - "TOTAL_UNITS": "Nbre d’AU initiales", - "TOTAL_OBJECTS_GROUPS": "Nbre grpes d’objets initiaux", - "TOTAL_OBJECTS": "Nbre d’objets initiaux", - "OBJECT_SIZE_INGESTED": "Volumétrie", + "TOTAL_UNITS": "Nbre initial d’AU", + "TOTAL_OBJECTS_GROUPS": "Nbre initial grpes d’objets ", + "TOTAL_OBJECTS": "Nbre initial d’objets", + "OBJECT_SIZE_INGESTED": "Volumétrie initiale", "STATUS": "Statut", "NO_RESULT": "Aucun résultat", "LOAD_MORE_RESULTS": "Afficher plus de résultats..." @@ -123,7 +124,7 @@ "FACETS": { "SHOW_SEARCH_FILTERS": "Afficher les filtres de recherche", "HIDE_SEARCH_FILTERS": "Masquer les filtres de recherche", - "DATE_SEARCH_FILTER_LABEL": "Filter par date d'opération d'entrée entre le", + "DATE_SEARCH_FILTER_LABEL": "Filtrer par date de prise en charge entre le", "TOTAL_OPERATION_ENTRIES": "Toutes les opérations d'entrées", "TOTAL_UNITS": "Toutes les unités archivistiques", "TOTAL_OBJECTS_GROUP": "Tous les groupes d'objets",