13 Commits

Author SHA1 Message Date
a3cb17e402 v1.1.3 pom 2021-08-24 21:15:19 -04:00
c6d0977b2f Merge branch 'develop' into stable 2021-08-24 21:13:44 -04:00
2405a8a313 v1.1.2 pom 2021-08-24 10:00:03 -04:00
173bfed44f Merge branch 'develop' into stable 2021-08-19 18:54:55 -04:00
dc5a7dad39 Merge branch 'develop' into stable 2021-08-19 17:50:01 -04:00
10ed99b0a2 v1.1.1 pom 2021-08-19 17:38:10 -04:00
4e4a6aca8d Merge branch 'develop' into stable 2021-08-19 17:24:24 -04:00
44d0bf533d Merge branch 'develop' into stable 2021-08-18 23:31:20 -04:00
807294881b v1.0.1 pom 2021-08-11 09:17:20 -04:00
a42c754a09 Merge branch 'develop' into stable 2021-08-11 09:08:26 -04:00
8b05c51ef6 Merge branch 'develop' into stable 2021-07-30 15:42:30 -04:00
8bc03e0ea9 Merge branch 'develop' into stable 2021-07-30 15:40:28 -04:00
d32e3c7051 v1.0.0 pom 2021-07-30 15:38:00 -04:00
7 changed files with 166 additions and 327 deletions

View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.inteligr8.activiti</groupId> <groupId>com.inteligr8.activiti</groupId>
<artifactId>keycloak-activiti-app-ext</artifactId> <artifactId>keycloak-activiti-app-ext</artifactId>
<version>1.2-SNAPSHOT</version> <version>1.1.3</version>
<name>Keycloak Authentication &amp; Authorization for APS</name> <name>Keycloak Authentication &amp; Authorization for APS</name>
<properties> <properties>
@@ -87,7 +87,7 @@
<distributionManagement> <distributionManagement>
<repository> <repository>
<id>inteligr8-releases</id> <id>inteligr8-releases</id>
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url> <url>https://repos.inteligr8.com/nexus/repository/inteligr8-private</url>
</repository> </repository>
<snapshotRepository> <snapshotRepository>
<id>inteligr8-snapshots</id> <id>inteligr8-snapshots</id>
@@ -95,4 +95,4 @@
</snapshotRepository> </snapshotRepository>
</distributionManagement> </distributionManagement>
</project> </project>

View File

@@ -1,128 +0,0 @@
package com.inteligr8.activiti;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.activiti.domain.idm.Group;
import com.activiti.domain.idm.GroupCapability;
import com.activiti.domain.idm.Tenant;
import com.activiti.service.api.GroupService;
/**
* This class/bean overrides the APS security configuration with a collection
* of implementations. The OOTB extension only provides one override. This
* uses that extension point, but delegates it out to multiple possible
* implementations.
*
* Order cannot be controlled, so it should not be assumed in any adapter
* implementation.
*
* @author brian@inteligr8.com
*/
@Component
public class ActivitiAppAdminGroupFixer implements DataFixer {
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(required = false)
private GroupService groupService;
@Autowired
private TenantFinderService tenantFinderService;
@Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId;
@Value("${keycloak-ext.group.admins.validate:false}")
private boolean validateAdministratorsGroup;
@Override
public void fix() {
this.logger.trace("fix()");
if (this.logger.isTraceEnabled())
this.logGroups();
if (this.validateAdministratorsGroup)
this.validateAdmins();
}
private void logGroups() {
if (this.groupService == null)
return;
Collection<Tenant> tenants = this.tenantFinderService.getTenants();
for (Tenant tenant : tenants) {
this.logger.trace("Tenant: {} => {}", tenant.getId(), tenant.getName());
this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(tenant.getId())));
this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(tenant.getId())));
}
this.logger.trace("Tenant: null");
this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(null)));
this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(null)));
}
private void validateAdmins() {
if (this.groupService == null)
return;
Long tenantId = this.tenantFinderService.findTenantId();
Group group = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group == null) {
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
if (!groups.isEmpty())
group = groups.iterator().next();
}
if (group == null) {
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 Collection<String> toGroupNames(Collection<Group> groups) {
List<String> groupNames = new ArrayList<>(groups.size());
for (Group group : groups)
groupNames.add(group.getName() + " [" + group.getExternalId() + "]");
return groupNames;
}
}

View File

@@ -1,86 +0,0 @@
package com.inteligr8.activiti;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.activiti.domain.idm.Group;
import com.activiti.domain.idm.User;
import com.activiti.service.api.GroupService;
import com.activiti.service.api.UserService;
/**
* This class/bean overrides the APS security configuration with a collection
* of implementations. The OOTB extension only provides one override. This
* uses that extension point, but delegates it out to multiple possible
* implementations.
*
* Order cannot be controlled, so it should not be assumed in any adapter
* implementation.
*
* @author brian@inteligr8.com
*/
@Component
public class ActivitiAppAdminMembersFixer implements DataFixer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired(required = false)
private UserService userService;
@Autowired(required = false)
private GroupService groupService;
@Autowired
private TenantFinderService tenantFinderService;
@Value("${keycloak-ext.default.admins.users:#{null}}")
private String adminUserStrs;
@Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId;
@Override
public void fix() {
this.logger.trace("fix()");
if (this.adminUserStrs != null && this.adminUserStrs.length() > 0)
this.associateAdmins();
}
private void associateAdmins() {
if (this.userService == null || this.groupService == null)
return;
List<String> adminUsers = Arrays.asList(this.adminUserStrs.split(","));
if (adminUsers.isEmpty())
return;
Long tenantId = this.tenantFinderService.findTenantId();
List<Group> groups;
Group group1 = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group1 != null) {
groups = Arrays.asList(group1);
} else {
groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
}
this.logger.debug("Found {} admin group(s)", groups.size());
for (String email : adminUsers) {
User user = this.userService.findUserByEmail(email);
this.logger.debug("Adding {} to admin group(s)", user.getEmail());
for (Group group : groups)
this.groupService.addUserToGroup(group, user);
}
}
}

View File

@@ -1,7 +0,0 @@
package com.inteligr8.activiti;
public interface DataFixer {
void fix();
}

View File

@@ -1,16 +1,31 @@
package com.inteligr8.activiti; package com.inteligr8.activiti;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
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;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; 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.GroupCapability;
import com.activiti.domain.idm.Tenant;
import com.activiti.domain.idm.User;
import com.activiti.service.api.GroupService;
import com.activiti.service.api.UserService;
import com.activiti.service.idm.TenantService;
import com.activiti.service.license.LicenseService;
/** /**
* This class/bean overrides the APS security configuration with a collection * This class/bean overrides the APS security configuration with a collection
@@ -28,11 +43,41 @@ 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;
@Autowired(required = false) @Autowired(required = false)
private List<DataFixer> fixers; private LicenseService licenseService;
@Autowired(required = false)
private TenantService tenantService;
@Autowired(required = false)
private UserService userService;
@Autowired(required = false)
private GroupService groupService;
@Value("${keycloak-ext.default.admins.users:#{null}}")
private String adminUserStrs;
@Value("${keycloak-ext.group.admins.name:admins}")
private String adminGroupName;
@Value("${keycloak-ext.group.admins.externalId:#{null}}")
private String adminGroupExternalId;
@Value("${keycloak-ext.group.admins.validate:false}")
private boolean validateAdministratorsGroup;
@Override @Override
public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) { public void configureGlobal(AuthenticationManagerBuilder authmanBuilder, UserDetailsService userDetailsService) {
@@ -40,10 +85,12 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
Collections.sort(this.adapters); Collections.sort(this.adapters);
if (this.fixers != null) { if (this.logger.isTraceEnabled())
for (DataFixer fixer : this.fixers) this.logGroups();
fixer.fix(); if (this.validateAdministratorsGroup)
} this.validateAdmins();
if (this.adminUserStrs != null && this.adminUserStrs.length() > 0)
this.associateAdmins();
for (ActivitiSecurityConfigAdapter adapter : this.adapters) { for (ActivitiSecurityConfigAdapter adapter : this.adapters) {
if (adapter.isEnabled()) { if (adapter.isEnabled()) {
@@ -55,5 +102,91 @@ public class Inteligr8SecurityConfigurationRegistry implements AlfrescoSecurityC
} }
} }
} }
private void logGroups() {
if (this.groupService == null)
return;
Long tenantId = this.findDefaultTenantId();
if (tenantId != null) {
// not first boot
this.logger.trace("Functional groups: {}", this.toGroupNames(this.groupService.getFunctionalGroups(tenantId)));
this.logger.trace("System groups: {}", this.toGroupNames(this.groupService.getSystemGroups(tenantId)));
}
}
private void validateAdmins() {
if (this.groupService == null)
return;
Long tenantId = this.findDefaultTenantId();
Group group = this.groupService.getGroupByExternalIdAndTenantId(this.adminGroupExternalId, tenantId);
if (group == null) {
List<Group> groups = this.groupService.getGroupByNameAndTenantId(this.adminGroupName, tenantId);
if (!groups.isEmpty())
group = groups.iterator().next();
}
if (group == null) {
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() {
if (this.userService == null || this.groupService == null)
return;
List<String> adminUsers = Arrays.asList(this.adminUserStrs.split(","));
if (adminUsers.isEmpty())
return;
Long tenantId = this.findDefaultTenantId();
List<Group> groups = this.groupService.getSystemGroupWithName("Administrators", tenantId);
for (String email : adminUsers) {
User user = this.userService.findUserByEmail(email);
this.logger.debug("Adding {} to {}", user.getEmail(), "Administrators");
for (Group group : groups)
this.groupService.addUserToGroup(group, user);
}
}
private Long findDefaultTenantId() {
String defaultTenantName = this.licenseService.getDefaultTenantName();
this.logger.trace("Default Tenant: {}", defaultTenantName);
List<Tenant> tenants = this.tenantService.findTenantsByName(defaultTenantName);
if (tenants == null || tenants.isEmpty()) {
this.logger.warn("Default tenant not found");
return null;
}
Tenant tenant = tenants.iterator().next();
return tenant.getId();
}
private Collection<String> toGroupNames(Collection<Group> groups) {
List<String> groupNames = new ArrayList<>(groups.size());
for (Group group : groups)
groupNames.add(group.getName() + " [" + group.getExternalId() + "]");
return groupNames;
}
} }

View File

@@ -1,82 +0,0 @@
package com.inteligr8.activiti;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.activiti.domain.idm.Tenant;
import com.activiti.service.idm.TenantService;
import com.activiti.service.license.LicenseService;
@Component
public class TenantFinderService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired(required = false)
private LicenseService licenseService;
@Autowired(required = false)
private TenantService tenantService;
@Value("${keycloak-ext.tenant:#{null}}")
private String tenant;
public Long findTenantId() {
Tenant tenant = this.findTenant();
return tenant == null ? null : tenant.getId();
}
public Tenant findTenant() {
this.logger.debug("Checking for a single tenant ...");
String tenantName = null;
if (this.tenant != null) {
tenantName = this.tenant;
} else {
List<Object[]> tenants = this.tenantService.getAllTenants();
if (tenants == null || tenants.isEmpty()) {
this.logger.warn("No tenants found!");
return null;
} else if (tenants.size() == 1) {
Object[] tenant = tenants.iterator().next();
this.logger.debug("Only one tenant available; selecting it: {}", tenant[0]);
return this.tenantService.getTenant((Long)tenant[0]);
} else {
tenantName = this.licenseService.getDefaultTenantName();
}
}
this.logger.debug("Trying to find by tenant name: {}", tenantName);
List<Tenant> tenants = this.tenantService.findTenantsByName(tenantName);
if (tenants == null || tenants.isEmpty()) {
this.logger.warn("Named tenant not found");
return null;
}
this.logger.debug("Found {} tenants with name {}; selecting the first one", tenants.size(), tenantName);
return tenants.iterator().next();
}
public Collection<Tenant> getTenants() {
List<Object[]> tenantObjs = this.tenantService.getAllTenants();
List<Tenant> tenants = new ArrayList<>(tenantObjs.size());
for (Object[] tenantObj : tenantObjs) {
if (tenantObj != null && tenantObj[0] != null) {
Tenant tenant = this.tenantService.getTenant((Long)tenantObj[0]);
tenants.add(tenant);
}
}
return tenants;
}
}

View File

@@ -21,10 +21,12 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.activiti.domain.idm.Group; import com.activiti.domain.idm.Group;
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;
import com.activiti.service.api.UserService; import com.activiti.service.api.UserService;
import com.inteligr8.activiti.TenantFinderService; import com.activiti.service.idm.TenantService;
import com.activiti.service.license.LicenseService;
/** /**
* This class/bean implements an Open ID Connect authenticator for Alfresco * This class/bean implements an Open ID Connect authenticator for Alfresco
@@ -46,6 +48,12 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*"); private final Pattern emailNamesPattern = Pattern.compile("([A-Za-z]+)[A-Za-z0-9]*\\.([A-Za-z]+)[A-Za-z0-9]*@.*");
@Autowired
private LicenseService licenseService;
@Autowired
private TenantService tenantService;
@Autowired @Autowired
private UserService userService; private UserService userService;
@@ -53,9 +61,6 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
@Autowired @Autowired
private GroupService groupService; private GroupService groupService;
@Autowired
private TenantFinderService tenantFinderService;
@Value("${keycloak-ext.external.id:ais}") @Value("${keycloak-ext.external.id:ais}")
protected String externalIdmSource; protected String externalIdmSource;
@@ -76,7 +81,7 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
*/ */
@Override @Override
public void preAuthenticate(Authentication auth) throws AuthenticationException { public void preAuthenticate(Authentication auth) throws AuthenticationException {
Long tenantId = this.tenantFinderService.findTenantId(); Long tenantId = this.findDefaultTenantId();
this.logger.trace("Tenant ID: {}", tenantId); this.logger.trace("Tenant ID: {}", tenantId);
User user = this.findUser(auth, tenantId); User user = this.findUser(auth, tenantId);
@@ -117,13 +122,27 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
*/ */
@Override @Override
public void postAuthenticate(Authentication auth) throws AuthenticationException { public void postAuthenticate(Authentication auth) throws AuthenticationException {
Long tenantId = this.tenantFinderService.findTenantId(); Long tenantId = this.findDefaultTenantId();
User user = this.findUser(auth, tenantId); User user = this.findUser(auth, tenantId);
this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId()); this.logger.debug("Inspecting user: {} => {}", user.getId(), user.getExternalId());
this.syncUserRoles(user, auth, tenantId); this.syncUserRoles(user, auth, tenantId);
} }
private Long findDefaultTenantId() {
String defaultTenantName = this.licenseService.getDefaultTenantName();
this.logger.trace("Default Tenant: {}", defaultTenantName);
List<Tenant> tenants = this.tenantService.findTenantsByName(defaultTenantName);
if (tenants == null || tenants.isEmpty()) {
this.logger.warn("Default tenant not found");
return null;
}
Tenant tenant = tenants.iterator().next();
return tenant.getId();
}
private User findUser(Authentication auth, Long tenantId) { private User findUser(Authentication auth, Long tenantId) {
String email = auth.getName(); String email = auth.getName();
@@ -174,18 +193,8 @@ public class KeycloakActivitiAppAuthenticator extends AbstractKeycloakActivitiAu
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 && this.removeMapEntriesByValue(roles, this.apsGroupExternalIdToKeycloakRole(group.getExternalId()))) { if (group.getExternalId() != null && this.removeMapEntriesByValue(roles, this.apsGroupExternalIdToKeycloakRole(group.getExternalId()))) {
if (group.getTenantId() == null) {
// fix stray groups
group.setTenantId(tenantId);
group.setLastUpdate(new Date());
this.groupService.save(group);
}
// role already existed and the user is already a member // role already existed and the user is already a member
} else if (group.getExternalId() == null && roles.remove(this.apsGroupNameToKeycloakRole(group.getName())) != null) { } else if (group.getExternalId() == null && roles.remove(this.apsGroupNameToKeycloakRole(group.getName())) != null) {
// register the group as external
group.setExternalId(this.keycloakRoleToApsGroupExternalId(this.apsGroupNameToKeycloakRole(group.getName())));
group.setLastUpdate(new Date());
this.groupService.save(group);
// internal role already existed and the user is already a member // 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 // at this point, we have a group that the user does not have a corresponding role for