diff --git a/README.md b/README.md index e79c3fa..ba576c0 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This extension requires the [`multiext-activiti-app-ext`](https://git.inteligr8. | Activiti App Extension | Activiti App | | --------------------------------------- | --------------- | | `keycloak-activiti-app-ext` v1.0 - v1.2 | v1.11.x | -| `keycloak-activiti-app-ext` v1.3 | v1.11.x - v2.x | +| `keycloak-activiti-app-ext` v1.3 - v1.4 | v1.11.x - v2.x | | `auth-activiti-app-ext` v2.0+ | v24.x+ | ## Configuration @@ -68,22 +68,22 @@ The following properties were added to increase the configurability of the built The following properties provide the core functionality of this extension. That is role synchronization. -| Property | Default | Description | -| ---------------------------------------------- | --------- | ----------- | -| `auth-ext.sync.user.createMissing` | `true` | If the user is authenticated, the user may be created in APS. | -| `auth-ext.sync.user.requireGroup` | | This is only applicable when `createMissing` is `true`. If this is unset or the OAuth Authorization Server gives the user the specified group/role, then the user record will be created in APS. | -| `auth-ext.sync.user.clearNewUserGroups` | `true` | This is only applicable when `createMissing` is `true`. All default APS groups will be deleted from the new user record. | -| `auth-ext.sync.group.createMissing` | `true` | If a filtered and translated OIDC group has no corresponding APS group, a group will be created in APS. See `auth-ext.sync.group.capabilities.patterns` for whether that group will be an APS Organization or APS Capability. | -| `auth-ext.sync.group.additions` | `true` | If the user isn't in an APS group but OAuth claims the OIDC group, then add them to it. | -| `auth-ext.sync.group.removals` | `true` | If the user is in APS group but OAuth claims no OIDC group, then remove them from it. | -| `auth-ext.sync.group.internal` | `false` | When considering groups for creation or user membership, include internal groups. Internal groups are ones without an `externalId`. | -| `auth-ext.sync.group.internal.externalize` | `false` | This is only applicable when `internal` is `true`. If an internal group is encountered during the operation of this extension, make it external with the current `externalId`. | -| `auth-ext.sync.group.tenantize` | `false` | If a group without a tenant is encountered during the operation of this extension, make it part of the selected tenant. | -| `auth-ext.sync.group.translate.patterns` | | A comma delimited set of regular expression patterns for the translation (reformatting) of authorities. | -| `auth-ext.sync.group.translate.replacements` | | A comma delimited set of regular expression replacement strings for the translation (reformatting) of authorities. | -| `auth-ext.sync.group.include.patterns` | | A comma delimited set of regular expression patterns on what authorities to include. This is processed before `translate` processing. A blank value includes everything. If anything is specified, then only matches could possibly be included; but could still be excluded explicitly. | -| `auth-ext.sync.group.exclude.patterns` | | A comma delimited set of regular expression patterns on what authorities to exclude. This is processed before `translate` processing. A blank value excludes nothing. If anything is specified and `include` is empty, then only matches will be excluded. If both are specified, `exclude` overrules `include` matches. | -| `auth-ext.sync.group.capabilities.patterns` | `Superusers` | A comma delimited set of regular expression patterns on what authorities to associate with APS Capabilities instead of APS Organizations (default). | +| Property | Default | Description | +| --------------------------------------------- | ------------ | ----------- | +| `auth-ext.sync.user.createMissing` | `true` | If the user is authenticated, the user may be created in APS. | +| `auth-ext.sync.user.requireOidcGroup` | | This is only applicable when `createMissing` is `true`. If this is unset or the OAuth Authorization Server gives the user the specified group/role, then the user record will be created in APS. | +| `auth-ext.sync.user.clearNewUserGroups` | `true` | This is only applicable when `createMissing` is `true`. All default APS groups will be deleted from the new user record. | +| `auth-ext.sync.group.createMissing` | `true` | If a filtered and translated OIDC group has no corresponding APS group, a group will be created in APS. See `auth-ext.sync.group.capabilities.patterns` for whether that group will be an APS Organization or APS Capability. | +| `auth-ext.sync.group.additions` | `true` | If the user isn't in an APS group but OAuth claims the OIDC group, then add them to it. | +| `auth-ext.sync.group.removals` | `true` | If the user is in APS group but OAuth claims no OIDC group, then remove them from it. | +| `auth-ext.sync.group.internal` | `false` | When considering groups for creation or user membership, include internal groups. Internal groups are ones without an `externalId`. | +| `auth-ext.sync.group.internal.externalize` | `false` | This is only applicable when `internal` is `true`. If an internal group is encountered during the operation of this extension, make it external with the current `externalId`. | +| `auth-ext.sync.group.tenantize` | `false` | If a group without a tenant is encountered during the operation of this extension, make it part of the selected tenant. | +| `auth-ext.sync.group.include.patterns` | | A comma delimited set of regular expression patterns on what OIDC groups to include. This is processed before `translate` processing. A blank value matches everything. If anything is specified, then only matches could possibly be included. Any matches of the `exclude` property patterns always override though. | +| `auth-ext.sync.group.exclude.patterns` | | A comma delimited set of regular expression patterns on what OIDC groups to exclude. This is processed before `translate` processing. A blank value matches nothing (includes all that pass the `include` constraint). If anything is specified and `include` is empty, then all non-matches are included. If both are specified, `exclude` matches override `include` matches. | +| `auth-ext.sync.group.translate.patterns` | | A comma delimited set of regular expression patterns for the translation (reformatting) of OIDC groups to APS groups. This list corresponds to the `replacements` property and must have the same number of commas. | +| `auth-ext.sync.group.translate.replacements` | | A comma delimited set of regular expression replacement strings for the translation (reformatting) of OIDC groups to APS groups. This list corresponds to the `patterns` property and must have the same number of commas. | +| `auth-ext.sync.group.capability.patterns` | `Superusers` | A comma delimited set of regular expression patterns on what translated OIDC groups to associate with APS Capability Groups instead of APS Organization Groups (default). This is processed after `translate` processing. | ### Authentication Data Fixers diff --git a/src/main/java/com/inteligr8/activiti/auth/service/GroupSyncService.java b/src/main/java/com/inteligr8/activiti/auth/service/GroupSyncService.java index 325ecc2..09b2588 100644 --- a/src/main/java/com/inteligr8/activiti/auth/service/GroupSyncService.java +++ b/src/main/java/com/inteligr8/activiti/auth/service/GroupSyncService.java @@ -139,9 +139,9 @@ public class GroupSyncService { this.logger.trace("Incoming OIDC groups: {}: {}", oidcUser.getEmail(), oidcGroups); oidcGroups = this.filterGroups(oidcGroups); - oidcGroups = this.translateGroups(oidcGroups); + Set translatedGroups = this.translateGroups(oidcGroups); - this.logger.debug("Filtered/translated OIDC groups: {}: {}", oidcUser.getEmail(), oidcGroups); + this.logger.debug("Filtered/translated OIDC groups: {}: {}", oidcUser.getEmail(), translatedGroups); long tenantId = this.tenantFinderService.findTenantId(); @@ -166,7 +166,7 @@ public class GroupSyncService { this.logger.trace("Inspecting APS group: {} => {} ({})", group.getId(), group.getName(), group.getExternalId()); if (group.getExternalId() != null) { - String oidcGroup = this.apsGroupExternalIdToOidcGroup(group.getExternalId()); + String translatedGroup = this.apsGroupExternalIdToTranslatedOidcGroup(group.getExternalId()); if (this.retenantUntenantedGroups && group.getTenantId() == null) { this.logger.warn("Moving tenant-less APS group to tenant: {} => {}", group.getName(), tenantId); @@ -175,20 +175,20 @@ public class GroupSyncService { this.groupService.save(group); } - if (oidcGroups.remove(oidcGroup)) { - this.logger.trace("User already belongs to APS group mapped to by OIDC group: {}: {} => {}", user.getExternalId(), oidcGroup, group.getName()); + if (translatedGroups.remove(translatedGroup)) { + this.logger.trace("User already belongs to APS group mapped to by (translated) OIDC group: {}: {} => {}", user.getExternalId(), translatedGroup, group.getName()); continue; } } else { - String oidcGroup = this.apsGroupNameToOidcGroup(group.getName()); + String translatedGroup = this.apsGroupNameToTranslatedOidcGroup(group.getName()); - if (oidcGroups.remove(oidcGroup)) { - this.logger.trace("User already belongs to APS group mapped to by OIDC group: {}: {} => {}", user.getExternalId(), oidcGroup, group.getName()); + if (translatedGroups.remove(translatedGroup)) { + this.logger.trace("User already belongs to APS group mapped to by (translated) OIDC group: {}: {} => {}", user.getExternalId(), translatedGroup, group.getName()); if (this.externalizeMatchingInternalGroups) { this.logger.warn("Classifying internal APS group as external: {} => {}", group.getName(), this.externalIdmSource); // register the group as external - group.setExternalId(this.oidcGroupToApsGroupExternalId(oidcGroup)); + group.setExternalId(this.translatedOidcGroupToApsGroupExternalId(translatedGroup)); group.setLastUpdate(new Date()); this.groupService.save(group); // internal role already existed and the user is already a member @@ -212,21 +212,21 @@ public class GroupSyncService { } // the user needs to be added to the remaining authorities - for (String oidcGroup : oidcGroups) { - this.logger.trace("Inspecting unaccounted for OIDC group: {}", oidcGroup); + for (String translatedGroup : translatedGroups) { + this.logger.trace("Inspecting unaccounted for (translated) OIDC group: {}", translatedGroup); Group group; try { - group = this.groupService.getGroupByExternalIdAndTenantId(this.oidcGroupToApsGroupExternalId(oidcGroup), tenantId); + group = this.groupService.getGroupByExternalIdAndTenantId(this.translatedOidcGroupToApsGroupExternalId(translatedGroup), tenantId); } catch (NonUniqueResultException nure) { - this.logger.warn("There are multiple groups matching the OIDC group for the external system: {} [{}]; skipping consideration of OIDC group", oidcGroup, this.externalIdmSource); + this.logger.warn("There are multiple groups matching the (translated) OIDC group for the external system: {} [{}]; skipping consideration of OIDC group", translatedGroup, this.externalIdmSource); continue; } if (group == null && this.syncInternalGroups) { - List groups = this.groupService.getGroupByNameAndTenantId(this.oidcGroupToApsGroupName(oidcGroup), tenantId); + List groups = this.groupService.getGroupByNameAndTenantId(this.translatedOidcGroupToApsGroupName(translatedGroup), tenantId); if (groups.size() > 1) { - this.logger.warn("There are multiple APS groups matching the OIDC group: {} [{}]; skipping consideration of OIDC group", oidcGroup, this.externalIdmSource); + this.logger.warn("There are multiple APS groups matching the (translated) OIDC group: {} [{}]; skipping consideration of OIDC group", translatedGroup, this.externalIdmSource); continue; } else if (groups.size() == 1) { group = groups.iterator().next(); @@ -234,7 +234,7 @@ public class GroupSyncService { if (this.externalizeMatchingInternalGroups) { this.logger.debug("Found an internal APS group; registering as external: {}", group.getName()); - group.setExternalId(this.oidcGroupToApsGroupExternalId(oidcGroup)); + group.setExternalId(this.translatedOidcGroupToApsGroupExternalId(translatedGroup)); group.setLastSyncTimeStamp(new Date()); group.setLastUpdate(new Date()); this.groupService.save(group); @@ -244,11 +244,11 @@ public class GroupSyncService { if (group == null) { if (!this.createMissing) { - this.logger.debug("APS Group does not exist for OIDC group; APS group creation is disabled; OIDC group will go unrecognized: {}", oidcGroup); + this.logger.debug("APS Group does not exist for (translated) OIDC group; APS group creation is disabled; OIDC group will go unrecognized: {}", translatedGroup); continue; } - group = this.createApsGroup(oidcGroup, tenantId); + group = this.createApsGroup(translatedGroup, tenantId); } if (this.syncAdditions) { @@ -261,13 +261,13 @@ public class GroupSyncService { } } - protected Group createApsGroup(String oidcGroup, long tenantId) { - this.logger.debug("APS Group does not exist for OIDC group; will attempt to create: {}", oidcGroup); - String name = this.oidcGroupToApsGroupName(oidcGroup); - String externalId = this.oidcGroupToApsGroupExternalId(oidcGroup); + protected Group createApsGroup(String translatedGroup, long tenantId) { + this.logger.debug("APS Group does not exist for (translated) OIDC group; will attempt to create: {}", translatedGroup); + String name = this.translatedOidcGroupToApsGroupName(translatedGroup); + String externalId = this.translatedOidcGroupToApsGroupExternalId(translatedGroup); - boolean syncAsOrg = this.isOidcGroupToBeOrganization(oidcGroup); - this.logger.trace("Creating new APS group as {}: {}", syncAsOrg ? "organization" : "capability", oidcGroup); + boolean syncAsOrg = this.isTranslatedOidcGroupToBeOrganization(translatedGroup); + this.logger.trace("Creating new APS group as {}: {}", syncAsOrg ? "organization" : "capability", translatedGroup); int type = syncAsOrg ? Group.TYPE_FUNCTIONAL_GROUP : Group.TYPE_SYSTEM_GROUP; Group apsGroup = this.groupService.createGroupFromExternalStore(name, tenantId, type, null, externalId, new Date()); @@ -331,29 +331,29 @@ public class GroupSyncService { return translatedGroups; } - private String oidcGroupToApsGroupExternalId(String group) { + private String translatedOidcGroupToApsGroupExternalId(String group) { return this.externalIdmSource + "_" + group; } - private String apsGroupExternalIdToOidcGroup(String externalId) { + private String apsGroupExternalIdToTranslatedOidcGroup(String externalId) { int underscorePos = externalId.indexOf('_'); return underscorePos < 0 ? externalId : externalId.substring(underscorePos + 1); } - private String oidcGroupToApsGroupName(String group) { + private String translatedOidcGroupToApsGroupName(String group) { return group; } - private String apsGroupNameToOidcGroup(String externalId) { + private String apsGroupNameToTranslatedOidcGroup(String externalId) { return externalId; } - private boolean isOidcGroupToBeOrganization(String role) { + private boolean isTranslatedOidcGroupToBeOrganization(String translatedGroup) { if (this.capabilities.isEmpty()) return true; for (Pattern regex : this.capabilities) { - Matcher matcher = regex.matcher(role); + Matcher matcher = regex.matcher(translatedGroup); if (matcher.matches()) return false; } diff --git a/src/main/java/com/inteligr8/activiti/auth/service/UserSyncService.java b/src/main/java/com/inteligr8/activiti/auth/service/UserSyncService.java index b0b3874..48f1763 100644 --- a/src/main/java/com/inteligr8/activiti/auth/service/UserSyncService.java +++ b/src/main/java/com/inteligr8/activiti/auth/service/UserSyncService.java @@ -42,7 +42,7 @@ public class UserSyncService { @Value("${auth-ext.sync.user.createMissing:true}") protected boolean createMissingUser; - @Value("${auth-ext.sync.user.requireGroup:#{null}}") + @Value("${auth-ext.sync.user.requireOidcGroup:#{null}}") protected String requiredGroup; @Value("${auth-ext.sync.user.clearNewUserGroups:true}")