Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: apply password validation #8716

Merged
merged 3 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/admin/scim/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions docs/admin/usermgmt/usermgmt-scim.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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-server>/jans-scim/restv1/v2/Fido2Devices
curl -k -G -H 'Authorization: Bearer ACCESS_TOKEN' --data-urlencode
'filter=deviceData co "ios"' -d count=10 https://<jans-server>/jans-scim/restv1/v2/Fido2Devices
```

The response will be like:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"bulkMaxOperations": 30,
"bulkMaxPayloadSize": 3072000,
"userExtensionSchemaURI": "urn:ietf:params:scim:schemas:extension:gluu:2.0:User",
"skipDefinedPasswordValidation": false,

"useLocalCache":true,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<JansAttribute> 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down