allowing org/cap group types; other fixes

This commit is contained in:
2021-08-23 16:06:44 -04:00
parent 76066f01dd
commit a0cc13dc02
4 changed files with 145 additions and 57 deletions

View File

@@ -5,7 +5,9 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -17,6 +19,7 @@ import org.springframework.stereotype.Component;
import com.activiti.api.security.AlfrescoSecurityConfigOverride; import com.activiti.api.security.AlfrescoSecurityConfigOverride;
import com.activiti.domain.idm.Group; import com.activiti.domain.idm.Group;
import com.activiti.domain.idm.GroupCapability;
import com.activiti.domain.idm.Tenant; import com.activiti.domain.idm.Tenant;
import com.activiti.domain.idm.User; import com.activiti.domain.idm.User;
import com.activiti.service.api.GroupService; import com.activiti.service.api.GroupService;
@@ -40,6 +43,15 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final List<String> adminCapabilities = Arrays.asList(
"access-all-models-in-tenant",
"access-editor",
"access-reports",
"publish-app-to-dashboard",
"tenant-admin",
"tenant-admin-api",
"upload-license");
@Autowired @Autowired
private List<ActivitiSecurityConfigAdapter> adapters; private List<ActivitiSecurityConfigAdapter> adapters;
@@ -61,7 +73,7 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
@Value("${keycloak-ext.group.admins.name:admins}") @Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName; private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:aps-admin}") @Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId; private String adminGroupExternalId;
@Value("${keycloak-ext.group.admins.validate:false}") @Value("${keycloak-ext.group.admins.validate:false}")
@@ -108,15 +120,32 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
return; return;
Long tenantId = this.findDefaultTenantId(); Long tenantId = this.findDefaultTenantId();
Group group = this.groupService.getGroupByExternalId(this.adminGroupExternalId); Group group = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group == null) { if (group == null) {
this.logger.info("Creating '{}' group ...", this.adminGroupName); List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
group = this.groupService.createGroupFromExternalStore( if (!groups.isEmpty())
this.adminGroupExternalId, tenantId, Group.TYPE_SYSTEM_GROUP, null, this.adminGroupName, new Date()); group = groups.iterator().next();
} }
this.logger.info("Granting '{}' group all capabilities ...", group.getName()); if (group == null) {
this.groupService.addCapabilitiesToGroup(group.getId(), Arrays.asList("access-all-models-in-tenant", "access-editor", "access-reports", "publish-app-to-dashboard", "tenant-admin", "tenant-admin-api", "upload-license")); this.logger.info("Creating group: {} ({})", this.adminGroupName, this.adminGroupExternalId);
if (this.adminGroupExternalId != null) {
group = this.groupService.createGroupFromExternalStore(
this.adminGroupExternalId, tenantId, Group.TYPE_SYSTEM_GROUP, null, this.adminGroupName, new Date());
} else {
group = this.groupService.createGroup(this.adminGroupName, tenantId, Group.TYPE_SYSTEM_GROUP, null);
}
}
this.logger.debug("Checking group capabilities: {}", group.getName());
Group groupWithCaps = this.groupService.getGroup(group.getId(), false, true, false, false);
Set<String> adminCaps = new HashSet<>(this.adminCapabilities);
for (GroupCapability cap : groupWithCaps.getCapabilities())
adminCaps.remove(cap.getName());
if (!adminCaps.isEmpty()) {
this.logger.info("Granting group '{}' capabilities: {}", group.getName(), adminCaps);
this.groupService.addCapabilitiesToGroup(group.getId(), new ArrayList<>(adminCaps));
}
} }
private void associateAdmins() { private void associateAdmins() {

View File

@@ -3,6 +3,7 @@ package com.inteligr8.activiti.keycloak;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -34,8 +35,8 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
@Value("${keycloak-ext.createMissingUser:true}") @Value("${keycloak-ext.createMissingUser:true}")
protected boolean createMissingUser; protected boolean createMissingUser;
@Value("${keycloak-ext.clearNewUserGroups:true}") @Value("${keycloak-ext.clearNewUserDefaultGroups:true}")
protected boolean clearNewUserGroups; protected boolean clearNewUserDefaultGroups;
@Value("${keycloak-ext.createMissingGroup:true}") @Value("${keycloak-ext.createMissingGroup:true}")
protected boolean createMissingGroup; protected boolean createMissingGroup;
@@ -46,6 +47,9 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
@Value("${keycloak-ext.syncGroupRemove:true}") @Value("${keycloak-ext.syncGroupRemove:true}")
protected boolean syncGroupRemove; protected boolean syncGroupRemove;
@Value("${keycloak-ext.syncInternalGroups:false}")
protected boolean syncInternalGroups;
@Value("${keycloak-ext.resource.include.regex.patterns:#{null}}") @Value("${keycloak-ext.resource.include.regex.patterns:#{null}}")
protected String resourceRegexIncludes; protected String resourceRegexIncludes;
@@ -99,7 +103,7 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
protected Map<String, String> getRoles(Authentication auth) { protected Map<String, String> getKeycloakRoles(Authentication auth) {
Map<String, String> authorities = new HashMap<>(); Map<String, String> authorities = new HashMap<>();
AccessToken atoken = this.getKeycloakAccessToken(auth); AccessToken atoken = this.getKeycloakAccessToken(auth);
@@ -234,6 +238,24 @@ public abstract class AbstractKeycloakActivitiAuthenticator implements Authentic
} }
} }
protected <K, V> boolean removeMapEntriesByValue(Map<K, V> map, V value) {
if (value == null)
throw new IllegalArgumentException();
int found = 0;
Iterator<Entry<K, V>> i = map.entrySet().iterator();
while (i.hasNext()) {
Entry<K, V> entry = i.next();
if (entry.getValue() != null && value.equals(entry.getValue())) {
i.remove();
found++;
}
}
return found > 0;
}
protected Set<String> toSet(Collection<? extends GrantedAuthority> grantedAuthorities) { protected Set<String> toSet(Collection<? extends GrantedAuthority> grantedAuthorities) {
Set<String> authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16)); Set<String> authorities = new HashSet<>(Math.max(grantedAuthorities.size(), 16));
for (GrantedAuthority grantedAuthority : grantedAuthorities) { for (GrantedAuthority grantedAuthority : grantedAuthorities) {

View File

@@ -1,7 +1,6 @@
package com.inteligr8.activiti.keycloak; package com.inteligr8.activiti.keycloak;
import java.util.Date; import java.util.Date;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@@ -15,6 +14,7 @@ import org.keycloak.representations.AccessToken;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
@@ -62,6 +62,17 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
@Autowired @Autowired
private GroupService groupService; private GroupService groupService;
@Value("${keycloak-ext.syncGroupAs:organization}")
protected String syncGroupAs;
protected boolean syncGroupAsOrganization() {
return !this.syncGroupAsCapability();
}
protected boolean syncGroupAsCapability() {
return this.syncGroupAs != null && this.syncGroupAs.toLowerCase().startsWith("cap");
}
/** /**
* This method validates that the user exists, if not, it creates the * This method validates that the user exists, if not, it creates the
* missing user. Without this functionality, SSO straight up fails in APS. * missing user. Without this functionality, SSO straight up fails in APS.
@@ -79,16 +90,26 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
user = this.createUser(auth, tenantId); user = this.createUser(auth, tenantId);
this.logger.debug("Created user: {} => {}", user.getId(), user.getExternalId()); this.logger.debug("Created user: {} => {}", user.getId(), user.getExternalId());
if (this.clearNewUserGroups) { if (this.clearNewUserDefaultGroups) {
this.logger.debug("Clearing groups: {}", user.getId()); this.logger.debug("Clearing groups: {}", user.getId());
// fetch and remove default groups // fetch and remove default groups
user = this.userService.findUserByEmailFetchGroups(user.getEmail()); user = this.userService.getUser(user.getId(), true);
for (Group group : user.getGroups()) for (Group group : user.getGroups())
this.groupService.deleteUserFromGroup(group, user); this.groupService.deleteUserFromGroup(group, user);
} }
} else { } else {
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName()); this.logger.info("User does not exist; user creation is disabled: {}", auth.getName());
} }
} else if (user.getExternalOriginalSrc() == null || user.getExternalOriginalSrc().length() == 0) {
this.logger.debug("User exists, but not created by an external source: {}", auth.getName());
this.logger.info("Linking user '{}' with external IDM '{}'", auth.getName(), this.externalIdmSource);
user.setExternalId(auth.getName());
user.setExternalOriginalSrc(this.externalIdmSource);
this.userService.save(user);
} else if (!this.externalIdmSource.equals(user.getExternalOriginalSrc())) {
this.logger.debug("User '{}' exists, but created by another source: {}", auth.getName(), user.getExternalOriginalSrc());
} else {
this.logger.trace("User already exists: {}", auth.getName());
} }
} }
@@ -141,7 +162,7 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName()); Matcher emailNamesMatcher = this.emailNamesPattern.matcher(auth.getName());
if (!emailNamesMatcher.matches()) { if (!emailNamesMatcher.matches()) {
this.logger.warn("The email address could not be parsed for names: {}", auth.getName()); this.logger.warn("The email address could not be parsed for names: {}", auth.getName());
return this.userService.createNewUserFromExternalStore(auth.getName(), "Unknown", "User", tenantId, auth.getName(), this.externalIdmSource, new Date()); return this.userService.createNewUserFromExternalStore(auth.getName(), "Unknown", "Person", tenantId, auth.getName(), this.externalIdmSource, new Date());
} else { } else {
String firstName = StringUtils.capitalize(emailNamesMatcher.group(1)); String firstName = StringUtils.capitalize(emailNamesMatcher.group(1));
String lastName = StringUtils.capitalize(emailNamesMatcher.group(2)); String lastName = StringUtils.capitalize(emailNamesMatcher.group(2));
@@ -153,22 +174,28 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
} }
private void syncUserRoles(User user, Authentication auth, Long tenantId) { private void syncUserRoles(User user, Authentication auth, Long tenantId) {
Map<String, String> roles = this.getRoles(auth); Map<String, String> roles = this.getKeycloakRoles(auth);
if (roles == null) { if (roles == null) {
this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail()); this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
return; return;
} }
boolean syncAsOrg = this.syncGroupAsOrganization();
// check Activiti groups // check Activiti groups
User userWithGroups = this.userService.findUserByEmailFetchGroups(user.getEmail()); User userWithGroups = this.userService.getUser(user.getId(), true);
for (Group group : userWithGroups.getGroups()) { for (Group group : userWithGroups.getGroups()) {
if (group.getExternalId() == null && !this.syncInternalGroups)
continue;
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId()); this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId());
if (group.getExternalId() == null) { if (group.getExternalId() != null && this.removeMapEntriesByValue(roles, this.apsGroupExternalIdToKeycloakRole(group.getExternalId()))) {
// skip APS system groups // role already existed and the user is already a member
} else if (roles.remove(group.getExternalId()) != null) { } else if (group.getExternalId() == null && roles.remove(this.apsGroupNameToKeycloakRole(group.getName())) != null) {
// all good // internal role already existed and the user is already a member
} else { } else {
// at this point, we have a group that the user does not have a corresponding role for
if (this.syncGroupRemove) { if (this.syncGroupRemove) {
this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getName()); this.logger.trace("Removing user '{}' from group '{}'", user.getExternalId(), group.getName());
this.groupService.deleteUserFromGroup(group, userWithGroups); this.groupService.deleteUserFromGroup(group, userWithGroups);
@@ -184,20 +211,29 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
Group group; Group group;
try { try {
group = this.groupService.getGroupByExternalId(role.getKey()); group = this.groupService.getGroupByExternalIdAndTenantId(this.keycloakRoleToApsGroupExternalId(role.getKey()), tenantId);
} catch (NonUniqueResultException nure) { } catch (NonUniqueResultException nure) {
if (this.logger.isDebugEnabled()) { this.logger.warn("There are multiple groups with the external ID; not adding user to group: {}", role.getKey());
// FIXME only added to address a former bug continue;
group = this.fixMultipleGroups(role.getKey(), tenantId); }
} else {
throw nure; if (group == null) {
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.keycloakRoleToApsGroupName(role.getValue()), tenantId);
if (groups.size() > 1) {
this.logger.warn("There are multiple groups with the same name; not adding user to group: {}", role.getValue());
continue;
} else if (groups.size() == 1) {
group = groups.iterator().next();
} }
} }
if (group == null) { if (group == null) {
if (this.createMissingGroup) { if (this.createMissingGroup) {
this.logger.trace("Creating new group: {}", role); this.logger.trace("Creating new group: {}", role);
group = this.groupService.createGroupFromExternalStore(role.getValue(), tenantId, Group.TYPE_SYSTEM_GROUP, null, role.getKey(), new Date()); String name = this.keycloakRoleToApsGroupName(role.getValue());
String externalId = this.keycloakRoleToApsGroupExternalId(role.getKey());
int type = syncAsOrg ? Group.TYPE_FUNCTIONAL_GROUP : Group.TYPE_SYSTEM_GROUP;
group = this.groupService.createGroupFromExternalStore(name, tenantId, type, null, externalId, new Date());
} else { } else {
this.logger.debug("Group does not exist; group creation is disabled: {}", role); this.logger.debug("Group does not exist; group creation is disabled: {}", role);
} }
@@ -212,28 +248,21 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
} }
} }
private Group fixMultipleGroups(String externalId, Long tenantId) { private String keycloakRoleToApsGroupExternalId(String role) {
List<Group> groupsToDelete = new LinkedList<>(); return this.externalIdmSource + "_" + role;
Date earliestDate = new Date();
Group earliestGroup = null;
for (Group group : this.groupService.getSystemGroups(tenantId)) {
if (externalId.equals(group.getExternalId())) {
if (group.getLastUpdate().before(earliestDate)) {
if (earliestGroup != null)
groupsToDelete.add(earliestGroup);
earliestDate = group.getLastUpdate();
earliestGroup = group;
} else {
groupsToDelete.add(group);
}
}
} }
for (Group group : groupsToDelete) private String apsGroupExternalIdToKeycloakRole(String externalId) {
this.groupService.deleteGroup(group.getId()); int underscorePos = externalId.indexOf('_');
return underscorePos < 0 ? externalId : externalId.substring(underscorePos + 1);
}
return earliestGroup; private String keycloakRoleToApsGroupName(String role) {
return role;
}
private String apsGroupNameToKeycloakRole(String externalId) {
return externalId;
} }
} }

View File

@@ -48,7 +48,7 @@ public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivit
user = this.createUser(auth); user = this.createUser(auth);
this.logger.debug("Created user: {} => {}", user.getId(), user.getEmail()); this.logger.debug("Created user: {} => {}", user.getId(), user.getEmail());
if (this.clearNewUserGroups) { if (this.clearNewUserDefaultGroups) {
this.logger.debug("Clearing groups: {}", user.getId()); this.logger.debug("Clearing groups: {}", user.getId());
List<Group> groups = this.identityService.createGroupQuery() List<Group> groups = this.identityService.createGroupQuery()
.groupMember(user.getId()) .groupMember(user.getId())
@@ -93,7 +93,7 @@ public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivit
} }
private void syncUserRoles(User user, Authentication auth) { private void syncUserRoles(User user, Authentication auth) {
Map<String, String> roles = this.getRoles(auth); Map<String, String> roles = this.getKeycloakRoles(auth);
if (roles == null) { if (roles == null) {
this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail()); this.logger.debug("The user roles could not be determined; skipping sync: {}", user.getEmail());
return; return;
@@ -105,11 +105,11 @@ public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivit
.list(); .list();
this.logger.debug("User is currently a member of {} groups", groups.size()); this.logger.debug("User is currently a member of {} groups", groups.size());
for (Group group : groups) { for (Group group : groups) {
if (!group.getId().startsWith(this.groupPrefix)) if (!group.getId().startsWith(this.groupPrefix) && this.syncInternalGroups)
continue; continue;
this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType()); this.logger.trace("Inspecting group: {} => {} ({})", group.getId(), group.getName(), group.getType());
if (roles.remove(group.getId().substring(this.groupPrefix.length())) != null) { if (roles.remove(this.activitiGroupIdToKeycloakRole(group.getId())) != null) {
this.logger.trace("Group and membership already exist: {} => {}", user.getEmail(), group.getName()); this.logger.trace("Group and membership already exist: {} => {}", user.getEmail(), group.getName());
// already a member of the group // already a member of the group
} else { } else {
@@ -129,16 +129,16 @@ public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivit
this.logger.trace("Inspecting role: {}", role); this.logger.trace("Inspecting role: {}", role);
Group group = this.identityService.createGroupQuery() Group group = this.identityService.createGroupQuery()
.groupId(this.groupPrefix + role.getKey()) .groupId(this.keycloakRoleToActivitiGroupId(role.getKey()))
.singleResult(); .singleResult();
if (group == null) { if (group == null) {
if (this.createMissingGroup) { if (this.createMissingGroup) {
this.logger.trace("Group does not exist; creating one"); this.logger.trace("Group does not exist; creating one");
group = this.identityService.newGroup(this.groupPrefix + role.getKey()); group = this.identityService.newGroup(this.keycloakRoleToActivitiGroupId(role.getKey()));
group.setName(role.getValue()); group.setName(role.getValue());
this.identityService.saveGroup(group); this.identityService.saveGroup(group);
} else { } else {
this.logger.info("User does not exist; user creation is disabled: {}", auth.getName()); this.logger.info("Group does not exist; group creation is disabled: {}", role.getKey());
} }
} }
@@ -151,4 +151,12 @@ public class KeycloakActivitiEngineAuthenticator extends AbstractKeycloakActivit
} }
} }
private String keycloakRoleToActivitiGroupId(String role) {
return this.groupPrefix + role;
}
private String activitiGroupIdToKeycloakRole(String groupId) {
return groupId.startsWith(this.groupPrefix) ? groupId.substring(this.groupPrefix.length()) : groupId;
}
} }