From 232a6eb59fefd919275f01a3e6f8978fa66cfe56 Mon Sep 17 00:00:00 2001 From: Jose Gonzalez Date: Mon, 17 Jun 2024 00:14:28 -0500 Subject: [PATCH] feat: apply password validation (#8716) * docs: include new config property #8146 Signed-off-by: jgomer2001 * feat: apply password validation when defined and enabled #8146 Signed-off-by: jgomer2001 * chore: add new property to installation template #8146 Signed-off-by: jgomer2001 --------- Signed-off-by: jgomer2001 --- docs/admin/scim/config.md | 1 + docs/admin/usermgmt/usermgmt-scim.md | 5 +-- .../templates/jans-scim/dynamic-conf.json | 1 + .../scim/model/conf/AppConfiguration.java | 10 +++++ .../scim/service/scim2/Scim2UserService.java | 39 +++++++++++++++++++ .../jans/scim/ws/rs/scim2/UserWebService.java | 21 ++++++++-- 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/docs/admin/scim/config.md b/docs/admin/scim/config.md index b6545b269ec..efda80dd19f 100644 --- a/docs/admin/scim/config.md +++ b/docs/admin/scim/config.md @@ -15,6 +15,7 @@ Relevant configuration properties of the Jans SCIM server are summarized in the |bulkMaxOperations|30|Maximum number of operations admitted in a single bulk request| |bulkMaxPayloadSize|3072000|Maximum payload size in bytes admitted in a single bulk request| |userExtensionSchemaURI|`urn:ietf:params:scim:schemas:extension:gluu:2.0:User`|URI schema associated to the User Extension| +|skipDefinedPasswordValidation|false|Whether the validation rules defined for the password attribute in the server should be bypassed when a user is created/updated| |loggingLevel|`INFO`|The logging [level](./logs.md)| ## Configuration management using CLI diff --git a/docs/admin/usermgmt/usermgmt-scim.md b/docs/admin/usermgmt/usermgmt-scim.md index a0fe3da1275..f7f918c70be 100644 --- a/docs/admin/usermgmt/usermgmt-scim.md +++ b/docs/admin/usermgmt/usermgmt-scim.md @@ -388,8 +388,6 @@ Use the inum of our dummy user, **Jensen Barbara**. Check your LDAP or via Jans TUI to see that **Bjensen** is gone. - - ## How is SCIM data stored? SCIM [schema spec](https://datatracker.ietf.org/doc/html/rfc7643) does not use LDAP attribute names but a different naming convention for resource attributes (note this is not the case of custom attributes where the SCIM name used is that of the LDAP attribute). @@ -418,7 +416,8 @@ To distinguish between regular FIDO2 and SuperGluu devices, note only SuperGluu Say we are interested in having a list of Super Gluu devices users have enrolled and whose operating system is iOS. We may issue a query like this: ``` -curl -k -G -H 'Authorization: Bearer ACCESS_TOKEN' --data-urlencode 'filter=deviceData co "ios"' -d count=10 https:///jans-scim/restv1/v2/Fido2Devices +curl -k -G -H 'Authorization: Bearer ACCESS_TOKEN' --data-urlencode +'filter=deviceData co "ios"' -d count=10 https:///jans-scim/restv1/v2/Fido2Devices ``` The response will be like: diff --git a/jans-linux-setup/jans_setup/templates/jans-scim/dynamic-conf.json b/jans-linux-setup/jans_setup/templates/jans-scim/dynamic-conf.json index 6b7ffe17f58..1029453a647 100644 --- a/jans-linux-setup/jans_setup/templates/jans-scim/dynamic-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-scim/dynamic-conf.json @@ -13,6 +13,7 @@ "bulkMaxOperations": 30, "bulkMaxPayloadSize": 3072000, "userExtensionSchemaURI": "urn:ietf:params:scim:schemas:extension:gluu:2.0:User", + "skipDefinedPasswordValidation": false, "useLocalCache":true, diff --git a/jans-scim/model/src/main/java/io/jans/scim/model/conf/AppConfiguration.java b/jans-scim/model/src/main/java/io/jans/scim/model/conf/AppConfiguration.java index 84fe9abafcc..3465112fa6a 100644 --- a/jans-scim/model/src/main/java/io/jans/scim/model/conf/AppConfiguration.java +++ b/jans-scim/model/src/main/java/io/jans/scim/model/conf/AppConfiguration.java @@ -51,6 +51,8 @@ public class AppConfiguration implements Configuration, Serializable { private Boolean disableJdkLogger = true; @DocProperty(description = "Boolean value specifying whether to enable local in-memory cache") private Boolean useLocalCache = false; + @DocProperty(description = "Boolean value specifying whether to bypass the validation defined upon the password attribute") + private boolean skipDefinedPasswordValidation; public String getBaseDN() { return baseDN; @@ -197,4 +199,12 @@ public void setUseLocalCache(Boolean useLocalCache) { this.useLocalCache = useLocalCache; } + public boolean isSkipDefinedPasswordValidation() { + return skipDefinedPasswordValidation; + } + + public void setSkipDefinedPasswordValidation(boolean skipDefinedPasswordValidation) { + this.skipDefinedPasswordValidation = skipDefinedPasswordValidation; + } + } diff --git a/jans-scim/server/src/main/java/io/jans/scim/service/scim2/Scim2UserService.java b/jans-scim/server/src/main/java/io/jans/scim/service/scim2/Scim2UserService.java index 4111a43ccc0..f2c71a07121 100644 --- a/jans-scim/server/src/main/java/io/jans/scim/service/scim2/Scim2UserService.java +++ b/jans-scim/server/src/main/java/io/jans/scim/service/scim2/Scim2UserService.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import jakarta.annotation.PostConstruct; @@ -30,7 +31,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.jans.scim.model.conf.AppConfiguration; +import io.jans.model.attribute.AttributeValidation; import io.jans.model.GluuStatus; +import io.jans.model.JansAttribute; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.model.PagedResult; import io.jans.orm.model.SortOrder; @@ -619,6 +622,42 @@ public void removePPIDsBranch(String dn) { log.error(e.getMessage(), e); } } + + public boolean passwordValidationPassed(String password) { + + try { + Filter filter = Filter.createEqualityFilter("jansAttrName", "userPassword"); + List attrs = ldapEntryManager.findEntries("ou=attributes,o=jans", + JansAttribute.class, filter, new String[] { "jansValidation" }, 1); + + AttributeValidation av = attrs.get(0).getAttributeValidation(); + + if (av == null) return true; + + int len = Optional.ofNullable(av.getMinLength()).orElse(0); + if (len > 0 && password.length() < len) { + log.error("Password is required to have at least {} characters", len); + return false; + } + + len = Optional.ofNullable(av.getMaxLength()).orElse(0); + if (len > 0 && password.length() > len) { + log.error("Password is required to have at most {} characters", len); + return false; + } + + String regex = av.getRegexp(); + if (regex != null && !Pattern.matches(regex, password)) { + log.error("Provided password does not match the regular expression {}", regex); + return false; + } + + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return true; + + } @PostConstruct private void init() { diff --git a/jans-scim/server/src/main/java/io/jans/scim/ws/rs/scim2/UserWebService.java b/jans-scim/server/src/main/java/io/jans/scim/ws/rs/scim2/UserWebService.java index 50e8e4ec20f..383a8067873 100644 --- a/jans-scim/server/src/main/java/io/jans/scim/ws/rs/scim2/UserWebService.java +++ b/jans-scim/server/src/main/java/io/jans/scim/ws/rs/scim2/UserWebService.java @@ -96,6 +96,21 @@ private void checkUidExistence(String uid, String id) throws DuplicateEntryExcep } } + + private void executeUserValidation(UserResource user, boolean laxRequiredness) + throws SCIMException { + + executeValidation(user, laxRequiredness); + //See #8146 + if (!appConfiguration.isSkipDefinedPasswordValidation()) { + String pwd = user.getPassword(); + + if (pwd != null && !scim2UserService.passwordValidationPassed(pwd)) + throw new SCIMException("Supplied password not conformant to validation " + + "rules defined upon password attribute"); + } + + } private Response doSearch(String filter, Integer startIndex, Integer count, String sortBy, String sortOrder, String attrsList, String excludedAttrsList, String method) { @@ -148,7 +163,7 @@ public Response createUser( try { log.debug("Executing web service method. createUser"); - executeValidation(user); + executeUserValidation(user, false); if (StringUtils.isEmpty(user.getUserName())) throw new SCIMException("Empty username not allowed"); checkUidExistence(user.getUserName()); @@ -244,7 +259,7 @@ public Response updateUser( httpHeaders, uriInfo, HttpMethod.PUT, userResourceType); if (response != null) return response; - executeValidation(user, true); + executeUserValidation(user, true); if (StringUtils.isNotEmpty(user.getUserName())) { checkUidExistence(user.getUserName(), id); } @@ -389,7 +404,7 @@ public Response patchUser( //Throws exception if final representation does not pass overall validation log.debug("patchUser. Revising final resource representation still passes validations"); - executeValidation(user); + executeUserValidation(user, false); ScimResourceUtil.adjustPrimarySubAttributes(user); //Update timestamp