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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLEGROUP;
import static org.entando.entando.keycloak.services.mapping.DynamicMappingKind.ROLEGROUPCLAIM;

import java.util.regex.Pattern;

import com.agiletec.aps.system.common.AbstractService;
import com.agiletec.aps.system.services.authorization.Authorization;
import com.agiletec.aps.system.services.authorization.AuthorizationManager;
Expand Down Expand Up @@ -37,6 +39,7 @@
import org.entando.entando.keycloak.services.mapping.DynamicMapping;
import org.entando.entando.keycloak.services.mapping.DynamicMappingElement;
import org.entando.entando.keycloak.services.mapping.DynamicMappingKind;
import org.entando.entando.keycloak.services.mapping.FallbackKind;
import org.entando.entando.keycloak.services.mapping.PersistKind;
import org.entando.entando.keycloak.services.oidc.OidcMappingHelper;
import org.entando.entando.keycloak.services.oidc.model.KeycloakUser;
Expand Down Expand Up @@ -102,6 +105,7 @@
List<String> ignore = new ArrayList<>();
List<String> roles = new ArrayList<>();
List<String> groups = new ArrayList<>();
List<String> excludeUsers = new ArrayList<>();
Boolean enabled = false;
PersistKind persist = PersistKind.FULL;

Expand Down Expand Up @@ -130,6 +134,8 @@
.orElseGet(List::of);
groups = ofNullable(dynConf.groups)
.orElse(List.of());
excludeUsers = ofNullable(dynConf.excludeUsers)
.orElse(List.of());
enabled = ofNullable(dynConf.enabled)
.orElse(false);
persist = ofNullable(dynConf.persist)
Expand All @@ -143,12 +149,12 @@
jwtMappings.forEach(m -> log.debug("jwt mapping active: {}", m.toString()));
}
// finally
KeycloakImportConfig cfg = new KeycloakImportConfig(profileMappings, jwtMappings, ignore, roles, groups, enabled, persist);
KeycloakImportConfig cfg = new KeycloakImportConfig(profileMappings, jwtMappings, ignore, roles, groups, enabled, persist, excludeUsers);

setImportConfiguration(cfg);
} catch (Exception e) {
log.error("Error initializing KeycloakAuthorizationManager", e);
KeycloakImportConfig cfg = new KeycloakImportConfig(List.of(), List.of(), List.of(), List.of(), List.of(), false, PersistKind.NONE);
KeycloakImportConfig cfg = new KeycloakImportConfig(List.of(), List.of(), List.of(), List.of(), List.of(), false, PersistKind.NONE, List.of());

setImportConfiguration(cfg);
throw e;
Expand Down Expand Up @@ -190,32 +196,50 @@
* @return true if the element is valid, false otherwise
*/
private boolean isValid(DynamicMappingElement elem) {
if (elem.kind == null) {
log.error("invalid dynamic mapping element, 'kind' is blank");
return false;
}
if (StringUtils.isBlank(elem.attribute) && !elem.kind.isJwtMapping()) {
log.error("invalid dynamic mapping element, 'attribute' is blank for kind {}", elem.kind);
return false;
}
if (StringUtils.isBlank(elem.path) && elem.kind.isJwtMapping()) {
log.error("invalid dynamic mapping element, 'path' is blank for {} kind", elem.kind);
return false;
}
if (StringUtils.isBlank(elem.separator) && (elem.kind == ROLEGROUP || elem.kind == ROLEGROUPCLAIM)) {
log.error("invalid dynamic mapping element, 'separator' is blank for {} kind", elem.kind);
try {
if (elem == null) {
log.error("invalid dynamic mapping element, element is null");
return false;
}
if (elem.kind == null) {
log.error("invalid dynamic mapping element, 'kind' is missing");
return false;
}
if (StringUtils.isBlank(elem.attribute) && !elem.kind.isJwtMapping()) {
log.error("invalid dynamic mapping element, 'attribute' is blank for kind {}", elem.kind);
return false;
}
if (StringUtils.isBlank(elem.path) && elem.kind.isJwtMapping()) {
log.error("invalid dynamic mapping element, 'path' is blank for {} kind", elem.kind);
return false;
}
if (StringUtils.isBlank(elem.separator) && (elem.kind == ROLEGROUP || elem.kind == ROLEGROUPCLAIM)) {
log.error("invalid dynamic mapping element, 'separator' is blank for {} kind", elem.kind);
return false;
}
if (elem.fallback != null && elem.kind != ROLEGROUP && elem.kind != ROLEGROUPCLAIM) {
log.warn("dynamic mapping element has 'fallback' set for kind {} which does not support it; "
+ "'fallback' is only applicable to {} and {}", elem.kind, ROLEGROUP, ROLEGROUPCLAIM);
}
return true;
} catch (Exception e) {
log.error("Unexpected error validating dynamic mapping element", e);
return false;
}
return true;
}

public void processNewUser(final UserDetails user, final String token, final boolean decode) {

Check failure on line 231 in keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=entando_app-engine&issues=AZ8DR7dQuGICQHVDvzlc&open=AZ8DR7dQuGICQHVDvzlc&pullRequest=341
processNewUser(user);
// safety net! Admin is exempted from group and roles assignment
if (ADMIN_USER_NAME.equals(user.getUsername())) return;

readLock.lock();
try {
final List<String> excludedUsers = getImportConfiguration().getExcludeUsers();
if (excludedUsers != null && user.getUsername() != null && excludedUsers.contains(user.getUsername())) {
log.debug("user {} is in the excludeUsers list, skipping dynamic sync", user.getUsername());
return;
}
if (!getImportConfiguration().getEnabled()) return;

// Authorizations coming from dynamic mapping (that is, external sources)
Expand Down Expand Up @@ -325,7 +349,7 @@
return jwtAuthorizations;
}

private List<Authorization> finalizeGroupRoleAssociation(KeycloakUser user, DynamicMappingElement elem, List<String> authorizations) {

Check failure on line 352 in keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakAuthorizationManager.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=entando_app-engine&issues=AZ8DR7dQuGICQHVDvzld&open=AZ8DR7dQuGICQHVDvzld&pullRequest=341
List<Authorization> result = new ArrayList<>();
if (authorizations == null) return result;

Expand All @@ -336,19 +360,18 @@
}

final String sep = StringUtils.isNotBlank(elem.separator) ? elem.separator : DEFAULT_SEPARATOR;
final String[] tokens = candidate.split(sep);

if (tokens.length < 2) {
// treat as a role
// result.addAll(finalizeRoleAssociation(user, elem, List.of(candidate.trim())));
if (!candidate.contains(sep)) {
result.addAll(applyFallbackForRoleGroup(user, elem, candidate.trim()));
continue;
}

final String[] tokens = candidate.split(Pattern.quote(sep), -1);
final String roleName = tokens[0].trim();
final String groupName = tokens[1].trim();
final String groupName = tokens.length > 1 ? tokens[1].trim() : "";

if (StringUtils.isBlank(roleName)) {
log.warn("Invalid role name extracted from candidate '{}' for user {}", candidate, user.getUsername());
if (StringUtils.isBlank(roleName) || StringUtils.isBlank(groupName)) {
log.warn("Blank role or group in token '{}' for user {}, discarding", candidate, user.getUsername());
continue;
}

Expand All @@ -363,6 +386,20 @@
return result;
}

private List<Authorization> applyFallbackForRoleGroup(KeycloakUser user, DynamicMappingElement elem, String token) {
if (elem.fallback == FallbackKind.IGNORE) {
log.warn("No separator in token '{}' for user {}, discarding (fallback=ignore)", token, user.getUsername());
return List.of();
}
if (elem.fallback == FallbackKind.GROUP) {
return finalizeGroupAssociation(user, elem, List.of(token));
}
if (elem.fallback == null) {
log.info("No separator in token '{}' for user {}, treating as role (implicit default fallback)", token, user.getUsername());
}
return finalizeRoleAssociation(user, elem, List.of(token));
}

private void processNewUser(final UserDetails user) {
if (StringUtils.isNotEmpty(KeycloackConfiguration.getDefaultAuthorizations())) {
// process group and role coming from the configuration
Expand Down Expand Up @@ -471,6 +508,10 @@
return result;
}
for (String groupRoleToken : authorizations) {
if (!groupRoleToken.contains(separator)) {
result.addAll(applyFallbackForRoleGroup(user, elem, groupRoleToken.trim()));
continue;
}
Authorization auth = parseAuthForRoleGroup(user, groupRoleToken, separator);
if (auth != null) {
result.add(auth);
Expand All @@ -483,18 +524,16 @@
}

private Authorization parseAuthForRoleGroup(KeycloakUser user, String groupRoleToken, String separator) {
final String[] tokens = groupRoleToken.split(separator);
final String[] tokens = groupRoleToken.split(Pattern.quote(separator), -1);

final String roleName = tokens[0].trim();
final String groupName = tokens.length > 1 ? tokens[1].trim() : "";

if (tokens.length != 2
|| StringUtils.isBlank(tokens[0])
|| StringUtils.isBlank(tokens[1])) {
log.error("invalid dynamic config configuration detected");
if (StringUtils.isBlank(roleName) || StringUtils.isBlank(groupName)) {
log.warn("Blank role or group in token '{}' for user {}, discarding", groupRoleToken, user.getUsername());
return null;
}

final String groupName = tokens[1];
final String roleName = tokens[0];

return finalizeAssociation(user, roleName, groupName, false);
}

Expand Down Expand Up @@ -553,11 +592,11 @@
}
// are they managed?
if (StringUtils.isNotBlank(roleName) && !getImportConfiguration().getRoles().contains(roleName)) {
log.info("Role {} is not managed. Skipping assignment for user {}", roleName, user.getUsername());
log.warn("Role {} is not in the roles allowlist. Skipping assignment for user {}", roleName, user.getUsername());
return null;
}
if (StringUtils.isNotBlank(groupName) && !getImportConfiguration().getGroups().contains(groupName)) {
log.info("Group {} is not managed. Skipping assignment for user {}", groupName, user.getUsername());
log.warn("Group {} is not in the groups allowlist. Skipping assignment for user {}", groupName, user.getUsername());
return null;
}
return createAuthorization(roleName, groupName, createRoleIfMissing);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
private transient List<String> groups;
private transient Boolean enabled;
private transient PersistKind persist;
private transient List<String> excludeUsers;

Check warning on line 23 in keycloak-plugin/src/main/java/org/entando/entando/keycloak/services/KeycloakImportConfig.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the "transient" modifier from this field.

See more on https://sonarcloud.io/project/issues?id=entando_app-engine&issues=AZ8DR7dzuGICQHVDvzle&open=AZ8DR7dzuGICQHVDvzle&pullRequest=341

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public class DynamicMapping {
@JacksonXmlElementWrapper(localName = "groups")
public List<String> groups;

@JacksonXmlElementWrapper(localName = "excludeUsers")
public List<String> excludeUsers;

public Boolean enabled;
public PersistKind persist;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class DynamicMappingElement {
public String attribute;
public DynamicMappingKind kind;
public String separator; // FOR ROLEGROUP and ROLEGROUPCLAIM ONLY
public FallbackKind fallback; // FOR ROLEGROUP and ROLEGROUPCLAIM ONLY
public String path; // FOR *CLAIM ONLY

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.entando.entando.keycloak.services.mapping;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.Arrays;

public enum FallbackKind {
ROLE("role"),
GROUP("group"),
IGNORE("ignore");

private final String kind;

FallbackKind(String kind) {
this.kind = kind;
}

@JsonValue
public String getXmlValue() {
return kind;
}

@JsonCreator
public static FallbackKind fromValue(String value) {
return Arrays.stream(values())
.filter(k -> k.kind.equalsIgnoreCase(value))
.findFirst()
.orElse(null);
}
}
Loading
Loading