-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* formatting and stub AdvancedClaimToOrgRoleMapper HardcodedOrgRoleMapper * config property definitions * grantOrgRole for hardcoded mapper * imports * added ability to add membership if the user is not already * test org membership before assigning roles/orgs * added saml * added saml mapper. refactored into a common interface * spi annotation to AdvancedAttributeToOrgRoleMapper
- Loading branch information
Showing
19 changed files
with
768 additions
and
127 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package io.phasetwo.service.broker; | ||
|
||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.Set; | ||
import lombok.extern.jbosslog.JBossLog; | ||
import org.keycloak.models.IdentityProviderSyncMode; | ||
|
||
@JBossLog | ||
public final class Mappers { | ||
|
||
public static final String ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME = | ||
"are.attribute.values.regex"; | ||
public static final String ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME = "are.claim.values.regex"; | ||
public static final String ATTRIBUTE_PROPERTY_NAME = "attributes"; | ||
public static final String CLAIM_PROPERTY_NAME = "claims"; | ||
public static final String ORG_ADD_PROPERTY_NAME = "org_add"; | ||
public static final String ORG_PROPERTY_NAME = "org"; | ||
public static final String ORG_ROLE_PROPERTY_NAME = "org_role"; | ||
|
||
public static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = | ||
new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); | ||
} |
181 changes: 181 additions & 0 deletions
181
src/main/java/io/phasetwo/service/broker/oidc/mappers/AdvancedClaimToOrgRoleMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package io.phasetwo.service.broker.oidc.mappers; | ||
|
||
import static io.phasetwo.service.broker.Mappers.*; | ||
import static org.keycloak.utils.RegexUtils.valueMatchesRegex; | ||
|
||
import com.google.auto.service.AutoService; | ||
import io.phasetwo.service.broker.OrgRoleMapper; | ||
import io.phasetwo.service.model.OrganizationModel; | ||
import io.phasetwo.service.model.OrganizationRoleModel; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import lombok.extern.jbosslog.JBossLog; | ||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; | ||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; | ||
import org.keycloak.broker.oidc.mappers.AbstractClaimMapper; | ||
import org.keycloak.broker.provider.BrokeredIdentityContext; | ||
import org.keycloak.broker.provider.IdentityProviderMapper; | ||
import org.keycloak.models.IdentityProviderMapperModel; | ||
import org.keycloak.models.IdentityProviderSyncMode; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.RealmModel; | ||
import org.keycloak.models.UserModel; | ||
import org.keycloak.provider.ProviderConfigProperty; | ||
|
||
@JBossLog | ||
@AutoService(IdentityProviderMapper.class) | ||
public class AdvancedClaimToOrgRoleMapper extends AbstractClaimMapper implements OrgRoleMapper { | ||
|
||
public static final String[] COMPATIBLE_PROVIDERS = { | ||
KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID | ||
}; | ||
|
||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>(); | ||
|
||
static { | ||
ProviderConfigProperty claimsProperty = new ProviderConfigProperty(); | ||
claimsProperty.setName(CLAIM_PROPERTY_NAME); | ||
claimsProperty.setLabel("Claims"); | ||
claimsProperty.setHelpText( | ||
"Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)"); | ||
claimsProperty.setType(ProviderConfigProperty.MAP_TYPE); | ||
configProperties.add(claimsProperty); | ||
ProviderConfigProperty isClaimValueRegexProperty = new ProviderConfigProperty(); | ||
isClaimValueRegexProperty.setName(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME); | ||
isClaimValueRegexProperty.setLabel("Regex Claim Values"); | ||
isClaimValueRegexProperty.setHelpText( | ||
"If enabled claim values are interpreted as regular expressions."); | ||
isClaimValueRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE); | ||
configProperties.add(isClaimValueRegexProperty); | ||
|
||
OrgRoleMapper.addOrgConfigProperties(configProperties); | ||
} | ||
|
||
public static final String PROVIDER_ID = "oidc-advanced-org-role-idp-mapper"; | ||
|
||
@Override | ||
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { | ||
return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); | ||
} | ||
|
||
@Override | ||
public List<ProviderConfigProperty> getConfigProperties() { | ||
return configProperties; | ||
} | ||
|
||
@Override | ||
public String getId() { | ||
return PROVIDER_ID; | ||
} | ||
|
||
@Override | ||
public String[] getCompatibleProviders() { | ||
return COMPATIBLE_PROVIDERS; | ||
} | ||
|
||
@Override | ||
public String getDisplayCategory() { | ||
return "Role Importer"; | ||
} | ||
|
||
@Override | ||
public String getDisplayType() { | ||
return "Advanced Claim to Org Role"; | ||
} | ||
|
||
@Override | ||
public String getHelpText() { | ||
return "If all claims exists, grant the user the specified organization role."; | ||
} | ||
|
||
protected boolean applies( | ||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { | ||
Map<String, List<String>> claims = mapperModel.getConfigMap(CLAIM_PROPERTY_NAME); | ||
boolean areClaimValuesRegex = | ||
Boolean.parseBoolean( | ||
mapperModel.getConfig().getOrDefault(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME, "false")); | ||
|
||
for (Map.Entry<String, List<String>> claim : claims.entrySet()) { | ||
Object claimValue = getClaimValue(context, claim.getKey()); | ||
for (String value : claim.getValue()) { | ||
boolean claimValuesMismatch = | ||
!(areClaimValuesRegex | ||
? valueMatchesRegex(value, claimValue) | ||
: valueEquals(value, claimValue)); | ||
if (claimValuesMismatch) { | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
@Override | ||
public void importNewUser( | ||
KeycloakSession session, | ||
RealmModel realm, | ||
UserModel user, | ||
IdentityProviderMapperModel mapperModel, | ||
BrokeredIdentityContext context) { | ||
OrganizationModel org = getOrg(session, realm, mapperModel); | ||
if (org == null) { | ||
return; | ||
} | ||
OrganizationRoleModel role = getOrgRole(session, realm, mapperModel); | ||
if (role == null) { | ||
return; | ||
} | ||
|
||
if (applies(mapperModel, context)) { | ||
addUserToOrg(org, user, mapperModel); | ||
grantOrgRole(org, role, user); | ||
} | ||
} | ||
|
||
@Override | ||
public void updateBrokeredUserLegacy( | ||
KeycloakSession session, | ||
RealmModel realm, | ||
UserModel user, | ||
IdentityProviderMapperModel mapperModel, | ||
BrokeredIdentityContext context) { | ||
OrganizationModel org = getOrg(session, realm, mapperModel); | ||
if (org == null) { | ||
return; | ||
} | ||
OrganizationRoleModel role = getOrgRole(session, realm, mapperModel); | ||
if (role == null) { | ||
return; | ||
} | ||
|
||
if (!applies(mapperModel, context)) { | ||
revokeOrgRole(org, role, user); | ||
} | ||
} | ||
|
||
@Override | ||
public void updateBrokeredUser( | ||
KeycloakSession session, | ||
RealmModel realm, | ||
UserModel user, | ||
IdentityProviderMapperModel mapperModel, | ||
BrokeredIdentityContext context) { | ||
OrganizationModel org = getOrg(session, realm, mapperModel); | ||
if (org == null) { | ||
return; | ||
} | ||
OrganizationRoleModel role = getOrgRole(session, realm, mapperModel); | ||
if (role == null) { | ||
return; | ||
} | ||
|
||
if (applies(mapperModel, context)) { | ||
addUserToOrg(org, user, mapperModel); | ||
grantOrgRole(org, role, user); | ||
} else { | ||
revokeOrgRole(org, role, user); | ||
} | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
src/main/java/io/phasetwo/service/broker/oidc/mappers/OrgRoleMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package io.phasetwo.service.broker; | ||
|
||
import static io.phasetwo.service.broker.Mappers.*; | ||
|
||
import com.google.common.base.Strings; | ||
import io.phasetwo.service.model.OrganizationModel; | ||
import io.phasetwo.service.model.OrganizationProvider; | ||
import io.phasetwo.service.model.OrganizationRoleModel; | ||
import java.util.List; | ||
import org.jboss.logging.Logger; | ||
import org.keycloak.models.IdentityProviderMapperModel; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.RealmModel; | ||
import org.keycloak.models.UserModel; | ||
import org.keycloak.provider.ProviderConfigProperty; | ||
|
||
public interface OrgRoleMapper { | ||
|
||
static final Logger log = Logger.getLogger(OrgRoleMapper.class); | ||
|
||
static void addOrgConfigProperties(List<ProviderConfigProperty> configProperties) { | ||
ProviderConfigProperty orgAdd = new ProviderConfigProperty(); | ||
orgAdd.setName(ORG_ADD_PROPERTY_NAME); | ||
orgAdd.setLabel("Add To Organization"); | ||
orgAdd.setHelpText("Add user to the organization as a member if not already."); | ||
orgAdd.setType(ProviderConfigProperty.BOOLEAN_TYPE); | ||
configProperties.add(orgAdd); | ||
ProviderConfigProperty org = new ProviderConfigProperty(); | ||
org.setName(ORG_PROPERTY_NAME); | ||
org.setLabel("Organization"); | ||
org.setHelpText("Organization containing the role to grant to user."); | ||
org.setType(ProviderConfigProperty.STRING_TYPE); | ||
configProperties.add(org); | ||
ProviderConfigProperty orgRole = new ProviderConfigProperty(); | ||
orgRole.setName(ORG_ROLE_PROPERTY_NAME); | ||
orgRole.setLabel("Organization Role"); | ||
orgRole.setHelpText("Organization role to grant to user."); | ||
orgRole.setType(ProviderConfigProperty.STRING_TYPE); | ||
configProperties.add(orgRole); | ||
} | ||
|
||
default boolean orgAdd(IdentityProviderMapperModel mapperModel) { | ||
return Boolean.parseBoolean( | ||
mapperModel.getConfig().getOrDefault(ORG_ADD_PROPERTY_NAME, "false")); | ||
} | ||
|
||
default void addUserToOrg( | ||
OrganizationModel org, UserModel user, IdentityProviderMapperModel mapperModel) { | ||
if (orgAdd(mapperModel) && !org.hasMembership(user)) { | ||
org.grantMembership(user); | ||
} | ||
} | ||
|
||
default OrganizationModel getOrg( | ||
KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel) { | ||
OrganizationProvider orgs = session.getProvider(OrganizationProvider.class); | ||
String orgName = mapperModel.getConfig().getOrDefault(ORG_PROPERTY_NAME, null); | ||
if (Strings.isNullOrEmpty(orgName)) return null; | ||
return orgs.getOrganizationByName(realm, orgName); | ||
} | ||
|
||
default OrganizationRoleModel getOrgRole( | ||
KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel) { | ||
OrganizationProvider orgs = session.getProvider(OrganizationProvider.class); | ||
String orgName = mapperModel.getConfig().getOrDefault(ORG_PROPERTY_NAME, null); | ||
String orgRoleName = mapperModel.getConfig().getOrDefault(ORG_ROLE_PROPERTY_NAME, null); | ||
if (Strings.isNullOrEmpty(orgName) || Strings.isNullOrEmpty(orgRoleName)) return null; | ||
else return getOrgRole(orgs, orgName, orgRoleName, realm); | ||
} | ||
|
||
default OrganizationRoleModel getOrgRole( | ||
OrganizationProvider orgs, String orgName, String orgRoleName, RealmModel realm) { | ||
OrganizationModel org = orgs.getOrganizationByName(realm, orgName); | ||
if (org == null) { | ||
log.debugf("Cannot map non-existent org %s", orgName); | ||
return null; | ||
} | ||
OrganizationRoleModel role = org.getRoleByName(orgRoleName); | ||
if (role == null) { | ||
log.debugf("Cannot map non-existent org role %s - %s", orgName, orgRoleName); | ||
return null; | ||
} | ||
return role; | ||
} | ||
|
||
default void grantOrgRole(OrganizationModel org, OrganizationRoleModel role, UserModel user) { | ||
if (org.hasMembership(user)) { | ||
role.grantRole(user); | ||
} | ||
} | ||
|
||
default void revokeOrgRole(OrganizationModel org, OrganizationRoleModel role, UserModel user) { | ||
if (org.hasMembership(user)) { | ||
role.revokeRole(user); | ||
} | ||
} | ||
|
||
default void grantOrgRole( | ||
KeycloakSession session, | ||
RealmModel realm, | ||
UserModel user, | ||
IdentityProviderMapperModel mapperModel) { | ||
OrganizationProvider orgs = session.getProvider(OrganizationProvider.class); | ||
String orgName = mapperModel.getConfig().getOrDefault(ORG_PROPERTY_NAME, null); | ||
String orgRoleName = mapperModel.getConfig().getOrDefault(ORG_ROLE_PROPERTY_NAME, null); | ||
if (Strings.isNullOrEmpty(orgName) || Strings.isNullOrEmpty(orgRoleName)) return; | ||
|
||
OrganizationModel org = orgs.getOrganizationByName(realm, orgName); | ||
OrganizationRoleModel role = getOrgRole(orgs, orgName, orgRoleName, realm); | ||
if (org != null && role != null) { | ||
if (orgAdd(mapperModel)) { | ||
if (!org.hasMembership(user)) { | ||
log.infof("Granting org: %s membership to %s", orgName, user.getUsername()); | ||
org.grantMembership(user); | ||
} | ||
} | ||
if (org.hasMembership(user)) { | ||
log.infof("Granting org: %s - role: %s to %s", orgName, orgRoleName, user.getUsername()); | ||
role.grantRole(user); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.