Merge remote-tracking branch 'upstream/master' into prod
This commit is contained in:
commit
c5fc4c0805
308 changed files with 13180 additions and 2957 deletions
10
.travis.yml
10
.travis.yml
|
@ -1,10 +1,18 @@
|
|||
language: java
|
||||
|
||||
env:
|
||||
global:
|
||||
- MAVEN_SKIP_RC=true
|
||||
- MAVEN_OPTS="-Xms512m -Xmx2048m"
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
|
||||
before_script:
|
||||
- export MAVEN_SKIP_RC=true
|
||||
|
||||
install:
|
||||
- travis_wait mvn install -Pdistribution -DskipTests=true -B -V -q
|
||||
- mvn install -Pdistribution -DskipTests=true -B -V -q
|
||||
|
||||
script:
|
||||
- mvn test -B
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package org.keycloak.broker.provider;
|
||||
|
||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -20,7 +17,7 @@ import java.util.List;
|
|||
*/
|
||||
public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
||||
public static final String ROLE = "role";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
|
@ -32,34 +29,6 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
|||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String[] parseRole(String role) {
|
||||
int scopeIndex = role.lastIndexOf('.');
|
||||
if (scopeIndex > -1) {
|
||||
String appName = role.substring(0, scopeIndex);
|
||||
role = role.substring(scopeIndex + 1);
|
||||
String[] rtn = {appName, role};
|
||||
return rtn;
|
||||
} else {
|
||||
String[] rtn = {null, role};
|
||||
return rtn;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static RoleModel getRoleFromString(RealmModel realm, String roleName) {
|
||||
String[] parsedRole = parseRole(roleName);
|
||||
RoleModel role = null;
|
||||
if (parsedRole[0] == null) {
|
||||
role = realm.getRole(parsedRole[1]);
|
||||
} else {
|
||||
ClientModel client = realm.getClientByClientId(parsedRole[0]);
|
||||
role = client.getRole(parsedRole[1]);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
|
@ -93,7 +62,7 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper {
|
|||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(ROLE);
|
||||
RoleModel role = getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -80,7 +81,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper {
|
|||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
|
@ -90,7 +91,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper {
|
|||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!hasClaimValue(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
|
@ -85,7 +86,7 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper {
|
|||
JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN);
|
||||
//if (token == null) return;
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
String[] parseRole = HardcodedRoleMapper.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE));
|
||||
String[] parseRole = KeycloakModelUtils.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE));
|
||||
String externalRoleName = parseRole[1];
|
||||
String claimName = null;
|
||||
if (parseRole[0] == null) {
|
||||
|
@ -95,7 +96,7 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper {
|
|||
}
|
||||
Object claim = getClaimValue(token, claimName);
|
||||
if (valueEquals(externalRoleName, claim)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
return role;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
|
||||
|
@ -95,7 +96,7 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper {
|
|||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.grantRole(role);
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper {
|
|||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedRoleMapper.ROLE);
|
||||
if (!isAttributePresent(mapperModel, context)) {
|
||||
RoleModel role = HardcodedRoleMapper.getRoleFromString(realm, roleName);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
|
||||
user.deleteRoleMapping(role);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ import java.util.Properties;
|
|||
*/
|
||||
public class Version {
|
||||
public static final String UNKNOWN = "UNKNOWN";
|
||||
public static String NAME;
|
||||
public static String NAME_HTML;
|
||||
public static String VERSION;
|
||||
public static String RESOURCES_VERSION;
|
||||
public static String BUILD_TIME;
|
||||
|
@ -21,6 +23,8 @@ public class Version {
|
|||
InputStream is = Version.class.getResourceAsStream("/keycloak-version.properties");
|
||||
try {
|
||||
props.load(is);
|
||||
Version.NAME = props.getProperty("name");
|
||||
Version.NAME_HTML = props.getProperty("name-html");
|
||||
Version.VERSION = props.getProperty("version");
|
||||
Version.BUILD_TIME = props.getProperty("build-time");
|
||||
Version.RESOURCES_VERSION = Version.VERSION.toLowerCase();
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
name=${product.name}
|
||||
name-html=${product.name-html}
|
||||
version=${product.version}
|
||||
build-time=${product.build-time}
|
|
@ -66,19 +66,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
|||
} else {
|
||||
initEmbedded();
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
if (cacheManager.getCacheConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) == null) {
|
||||
logger.debugf("No configuration provided for '%s' cache. Using '%s' configuration as template",
|
||||
InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, InfinispanConnectionProvider.SESSION_CACHE_NAME);
|
||||
|
||||
Configuration sessionCacheConfig = cacheManager.getCacheConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME);
|
||||
if (sessionCacheConfig != null) {
|
||||
ConfigurationBuilder confBuilder = new ConfigurationBuilder().read(sessionCacheConfig);
|
||||
Configuration offlineSessionConfig = confBuilder.build();
|
||||
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +126,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
|||
|
||||
Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
|
||||
cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
|
||||
cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration);
|
||||
cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,50 @@
|
|||
<column name="REALM_ID" type="VARCHAR(36)"/>
|
||||
<column name="DESCRIPTION" type="VARCHAR(255)"/>
|
||||
<column name="PROTOCOL" type="VARCHAR(255)"/>
|
||||
<column name="FULL_SCOPE_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="CONSENT_REQUIRED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="STANDARD_FLOW_ENABLED" type="BOOLEAN" defaultValueBoolean="true">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="IMPLICIT_FLOW_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="DIRECT_ACCESS_GRANTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="SERVICE_ACCOUNTS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="FRONTCHANNEL_LOGOUT" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="BEARER_ONLY" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="PUBLIC_CLIENT" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="CLIENT_TEMPLATE_ATTRIBUTES">
|
||||
<column name="TEMPLATE_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="VALUE" type="VARCHAR(2048)"/>
|
||||
<column name="NAME" type="VARCHAR(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
<createTable tableName="TEMPLATE_SCOPE_MAPPING">
|
||||
<column name="TEMPLATE_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="ROLE_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
|
||||
|
@ -24,6 +68,15 @@
|
|||
<column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
<column name="USE_TEMPLATE_CONFIG" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="USE_TEMPLATE_SCOPE" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="USE_TEMPLATE_MAPPERS" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="PROTOCOL_MAPPER">
|
||||
<column name="CLIENT_TEMPLATE_ID" type="VARCHAR(36)">
|
||||
|
@ -46,6 +99,11 @@
|
|||
<addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="CLIENT" constraintName="FK_CLI_TMPLT_CLIENT" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
|
||||
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_RLM" referencedColumnNames="ID" referencedTableName="REALM"/>
|
||||
<addForeignKeyConstraint baseColumnNames="CLIENT_TEMPLATE_ID" baseTableName="REALM_CLIENT_TEMPLATE" constraintName="FK_RLM_CLI_TMPLT_CLI" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
|
||||
<addPrimaryKey columnNames="TEMPLATE_ID, ROLE_ID" constraintName="PK_TEMPLATE_SCOPE" tableName="TEMPLATE_SCOPE_MAPPING"/>
|
||||
<addForeignKeyConstraint baseColumnNames="TEMPLATE_ID" baseTableName="TEMPLATE_SCOPE_MAPPING" constraintName="FK_TEMPL_SCOPE_TEMPL" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
|
||||
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="TEMPLATE_SCOPE_MAPPING" constraintName="FK_TEMPL_SCOPE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
|
||||
<addPrimaryKey columnNames="TEMPLATE_ID, NAME" constraintName="PK_CL_TMPL_ATTR" tableName="CLIENT_TEMPLATE_ATTRIBUTES"/>
|
||||
<addForeignKeyConstraint baseColumnNames="TEMPLATE_ID" baseTableName="CLIENT_TEMPLATE_ATTRIBUTES" constraintName="FK_CL_TEMPL_ATTR_TEMPL" referencedColumnNames="ID" referencedTableName="CLIENT_TEMPLATE"/>
|
||||
|
||||
|
||||
</changeSet>
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.ClientTemplateEntity</class>
|
||||
<class>org.keycloak.models.jpa.entities.TemplateScopeMappingEntity</class>
|
||||
|
||||
<!-- JpaAuditProviders -->
|
||||
<class>org.keycloak.events.jpa.EventEntity</class>
|
||||
|
|
|
@ -37,6 +37,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
|
|||
"org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoClientTemplateEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
|
||||
"org.keycloak.models.mongo.keycloak.entities.MongoOnlineUserSessionEntity",
|
||||
|
|
|
@ -41,6 +41,10 @@ public class ClientRepresentation {
|
|||
protected Map<String, Integer> registeredNodes;
|
||||
protected List<ProtocolMapperRepresentation> protocolMappers;
|
||||
protected String clientTemplate;
|
||||
private Boolean useTemplateConfig;
|
||||
private Boolean useTemplateScope;
|
||||
private Boolean useTemplateMappers;
|
||||
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
|
@ -298,4 +302,29 @@ public class ClientRepresentation {
|
|||
public void setClientTemplate(String clientTemplate) {
|
||||
this.clientTemplate = clientTemplate;
|
||||
}
|
||||
|
||||
public Boolean isUseTemplateConfig() {
|
||||
return useTemplateConfig;
|
||||
}
|
||||
|
||||
public void setUseTemplateConfig(Boolean useTemplateConfig) {
|
||||
this.useTemplateConfig = useTemplateConfig;
|
||||
}
|
||||
|
||||
public Boolean isUseTemplateScope() {
|
||||
return useTemplateScope;
|
||||
}
|
||||
|
||||
public void setUseTemplateScope(Boolean useTemplateScope) {
|
||||
this.useTemplateScope = useTemplateScope;
|
||||
}
|
||||
|
||||
public Boolean isUseTemplateMappers() {
|
||||
return useTemplateMappers;
|
||||
}
|
||||
|
||||
public void setUseTemplateMappers(Boolean useTemplateMappers) {
|
||||
this.useTemplateMappers = useTemplateMappers;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,6 +16,17 @@ public class ClientTemplateRepresentation {
|
|||
protected String name;
|
||||
protected String description;
|
||||
protected String protocol;
|
||||
protected Boolean fullScopeAllowed;
|
||||
protected Boolean bearerOnly;
|
||||
protected Boolean consentRequired;
|
||||
protected Boolean standardFlowEnabled;
|
||||
protected Boolean implicitFlowEnabled;
|
||||
protected Boolean directAccessGrantsEnabled;
|
||||
protected Boolean serviceAccountsEnabled;
|
||||
protected Boolean publicClient;
|
||||
protected Boolean frontchannelLogout;
|
||||
protected Map<String, String> attributes;
|
||||
|
||||
protected List<ProtocolMapperRepresentation> protocolMappers;
|
||||
|
||||
public String getId() {
|
||||
|
@ -58,4 +69,84 @@ public class ClientTemplateRepresentation {
|
|||
public void setProtocol(String protocol) {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
public Boolean isFullScopeAllowed() {
|
||||
return fullScopeAllowed;
|
||||
}
|
||||
|
||||
public void setFullScopeAllowed(Boolean fullScopeAllowed) {
|
||||
this.fullScopeAllowed = fullScopeAllowed;
|
||||
}
|
||||
|
||||
public Boolean isBearerOnly() {
|
||||
return bearerOnly;
|
||||
}
|
||||
|
||||
public void setBearerOnly(Boolean bearerOnly) {
|
||||
this.bearerOnly = bearerOnly;
|
||||
}
|
||||
|
||||
public Boolean isConsentRequired() {
|
||||
return consentRequired;
|
||||
}
|
||||
|
||||
public void setConsentRequired(Boolean consentRequired) {
|
||||
this.consentRequired = consentRequired;
|
||||
}
|
||||
|
||||
public Boolean isStandardFlowEnabled() {
|
||||
return standardFlowEnabled;
|
||||
}
|
||||
|
||||
public void setStandardFlowEnabled(Boolean standardFlowEnabled) {
|
||||
this.standardFlowEnabled = standardFlowEnabled;
|
||||
}
|
||||
|
||||
public Boolean isImplicitFlowEnabled() {
|
||||
return implicitFlowEnabled;
|
||||
}
|
||||
|
||||
public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
|
||||
this.implicitFlowEnabled = implicitFlowEnabled;
|
||||
}
|
||||
|
||||
public Boolean isDirectAccessGrantsEnabled() {
|
||||
return directAccessGrantsEnabled;
|
||||
}
|
||||
|
||||
public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) {
|
||||
this.directAccessGrantsEnabled = directAccessGrantsEnabled;
|
||||
}
|
||||
|
||||
public Boolean isServiceAccountsEnabled() {
|
||||
return serviceAccountsEnabled;
|
||||
}
|
||||
|
||||
public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
|
||||
this.serviceAccountsEnabled = serviceAccountsEnabled;
|
||||
}
|
||||
|
||||
public Boolean isPublicClient() {
|
||||
return publicClient;
|
||||
}
|
||||
|
||||
public void setPublicClient(Boolean publicClient) {
|
||||
this.publicClient = publicClient;
|
||||
}
|
||||
|
||||
public Boolean isFrontchannelLogout() {
|
||||
return frontchannelLogout;
|
||||
}
|
||||
|
||||
public void setFrontchannelLogout(Boolean frontchannelLogout) {
|
||||
this.frontchannelLogout = frontchannelLogout;
|
||||
}
|
||||
|
||||
public Map<String, String> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, String> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import java.util.*;
|
|||
public class RealmRepresentation {
|
||||
protected String id;
|
||||
protected String realm;
|
||||
protected String displayName;
|
||||
protected String displayNameHtml;
|
||||
protected Integer notBefore;
|
||||
protected Boolean revokeRefreshToken;
|
||||
protected Integer accessTokenLifespan;
|
||||
|
@ -129,6 +131,22 @@ public class RealmRepresentation {
|
|||
this.realm = realm;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayNameHtml() {
|
||||
return displayNameHtml;
|
||||
}
|
||||
|
||||
public void setDisplayNameHtml(String displayNameHtml) {
|
||||
this.displayNameHtml = displayNameHtml;
|
||||
}
|
||||
|
||||
public List<UserRepresentation> getUsers() {
|
||||
return users;
|
||||
}
|
||||
|
@ -624,11 +642,6 @@ public class RealmRepresentation {
|
|||
identityProviders.add(identityProviderRepresentation);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean isIdentityFederationEnabled() {
|
||||
return identityProviders != null && !identityProviders.isEmpty();
|
||||
}
|
||||
|
||||
public List<ProtocolMapperRepresentation> getProtocolMappers() {
|
||||
return protocolMappers;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package org.keycloak.representations.idm;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserFederationMapperSyncConfigRepresentation {
|
||||
|
||||
private Boolean fedToKeycloakSyncSupported;
|
||||
private String fedToKeycloakSyncMessage; // applicable just if fedToKeycloakSyncSupported is true
|
||||
|
||||
private Boolean keycloakToFedSyncSupported;
|
||||
private String keycloakToFedSyncMessage; // applicable just if keycloakToFedSyncSupported is true
|
||||
|
||||
public UserFederationMapperSyncConfigRepresentation() {
|
||||
}
|
||||
|
||||
public UserFederationMapperSyncConfigRepresentation(boolean fedToKeycloakSyncSupported, String fedToKeycloakSyncMessage,
|
||||
boolean keycloakToFedSyncSupported, String keycloakToFedSyncMessage) {
|
||||
this.fedToKeycloakSyncSupported = fedToKeycloakSyncSupported;
|
||||
this.fedToKeycloakSyncMessage = fedToKeycloakSyncMessage;
|
||||
this.keycloakToFedSyncSupported = keycloakToFedSyncSupported;
|
||||
this.keycloakToFedSyncMessage = keycloakToFedSyncMessage;
|
||||
}
|
||||
|
||||
public Boolean isFedToKeycloakSyncSupported() {
|
||||
return fedToKeycloakSyncSupported;
|
||||
}
|
||||
|
||||
public void setFedToKeycloakSyncSupported(Boolean fedToKeycloakSyncSupported) {
|
||||
this.fedToKeycloakSyncSupported = fedToKeycloakSyncSupported;
|
||||
}
|
||||
|
||||
public String getFedToKeycloakSyncMessage() {
|
||||
return fedToKeycloakSyncMessage;
|
||||
}
|
||||
|
||||
public void setFedToKeycloakSyncMessage(String fedToKeycloakSyncMessage) {
|
||||
this.fedToKeycloakSyncMessage = fedToKeycloakSyncMessage;
|
||||
}
|
||||
|
||||
public Boolean isKeycloakToFedSyncSupported() {
|
||||
return keycloakToFedSyncSupported;
|
||||
}
|
||||
|
||||
public void setKeycloakToFedSyncSupported(Boolean keycloakToFedSyncSupported) {
|
||||
this.keycloakToFedSyncSupported = keycloakToFedSyncSupported;
|
||||
}
|
||||
|
||||
public String getKeycloakToFedSyncMessage() {
|
||||
return keycloakToFedSyncMessage;
|
||||
}
|
||||
|
||||
public void setKeycloakToFedSyncMessage(String keycloakToFedSyncMessage) {
|
||||
this.keycloakToFedSyncMessage = keycloakToFedSyncMessage;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
package org.keycloak.representations.idm;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -12,7 +14,10 @@ public class UserFederationMapperTypeRepresentation {
|
|||
protected String category;
|
||||
protected String helpText;
|
||||
|
||||
protected UserFederationMapperSyncConfigRepresentation syncConfig;
|
||||
protected List<ConfigPropertyRepresentation> properties = new LinkedList<>();
|
||||
protected Map<String, String> defaultConfig = new HashMap<>();
|
||||
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
|
@ -46,6 +51,14 @@ public class UserFederationMapperTypeRepresentation {
|
|||
this.helpText = helpText;
|
||||
}
|
||||
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return syncConfig;
|
||||
}
|
||||
|
||||
public void setSyncConfig(UserFederationMapperSyncConfigRepresentation syncConfig) {
|
||||
this.syncConfig = syncConfig;
|
||||
}
|
||||
|
||||
public List<ConfigPropertyRepresentation> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
@ -53,4 +66,12 @@ public class UserFederationMapperTypeRepresentation {
|
|||
public void setProperties(List<ConfigPropertyRepresentation> properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public Map<String, String> getDefaultConfig() {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
public void setDefaultConfig(Map<String, String> defaultConfig) {
|
||||
this.defaultConfig = defaultConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package org.keycloak.representations.info;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ClientInstallationRepresentation {
|
||||
protected String id;
|
||||
protected String protocol;
|
||||
protected boolean downloadOnly;
|
||||
protected String displayType;
|
||||
protected String helpText;
|
||||
protected String filename;
|
||||
protected String mediaType;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public void setProtocol(String protocol) {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
public boolean isDownloadOnly() {
|
||||
return downloadOnly;
|
||||
}
|
||||
|
||||
public void setDownloadOnly(boolean downloadOnly) {
|
||||
this.downloadOnly = downloadOnly;
|
||||
}
|
||||
|
||||
public String getDisplayType() {
|
||||
return displayType;
|
||||
}
|
||||
|
||||
public void setDisplayType(String displayType) {
|
||||
this.displayType = displayType;
|
||||
}
|
||||
|
||||
public String getHelpText() {
|
||||
return helpText;
|
||||
}
|
||||
|
||||
public void setHelpText(String helpText) {
|
||||
this.helpText = helpText;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public String getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
public void setMediaType(String mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
}
|
9
core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java
Normal file → Executable file
9
core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java
Normal file → Executable file
|
@ -24,6 +24,7 @@ public class ServerInfoRepresentation {
|
|||
|
||||
private Map<String, List<ProtocolMapperTypeRepresentation>> protocolMapperTypes;
|
||||
private Map<String, List<ProtocolMapperRepresentation>> builtinProtocolMappers;
|
||||
private Map<String, List<ClientInstallationRepresentation>> clientInstallations;
|
||||
|
||||
private Map<String, List<String>> enums;
|
||||
|
||||
|
@ -105,4 +106,12 @@ public class ServerInfoRepresentation {
|
|||
public void setEnums(Map<String, List<String>> enums) {
|
||||
this.enums = enums;
|
||||
}
|
||||
|
||||
public Map<String, List<ClientInstallationRepresentation>> getClientInstallations() {
|
||||
return clientInstallations;
|
||||
}
|
||||
|
||||
public void setClientInstallations(Map<String, List<ClientInstallationRepresentation>> clientInstallations) {
|
||||
this.clientInstallations = clientInstallations;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
<local-cache name="realms"/>
|
||||
<local-cache name="users"/>
|
||||
<local-cache name="sessions"/>
|
||||
<local-cache name="offlineSessions"/>
|
||||
<local-cache name="loginFailures"/>
|
||||
</cache-container>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
|
|
|
@ -1,4 +1,26 @@
|
|||
<build xmlns="urn:wildfly:feature-pack-build:1.0">
|
||||
<!--
|
||||
~ JBoss, Home of Professional Open Source.
|
||||
~ Copyright 2014, Red Hat, Inc., and individual contributors
|
||||
~ as indicated by the @author tags. See the copyright.txt file in the
|
||||
~ distribution for a full listing of individual contributors.
|
||||
~
|
||||
~ This is free software; you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU Lesser General Public License as
|
||||
~ published by the Free Software Foundation; either version 2.1 of
|
||||
~ the License, or (at your option) any later version.
|
||||
~
|
||||
~ This software is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
~ Lesser General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Lesser General Public
|
||||
~ License along with this software; if not, write to the Free
|
||||
~ Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
-->
|
||||
|
||||
<build xmlns="urn:wildfly:feature-pack-build:1.1">
|
||||
<dependencies>
|
||||
<artifact name="org.wildfly:wildfly-feature-pack" />
|
||||
</dependencies>
|
||||
|
@ -8,20 +30,31 @@
|
|||
<property name="jgroups.supplement" value="" />
|
||||
</standalone>
|
||||
<domain template="configuration/domain/template.xml" subsystems="configuration/domain/subsystems.xml" output-file="domain/configuration/domain.xml" />
|
||||
<host template="configuration/host/host.xml" subsystems="configuration/host/subsystems.xml" output-file="domain/configuration/host.xml" />
|
||||
<host template="configuration/host/host-master.xml" subsystems="configuration/host/subsystems.xml" output-file="domain/configuration/host-master.xml" />
|
||||
<host template="configuration/host/host-slave.xml" subsystems="configuration/host/subsystems.xml" output-file="domain/configuration/host-slave.xml" />
|
||||
</config>
|
||||
|
||||
<mkdirs>
|
||||
<dir name="domain/data/content"/>
|
||||
<dir name="standalone/lib/ext"/>
|
||||
<dir name="domain/tmp/auth"/>
|
||||
<dir name="standalone/tmp/auth"/>
|
||||
<dir name=".installation"/>
|
||||
</mkdirs>
|
||||
<file-permissions>
|
||||
<permission value="755">
|
||||
<filter pattern="*.sh" include="true"/>
|
||||
<filter pattern="*" include="false"/>
|
||||
</permission>
|
||||
<permission value="700">
|
||||
<filter pattern="*/tmp/auth" include="true"/>
|
||||
<filter pattern="*" include="false"/>
|
||||
</permission>
|
||||
<permission value="600">
|
||||
<filter pattern="*-users.properties" include="true" />
|
||||
<filter pattern="*/.installation" include="true"/>
|
||||
<filter pattern="*" include="false"/>
|
||||
</permission>
|
||||
</file-permissions>
|
||||
<line-endings>
|
||||
|
|
|
@ -53,63 +53,4 @@
|
|||
<subsystem supplement="ha">undertow.xml</subsystem>
|
||||
<subsystem>keycloak-server.xml</subsystem>
|
||||
</subsystems>
|
||||
<subsystems name="full">
|
||||
<!-- Each subsystem to be included relative to the src/main/resources directory -->
|
||||
<subsystem>logging.xml</subsystem>
|
||||
<subsystem>bean-validation.xml</subsystem>
|
||||
<subsystem>keycloak-datasources.xml</subsystem>
|
||||
<subsystem supplement="full">ee.xml</subsystem>
|
||||
<subsystem supplement="full">ejb3.xml</subsystem>
|
||||
<subsystem>io.xml</subsystem>
|
||||
<subsystem>keycloak-infinispan.xml</subsystem>
|
||||
<subsystem>iiop-openjdk.xml</subsystem>
|
||||
<subsystem>jaxrs.xml</subsystem>
|
||||
<subsystem>jca.xml</subsystem>
|
||||
<subsystem>jdr.xml</subsystem>
|
||||
<subsystem supplement="domain">jmx.xml</subsystem>
|
||||
<subsystem>jpa.xml</subsystem>
|
||||
<subsystem>jsf.xml</subsystem>
|
||||
<subsystem>jsr77.xml</subsystem>
|
||||
<subsystem>mail.xml</subsystem>
|
||||
<subsystem>messaging.xml</subsystem>
|
||||
<subsystem>naming.xml</subsystem>
|
||||
<subsystem>remoting.xml</subsystem>
|
||||
<subsystem>request-controller.xml</subsystem>
|
||||
<subsystem>security.xml</subsystem>
|
||||
<subsystem>security-manager.xml</subsystem>
|
||||
<subsystem>transactions.xml</subsystem>
|
||||
<subsystem>undertow.xml</subsystem>
|
||||
<subsystem>keycloak-server.xml</subsystem>
|
||||
</subsystems>
|
||||
<subsystems name="full-ha">
|
||||
<!-- Each subsystem to be included relative to the src/main/resources directory -->
|
||||
<subsystem>logging.xml</subsystem>
|
||||
<subsystem>bean-validation.xml</subsystem>
|
||||
<subsystem>keycloak-datasources.xml</subsystem>
|
||||
<subsystem supplement="full">ee.xml</subsystem>
|
||||
<subsystem supplement="full-ha">ejb3.xml</subsystem>
|
||||
<subsystem>io.xml</subsystem>
|
||||
<subsystem supplement="ha">keycloak-infinispan.xml</subsystem>
|
||||
<subsystem>iiop-openjdk.xml</subsystem>
|
||||
<subsystem>jaxrs.xml</subsystem>
|
||||
<subsystem>jca.xml</subsystem>
|
||||
<subsystem>jdr.xml</subsystem>
|
||||
<subsystem>jgroups.xml</subsystem>
|
||||
<subsystem supplement="domain">jmx.xml</subsystem>
|
||||
<subsystem>jpa.xml</subsystem>
|
||||
<subsystem>jsf.xml</subsystem>
|
||||
<subsystem>jsr77.xml</subsystem>
|
||||
<subsystem>mail.xml</subsystem>
|
||||
<subsystem supplement="ha">messaging.xml</subsystem>
|
||||
<subsystem>mod_cluster.xml</subsystem>
|
||||
<subsystem>naming.xml</subsystem>
|
||||
<subsystem>remoting.xml</subsystem>
|
||||
<subsystem>resource-adapters.xml</subsystem>
|
||||
<subsystem>request-controller.xml</subsystem>
|
||||
<subsystem>security.xml</subsystem>
|
||||
<subsystem>security-manager.xml</subsystem>
|
||||
<subsystem>transactions.xml</subsystem>
|
||||
<subsystem supplement="ha">undertow.xml</subsystem>
|
||||
<subsystem>keycloak-server.xml</subsystem>
|
||||
</subsystems>
|
||||
</config>
|
||||
|
|
|
@ -30,12 +30,6 @@
|
|||
<profile name="ha">
|
||||
<?SUBSYSTEMS socket-binding-group="ha-sockets"?>
|
||||
</profile>
|
||||
<profile name="full">
|
||||
<?SUBSYSTEMS socket-binding-group="full-sockets"?>
|
||||
</profile>
|
||||
<profile name="full-ha">
|
||||
<?SUBSYSTEMS socket-binding-group="full-ha-sockets"?>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<!--
|
||||
|
@ -60,28 +54,20 @@
|
|||
<!-- Needed for server groups using the 'ha' profile -->
|
||||
<?SOCKET-BINDINGS?>
|
||||
</socket-binding-group>
|
||||
<socket-binding-group name="full-sockets" default-interface="public">
|
||||
<!-- Needed for server groups using the 'full' profile -->
|
||||
<?SOCKET-BINDINGS?>
|
||||
</socket-binding-group>
|
||||
<socket-binding-group name="full-ha-sockets" default-interface="public">
|
||||
<!-- Needed for server groups using the 'full-ha' profile -->
|
||||
<?SOCKET-BINDINGS?>
|
||||
</socket-binding-group>
|
||||
</socket-binding-groups>
|
||||
|
||||
<server-groups>
|
||||
<server-group name="main-server-group" profile="full">
|
||||
<server-group name="main-server-group" profile="default">
|
||||
<jvm name="default">
|
||||
<heap size="64m" max-size="512m"/>
|
||||
</jvm>
|
||||
<socket-binding-group ref="full-sockets"/>
|
||||
<socket-binding-group ref="standard-sockets"/>
|
||||
</server-group>
|
||||
<server-group name="other-server-group" profile="full-ha">
|
||||
<server-group name="other-server-group" profile="ha">
|
||||
<jvm name="default">
|
||||
<heap size="64m" max-size="512m"/>
|
||||
</jvm>
|
||||
<socket-binding-group ref="full-ha-sockets"/>
|
||||
<socket-binding-group ref="ha-sockets"/>
|
||||
</server-group>
|
||||
</server-groups>
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
|
||||
<!--
|
||||
A simple configuration for a Host Controller that only acts as the master domain controller
|
||||
and does not itself directly control any servers.
|
||||
-->
|
||||
<host name="master" xmlns="urn:jboss:domain:4.0">
|
||||
<extensions>
|
||||
<?EXTENSIONS?>
|
||||
</extensions>
|
||||
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="host-file" formatter="json-formatter" relative-to="jboss.domain.data.dir" path="audit-log.log"/>
|
||||
<file-handler name="server-file" formatter="json-formatter" relative-to="jboss.server.data.dir" path="audit-log.log"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="host-file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
<server-logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="server-file"/>
|
||||
</handlers>
|
||||
</server-logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<native-interface security-realm="ManagementRealm">
|
||||
<socket interface="management" port="${jboss.management.native.port:9999}"/>
|
||||
</native-interface>
|
||||
<http-interface security-realm="ManagementRealm" http-upgrade-enabled="true">
|
||||
<socket interface="management" port="${jboss.management.http.port:9990}"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
</management>
|
||||
|
||||
<domain-controller>
|
||||
<local/>
|
||||
</domain-controller>
|
||||
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
|
||||
<jvms>
|
||||
<jvm name="default">
|
||||
<heap size="64m" max-size="256m"/>
|
||||
<jvm-options>
|
||||
<option value="-server"/>
|
||||
</jvm-options>
|
||||
</jvm>
|
||||
</jvms>
|
||||
|
||||
<profile>
|
||||
<?SUBSYSTEMS socket-binding-group="standard-sockets"?>
|
||||
</profile>
|
||||
|
||||
</host>
|
|
@ -0,0 +1,101 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
|
||||
<host xmlns="urn:jboss:domain:4.0">
|
||||
<extensions>
|
||||
<?EXTENSIONS?>
|
||||
</extensions>
|
||||
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<server-identities>
|
||||
<!-- Replace this with either a base64 password of your own, or use a vault with a vault expression -->
|
||||
<secret value="c2xhdmVfdXNlcl9wYXNzd29yZA=="/>
|
||||
</server-identities>
|
||||
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="host-file" formatter="json-formatter" relative-to="jboss.domain.data.dir" path="audit-log.log"/>
|
||||
<file-handler name="server-file" formatter="json-formatter" relative-to="jboss.server.data.dir" path="audit-log.log"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="host-file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
<server-logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="server-file"/>
|
||||
</handlers>
|
||||
</server-logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<native-interface security-realm="ManagementRealm">
|
||||
<socket interface="management" port="${jboss.management.native.port:9999}"/>
|
||||
</native-interface>
|
||||
</management-interfaces>
|
||||
</management>
|
||||
|
||||
<domain-controller>
|
||||
<remote security-realm="ManagementRealm">
|
||||
<discovery-options>
|
||||
<static-discovery name="primary" protocol="${jboss.domain.master.protocol:remote}" host="${jboss.domain.master.address}" port="${jboss.domain.master.port:9999}"/>
|
||||
</discovery-options>
|
||||
</remote>
|
||||
</domain-controller>
|
||||
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
|
||||
<?INTERFACES?>
|
||||
|
||||
</interfaces>
|
||||
|
||||
<jvms>
|
||||
<jvm name="default">
|
||||
<heap size="64m" max-size="256m"/>
|
||||
<jvm-options>
|
||||
<option value="-server"/>
|
||||
</jvm-options>
|
||||
</jvm>
|
||||
</jvms>
|
||||
|
||||
<servers>
|
||||
<server name="server-one" group="main-server-group"/>
|
||||
<server name="server-two" group="other-server-group">
|
||||
<!-- server-two avoids port conflicts by incrementing the ports in
|
||||
the default socket-group declared in the server-group -->
|
||||
<socket-bindings port-offset="150"/>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
<profile>
|
||||
<?SUBSYSTEMS socket-binding-group="standard-sockets"?>
|
||||
</profile>
|
||||
</host>
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
|
||||
<host name="master" xmlns="urn:jboss:domain:4.0">
|
||||
<extensions>
|
||||
<?EXTENSIONS?>
|
||||
</extensions>
|
||||
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.domain.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="host-file" formatter="json-formatter" relative-to="jboss.domain.data.dir" path="audit-log.log"/>
|
||||
<file-handler name="server-file" formatter="json-formatter" relative-to="jboss.server.data.dir" path="audit-log.log"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="host-file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
<server-logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="server-file"/>
|
||||
</handlers>
|
||||
</server-logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<native-interface security-realm="ManagementRealm">
|
||||
<socket interface="management" port="${jboss.management.native.port:9999}"/>
|
||||
</native-interface>
|
||||
<http-interface security-realm="ManagementRealm" http-upgrade-enabled="true">
|
||||
<socket interface="management" port="${jboss.management.http.port:9990}"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
</management>
|
||||
|
||||
<domain-controller>
|
||||
<local/>
|
||||
<!-- Alternative remote domain controller configuration with a host and port -->
|
||||
<!-- <remote protocol="remote" host="${jboss.domain.master.address}" port="${jboss.domain.master.port:9999}" security-realm="ManagementRealm"/> -->
|
||||
</domain-controller>
|
||||
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
|
||||
<?INTERFACES?>
|
||||
|
||||
</interfaces>
|
||||
|
||||
<jvms>
|
||||
<jvm name="default">
|
||||
<heap size="64m" max-size="256m"/>
|
||||
<jvm-options>
|
||||
<option value="-server"/>
|
||||
</jvm-options>
|
||||
</jvm>
|
||||
</jvms>
|
||||
|
||||
<servers>
|
||||
<server name="server-one" group="main-server-group">
|
||||
<!-- Remote JPDA debugging for a specific server
|
||||
<jvm name="default">
|
||||
<jvm-options>
|
||||
<option value="-agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n"/>
|
||||
</jvm-options>
|
||||
</jvm>
|
||||
-->
|
||||
</server>
|
||||
<server name="server-two" group="main-server-group" auto-start="true">
|
||||
<!-- server-two avoids port conflicts by incrementing the ports in
|
||||
the default socket-group declared in the server-group -->
|
||||
<socket-bindings port-offset="150"/>
|
||||
</server>
|
||||
<server name="server-three" group="other-server-group" auto-start="false">
|
||||
<!-- server-three avoids port conflicts by incrementing the ports in
|
||||
the default socket-group declared in the server-group -->
|
||||
<socket-bindings port-offset="250"/>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
<profile>
|
||||
<?SUBSYSTEMS socket-binding-group="standard-sockets"?>
|
||||
</profile>
|
||||
</host>
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!--
|
||||
~ JBoss, Home of Professional Open Source.
|
||||
~ Copyright 2015, Red Hat, Inc., and individual contributors
|
||||
~ as indicated by the @author tags. See the copyright.txt file in the
|
||||
~ distribution for a full listing of individual contributors.
|
||||
~
|
||||
~ This is free software; you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU Lesser General Public License as
|
||||
~ published by the Free Software Foundation; either version 2.1 of
|
||||
~ the License, or (at your option) any later version.
|
||||
~
|
||||
~ This software is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
~ Lesser General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Lesser General Public
|
||||
~ License along with this software; if not, write to the Free
|
||||
~ Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
-->
|
||||
|
||||
<!-- See src/resources/configuration/ReadMe.txt for how the configuration assembly works -->
|
||||
<config>
|
||||
<subsystems>
|
||||
<subsystem>jmx.xml</subsystem>
|
||||
</subsystems>
|
||||
</config>
|
|
@ -69,8 +69,9 @@
|
|||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
<!-- TODO - only show this if the jacorb subsystem is added -->
|
||||
|
||||
<?INTERFACES?>
|
||||
|
||||
</interfaces>
|
||||
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<module name="org.keycloak.keycloak-connections-http-client" services="import"/>
|
||||
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.xml.soap.api"/>
|
||||
</dependencies>
|
||||
|
||||
</module>
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
<invalidation-cache name="realms" mode="SYNC"/>
|
||||
<invalidation-cache name="users" mode="SYNC"/>
|
||||
<distributed-cache name="sessions" mode="SYNC" owners="1"/>
|
||||
<distributed-cache name="offlineSessions" mode="SYNC" owners="1"/>
|
||||
<distributed-cache name="loginFailures" mode="SYNC" owners="1"/>
|
||||
</cache-container>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
<local-cache name="realms"/>
|
||||
<local-cache name="users"/>
|
||||
<local-cache name="sessions"/>
|
||||
<local-cache name="offlineSessions"/>
|
||||
<local-cache name="loginFailures"/>
|
||||
</cache-container>
|
||||
<xsl:apply-templates select="node()|@*"/>
|
||||
|
|
|
@ -193,8 +193,8 @@ String initialAccessToken = "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLT
|
|||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setClientId(CLIENT_ID);
|
||||
|
||||
ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm").build();
|
||||
reg.auth(initialAccessToken);
|
||||
ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm/clients").build();
|
||||
reg.auth(Auth.token(initialAccessToken));
|
||||
|
||||
client = reg.create(client);
|
||||
|
||||
|
|
|
@ -130,15 +130,14 @@ cd <WILDFLY_HOME>/bin
|
|||
<section>
|
||||
<title>Admin User</title>
|
||||
<para>
|
||||
To access the admin console you need an account to login. Currently, there's a default account added
|
||||
with the username <literal>admin</literal> and password <literal>admin</literal>. You will be required
|
||||
to change the password on first login. We are planning on removing the built-in account soon and will
|
||||
instead have an initial step to create the user.
|
||||
To access the admin console to configure Keycloak you need an account to login. There is no built in user,
|
||||
instead you have to first create an admin account. This can done either by opening <ulink url="http://localhost:8080/auth">http://localhost:8080/auth</ulink>
|
||||
(creating a user through the browser can only be done through localhost) or you can use the add-user script from
|
||||
the command-line.
|
||||
</para>
|
||||
<para>
|
||||
You can also create a user with the <literal>add-user</literal> script found in <literal>bin</literal>.
|
||||
This script will create a temporary file with the details of the user, which are imported at startup.
|
||||
To add a user with this script run:
|
||||
The <literal>add-user</literal> script creates a temporary file with the details of the user,
|
||||
which are imported at startup. To add a user with this script run:
|
||||
<programlisting><![CDATA[
|
||||
bin/add-user.[sh|bat] -r master -u <username> -p <password>
|
||||
]]></programlisting>
|
||||
|
@ -372,86 +371,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
|
|||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>JSON File based model</title>
|
||||
<para>
|
||||
Keycloak provides a JSON file based model implementation, which means that your identity data will be saved
|
||||
in a flat JSON text file instead of traditional RDBMS. The performance of this implementaion is likely to
|
||||
be slower because it reads and writes the entire file with each call to the Keycloak REST API. But it is
|
||||
very useful in development to see exactly what is being saved. <emphasis>It is not recommended for
|
||||
production.</emphasis>
|
||||
</para>
|
||||
<para>
|
||||
Note that this only applies to realm and user data. There is currently no file implementation for
|
||||
event persistence. So you will need to use JPA or Mongo for that.
|
||||
</para>
|
||||
<para>
|
||||
To configure Keycloak to use file persistence open <literal>standalone/configuration/keycloak-server.json</literal>
|
||||
in your favourite editor. Change the realm and user providers and disable caching. Change:
|
||||
|
||||
<programlisting><![CDATA[
|
||||
"realm": {
|
||||
"provider": "jpa"
|
||||
},
|
||||
|
||||
"user": {
|
||||
"provider": "jpa"
|
||||
},
|
||||
|
||||
"userSessions": {
|
||||
"provider" : "mem"
|
||||
},
|
||||
|
||||
"realmCache": {
|
||||
"provider": "mem"
|
||||
},
|
||||
|
||||
"userCache": {
|
||||
"provider": "mem",
|
||||
"mem": {
|
||||
"maxSize": 20000
|
||||
}
|
||||
},
|
||||
]]></programlisting>
|
||||
|
||||
to:
|
||||
|
||||
<programlisting><![CDATA[
|
||||
"realm": {
|
||||
"provider": "file"
|
||||
},
|
||||
|
||||
"user": {
|
||||
"provider": "file"
|
||||
},
|
||||
|
||||
"userSessions": {
|
||||
"provider" : "mem"
|
||||
},
|
||||
|
||||
"realmCache": {
|
||||
"provider": "none"
|
||||
},
|
||||
|
||||
"userCache": {
|
||||
"provider": "none",
|
||||
},
|
||||
]]></programlisting>
|
||||
|
||||
You can also change the location of the data file by adding a connectionsFile snippet:<programlisting><![CDATA[
|
||||
"connectionsFile": {
|
||||
"default" : {
|
||||
"directory": "/mydirectory",
|
||||
"fileName": "myfilename.json"
|
||||
}
|
||||
}
|
||||
]]></programlisting>
|
||||
|
||||
All configuration options are optional. Default value for directory is <literal>${jboss.server.data.dir}</literal>. Default file name
|
||||
is <literal>keycloak-model.json</literal>.
|
||||
</para>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<title>Outgoing Server HTTP Requests</title>
|
||||
<para>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<section id="spring-security-adapter">
|
||||
<title>Spring Security Adapter</title>
|
||||
<para>
|
||||
To to secure an application with Spring Security and Keyloak, add this adapter as a dependency to your project.
|
||||
To secure an application with Spring Security and Keycloak, add this adapter as a dependency to your project.
|
||||
You then have to provide some extra beans in your Spring Security configuration file and add the Keycloak security
|
||||
filter to your pipeline.
|
||||
</para>
|
||||
|
@ -115,7 +115,10 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
|
|||
<security:authentication-provider ref="keycloakAuthenticationProvider" />
|
||||
</security:authentication-manager>
|
||||
|
||||
<bean id="adapterDeploymentContextBean" class="org.keycloak.adapters.springsecurity.AdapterDeploymentContextBean" />
|
||||
<bean id="adapterDeploymentContext" class="org.keycloak.adapters.springsecurity.AdapterDeploymentContextFactoryBean">
|
||||
<constructor-arg value="/WEB-INF/keycloak.json" />
|
||||
</bean>
|
||||
|
||||
<bean id="keycloakAuthenticationEntryPoint" class="org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationEntryPoint" />
|
||||
<bean id="keycloakAuthenticationProvider" class="org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider" />
|
||||
<bean id="keycloakPreAuthActionsFilter" class="org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter" />
|
||||
|
@ -124,7 +127,7 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
|
|||
</bean>
|
||||
|
||||
<bean id="keycloakLogoutHandler" class="org.keycloak.adapters.springsecurity.authentication.KeycloakLogoutHandler">
|
||||
<constructor-arg ref="adapterDeploymentContextBean" />
|
||||
<constructor-arg ref="adapterDeploymentContext" />
|
||||
</bean>
|
||||
|
||||
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
|
||||
|
@ -157,6 +160,15 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
|
|||
</para>
|
||||
</section>
|
||||
</section>
|
||||
<section>
|
||||
<title>Multi Tenancy</title>
|
||||
<para>
|
||||
The Keycloak Spring Security adapter also supports multi tenancy. Instead of injecting
|
||||
<literal>AdapterDeploymentContextFactoryBean</literal> with the path to <literal>keycloak.json</literal> you
|
||||
can inject an implementation of the <literal>KeycloakConfigResolver</literal> interface. More details on how
|
||||
to implement the <literal>KeycloakConfigResolver</literal> can be found in <xref linkend="multi_tenancy" />.
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Naming Security Roles</title>
|
||||
<para>
|
||||
|
@ -164,6 +176,14 @@ public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter
|
|||
For example, an administrator role must be declared in Keycloak as <code>ROLE_ADMIN</code> or similar, not simply
|
||||
<code>ADMIN</code>.
|
||||
</para>
|
||||
<para>
|
||||
The class <code>org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider</code>
|
||||
supports an optional <code>org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper</code>
|
||||
which can be used to map roles coming from Keycloak to roles recognized by Spring Security. Use, for example,
|
||||
<code>org.springframework.security.core.authority.mapping.SimpleAuthorityMapper</code> to insert the
|
||||
<code>ROLE_</code> prefix and convert the role name to upper case. The class is part of Spring Security
|
||||
Core module.
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Client to Client Support</title>
|
||||
|
|
|
@ -202,6 +202,54 @@
|
|||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>Hardcoded Role Mapper</term>
|
||||
<listitem>
|
||||
<para>
|
||||
This mapper will grant specified Keycloak role to each Keycloak user linked with LDAP.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>Group Mapper</term>
|
||||
<listitem>
|
||||
<para>
|
||||
This allows to configure group mappings from LDAP into Keycloak group mappings. Group mapper can be used to map LDAP groups from particular branch of LDAP tree
|
||||
into groups in Keycloak. And it will also propagate user-group mappings from LDAP into user-group mappings in Keycloak.
|
||||
</para>
|
||||
<para>
|
||||
You can choose to preserve group inheritance from LDAP as well, but this may fail as Keycloak inheritance is more restrictive than LDAP
|
||||
(For example in Keycloak each group can have just one parent and there is no recursion allowed. In LDAP the recursion is possible and every group can be member of more
|
||||
other groups too).
|
||||
</para>
|
||||
<para>
|
||||
As of now, the mapper doesn't provide mapping of LDAP roles-groups to Keycloak roles-groups
|
||||
(For example when LDAP group <literal>cn=role1,ou=roles,dc=example,dc=com</literal> is member of LDAP group
|
||||
<literal>cn=group1,ou=groups,dc=example,dc=com</literal> , we don't support the mapping of Keycloak role <literal>role1</literal> imported from LDAP to corresponding Keycloak group <literal>group1</literal> imported from LDAP).
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>MSAD User Account Mapper</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Mapper specific to Microsoft Active Directory (MSAD). It's able to tightly integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc).
|
||||
It's using <literal>userAccountControl</literal> and <literal>pwdLastSet</literal> LDAP attributes for that (both are specific to MSAD and are not LDAP standard).
|
||||
For example if pwdLastSet is 0, the Keycloak user is required to update password (there will be UPDATE_PASSWORD required action added to him in Keycloak). Or if userAccountControl
|
||||
is 514 (disabled account) the Keycloak user is disabled as well etc.
|
||||
</para>
|
||||
<para>
|
||||
For writable LDAP, the mapping is bi-directional and the state from Keycloak is propagated to LDAP (For example enable user
|
||||
in Keycloak admin console will update the value of userAccountControl in MSAD and effectively enable him in MSAD as well).
|
||||
</para>
|
||||
<para>
|
||||
For writable LDAPs, mapper also provides mapping of error codes during MSAD user authentication to the
|
||||
appropriate action in Keycloak. For example if MSAD user authentication fails due to the fact, that MSAD password is expired,
|
||||
the mapper will allow user to authenticate into Keycloak, but it will add UPDATE_PASSWORD required action to the user, so user
|
||||
must update his password.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</para>
|
||||
<para>By default, there is set of User Attribute mappers to map basic UserModel attributes username, first name, lastname and email to corresponding LDAP attributes. You are free to extend this and provide
|
||||
|
|
|
@ -96,6 +96,11 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm) {
|
||||
// complete We don't care about the realm being removed
|
||||
|
|
|
@ -96,6 +96,11 @@ public class KerberosFederationProvider implements UserFederationProvider {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preRemove(RealmModel realm) {
|
||||
|
||||
|
|
|
@ -113,12 +113,6 @@ public class LDAPConfig {
|
|||
return uuidAttrName;
|
||||
}
|
||||
|
||||
// TODO: Remove and use mapper instead?
|
||||
public boolean isUserAccountControlsAfterPasswordUpdate() {
|
||||
String userAccountCtrls = config.get(LDAPConstants.USER_ACCOUNT_CONTROLS_AFTER_PASSWORD_UPDATE);
|
||||
return userAccountCtrls==null ? false : Boolean.parseBoolean(userAccountCtrls);
|
||||
}
|
||||
|
||||
public boolean isPagination() {
|
||||
String pagination = config.get(LDAPConstants.PAGINATION);
|
||||
return pagination==null ? false : Boolean.parseBoolean(pagination);
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.federation.ldap;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
|
||||
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -10,6 +11,8 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilde
|
|||
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
|
||||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory;
|
||||
import org.keycloak.models.CredentialValidationOutput;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -29,6 +32,8 @@ import org.keycloak.common.constants.KerberosConstants;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
|
@ -36,6 +41,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -185,6 +192,35 @@ public class LDAPFederationProvider implements UserFederationProvider {
|
|||
return searchResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||
Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(model.getId());
|
||||
for (UserFederationMapperModel mapperModel : federationMappers) {
|
||||
LDAPFederationMapper ldapMapper = getMapper(mapperModel);
|
||||
List<UserModel> users = ldapMapper.getGroupMembers(mapperModel, this, realm, group, firstResult, maxResults);
|
||||
|
||||
// Sufficient for now
|
||||
if (users.size() > 0) {
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<UserModel> loadUsersByUsernames(List<String> usernames, RealmModel realm) {
|
||||
List<UserModel> result = new ArrayList<>();
|
||||
for (String username : usernames) {
|
||||
UserModel kcUser = session.users().getUserByUsername(username, realm);
|
||||
if (!model.getId().equals(kcUser.getFederationLink())) {
|
||||
logger.warnf("Incorrect federation provider of user %s" + kcUser.getUsername());
|
||||
} else {
|
||||
result.add(kcUser);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected List<LDAPObject> searchLDAP(RealmModel realm, Map<String, String> attributes, int maxResults) {
|
||||
|
||||
List<LDAPObject> results = new ArrayList<LDAPObject>();
|
||||
|
@ -332,7 +368,22 @@ public class LDAPFederationProvider implements UserFederationProvider {
|
|||
} else {
|
||||
// Use Naming LDAP API
|
||||
LDAPObject ldapUser = loadAndValidateUser(realm, user);
|
||||
return ldapIdentityStore.validatePassword(ldapUser, password);
|
||||
|
||||
try {
|
||||
ldapIdentityStore.validatePassword(ldapUser, password);
|
||||
return true;
|
||||
} catch (AuthenticationException ae) {
|
||||
|
||||
// Check if any mapper provides callback for handle LDAP AuthenticationException
|
||||
Set<UserFederationMapperModel> federationMappers = realm.getUserFederationMappersByFederationProvider(getModel().getId());
|
||||
boolean processed = false;
|
||||
for (UserFederationMapperModel mapperModel : federationMappers) {
|
||||
LDAPFederationMapper ldapMapper = getMapper(mapperModel);
|
||||
processed = processed || ldapMapper.onAuthenticationFailure(mapperModel, this, ldapUser, user, ae, realm);
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
|
|||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
|
||||
import org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.KeycloakSessionTask;
|
||||
|
@ -187,11 +189,19 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
|||
UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
|
||||
UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
|
||||
realm.addUserFederationMapper(mapperModel);
|
||||
|
||||
// MSAD specific mapper for account state propagation
|
||||
if (activeDirectory) {
|
||||
mapperModel = KeycloakModelUtils.createUserFederationMapperModel("MSAD account controls", newProviderModel.getId(), MSADUserAccountControlMapperFactory.PROVIDER_ID);
|
||||
realm.addUserFederationMapper(mapperModel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UserFederationSyncResult syncAllUsers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
|
||||
syncMappers(sessionFactory, realmId, model);
|
||||
|
||||
logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getDisplayName());
|
||||
|
||||
LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
|
||||
|
@ -205,6 +215,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
|||
|
||||
@Override
|
||||
public UserFederationSyncResult syncChangedUsers(KeycloakSessionFactory sessionFactory, String realmId, UserFederationProviderModel model, Date lastSync) {
|
||||
syncMappers(sessionFactory, realmId, model);
|
||||
|
||||
logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getDisplayName());
|
||||
|
||||
// Sync newly created and updated users
|
||||
|
@ -221,6 +233,26 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
|
|||
return result;
|
||||
}
|
||||
|
||||
protected void syncMappers(KeycloakSessionFactory sessionFactory, final String realmId, final UserFederationProviderModel model) {
|
||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||
|
||||
@Override
|
||||
public void run(KeycloakSession session) {
|
||||
LDAPFederationProvider ldapProvider = getInstance(session, model);
|
||||
RealmModel realm = session.realms().getRealm(realmId);
|
||||
Set<UserFederationMapperModel> mappers = realm.getUserFederationMappersByFederationProvider(model.getId());
|
||||
for (UserFederationMapperModel mapperModel : mappers) {
|
||||
UserFederationMapper ldapMapper = session.getProvider(UserFederationMapper.class, mapperModel.getFederationMapperType());
|
||||
UserFederationSyncResult syncResult = ldapMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
|
||||
if (syncResult.getAdded() > 0 || syncResult.getUpdated() > 0 || syncResult.getRemoved() > 0 || syncResult.getFailed() > 0) {
|
||||
logger.infof("Sync of federation mapper '%s' finished. Status: %s", mapperModel.getName(), syncResult.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
protected UserFederationSyncResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final UserFederationProviderModel fedModel) {
|
||||
|
||||
final UserFederationSyncResult syncResult = new UserFederationSyncResult();
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package org.keycloak.federation.ldap;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
|
@ -9,6 +12,8 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
|||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
|
@ -97,4 +102,109 @@ public class LDAPUtils {
|
|||
"Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// roles & groups
|
||||
|
||||
public static LDAPObject createLDAPGroup(LDAPFederationProvider ldapProvider, String groupName, String groupNameAttribute, Collection<String> objectClasses,
|
||||
String parentDn, Map<String, Set<String>> additionalAttributes) {
|
||||
LDAPObject ldapObject = new LDAPObject();
|
||||
|
||||
ldapObject.setRdnAttributeName(groupNameAttribute);
|
||||
ldapObject.setObjectClasses(objectClasses);
|
||||
ldapObject.setSingleAttribute(groupNameAttribute, groupName);
|
||||
|
||||
LDAPDn roleDn = LDAPDn.fromString(parentDn);
|
||||
roleDn.addFirst(groupNameAttribute, groupName);
|
||||
ldapObject.setDn(roleDn);
|
||||
|
||||
for (Map.Entry<String, Set<String>> attrEntry : additionalAttributes.entrySet()) {
|
||||
ldapObject.setAttribute(attrEntry.getKey(), attrEntry.getValue());
|
||||
}
|
||||
|
||||
ldapProvider.getLdapIdentityStore().add(ldapObject);
|
||||
return ldapObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ldapChild as member of ldapParent and save ldapParent to LDAP.
|
||||
*
|
||||
* @param ldapProvider
|
||||
* @param membershipType how is 'member' attribute saved (full DN or just uid)
|
||||
* @param memberAttrName usually 'member'
|
||||
* @param ldapParent role or group
|
||||
* @param ldapChild usually user (or child group or child role)
|
||||
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
|
||||
*/
|
||||
public static void addMember(LDAPFederationProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
|
||||
|
||||
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
|
||||
|
||||
// Remove membership placeholder if present
|
||||
if (membershipType == MembershipType.DN) {
|
||||
for (String membership : memberships) {
|
||||
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
|
||||
memberships.remove(membership);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String membership = getMemberValueOfChildObject(ldapChild, membershipType);
|
||||
|
||||
memberships.add(membership);
|
||||
ldapParent.setAttribute(memberAttrName, memberships);
|
||||
|
||||
if (sendLDAPUpdateRequest) {
|
||||
ldapProvider.getLdapIdentityStore().update(ldapParent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove ldapChild as member of ldapParent and save ldapParent to LDAP.
|
||||
*
|
||||
* @param ldapProvider
|
||||
* @param membershipType how is 'member' attribute saved (full DN or just uid)
|
||||
* @param memberAttrName usually 'member'
|
||||
* @param ldapParent role or group
|
||||
* @param ldapChild usually user (or child group or child role)
|
||||
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
|
||||
*/
|
||||
public static void deleteMember(LDAPFederationProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
|
||||
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
|
||||
|
||||
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType);
|
||||
|
||||
memberships.remove(userMembership);
|
||||
|
||||
// Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
|
||||
if (memberships.size() == 0 && membershipType== MembershipType.DN && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
||||
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
|
||||
}
|
||||
|
||||
ldapParent.setAttribute(memberAttrName, memberships);
|
||||
ldapProvider.getLdapIdentityStore().update(ldapParent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all existing memberships (values of attribute 'member' ) from the given ldapRole or ldapGroup
|
||||
*
|
||||
* @param memberAttrName usually 'member'
|
||||
* @param ldapRole
|
||||
* @return
|
||||
*/
|
||||
public static Set<String> getExistingMemberships(String memberAttrName, LDAPObject ldapRole) {
|
||||
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
|
||||
if (memberships == null) {
|
||||
memberships = new HashSet<>();
|
||||
}
|
||||
return memberships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value to be used as attribute 'member' in some parent ldapObject
|
||||
*/
|
||||
public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
|
||||
return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,20 @@ public class LDAPDn {
|
|||
return dn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof LDAPDn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return toString().equals(obj.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(entries);
|
||||
|
@ -62,6 +76,14 @@ public class LDAPDn {
|
|||
return firstEntry.attrName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string attribute value like "joe" from the DN like "uid=joe,dc=something,dc=org"
|
||||
*/
|
||||
public String getFirstRdnAttrValue() {
|
||||
Entry firstEntry = entries.getFirst();
|
||||
return firstEntry.attrValue;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
|
||||
|
@ -72,6 +94,21 @@ public class LDAPDn {
|
|||
return toString(parentDnEntries);
|
||||
}
|
||||
|
||||
public boolean isDescendantOf(LDAPDn expectedParentDn) {
|
||||
int parentEntriesCount = expectedParentDn.entries.size();
|
||||
|
||||
Deque<Entry> myEntries = new LinkedList<>(this.entries);
|
||||
boolean someRemoved = false;
|
||||
while (myEntries.size() > parentEntriesCount) {
|
||||
myEntries.removeFirst();
|
||||
someRemoved = true;
|
||||
}
|
||||
|
||||
String myEntriesParentStr = toString(myEntries).toLowerCase();
|
||||
String expectedParentDnStr = expectedParentDn.toString().toLowerCase();
|
||||
return someRemoved && myEntriesParentStr.equals(expectedParentDnStr);
|
||||
}
|
||||
|
||||
public void addFirst(String rdnName, String rdnValue) {
|
||||
rdnValue = escape(rdnValue);
|
||||
entries.addFirst(new Entry(rdnName, rdnValue));
|
||||
|
|
|
@ -66,6 +66,10 @@ public class LDAPObject {
|
|||
readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public void removeReadOnlyAttributeName(String readOnlyAttribute) {
|
||||
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public String getRdnAttributeName() {
|
||||
return rdnAttributeName;
|
||||
}
|
||||
|
|
|
@ -189,4 +189,8 @@ public class LDAPQuery {
|
|||
return this.conditions;
|
||||
}
|
||||
|
||||
public LDAPFederationProvider getLdapProvider() {
|
||||
return ldapFedProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.keycloak.federation.ldap.idm.store;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -65,8 +67,9 @@ public interface IdentityStore {
|
|||
*
|
||||
* @param user Keycloak user
|
||||
* @param password Ldap password
|
||||
* @throws AuthenticationException if authentication is not successful
|
||||
*/
|
||||
boolean validatePassword(LDAPObject user, String password);
|
||||
void validatePassword(LDAPObject user, String password) throws AuthenticationException;
|
||||
|
||||
/**
|
||||
* Updates the specified credential value.
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.util.NoSuchElementException;
|
|||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.Attribute;
|
||||
|
@ -173,18 +174,14 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
// *************** CREDENTIALS AND USER SPECIFIC STUFF
|
||||
|
||||
@Override
|
||||
public boolean validatePassword(LDAPObject user, String password) {
|
||||
public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
|
||||
String userDN = user.getDn().toString();
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Using DN [%s] for authentication of user", userDN);
|
||||
}
|
||||
|
||||
if (operationManager.authenticate(userDN, password)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
operationManager.authenticate(userDN, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -225,15 +222,6 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
List<ModificationItem> modItems = new ArrayList<ModificationItem>();
|
||||
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, unicodePwd));
|
||||
|
||||
// Used in ActiveDirectory to put account into "enabled" state (aka userAccountControl=512, see http://support.microsoft.com/kb/305144/en ) after password update. If value is -1, it's ignored
|
||||
// TODO: Remove and use mapper instead
|
||||
if (getConfig().isUserAccountControlsAfterPasswordUpdate()) {
|
||||
BasicAttribute userAccountControl = new BasicAttribute("userAccountControl", "512");
|
||||
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userAccountControl));
|
||||
|
||||
logger.debugf("Attribute userAccountControls will be switched to 512 after password update of user [%s]", userDN);
|
||||
}
|
||||
|
||||
operationManager.modifyAttributes(userDN, modItems.toArray(new ModificationItem[] {}));
|
||||
} catch (Exception e) {
|
||||
throw new ModelException(e);
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.Map;
|
|||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
import javax.naming.Binding;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.InitialContext;
|
||||
|
@ -320,15 +321,15 @@ public class LDAPOperationManager {
|
|||
*
|
||||
* @param dn
|
||||
* @param password
|
||||
* @throws AuthenticationException if authentication is not successful
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean authenticate(String dn, String password) {
|
||||
public void authenticate(String dn, String password) throws AuthenticationException {
|
||||
InitialContext authCtx = null;
|
||||
|
||||
try {
|
||||
if (password == null || password.isEmpty()) {
|
||||
throw new Exception("Empty password used");
|
||||
throw new AuthenticationException("Empty password used");
|
||||
}
|
||||
|
||||
Hashtable<String, Object> env = new Hashtable<String, Object>(this.connectionProperties);
|
||||
|
@ -342,13 +343,15 @@ public class LDAPOperationManager {
|
|||
|
||||
authCtx = new InitialLdapContext(env, null);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
} catch (AuthenticationException ae) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf(e, "Authentication failed for DN [%s]", dn);
|
||||
logger.debugf(ae, "Authentication failed for DN [%s]", dn);
|
||||
}
|
||||
|
||||
return false;
|
||||
throw ae;
|
||||
} catch (Exception e) {
|
||||
logger.errorf(e, "Unexpected exception when validating password of DN [%s]", dn);
|
||||
throw new AuthenticationException("Unexpected exception when validating password of user");
|
||||
} finally {
|
||||
if (authCtx != null) {
|
||||
try {
|
||||
|
|
|
@ -1,19 +1,92 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
|
||||
/**
|
||||
* Stateful per-request object
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapper {
|
||||
public abstract class AbstractLDAPFederationMapper {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
protected final UserFederationMapperModel mapperModel;
|
||||
protected final LDAPFederationProvider ldapProvider;
|
||||
protected final RealmModel realm;
|
||||
|
||||
public AbstractLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
this.mapperModel = mapperModel;
|
||||
this.ldapProvider = ldapProvider;
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
protected boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
|
||||
/**
|
||||
* @see UserFederationMapper#syncDataFromFederationProviderToKeycloak(UserFederationMapperModel, UserFederationProvider, KeycloakSession, RealmModel)
|
||||
*/
|
||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() {
|
||||
return new UserFederationSyncResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see UserFederationMapper#syncDataFromKeycloakToFederationProvider(UserFederationMapperModel, UserFederationProvider, KeycloakSession, RealmModel)
|
||||
*/
|
||||
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() {
|
||||
return new UserFederationSyncResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see LDAPFederationMapper#beforeLDAPQuery(UserFederationMapperModel, LDAPQuery)
|
||||
*/
|
||||
public abstract void beforeLDAPQuery(LDAPQuery query);
|
||||
|
||||
/**
|
||||
* @see LDAPFederationMapper#proxy(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel)
|
||||
*/
|
||||
public abstract UserModel proxy(LDAPObject ldapUser, UserModel delegate);
|
||||
|
||||
/**
|
||||
* @see LDAPFederationMapper#onRegisterUserToLDAP(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel)
|
||||
*/
|
||||
public abstract void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser);
|
||||
|
||||
/**
|
||||
* @see LDAPFederationMapper#onImportUserFromLDAP(UserFederationMapperModel, LDAPFederationProvider, LDAPObject, UserModel, RealmModel, boolean)
|
||||
*/
|
||||
public abstract void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate);
|
||||
|
||||
public List<UserModel> getGroupMembers(GroupModel group, int firstResult, int maxResults) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
|
||||
String paramm = mapperModel.getConfig().get(paramName);
|
||||
return Boolean.parseBoolean(paramm);
|
||||
}
|
||||
|
||||
public LDAPFederationProvider getLdapProvider() {
|
||||
return ldapProvider;
|
||||
}
|
||||
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,18 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.mappers.UserFederationMapperFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -22,10 +28,22 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat
|
|||
// Used to map roles from LDAP to UserModel users
|
||||
public static final String ROLE_MAPPER_CATEGORY = "Role Mapper";
|
||||
|
||||
|
||||
// Used to map group from LDAP to UserModel users
|
||||
public static final String GROUP_MAPPER_CATEGORY = "Group Mapper";
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapper create(KeycloakSession session) {
|
||||
return new LDAPFederationMapperBridge(this);
|
||||
}
|
||||
|
||||
// Used just by LDAPFederationMapperBridge.
|
||||
protected abstract AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm);
|
||||
|
||||
@Override
|
||||
public String getFederationProviderType() {
|
||||
return LDAPFederationProviderFactory.PROVIDER_NAME;
|
||||
|
@ -35,6 +53,11 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat
|
|||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return new UserFederationMapperSyncConfigRepresentation(false, null, false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
|
|
@ -24,9 +24,13 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute";
|
||||
public static final String READ_ONLY = "read.only";
|
||||
|
||||
public FullNameLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||
String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
|
||||
if (fullName == null) {
|
||||
return;
|
||||
|
@ -45,19 +49,19 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||
String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
|
||||
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
|
||||
|
||||
if (isReadOnly(mapperModel)) {
|
||||
if (isReadOnly()) {
|
||||
ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
|
||||
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly()) {
|
||||
|
||||
|
||||
TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
|
||||
|
@ -82,7 +86,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
|
||||
ensureTransactionStarted();
|
||||
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||
ldapUser.setSingleAttribute(ldapFullNameAttrName, fullName);
|
||||
}
|
||||
|
||||
|
@ -95,8 +99,8 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
String ldapFullNameAttrName = getLdapFullNameAttrName();
|
||||
query.addReturningLdapAttribute(ldapFullNameAttrName);
|
||||
|
||||
// Change conditions and compute condition for fullName from the conditions for firstName and lastName. Right now just "equal" condition is supported
|
||||
|
@ -137,7 +141,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
query.addWhereCondition(fullNameCondition);
|
||||
}
|
||||
|
||||
protected String getLdapFullNameAttrName(UserFederationMapperModel mapperModel) {
|
||||
protected String getLdapFullNameAttrName() {
|
||||
String ldapFullNameAttrName = mapperModel.getConfig().get(LDAP_FULL_NAME_ATTRIBUTE);
|
||||
return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName;
|
||||
}
|
||||
|
@ -154,7 +158,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isReadOnly(UserFederationMapperModel mapperModel) {
|
||||
private boolean isReadOnly() {
|
||||
return parseBooleanParameter(mapperModel, READ_ONLY);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
|
@ -22,11 +28,11 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
|
|||
|
||||
static {
|
||||
ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute",
|
||||
"Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
|
||||
"Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(userModelAttribute);
|
||||
|
||||
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
|
||||
"For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
||||
"For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(readOnly);
|
||||
}
|
||||
|
||||
|
@ -50,18 +56,31 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
|
|||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
Map<String, String> defaultValues = new HashMap<>();
|
||||
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||
|
||||
defaultValues.put(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN);
|
||||
|
||||
String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true";
|
||||
defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
|
||||
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
checkMandatoryConfigAttribute(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", mapperModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapper create(KeycloakSession session) {
|
||||
return new FullNameLDAPFederationMapper();
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new FullNameLDAPFederationMapper(mapperModel, federationProvider, realm);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class HardcodedLDAPRoleMapper extends AbstractLDAPFederationMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(HardcodedLDAPRoleMapper.class);
|
||||
|
||||
public static final String ROLE = "role";
|
||||
|
||||
public HardcodedLDAPRoleMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||
return new UserModelDelegate(delegate) {
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getRealmRoleMappings() {
|
||||
Set<RoleModel> roles = super.getRealmRoleMappings();
|
||||
|
||||
RoleModel role = getRole();
|
||||
if (role != null && role.getContainer().equals(realm)) {
|
||||
roles.add(role);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
|
||||
Set<RoleModel> roles = super.getClientRoleMappings(app);
|
||||
|
||||
RoleModel role = getRole();
|
||||
if (role != null && role.getContainer().equals(app)) {
|
||||
roles.add(role);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRole(RoleModel role) {
|
||||
return super.hasRole(role) || role.equals(getRole());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getRoleMappings() {
|
||||
Set<RoleModel> roles = super.getRoleMappings();
|
||||
|
||||
RoleModel role = getRole();
|
||||
if (role != null) {
|
||||
roles.add(role);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRoleMapping(RoleModel role) {
|
||||
if (role.equals(getRole())) {
|
||||
throw new ModelException("Not possible to delete role. It's hardcoded by LDAP mapper");
|
||||
} else {
|
||||
super.deleteRoleMapping(role);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
|
||||
}
|
||||
|
||||
private RoleModel getRole() {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedLDAPRoleMapper.ROLE);
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) {
|
||||
logger.warnf("Hardcoded role '%s' configured in mapper '%s' is not available anymore");
|
||||
}
|
||||
return role;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class HardcodedLDAPRoleMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "hardcoded-ldap-role-mapper";
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty roleAttr = createConfigProperty(HardcodedLDAPRoleMapper.ROLE, "Role",
|
||||
"Role to grant to user. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference an application role the syntax is appname.approle, i.e. myapp.myrole",
|
||||
ProviderConfigProperty.ROLE_TYPE, null);
|
||||
configProperties.add(roleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "When user is imported from LDAP, he will be automatically added into this configured role.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return ROLE_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Hardcoded Role";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
String roleName = mapperModel.getConfig().get(HardcodedLDAPRoleMapper.ROLE);
|
||||
if (roleName == null) {
|
||||
throw new MapperConfigValidationException("Role can't be null");
|
||||
}
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
|
||||
if (role == null) {
|
||||
throw new MapperConfigValidationException("There is no role corresponding to configured value");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new HardcodedLDAPRoleMapper(mapperModel, federationProvider, realm);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
@ -59,4 +61,17 @@ public interface LDAPFederationMapper extends UserFederationMapper {
|
|||
* @param query
|
||||
*/
|
||||
void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query);
|
||||
|
||||
/**
|
||||
* Called when LDAP authentication of specified user fails. If any mapper returns true from this method, AuthenticationException won't be rethrown!
|
||||
*
|
||||
* @param mapperModel
|
||||
* @param ldapProvider
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param ldapUser
|
||||
* @param ldapException
|
||||
* @return true if mapper processed the AuthenticationException and did some actions based on that. In that case, AuthenticationException won't be rethrown!
|
||||
*/
|
||||
boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* Sufficient if mapper implementation is stateless and doesn't need to "close" any state
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LDAPFederationMapperBridge implements LDAPFederationMapper {
|
||||
|
||||
private final AbstractLDAPFederationMapperFactory factory;
|
||||
|
||||
public LDAPFederationMapperBridge(AbstractLDAPFederationMapperFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
// Sync groups from LDAP to Keycloak DB
|
||||
@Override
|
||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
||||
return getDelegate(mapperModel, federationProvider, realm).syncDataFromFederationProviderToKeycloak();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, KeycloakSession session, RealmModel realm) {
|
||||
return getDelegate(mapperModel, federationProvider, realm).syncDataFromKeycloakToFederationProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||
getDelegate(mapperModel, ldapProvider, realm).onImportUserFromLDAP(ldapUser, user, isCreate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||
getDelegate(mapperModel, ldapProvider, realm).onRegisterUserToLDAP(ldapUser, localUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||
return getDelegate(mapperModel, ldapProvider, realm).proxy(ldapUser, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
// Improve if needed
|
||||
getDelegate(mapperModel, query.getLdapProvider(), null).beforeLDAPQuery(query);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(UserFederationMapperModel mapperModel, UserFederationProvider ldapProvider, RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||
return getDelegate(mapperModel, ldapProvider, realm).getGroupMembers(group, firstResult, maxResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticationFailure(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, AuthenticationException ldapException, RealmModel realm) {
|
||||
return getDelegate(mapperModel, ldapProvider, realm).onAuthenticationFailure(ldapUser, user, ldapException);
|
||||
}
|
||||
|
||||
private AbstractLDAPFederationMapper getDelegate(UserFederationMapperModel mapperModel, UserFederationProvider federationProvider, RealmModel realm) {
|
||||
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) federationProvider;
|
||||
return factory.createMapper(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,487 +0,0 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
/**
|
||||
* Map realm roles or roles of particular client to LDAP groups
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class);
|
||||
|
||||
// LDAP DN where are roles of this tree saved.
|
||||
public static final String ROLES_DN = "roles.dn";
|
||||
|
||||
// Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
|
||||
public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
|
||||
|
||||
// Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
|
||||
public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
|
||||
|
||||
// Object classes of the role object.
|
||||
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
||||
|
||||
// Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID)
|
||||
public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping";
|
||||
|
||||
// ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false
|
||||
public static final String CLIENT_ID = "client.id";
|
||||
|
||||
// See docs for Mode enum
|
||||
public static final String MODE = "mode";
|
||||
|
||||
// Customized LDAP filter which is added to the whole LDAP query
|
||||
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
|
||||
|
||||
|
||||
// List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance
|
||||
// TODO: Rather address this with caching at LDAPIdentityStore level?
|
||||
private Set<String> rolesSyncedModels = new TreeSet<>();
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||
syncRolesFromLDAP(mapperModel, ldapProvider, realm);
|
||||
|
||||
Mode mode = getMode(mapperModel);
|
||||
|
||||
// For now, import LDAP role mappings just during create
|
||||
if (mode == Mode.IMPORT && isCreate) {
|
||||
|
||||
List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
|
||||
|
||||
// Import role mappings from LDAP into Keycloak DB
|
||||
String roleNameAttr = getRoleNameLdapAttribute(mapperModel);
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(roleNameAttr);
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
RoleModel role = roleContainer.getRole(roleName);
|
||||
|
||||
logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername());
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||
syncRolesFromLDAP(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
// Sync roles from LDAP tree and create them in local Keycloak DB (if they don't exist here yet)
|
||||
protected void syncRolesFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
if (!rolesSyncedModels.contains(mapperModel.getId())) {
|
||||
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||
|
||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
|
||||
// Send query
|
||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||
|
||||
if (roleContainer.getRole(roleName) == null) {
|
||||
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
|
||||
roleContainer.addRole(roleName);
|
||||
}
|
||||
}
|
||||
|
||||
rolesSyncedModels.add(mapperModel.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public LDAPQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
|
||||
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
||||
|
||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
|
||||
|
||||
String rolesDn = getRolesDn(mapperModel);
|
||||
ldapQuery.setSearchDn(rolesDn);
|
||||
|
||||
Collection<String> roleObjectClasses = getRoleObjectClasses(mapperModel, ldapProvider);
|
||||
ldapQuery.addObjectClasses(roleObjectClasses);
|
||||
|
||||
String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
|
||||
|
||||
String customFilter = mapperModel.getConfig().get(RoleLDAPFederationMapper.ROLES_LDAP_FILTER);
|
||||
if (customFilter != null && customFilter.trim().length() > 0) {
|
||||
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
|
||||
ldapQuery.addWhereCondition(customFilterCondition);
|
||||
}
|
||||
|
||||
String membershipAttr = getMembershipLdapAttribute(mapperModel);
|
||||
ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
|
||||
ldapQuery.addReturningLdapAttribute(membershipAttr);
|
||||
|
||||
return ldapQuery;
|
||||
}
|
||||
|
||||
protected RoleContainerModel getTargetRoleContainer(UserFederationMapperModel mapperModel, RealmModel realm) {
|
||||
boolean realmRolesMapping = parseBooleanParameter(mapperModel, USE_REALM_ROLES_MAPPING);
|
||||
if (realmRolesMapping) {
|
||||
return realm;
|
||||
} else {
|
||||
String clientId = mapperModel.getConfig().get(CLIENT_ID);
|
||||
if (clientId == null) {
|
||||
throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!");
|
||||
}
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
if (client == null) {
|
||||
throw new ModelException("Can't found requested client with clientId: " + clientId);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
protected String getRolesDn(UserFederationMapperModel mapperModel) {
|
||||
String rolesDn = mapperModel.getConfig().get(ROLES_DN);
|
||||
if (rolesDn == null) {
|
||||
throw new ModelException("Roles DN is null! Check your configuration");
|
||||
}
|
||||
return rolesDn;
|
||||
}
|
||||
|
||||
protected String getRoleNameLdapAttribute(UserFederationMapperModel mapperModel) {
|
||||
String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE);
|
||||
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
||||
}
|
||||
|
||||
protected String getMembershipLdapAttribute(UserFederationMapperModel mapperModel) {
|
||||
String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
|
||||
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||
}
|
||||
|
||||
protected Collection<String> getRoleObjectClasses(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
|
||||
String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES);
|
||||
if (objectClasses == null) {
|
||||
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
||||
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
}
|
||||
String[] objClasses = objectClasses.split(",");
|
||||
|
||||
Set<String> trimmed = new HashSet<>();
|
||||
for (String objectClass : objClasses) {
|
||||
objectClass = objectClass.trim();
|
||||
if (objectClass.length() > 0) {
|
||||
trimmed.add(objectClass);
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private Mode getMode(UserFederationMapperModel mapperModel) {
|
||||
String modeString = mapperModel.getConfig().get(MODE);
|
||||
if (modeString == null || modeString.isEmpty()) {
|
||||
throw new ModelException("Mode is missing! Check your configuration");
|
||||
}
|
||||
|
||||
return Enum.valueOf(Mode.class, modeString.toUpperCase());
|
||||
}
|
||||
|
||||
public LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) {
|
||||
LDAPObject ldapObject = new LDAPObject();
|
||||
String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
|
||||
ldapObject.setRdnAttributeName(roleNameAttribute);
|
||||
ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel, ldapProvider));
|
||||
ldapObject.setSingleAttribute(roleNameAttribute, roleName);
|
||||
|
||||
LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel));
|
||||
roleDn.addFirst(roleNameAttribute, roleName);
|
||||
ldapObject.setDn(roleDn);
|
||||
|
||||
logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, roleDn.toString());
|
||||
ldapProvider.getLdapIdentityStore().add(ldapObject);
|
||||
return ldapObject;
|
||||
}
|
||||
|
||||
public void addRoleMappingInLDAP(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
||||
LDAPObject ldapRole = loadLDAPRoleByName(mapperModel, ldapProvider, roleName);
|
||||
if (ldapRole == null) {
|
||||
ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider);
|
||||
}
|
||||
|
||||
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
|
||||
|
||||
// Remove membership placeholder if present
|
||||
for (String membership : memberships) {
|
||||
if (LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE.equals(membership)) {
|
||||
memberships.remove(membership);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
memberships.add(ldapUser.getDn().toString());
|
||||
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
|
||||
|
||||
ldapProvider.getLdapIdentityStore().update(ldapRole);
|
||||
}
|
||||
|
||||
public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) {
|
||||
Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
|
||||
memberships.remove(ldapUser.getDn().toString());
|
||||
|
||||
// Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers. But not on active directory! (Placeholder, which not matches any real object is not allowed here)
|
||||
if (memberships.size() == 0 && !ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory()) {
|
||||
memberships.add(LDAPConstants.EMPTY_MEMBER_ATTRIBUTE_VALUE);
|
||||
}
|
||||
|
||||
ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
|
||||
ldapProvider.getLdapIdentityStore().update(ldapRole);
|
||||
}
|
||||
|
||||
public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) {
|
||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(getRoleNameLdapAttribute(mapperModel), roleName);
|
||||
ldapQuery.addWhereCondition(roleNameCondition);
|
||||
return ldapQuery.getFirstResult();
|
||||
}
|
||||
|
||||
protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
|
||||
String memberAttrName = getMembershipLdapAttribute(mapperModel);
|
||||
Set<String> memberships = ldapRole.getAttributeAsSet(memberAttrName);
|
||||
if (memberships == null) {
|
||||
memberships = new HashSet<>();
|
||||
}
|
||||
return memberships;
|
||||
}
|
||||
|
||||
protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
|
||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
String membershipAttr = getMembershipLdapAttribute(mapperModel);
|
||||
Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(membershipAttr, ldapUser.getDn().toString());
|
||||
ldapQuery.addWhereCondition(membershipCondition);
|
||||
return ldapQuery.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
|
||||
final Mode mode = getMode(mapperModel);
|
||||
|
||||
// For IMPORT mode, all operations are performed against local DB
|
||||
if (mode == Mode.IMPORT) {
|
||||
return delegate;
|
||||
} else {
|
||||
return new LDAPRoleMappingsUserDelegate(delegate, mapperModel, ldapProvider, ldapUser, realm, mode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
|
||||
|
||||
private final UserFederationMapperModel mapperModel;
|
||||
private final LDAPFederationProvider ldapProvider;
|
||||
private final LDAPObject ldapUser;
|
||||
private final RealmModel realm;
|
||||
private final Mode mode;
|
||||
|
||||
// Avoid loading role mappings from LDAP more times per-request
|
||||
private Set<RoleModel> cachedLDAPRoleMappings;
|
||||
|
||||
public LDAPRoleMappingsUserDelegate(UserModel user, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser,
|
||||
RealmModel realm, Mode mode) {
|
||||
super(user);
|
||||
this.mapperModel = mapperModel;
|
||||
this.ldapProvider = ldapProvider;
|
||||
this.ldapUser = ldapUser;
|
||||
this.realm = realm;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getRealmRoleMappings() {
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
if (roleContainer.equals(realm)) {
|
||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer);
|
||||
|
||||
if (mode == Mode.LDAP_ONLY) {
|
||||
// Use just role mappings from LDAP
|
||||
return ldapRoleMappings;
|
||||
} else {
|
||||
// Merge mappings from both DB and LDAP
|
||||
Set<RoleModel> modelRoleMappings = super.getRealmRoleMappings();
|
||||
ldapRoleMappings.addAll(modelRoleMappings);
|
||||
return ldapRoleMappings;
|
||||
}
|
||||
} else {
|
||||
return super.getRealmRoleMappings();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getClientRoleMappings(ClientModel client) {
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
if (roleContainer.equals(client)) {
|
||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer);
|
||||
|
||||
if (mode == Mode.LDAP_ONLY) {
|
||||
// Use just role mappings from LDAP
|
||||
return ldapRoleMappings;
|
||||
} else {
|
||||
// Merge mappings from both DB and LDAP
|
||||
Set<RoleModel> modelRoleMappings = super.getClientRoleMappings(client);
|
||||
ldapRoleMappings.addAll(modelRoleMappings);
|
||||
return ldapRoleMappings;
|
||||
}
|
||||
} else {
|
||||
return super.getClientRoleMappings(client);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRole(RoleModel role) {
|
||||
Set<RoleModel> roles = getRoleMappings();
|
||||
return KeycloakModelUtils.hasRole(roles, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantRole(RoleModel role) {
|
||||
if (mode == Mode.LDAP_ONLY) {
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
|
||||
if (role.getContainer().equals(roleContainer)) {
|
||||
|
||||
// We need to create new role mappings in LDAP
|
||||
cachedLDAPRoleMappings = null;
|
||||
addRoleMappingInLDAP(mapperModel, role.getName(), ldapProvider, ldapUser);
|
||||
} else {
|
||||
super.grantRole(role);
|
||||
}
|
||||
} else {
|
||||
super.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getRoleMappings() {
|
||||
Set<RoleModel> modelRoleMappings = super.getRoleMappings();
|
||||
|
||||
RoleContainerModel targetRoleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, targetRoleContainer);
|
||||
|
||||
if (mode == Mode.LDAP_ONLY) {
|
||||
// For LDAP-only we want to retrieve role mappings of target container just from LDAP
|
||||
Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
|
||||
for (RoleModel role : modelRolesCopy) {
|
||||
if (role.getContainer().equals(targetRoleContainer)) {
|
||||
modelRoleMappings.remove(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelRoleMappings.addAll(ldapRoleMappings);
|
||||
return modelRoleMappings;
|
||||
}
|
||||
|
||||
protected Set<RoleModel> getLDAPRoleMappingsConverted(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, RoleContainerModel roleContainer) {
|
||||
if (cachedLDAPRoleMappings != null) {
|
||||
return new HashSet<>(cachedLDAPRoleMappings);
|
||||
}
|
||||
|
||||
List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
|
||||
|
||||
Set<RoleModel> roles = new HashSet<>();
|
||||
String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
|
||||
for (LDAPObject role : ldapRoles) {
|
||||
String roleName = role.getAttributeAsString(roleNameLdapAttr);
|
||||
RoleModel modelRole = roleContainer.getRole(roleName);
|
||||
if (modelRole == null) {
|
||||
// Add role to local DB
|
||||
modelRole = roleContainer.addRole(roleName);
|
||||
}
|
||||
roles.add(modelRole);
|
||||
}
|
||||
|
||||
cachedLDAPRoleMappings = new HashSet<>(roles);
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRoleMapping(RoleModel role) {
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
|
||||
if (role.getContainer().equals(roleContainer)) {
|
||||
|
||||
LDAPQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
|
||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||
Condition roleNameCondition = conditionsBuilder.equal(getRoleNameLdapAttribute(mapperModel), role.getName());
|
||||
Condition membershipCondition = conditionsBuilder.equal(getMembershipLdapAttribute(mapperModel), ldapUser.getDn().toString());
|
||||
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
||||
LDAPObject ldapRole = ldapQuery.getFirstResult();
|
||||
|
||||
if (ldapRole == null) {
|
||||
// Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
|
||||
if (mode == Mode.READ_ONLY) {
|
||||
super.deleteRoleMapping(role);
|
||||
}
|
||||
} else {
|
||||
// Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
|
||||
if (mode == Mode.READ_ONLY) {
|
||||
throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY");
|
||||
} else {
|
||||
// Delete ldap role mappings
|
||||
cachedLDAPRoleMappings = null;
|
||||
deleteRoleMappingInLDAP(mapperModel, ldapProvider, ldapUser, ldapRole);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.deleteRoleMapping(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
/**
|
||||
* All role mappings are retrieved from LDAP and saved into LDAP
|
||||
*/
|
||||
LDAP_ONLY,
|
||||
|
||||
/**
|
||||
* Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then
|
||||
* they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP.
|
||||
* Creating or deleting of role mapping is propagated only to DB.
|
||||
*
|
||||
* This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it
|
||||
* won't be seen by Keycloak
|
||||
*/
|
||||
IMPORT,
|
||||
|
||||
/**
|
||||
* Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB.
|
||||
* Deleting role mappings, which is mapped to LDAP, will throw an error.
|
||||
*/
|
||||
READ_ONLY
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationProviderFactory;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderEvent;
|
||||
import org.keycloak.provider.ProviderEventListener;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapperFactory.class);
|
||||
|
||||
public static final String PROVIDER_ID = "role-ldap-mapper";
|
||||
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty rolesDn = createConfigProperty(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN",
|
||||
"LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(rolesDn);
|
||||
|
||||
ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute",
|
||||
"Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ",
|
||||
ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
|
||||
configProperties.add(roleNameLDAPAttribute);
|
||||
|
||||
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
||||
"Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ",
|
||||
ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER);
|
||||
configProperties.add(membershipLDAPAttribute);
|
||||
|
||||
ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes",
|
||||
"Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(roleObjectClasses);
|
||||
|
||||
ProviderConfigProperty ldapFilter = createConfigProperty(RoleLDAPFederationMapper.ROLES_LDAP_FILTER,
|
||||
"LDAP Filter",
|
||||
"LDAP Filter adds additional custom filter to the whole query. Make sure that it starts with '(' and ends with ')'",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(ldapFilter);
|
||||
|
||||
List<String> modes = new LinkedList<String>();
|
||||
for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) {
|
||||
modes.add(mode.toString());
|
||||
}
|
||||
ProviderConfigProperty mode = createConfigProperty(RoleLDAPFederationMapper.MODE, "Mode",
|
||||
"LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " +
|
||||
"retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
||||
"they are saved to local keycloak DB.",
|
||||
ProviderConfigProperty.LIST_TYPE, modes);
|
||||
configProperties.add(mode);
|
||||
|
||||
ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping",
|
||||
"If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true");
|
||||
configProperties.add(useRealmRolesMappings);
|
||||
|
||||
ProviderConfigProperty clientIdProperty = createConfigProperty(RoleLDAPFederationMapper.CLIENT_ID, "Client ID",
|
||||
"Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false",
|
||||
ProviderConfigProperty.CLIENT_LIST_TYPE, null);
|
||||
configProperties.add(clientIdProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Used to map role mappings of roles from some LDAP DN to Keycloak role mappings of either realm roles or client roles of particular client";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return ROLE_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Role mappings";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
// Sync roles from LDAP to Keycloak DB during creation or update of mapperModel
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
factory.register(new ProviderEventListener() {
|
||||
|
||||
@Override
|
||||
public void onEvent(ProviderEvent event) {
|
||||
if (event instanceof RealmModel.UserFederationMapperEvent) {
|
||||
RealmModel.UserFederationMapperEvent mapperEvent = (RealmModel.UserFederationMapperEvent)event;
|
||||
UserFederationMapperModel mapperModel = mapperEvent.getFederationMapper();
|
||||
RealmModel realm = mapperEvent.getRealm();
|
||||
KeycloakSession session = mapperEvent.getSession();
|
||||
|
||||
if (mapperModel.getFederationMapperType().equals(PROVIDER_ID)) {
|
||||
try {
|
||||
String federationProviderId = mapperModel.getFederationProviderId();
|
||||
UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderById(federationProviderId, realm);
|
||||
if (providerModel == null) {
|
||||
throw new IllegalStateException("Can't find federation provider with ID [" + federationProviderId + "] in realm " + realm.getName());
|
||||
}
|
||||
|
||||
UserFederationProviderFactory ldapFactory = (UserFederationProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, providerModel.getProviderName());
|
||||
LDAPFederationProvider ldapProvider = (LDAPFederationProvider) ldapFactory.getInstance(session, providerModel);
|
||||
|
||||
// Sync roles
|
||||
new RoleLDAPFederationMapper().syncRolesFromLDAP(mapperModel, ldapProvider, realm);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Exception during initial sync of roles from LDAP.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
checkMandatoryConfigAttribute(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", mapperModel);
|
||||
checkMandatoryConfigAttribute(RoleLDAPFederationMapper.MODE, "Mode", mapperModel);
|
||||
|
||||
String realmMappings = mapperModel.getConfig().get(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING);
|
||||
boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
|
||||
if (!useRealmMappings) {
|
||||
String clientId = mapperModel.getConfig().get(RoleLDAPFederationMapper.CLIENT_ID);
|
||||
if (clientId == null || clientId.trim().isEmpty()) {
|
||||
throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used");
|
||||
}
|
||||
}
|
||||
|
||||
String customLdapFilter = mapperModel.getConfig().get(RoleLDAPFederationMapper.ROLES_LDAP_FILTER);
|
||||
if ((customLdapFilter != null && customLdapFilter.trim().length() > 0) && (!customLdapFilter.startsWith("(") || !customLdapFilter.endsWith(")"))) {
|
||||
throw new MapperConfigValidationException("Custom Roles LDAP filter must starts with '(' and ends with ')'");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapper create(KeycloakSession session) {
|
||||
return new RoleLDAPFederationMapper();
|
||||
}
|
||||
}
|
|
@ -64,9 +64,12 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
|||
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
|
||||
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
|
||||
|
||||
public UserAttributeLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||
|
||||
|
@ -93,7 +96,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||
boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||
|
@ -130,7 +133,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
|||
}
|
||||
}
|
||||
|
||||
if (isReadOnly(mapperModel)) {
|
||||
if (isReadOnly()) {
|
||||
ldapUser.addReadOnlyAttributeName(ldapAttrName);
|
||||
}
|
||||
}
|
||||
|
@ -151,14 +154,14 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
|||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(UserFederationMapperModel mapperModel, final LDAPFederationProvider ldapProvider, final LDAPObject ldapUser, UserModel delegate, final RealmModel realm) {
|
||||
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate) {
|
||||
final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||
final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
||||
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||
|
||||
// For writable mode, we want to propagate writing of attribute to LDAP as well
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly()) {
|
||||
|
||||
delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
|
||||
|
||||
|
@ -309,13 +312,13 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
|||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPQuery query) {
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
|
||||
|
||||
// Add mapped attribute to returning ldap attributes
|
||||
query.addReturningLdapAttribute(ldapAttrName);
|
||||
if (isReadOnly(mapperModel)) {
|
||||
if (isReadOnly()) {
|
||||
query.addReturningReadOnlyLdapAttribute(ldapAttrName);
|
||||
}
|
||||
|
||||
|
@ -328,7 +331,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isReadOnly(UserFederationMapperModel mapperModel) {
|
||||
private boolean isReadOnly() {
|
||||
return parseBooleanParameter(mapperModel, READ_ONLY);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
package org.keycloak.federation.ldap.mappers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.mappers.UserFederationMapper;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
|
@ -28,15 +35,15 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
|
|||
configProperties.add(ldapAttribute);
|
||||
|
||||
ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
|
||||
"Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
||||
"Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(readOnly);
|
||||
|
||||
ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always Read Value From LDAP",
|
||||
"If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
||||
"If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(alwaysReadValueFromLDAP);
|
||||
|
||||
ProviderConfigProperty isMandatoryInLdap = createConfigProperty(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "Is Mandatory In LDAP",
|
||||
"If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, "false");
|
||||
"If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(isMandatoryInLdap);
|
||||
}
|
||||
|
||||
|
@ -60,19 +67,33 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
|
|||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
Map<String, String> defaultValues = new HashMap<>();
|
||||
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||
|
||||
String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true";
|
||||
defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
|
||||
|
||||
defaultValues.put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
|
||||
defaultValues.put(UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false");
|
||||
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", mapperModel);
|
||||
checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute", mapperModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapper create(KeycloakSession session) {
|
||||
return new UserAttributeLDAPFederationMapper();
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new UserAttributeLDAPFederationMapper(mapperModel, federationProvider, realm);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership;
|
||||
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
|
||||
/**
|
||||
* Mapper related to mapping of LDAP groups to keycloak model objects (either keycloak roles or keycloak groups)
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface CommonLDAPGroupMapper {
|
||||
|
||||
LDAPQuery createLDAPGroupQuery();
|
||||
|
||||
CommonLDAPGroupMapperConfig getConfig();
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public abstract class CommonLDAPGroupMapperConfig {
|
||||
|
||||
// Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
|
||||
public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
|
||||
|
||||
// See docs for MembershipType enum
|
||||
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
|
||||
|
||||
// See docs for Mode enum
|
||||
public static final String MODE = "mode";
|
||||
|
||||
// See docs for UserRolesRetriever enum
|
||||
public static final String USER_ROLES_RETRIEVE_STRATEGY = "user.roles.retrieve.strategy";
|
||||
|
||||
|
||||
protected final UserFederationMapperModel mapperModel;
|
||||
|
||||
public CommonLDAPGroupMapperConfig(UserFederationMapperModel mapperModel) {
|
||||
this.mapperModel = mapperModel;
|
||||
}
|
||||
|
||||
public String getMembershipLdapAttribute() {
|
||||
String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
|
||||
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||
}
|
||||
|
||||
public MembershipType getMembershipTypeLdapAttribute() {
|
||||
String membershipType = mapperModel.getConfig().get(MEMBERSHIP_ATTRIBUTE_TYPE);
|
||||
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
|
||||
}
|
||||
|
||||
public LDAPGroupMapperMode getMode() {
|
||||
String modeString = mapperModel.getConfig().get(MODE);
|
||||
if (modeString == null || modeString.isEmpty()) {
|
||||
throw new ModelException("Mode is missing! Check your configuration");
|
||||
}
|
||||
|
||||
return Enum.valueOf(LDAPGroupMapperMode.class, modeString.toUpperCase());
|
||||
}
|
||||
|
||||
protected Set<String> getConfigValues(String str) {
|
||||
String[] objClasses = str.split(",");
|
||||
Set<String> trimmed = new HashSet<>();
|
||||
for (String objectClass : objClasses) {
|
||||
objectClass = objectClass.trim();
|
||||
if (objectClass.length() > 0) {
|
||||
trimmed.add(objectClass);
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
public abstract String getLDAPGroupsDn();
|
||||
|
||||
public abstract String getLDAPGroupNameLdapAttribute();
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public enum LDAPGroupMapperMode {
|
||||
|
||||
/**
|
||||
* All role mappings are retrieved from LDAP and saved into LDAP
|
||||
*/
|
||||
LDAP_ONLY,
|
||||
|
||||
/**
|
||||
* Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then
|
||||
* they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP.
|
||||
* Creating or deleting of role mapping is propagated only to DB.
|
||||
*
|
||||
* This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it
|
||||
* won't be seen by Keycloak
|
||||
*/
|
||||
IMPORT,
|
||||
|
||||
/**
|
||||
* Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB.
|
||||
* Deleting role mappings, which is mapped to LDAP, will throw an error.
|
||||
*/
|
||||
READ_ONLY
|
||||
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.LDAPUtils;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapper;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public enum MembershipType {
|
||||
|
||||
/**
|
||||
* Used if LDAP role has it's members declared in form of their full DN. For example ( "member: uid=john,ou=users,dc=example,dc=com" )
|
||||
*/
|
||||
DN {
|
||||
|
||||
@Override
|
||||
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup) {
|
||||
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
||||
return getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), LDAPDn.fromString(config.getLDAPGroupsDn()));
|
||||
}
|
||||
|
||||
// Get just those members of specified group, which are descendants of "requiredParentDn"
|
||||
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
|
||||
Set<String> allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup);
|
||||
|
||||
// Filter and keep just groups
|
||||
Set<LDAPDn> result = new HashSet<>();
|
||||
for (String membership : allMemberships) {
|
||||
LDAPDn childDn = LDAPDn.fromString(membership);
|
||||
if (childDn.isDescendantOf(requiredParentDn)) {
|
||||
result.add(childDn);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) {
|
||||
RealmModel realm = groupMapper.getRealm();
|
||||
LDAPFederationProvider ldapProvider = groupMapper.getLdapProvider();
|
||||
CommonLDAPGroupMapperConfig config = groupMapper.getConfig();
|
||||
|
||||
LDAPDn usersDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
||||
Set<LDAPDn> userDns = getLDAPMembersWithParent(ldapGroup, config.getMembershipLdapAttribute(), usersDn);
|
||||
|
||||
if (userDns == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (userDns.size() <= firstResult) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<LDAPDn> dns = new ArrayList<>(userDns);
|
||||
int max = Math.min(dns.size(), firstResult + maxResults);
|
||||
dns = dns.subList(firstResult, max);
|
||||
|
||||
// If usernameAttrName is same like DN, we can just retrieve usernames from DNs
|
||||
List<String> usernames = new LinkedList<>();
|
||||
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
|
||||
if (ldapConfig.getUsernameLdapAttribute().equals(ldapConfig.getRdnLdapAttribute())) {
|
||||
for (LDAPDn userDn : dns) {
|
||||
String username = userDn.getFirstRdnAttrValue();
|
||||
usernames.add(username);
|
||||
}
|
||||
} else {
|
||||
LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
|
||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||
Condition[] orSubconditions = new Condition[dns.size()];
|
||||
int index = 0;
|
||||
for (LDAPDn userDn : dns) {
|
||||
Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue());
|
||||
orSubconditions[index] = condition;
|
||||
index++;
|
||||
}
|
||||
Condition orCondition = conditionsBuilder.orCondition(orSubconditions);
|
||||
query.addWhereCondition(orCondition);
|
||||
List<LDAPObject> ldapUsers = query.getResultList();
|
||||
for (LDAPObject ldapUser : ldapUsers) {
|
||||
String username = LDAPUtils.getUsername(ldapUser, ldapConfig);
|
||||
usernames.add(username);
|
||||
}
|
||||
}
|
||||
|
||||
// We have dns of users, who are members of our group. Load them now
|
||||
return ldapProvider.loadUsersByUsernames(usernames, realm);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Used if LDAP role has it's members declared in form of pure user uids. For example ( "memberUid: john" )
|
||||
*/
|
||||
UID {
|
||||
|
||||
// Group inheritance not supported for this config
|
||||
@Override
|
||||
public Set<LDAPDn> getLDAPSubgroups(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) {
|
||||
String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute();
|
||||
Set<String> memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup);
|
||||
|
||||
if (memberUids == null || memberUids.size() <= firstResult) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<String> uids = new ArrayList<>(memberUids);
|
||||
int max = Math.min(memberUids.size(), firstResult + maxResults);
|
||||
uids = uids.subList(firstResult, max);
|
||||
|
||||
return groupMapper.getLdapProvider().loadUsersByUsernames(uids, groupMapper.getRealm());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public abstract Set<LDAPDn> getLDAPSubgroups(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup);
|
||||
|
||||
public abstract List<UserModel> getGroupMembers(GroupLDAPFederationMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPUtils;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
|
||||
/**
|
||||
* Strategy for how to retrieve LDAP roles of user
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface UserRolesRetrieveStrategy {
|
||||
|
||||
|
||||
List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser);
|
||||
|
||||
void beforeUserLDAPQuery(LDAPQuery query);
|
||||
|
||||
|
||||
// Impl subclasses
|
||||
|
||||
/**
|
||||
* Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user
|
||||
*/
|
||||
class LoadRolesByMember implements UserRolesRetrieveStrategy {
|
||||
|
||||
@Override
|
||||
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
|
||||
LDAPQuery ldapQuery = roleOrGroupMapper.createLDAPGroupQuery();
|
||||
String membershipAttr = roleOrGroupMapper.getConfig().getMembershipLdapAttribute();
|
||||
|
||||
String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute());
|
||||
|
||||
Condition membershipCondition = getMembershipCondition(membershipAttr, userMembership);
|
||||
ldapQuery.addWhereCondition(membershipCondition);
|
||||
return ldapQuery.getResultList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUserLDAPQuery(LDAPQuery query) {
|
||||
}
|
||||
|
||||
protected Condition getMembershipCondition(String membershipAttr, String userMembership) {
|
||||
return new LDAPQueryConditionsBuilder().equal(membershipAttr, userMembership);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Roles of user will be retrieved from "memberOf" attribute of our user
|
||||
*/
|
||||
class GetRolesFromUserMemberOfAttribute implements UserRolesRetrieveStrategy {
|
||||
|
||||
@Override
|
||||
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
|
||||
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
|
||||
if (memberOfValues == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<LDAPObject> roles = new LinkedList<>();
|
||||
LDAPDn parentDn = LDAPDn.fromString(roleOrGroupMapper.getConfig().getLDAPGroupsDn());
|
||||
|
||||
for (String roleDn : memberOfValues) {
|
||||
LDAPDn roleDN = LDAPDn.fromString(roleDn);
|
||||
if (roleDN.isDescendantOf(parentDn)) {
|
||||
LDAPObject role = new LDAPObject();
|
||||
role.setDn(roleDN);
|
||||
|
||||
String firstDN = roleDN.getFirstRdnAttrName();
|
||||
if (firstDN.equalsIgnoreCase(roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute())) {
|
||||
role.setRdnAttributeName(firstDN);
|
||||
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue());
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeUserLDAPQuery(LDAPQuery query) {
|
||||
query.addReturningLdapAttribute(LDAPConstants.MEMBER_OF);
|
||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.MEMBER_OF);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Extension specific to Active Directory. Roles of user will be retrieved by sending LDAP query to retrieve all roles where "member" is our user.
|
||||
* The query will be able to retrieve memberships recursively with usage of AD specific extension LDAP_MATCHING_RULE_IN_CHAIN, so likely doesn't work on other LDAP servers
|
||||
*/
|
||||
class LoadRolesByMemberRecursively extends LoadRolesByMember {
|
||||
|
||||
protected Condition getMembershipCondition(String membershipAttr, String userMembership) {
|
||||
return new LDAPQueryConditionsBuilder().equal(membershipAttr + LDAPConstants.LDAP_MATCHING_RULE_IN_CHAIN, userMembership);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,602 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.LDAPUtils;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper implements CommonLDAPGroupMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(GroupLDAPFederationMapper.class);
|
||||
|
||||
private final GroupMapperConfig config;
|
||||
private final GroupLDAPFederationMapperFactory factory;
|
||||
|
||||
// Flag to avoid syncing multiple times per transaction
|
||||
private boolean syncFromLDAPPerformedInThisTransaction = false;
|
||||
|
||||
public GroupLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm, GroupLDAPFederationMapperFactory factory) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
this.config = new GroupMapperConfig(mapperModel);
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
|
||||
// CommonLDAPGroupMapper interface
|
||||
|
||||
@Override
|
||||
public LDAPQuery createLDAPGroupQuery() {
|
||||
return createGroupQuery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonLDAPGroupMapperConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// LDAP Group CRUD operations
|
||||
|
||||
public LDAPQuery createGroupQuery() {
|
||||
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
||||
|
||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
|
||||
|
||||
String groupsDn = config.getGroupsDn();
|
||||
ldapQuery.setSearchDn(groupsDn);
|
||||
|
||||
Collection<String> groupObjectClasses = config.getGroupObjectClasses(ldapProvider);
|
||||
ldapQuery.addObjectClasses(groupObjectClasses);
|
||||
|
||||
String customFilter = config.getCustomLdapFilter();
|
||||
if (customFilter != null && customFilter.trim().length() > 0) {
|
||||
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
|
||||
ldapQuery.addWhereCondition(customFilterCondition);
|
||||
}
|
||||
|
||||
ldapQuery.addReturningLdapAttribute(config.getGroupNameLdapAttribute());
|
||||
ldapQuery.addReturningLdapAttribute(config.getMembershipLdapAttribute());
|
||||
|
||||
for (String groupAttr : config.getGroupAttributes()) {
|
||||
ldapQuery.addReturningLdapAttribute(groupAttr);
|
||||
}
|
||||
|
||||
return ldapQuery;
|
||||
}
|
||||
|
||||
public LDAPObject createLDAPGroup(String groupName, Map<String, Set<String>> additionalAttributes) {
|
||||
LDAPObject ldapGroup = LDAPUtils.createLDAPGroup(ldapProvider, groupName, config.getGroupNameLdapAttribute(), config.getGroupObjectClasses(ldapProvider),
|
||||
config.getGroupsDn(), additionalAttributes);
|
||||
|
||||
logger.debugf("Creating group [%s] to LDAP with DN [%s]", groupName, ldapGroup.getDn().toString());
|
||||
return ldapGroup;
|
||||
}
|
||||
|
||||
public LDAPObject loadLDAPGroupByName(String groupName) {
|
||||
LDAPQuery ldapQuery = createGroupQuery();
|
||||
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getGroupNameLdapAttribute(), groupName);
|
||||
ldapQuery.addWhereCondition(roleNameCondition);
|
||||
return ldapQuery.getFirstResult();
|
||||
}
|
||||
|
||||
protected Set<LDAPDn> getLDAPSubgroups(LDAPObject ldapGroup) {
|
||||
MembershipType membershipType = config.getMembershipTypeLdapAttribute();
|
||||
return membershipType.getLDAPSubgroups(this, ldapGroup);
|
||||
}
|
||||
|
||||
|
||||
// Sync from Ldap to KC
|
||||
|
||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() {
|
||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return String.format("%d imported groups, %d updated groups, %d removed groups", getAdded(), getUpdated(), getRemoved());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
logger.debugf("Syncing groups from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||
|
||||
// Get all LDAP groups
|
||||
LDAPQuery ldapQuery = createGroupQuery();
|
||||
List<LDAPObject> ldapGroups = ldapQuery.getResultList();
|
||||
|
||||
// Convert to internal format
|
||||
Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
|
||||
List<GroupTreeResolver.Group> ldapGroupsRep = new LinkedList<>();
|
||||
|
||||
String groupsRdnAttr = config.getGroupNameLdapAttribute();
|
||||
for (LDAPObject ldapGroup : ldapGroups) {
|
||||
String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr);
|
||||
|
||||
Set<String> subgroupNames = new HashSet<>();
|
||||
for (LDAPDn groupDn : getLDAPSubgroups(ldapGroup)) {
|
||||
subgroupNames.add(groupDn.getFirstRdnAttrValue());
|
||||
}
|
||||
|
||||
ldapGroupsRep.add(new GroupTreeResolver.Group(groupName, subgroupNames));
|
||||
ldapGroupsMap.put(groupName, ldapGroup);
|
||||
}
|
||||
|
||||
// Now we have list of LDAP groups. Let's form the tree (if needed)
|
||||
if (config.isPreserveGroupsInheritance()) {
|
||||
try {
|
||||
List<GroupTreeResolver.GroupTreeEntry> groupTrees = new GroupTreeResolver().resolveGroupTree(ldapGroupsRep);
|
||||
|
||||
updateKeycloakGroupTree(groupTrees, ldapGroupsMap, syncResult);
|
||||
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||
throw new ModelException("Couldn't resolve groups from LDAP. Fix LDAP or skip preserve inheritance. Details: " + gre.getMessage(), gre);
|
||||
}
|
||||
} else {
|
||||
Set<String> visitedGroupIds = new HashSet<>();
|
||||
|
||||
// Just add flat structure of groups with all groups at top-level
|
||||
for (Map.Entry<String, LDAPObject> groupEntry : ldapGroupsMap.entrySet()) {
|
||||
String groupName = groupEntry.getKey();
|
||||
GroupModel kcExistingGroup = KeycloakModelUtils.findGroupByPath(realm, "/" + groupName);
|
||||
|
||||
if (kcExistingGroup != null) {
|
||||
updateAttributesOfKCGroup(kcExistingGroup, groupEntry.getValue());
|
||||
syncResult.increaseUpdated();
|
||||
visitedGroupIds.add(kcExistingGroup.getId());
|
||||
} else {
|
||||
GroupModel kcGroup = realm.createGroup(groupName);
|
||||
updateAttributesOfKCGroup(kcGroup, groupEntry.getValue());
|
||||
realm.moveGroup(kcGroup, null);
|
||||
syncResult.increaseAdded();
|
||||
visitedGroupIds.add(kcGroup.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// Possibly remove keycloak groups, which doesn't exists in LDAP
|
||||
if (config.isDropNonExistingGroupsDuringSync()) {
|
||||
dropNonExistingKcGroups(syncResult, visitedGroupIds);
|
||||
}
|
||||
}
|
||||
|
||||
syncFromLDAPPerformedInThisTransaction = true;
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
private void updateKeycloakGroupTree(List<GroupTreeResolver.GroupTreeEntry> groupTrees, Map<String, LDAPObject> ldapGroups, UserFederationSyncResult syncResult) {
|
||||
Set<String> visitedGroupIds = new HashSet<>();
|
||||
|
||||
for (GroupTreeResolver.GroupTreeEntry groupEntry : groupTrees) {
|
||||
updateKeycloakGroupTreeEntry(groupEntry, ldapGroups, null, syncResult, visitedGroupIds);
|
||||
}
|
||||
|
||||
// Possibly remove keycloak groups, which doesn't exists in LDAP
|
||||
if (config.isDropNonExistingGroupsDuringSync()) {
|
||||
dropNonExistingKcGroups(syncResult, visitedGroupIds);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateKeycloakGroupTreeEntry(GroupTreeResolver.GroupTreeEntry groupTreeEntry, Map<String, LDAPObject> ldapGroups, GroupModel kcParent, UserFederationSyncResult syncResult, Set<String> visitedGroupIds) {
|
||||
String groupName = groupTreeEntry.getGroupName();
|
||||
|
||||
// Check if group already exists
|
||||
GroupModel kcGroup = null;
|
||||
Collection<GroupModel> subgroups = kcParent == null ? realm.getTopLevelGroups() : kcParent.getSubGroups();
|
||||
for (GroupModel group : subgroups) {
|
||||
if (group.getName().equals(groupName)) {
|
||||
kcGroup = group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (kcGroup != null) {
|
||||
logger.debugf("Updated Keycloak group '%s' from LDAP", kcGroup.getName());
|
||||
updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName()));
|
||||
syncResult.increaseUpdated();
|
||||
} else {
|
||||
kcGroup = realm.createGroup(groupTreeEntry.getGroupName());
|
||||
if (kcParent == null) {
|
||||
realm.moveGroup(kcGroup, null);
|
||||
logger.debugf("Imported top-level group '%s' from LDAP", kcGroup.getName());
|
||||
} else {
|
||||
realm.moveGroup(kcGroup, kcParent);
|
||||
logger.debugf("Imported group '%s' from LDAP as child of group '%s'", kcGroup.getName(), kcParent.getName());
|
||||
}
|
||||
|
||||
updateAttributesOfKCGroup(kcGroup, ldapGroups.get(kcGroup.getName()));
|
||||
syncResult.increaseAdded();
|
||||
}
|
||||
|
||||
visitedGroupIds.add(kcGroup.getId());
|
||||
|
||||
for (GroupTreeResolver.GroupTreeEntry childEntry : groupTreeEntry.getChildren()) {
|
||||
updateKeycloakGroupTreeEntry(childEntry, ldapGroups, kcGroup, syncResult, visitedGroupIds);
|
||||
}
|
||||
}
|
||||
|
||||
private void dropNonExistingKcGroups(UserFederationSyncResult syncResult, Set<String> visitedGroupIds) {
|
||||
// Remove keycloak groups, which doesn't exists in LDAP
|
||||
List<GroupModel> allGroups = realm.getGroups();
|
||||
for (GroupModel kcGroup : allGroups) {
|
||||
if (!visitedGroupIds.contains(kcGroup.getId())) {
|
||||
logger.debugf("Removing Keycloak group '%s', which doesn't exist in LDAP", kcGroup.getName());
|
||||
realm.removeGroup(kcGroup);
|
||||
syncResult.increaseRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAttributesOfKCGroup(GroupModel kcGroup, LDAPObject ldapGroup) {
|
||||
Collection<String> groupAttributes = config.getGroupAttributes();
|
||||
|
||||
for (String attrName : groupAttributes) {
|
||||
Set<String> attrValues = ldapGroup.getAttributeAsSet(attrName);
|
||||
if (attrValues==null) {
|
||||
kcGroup.removeAttribute(attrName);
|
||||
} else {
|
||||
kcGroup.setAttribute(attrName, new LinkedList<>(attrValues));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override if better effectivity or different algorithm is needed
|
||||
protected GroupModel findKcGroupByLDAPGroup(LDAPObject ldapGroup) {
|
||||
String groupNameAttr = config.getGroupNameLdapAttribute();
|
||||
String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
|
||||
|
||||
List<GroupModel> groups = realm.getGroups();
|
||||
for (GroupModel group : groups) {
|
||||
if (group.getName().equals(groupName)) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected GroupModel findKcGroupOrSyncFromLDAP(LDAPObject ldapGroup, UserModel user) {
|
||||
GroupModel kcGroup = findKcGroupByLDAPGroup(ldapGroup);
|
||||
|
||||
if (kcGroup == null) {
|
||||
// Sync groups from LDAP
|
||||
if (!syncFromLDAPPerformedInThisTransaction) {
|
||||
syncDataFromFederationProviderToKeycloak();
|
||||
kcGroup = findKcGroupByLDAPGroup(ldapGroup);
|
||||
}
|
||||
|
||||
// Could theoretically happen on some LDAP servers if 'memberof' style is used and 'memberof' attribute of user references non-existing group
|
||||
if (kcGroup == null) {
|
||||
String groupName = ldapGroup.getAttributeAsString(config.getGroupNameLdapAttribute());
|
||||
logger.warnf("User '%s' is member of group '%s', which doesn't exists in LDAP", user.getUsername(), groupName);
|
||||
}
|
||||
}
|
||||
|
||||
return kcGroup;
|
||||
}
|
||||
|
||||
|
||||
// Sync from Keycloak to LDAP
|
||||
|
||||
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() {
|
||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return String.format("%d groups imported to LDAP, %d groups updated to LDAP, %d groups removed from LDAP", getAdded(), getUpdated(), getRemoved());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString());
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
logger.debugf("Syncing groups from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||
|
||||
// Query existing LDAP groups
|
||||
LDAPQuery ldapQuery = createGroupQuery();
|
||||
List<LDAPObject> ldapGroups = ldapQuery.getResultList();
|
||||
|
||||
// Convert them to Map<String, LDAPObject>
|
||||
Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
|
||||
String groupsRdnAttr = config.getGroupNameLdapAttribute();
|
||||
for (LDAPObject ldapGroup : ldapGroups) {
|
||||
String groupName = ldapGroup.getAttributeAsString(groupsRdnAttr);
|
||||
ldapGroupsMap.put(groupName, ldapGroup);
|
||||
}
|
||||
|
||||
// Map to track all LDAP groups also exists in Keycloak
|
||||
Set<String> ldapGroupNames = new HashSet<>();
|
||||
|
||||
// Create or update KC groups to LDAP including their attributes
|
||||
for (GroupModel kcGroup : realm.getTopLevelGroups()) {
|
||||
processLdapGroupSyncToLDAP(kcGroup, ldapGroupsMap, ldapGroupNames, syncResult);
|
||||
}
|
||||
|
||||
// If dropNonExisting, then drop all groups, which doesn't exist in KC from LDAP as well
|
||||
if (config.isDropNonExistingGroupsDuringSync()) {
|
||||
Set<String> copy = new HashSet<>(ldapGroupsMap.keySet());
|
||||
for (String groupName : copy) {
|
||||
if (!ldapGroupNames.contains(groupName)) {
|
||||
LDAPObject ldapGroup = ldapGroupsMap.remove(groupName);
|
||||
ldapProvider.getLdapIdentityStore().remove(ldapGroup);
|
||||
syncResult.increaseRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally process memberships,
|
||||
if (config.isPreserveGroupsInheritance()) {
|
||||
for (GroupModel kcGroup : realm.getTopLevelGroups()) {
|
||||
processLdapGroupMembershipsSyncToLDAP(kcGroup, ldapGroupsMap);
|
||||
}
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
// For given kcGroup check if it exists in LDAP (map) by name
|
||||
// If not, create it in LDAP including attributes. Otherwise update attributes in LDAP.
|
||||
// Process this recursively for all subgroups of KC group
|
||||
private void processLdapGroupSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap, Set<String> ldapGroupNames, UserFederationSyncResult syncResult) {
|
||||
String groupName = kcGroup.getName();
|
||||
|
||||
// extract group attributes to be updated to LDAP
|
||||
Map<String, Set<String>> supportedLdapAttributes = new HashMap<>();
|
||||
for (String attrName : config.getGroupAttributes()) {
|
||||
List<String> kcAttrValues = kcGroup.getAttribute(attrName);
|
||||
Set<String> attrValues2 = (kcAttrValues == null || kcAttrValues.isEmpty()) ? null : new HashSet<>(kcAttrValues);
|
||||
supportedLdapAttributes.put(attrName, attrValues2);
|
||||
}
|
||||
|
||||
LDAPObject ldapGroup = ldapGroupsMap.get(groupName);
|
||||
|
||||
if (ldapGroup == null) {
|
||||
ldapGroup = createLDAPGroup(groupName, supportedLdapAttributes);
|
||||
syncResult.increaseAdded();
|
||||
} else {
|
||||
for (Map.Entry<String, Set<String>> attrEntry : supportedLdapAttributes.entrySet()) {
|
||||
ldapGroup.setAttribute(attrEntry.getKey(), attrEntry.getValue());
|
||||
}
|
||||
|
||||
ldapProvider.getLdapIdentityStore().update(ldapGroup);
|
||||
syncResult.increaseUpdated();
|
||||
}
|
||||
|
||||
ldapGroupsMap.put(groupName, ldapGroup);
|
||||
ldapGroupNames.add(groupName);
|
||||
|
||||
// process KC subgroups
|
||||
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
|
||||
processLdapGroupSyncToLDAP(kcSubgroup, ldapGroupsMap, ldapGroupNames, syncResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync memberships update. Update memberships of group in LDAP based on subgroups from KC. Do it recursively
|
||||
private void processLdapGroupMembershipsSyncToLDAP(GroupModel kcGroup, Map<String, LDAPObject> ldapGroupsMap) {
|
||||
LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName());
|
||||
Set<LDAPDn> toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup);
|
||||
|
||||
// Add LDAP subgroups, which are KC subgroups
|
||||
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
|
||||
for (GroupModel kcSubgroup : kcSubgroups) {
|
||||
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
|
||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, ldapSubgroup, false);
|
||||
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
|
||||
}
|
||||
|
||||
// Remove LDAP subgroups, which are not members in KC anymore
|
||||
for (LDAPDn toRemoveDN : toRemoveSubgroupsDNs) {
|
||||
LDAPObject fakeGroup = new LDAPObject();
|
||||
fakeGroup.setDn(toRemoveDN);
|
||||
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, fakeGroup, false);
|
||||
}
|
||||
|
||||
// Update group to LDAP
|
||||
if (!kcGroup.getSubGroups().isEmpty() || !toRemoveSubgroupsDNs.isEmpty()) {
|
||||
ldapProvider.getLdapIdentityStore().update(ldapGroup);
|
||||
}
|
||||
|
||||
for (GroupModel kcSubgroup : kcGroup.getSubGroups()) {
|
||||
processLdapGroupMembershipsSyncToLDAP(kcSubgroup, ldapGroupsMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// group-user membership operations
|
||||
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(GroupModel kcGroup, int firstResult, int maxResults) {
|
||||
LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName());
|
||||
if (ldapGroup == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
MembershipType membershipType = config.getMembershipTypeLdapAttribute();
|
||||
return membershipType.getGroupMembers(this, ldapGroup, firstResult, maxResults);
|
||||
}
|
||||
|
||||
public void addGroupMappingInLDAP(String groupName, LDAPObject ldapUser) {
|
||||
LDAPObject ldapGroup = loadLDAPGroupByName(groupName);
|
||||
if (ldapGroup == null) {
|
||||
syncDataFromKeycloakToFederationProvider();
|
||||
ldapGroup = loadLDAPGroupByName(groupName);
|
||||
}
|
||||
|
||||
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
|
||||
}
|
||||
|
||||
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
|
||||
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
|
||||
}
|
||||
|
||||
protected List<LDAPObject> getLDAPGroupMappings(LDAPObject ldapUser) {
|
||||
String strategyKey = config.getUserGroupsRetrieveStrategy();
|
||||
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
|
||||
return strategy.getLDAPRoleMappings(this, ldapUser);
|
||||
}
|
||||
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
String strategyKey = config.getUserGroupsRetrieveStrategy();
|
||||
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
|
||||
strategy.beforeUserLDAPQuery(query);
|
||||
}
|
||||
|
||||
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||
final LDAPGroupMapperMode mode = config.getMode();
|
||||
|
||||
// For IMPORT mode, all operations are performed against local DB
|
||||
if (mode == LDAPGroupMapperMode.IMPORT) {
|
||||
return delegate;
|
||||
} else {
|
||||
return new LDAPGroupMappingsUserDelegate(delegate, ldapUser);
|
||||
}
|
||||
}
|
||||
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
}
|
||||
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
LDAPGroupMapperMode mode = config.getMode();
|
||||
|
||||
// For now, import LDAP group mappings just during create
|
||||
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
|
||||
|
||||
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
|
||||
|
||||
// Import role mappings from LDAP into Keycloak DB
|
||||
for (LDAPObject ldapGroup : ldapGroups) {
|
||||
|
||||
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, user);
|
||||
if (kcGroup != null) {
|
||||
logger.debugf("User '%s' joins group '%s' during import from LDAP", user.getUsername(), kcGroup.getName());
|
||||
user.joinGroup(kcGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class LDAPGroupMappingsUserDelegate extends UserModelDelegate {
|
||||
|
||||
private final LDAPObject ldapUser;
|
||||
|
||||
// Avoid loading group mappings from LDAP more times per-request
|
||||
private Set<GroupModel> cachedLDAPGroupMappings;
|
||||
|
||||
public LDAPGroupMappingsUserDelegate(UserModel user, LDAPObject ldapUser) {
|
||||
super(user);
|
||||
this.ldapUser = ldapUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<GroupModel> getGroups() {
|
||||
Set<GroupModel> ldapGroupMappings = getLDAPGroupMappingsConverted();
|
||||
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
// Use just group mappings from LDAP
|
||||
return ldapGroupMappings;
|
||||
} else {
|
||||
// Merge mappings from both DB and LDAP
|
||||
Set<GroupModel> modelGroupMappings = super.getGroups();
|
||||
ldapGroupMappings.addAll(modelGroupMappings);
|
||||
return ldapGroupMappings;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void joinGroup(GroupModel group) {
|
||||
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
// We need to create new role mappings in LDAP
|
||||
cachedLDAPGroupMappings = null;
|
||||
addGroupMappingInLDAP(group.getName(), ldapUser);
|
||||
} else {
|
||||
super.joinGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void leaveGroup(GroupModel group) {
|
||||
LDAPQuery ldapQuery = createGroupQuery();
|
||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||
Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName());
|
||||
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
|
||||
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
|
||||
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
||||
LDAPObject ldapGroup = ldapQuery.getFirstResult();
|
||||
|
||||
if (ldapGroup == null) {
|
||||
// Group mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
|
||||
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||
super.leaveGroup(group);
|
||||
}
|
||||
} else {
|
||||
// Group mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
|
||||
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||
throw new ModelException("Not possible to delete LDAP group mappings as mapper mode is READ_ONLY");
|
||||
} else {
|
||||
// Delete ldap role mappings
|
||||
cachedLDAPGroupMappings = null;
|
||||
deleteGroupMappingInLDAP(ldapUser, ldapGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMemberOf(GroupModel group) {
|
||||
Set<GroupModel> ldapGroupMappings = getGroups();
|
||||
return ldapGroupMappings.contains(group);
|
||||
}
|
||||
|
||||
protected Set<GroupModel> getLDAPGroupMappingsConverted() {
|
||||
if (cachedLDAPGroupMappings != null) {
|
||||
return new HashSet<>(cachedLDAPGroupMappings);
|
||||
}
|
||||
|
||||
List<LDAPObject> ldapGroups = getLDAPGroupMappings(ldapUser);
|
||||
|
||||
Set<GroupModel> result = new HashSet<>();
|
||||
for (LDAPObject ldapGroup : ldapGroups) {
|
||||
GroupModel kcGroup = findKcGroupOrSyncFromLDAP(ldapGroup, this);
|
||||
if (kcGroup != null) {
|
||||
result.add(kcGroup);
|
||||
}
|
||||
}
|
||||
|
||||
cachedLDAPGroupMappings = new HashSet<>(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||
import org.keycloak.federation.ldap.mappers.membership.role.RoleMapperConfig;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "group-ldap-mapper";
|
||||
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
protected static final Map<String, UserRolesRetrieveStrategy> userGroupsStrategies = new LinkedHashMap<>();
|
||||
|
||||
// TODO: Merge with RoleLDAPFederationMapperFactory as there are lot of similar properties
|
||||
static {
|
||||
userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
|
||||
userGroupsStrategies.put(GroupMapperConfig.GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
|
||||
userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
|
||||
|
||||
ProviderConfigProperty groupsDn = createConfigProperty(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN",
|
||||
"LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(groupsDn);
|
||||
|
||||
ProviderConfigProperty groupNameLDAPAttribute = createConfigProperty(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE, "Group Name LDAP Attribute",
|
||||
"Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(groupNameLDAPAttribute);
|
||||
|
||||
ProviderConfigProperty groupObjectClasses = createConfigProperty(GroupMapperConfig.GROUP_OBJECT_CLASSES, "Group Object Classes",
|
||||
"Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(groupObjectClasses);
|
||||
|
||||
ProviderConfigProperty preserveGroupInheritance = createConfigProperty(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "Preserve Group Inheritance",
|
||||
"Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " +
|
||||
"preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups",
|
||||
ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(preserveGroupInheritance);
|
||||
|
||||
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
||||
"Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(membershipLDAPAttribute);
|
||||
|
||||
List<String> membershipTypes = new LinkedList<>();
|
||||
for (MembershipType membershipType : MembershipType.values()) {
|
||||
membershipTypes.add(membershipType.toString());
|
||||
}
|
||||
ProviderConfigProperty membershipType = createConfigProperty(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type",
|
||||
"DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
|
||||
ProviderConfigProperty.LIST_TYPE, membershipTypes);
|
||||
configProperties.add(membershipType);
|
||||
|
||||
ProviderConfigProperty ldapFilter = createConfigProperty(GroupMapperConfig.GROUPS_LDAP_FILTER,
|
||||
"LDAP Filter",
|
||||
"LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(ldapFilter);
|
||||
|
||||
List<String> modes = new LinkedList<>();
|
||||
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
|
||||
modes.add(mode.toString());
|
||||
}
|
||||
ProviderConfigProperty mode = createConfigProperty(GroupMapperConfig.MODE, "Mode",
|
||||
"LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " +
|
||||
"retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are " +
|
||||
"retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
||||
"they are saved to local keycloak DB.",
|
||||
ProviderConfigProperty.LIST_TYPE, modes);
|
||||
configProperties.add(mode);
|
||||
|
||||
List<String> roleRetrievers = new LinkedList<>(userGroupsStrategies.keySet());
|
||||
ProviderConfigProperty retriever = createConfigProperty(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, "User Groups Retrieve Strategy",
|
||||
"Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " +
|
||||
"GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||
"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension."
|
||||
,
|
||||
ProviderConfigProperty.LIST_TYPE, roleRetrievers);
|
||||
configProperties.add(retriever);
|
||||
|
||||
ProviderConfigProperty mappedGroupAttributes = createConfigProperty(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES, "Mapped Group Attributes",
|
||||
"List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " +
|
||||
"Leave this empty if no additional group attributes are required to be mapped in Keycloak. ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(mappedGroupAttributes);
|
||||
|
||||
ProviderConfigProperty dropNonExistingGroupsDuringSync = createConfigProperty(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "Drop non-existing groups during sync",
|
||||
"If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted",
|
||||
ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(dropNonExistingGroupsDuringSync);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Used to map group mappings of groups from some LDAP DN to Keycloak group mappings";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return GROUP_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Group mappings";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
Map<String, String> defaultValues = new HashMap<>();
|
||||
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||
|
||||
defaultValues.put(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE, LDAPConstants.CN);
|
||||
|
||||
String roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
defaultValues.put(GroupMapperConfig.GROUP_OBJECT_CLASSES, roleObjectClasses);
|
||||
|
||||
defaultValues.put(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true");
|
||||
defaultValues.put(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, LDAPConstants.MEMBER);
|
||||
|
||||
String mode = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
|
||||
defaultValues.put(GroupMapperConfig.MODE, mode);
|
||||
defaultValues.put(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE);
|
||||
|
||||
defaultValues.put(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "false");
|
||||
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-groups-to-keycloak", true, "sync-keycloak-groups-to-ldap");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", mapperModel);
|
||||
checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", mapperModel);
|
||||
|
||||
String mt = mapperModel.getConfig().get(CommonLDAPGroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE);
|
||||
MembershipType membershipType = mt==null ? MembershipType.DN : Enum.valueOf(MembershipType.class, mt);
|
||||
boolean preserveGroupInheritance = Boolean.parseBoolean(mapperModel.getConfig().get(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE));
|
||||
if (preserveGroupInheritance && membershipType != MembershipType.DN) {
|
||||
throw new MapperConfigValidationException("Not possible to preserve group inheritance and use UID membership type together");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new GroupLDAPFederationMapper(mapperModel, federationProvider, realm, this);
|
||||
}
|
||||
|
||||
protected UserRolesRetrieveStrategy getUserGroupsRetrieveStrategy(String strategyKey) {
|
||||
return userGroupsStrategies.get(strategyKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GroupMapperConfig extends CommonLDAPGroupMapperConfig {
|
||||
|
||||
// LDAP DN where are groups of this tree saved.
|
||||
public static final String GROUPS_DN = "groups.dn";
|
||||
|
||||
// Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be "cn"
|
||||
public static final String GROUP_NAME_LDAP_ATTRIBUTE = "group.name.ldap.attribute";
|
||||
|
||||
// Object classes of the group object.
|
||||
public static final String GROUP_OBJECT_CLASSES = "group.object.classes";
|
||||
|
||||
// Flag whether group inheritance from LDAP should be propagated to Keycloak group inheritance.
|
||||
public static final String PRESERVE_GROUP_INHERITANCE = "preserve.group.inheritance";
|
||||
|
||||
// Customized LDAP filter which is added to the whole LDAP query
|
||||
public static final String GROUPS_LDAP_FILTER = "groups.ldap.filter";
|
||||
|
||||
// Name of attributes of the LDAP group object, which will be mapped as attributes of Group in Keycloak
|
||||
public static final String MAPPED_GROUP_ATTRIBUTES = "mapped.group.attributes";
|
||||
|
||||
// During sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted
|
||||
public static final String DROP_NON_EXISTING_GROUPS_DURING_SYNC = "drop.non.existing.groups.during.sync";
|
||||
|
||||
// See UserRolesRetrieveStrategy
|
||||
public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE";
|
||||
public static final String GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE";
|
||||
public static final String LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
|
||||
|
||||
public GroupMapperConfig(UserFederationMapperModel mapperModel) {
|
||||
super(mapperModel);
|
||||
}
|
||||
|
||||
|
||||
public String getGroupsDn() {
|
||||
String groupsDn = mapperModel.getConfig().get(GROUPS_DN);
|
||||
if (groupsDn == null) {
|
||||
throw new ModelException("Groups DN is null! Check your configuration");
|
||||
}
|
||||
return groupsDn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLDAPGroupsDn() {
|
||||
return getGroupsDn();
|
||||
}
|
||||
|
||||
public String getGroupNameLdapAttribute() {
|
||||
String rolesRdnAttr = mapperModel.getConfig().get(GROUP_NAME_LDAP_ATTRIBUTE);
|
||||
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLDAPGroupNameLdapAttribute() {
|
||||
return getGroupNameLdapAttribute();
|
||||
}
|
||||
|
||||
public boolean isPreserveGroupsInheritance() {
|
||||
return AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, PRESERVE_GROUP_INHERITANCE);
|
||||
}
|
||||
|
||||
public String getMembershipLdapAttribute() {
|
||||
String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
|
||||
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
|
||||
}
|
||||
|
||||
public Collection<String> getGroupObjectClasses(LDAPFederationProvider ldapProvider) {
|
||||
String objectClasses = mapperModel.getConfig().get(GROUP_OBJECT_CLASSES);
|
||||
if (objectClasses == null) {
|
||||
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
||||
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
}
|
||||
|
||||
return getConfigValues(objectClasses);
|
||||
}
|
||||
|
||||
public Collection<String> getGroupAttributes() {
|
||||
String groupAttrs = mapperModel.getConfig().get(MAPPED_GROUP_ATTRIBUTES);
|
||||
return (groupAttrs == null) ? Collections.<String>emptySet() : getConfigValues(groupAttrs);
|
||||
}
|
||||
|
||||
public String getCustomLdapFilter() {
|
||||
return mapperModel.getConfig().get(GROUPS_LDAP_FILTER);
|
||||
}
|
||||
|
||||
public boolean isDropNonExistingGroupsDuringSync() {
|
||||
return AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, DROP_NON_EXISTING_GROUPS_DURING_SYNC);
|
||||
}
|
||||
|
||||
public String getUserGroupsRetrieveStrategy() {
|
||||
String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY);
|
||||
return strategyString!=null ? strategyString : LOAD_GROUPS_BY_MEMBER_ATTRIBUTE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.group;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GroupTreeResolver {
|
||||
|
||||
|
||||
/**
|
||||
* Fully resolves list of group trees to be used in Keycloak. The input is group info (usually from LDAP) where each "Group" object contains
|
||||
* just it's name and direct children.
|
||||
*
|
||||
* The operation also performs validation as rules for LDAP are less strict than for Keycloak (In LDAP, the recursion is possible and multiple parents of single group is also allowed)
|
||||
*
|
||||
* @param groups
|
||||
* @return
|
||||
* @throws GroupTreeResolveException
|
||||
*/
|
||||
public List<GroupTreeEntry> resolveGroupTree(List<Group> groups) throws GroupTreeResolveException {
|
||||
// 1- Get parents of each group
|
||||
Map<String, List<String>> parentsTree = getParentsTree(groups);
|
||||
|
||||
// 2 - Get rootGroups (groups without parent) and check if there is no group with multiple parents
|
||||
List<String> rootGroups = new LinkedList<>();
|
||||
for (Map.Entry<String, List<String>> group : parentsTree.entrySet()) {
|
||||
int parentCount = group.getValue().size();
|
||||
if (parentCount == 0) {
|
||||
rootGroups.add(group.getKey());
|
||||
} else if (parentCount > 1) {
|
||||
throw new GroupTreeResolveException("Group '" + group.getKey() + "' detected to have multiple parents. This is not allowed in Keycloak. Parents are: " + group.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 3 - Just convert to map for easier retrieval
|
||||
Map<String, Group> asMap = new TreeMap<>();
|
||||
for (Group group : groups) {
|
||||
asMap.put(group.getGroupName(), group);
|
||||
}
|
||||
|
||||
// 4 - Now we have rootGroups. Let's resolve them
|
||||
List<GroupTreeEntry> finalResult = new LinkedList<>();
|
||||
Set<String> visitedGroups = new TreeSet<>();
|
||||
for (String rootGroupName : rootGroups) {
|
||||
List<String> subtree = new LinkedList<>();
|
||||
subtree.add(rootGroupName);
|
||||
GroupTreeEntry groupTree = resolveGroupTree(rootGroupName, asMap, visitedGroups, subtree);
|
||||
finalResult.add(groupTree);
|
||||
}
|
||||
|
||||
|
||||
// 5 - Check recursion
|
||||
if (visitedGroups.size() != asMap.size()) {
|
||||
// Recursion detected. Try to find where it is
|
||||
for (Map.Entry<String, Group> entry : asMap.entrySet()) {
|
||||
String groupName = entry.getKey();
|
||||
if (!visitedGroups.contains(groupName)) {
|
||||
List<String> subtree = new LinkedList<>();
|
||||
subtree.add(groupName);
|
||||
|
||||
Set<String> newVisitedGroups = new TreeSet<>();
|
||||
resolveGroupTree(groupName, asMap, newVisitedGroups, subtree);
|
||||
visitedGroups.addAll(newVisitedGroups);
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't happen
|
||||
throw new GroupTreeResolveException("Illegal state: Recursion detected, but wasn't able to find it");
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
|
||||
private Map<String, List<String>> getParentsTree(List<Group> groups) throws GroupTreeResolveException {
|
||||
Map<String, List<String>> result = new TreeMap<>();
|
||||
|
||||
for (Group group : groups) {
|
||||
result.put(group.getGroupName(), new LinkedList<String>());
|
||||
}
|
||||
|
||||
for (Group group : groups) {
|
||||
for (String child : group.getChildrenNames()) {
|
||||
List<String> list = result.get(child);
|
||||
if (list == null) {
|
||||
throw new GroupTreeResolveException("Group '" + child + "' referenced as member of group '" + group.getGroupName() + "' doesn't exists");
|
||||
}
|
||||
list.add(group.getGroupName());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private GroupTreeEntry resolveGroupTree(String groupName, Map<String, Group> asMap, Set<String> visitedGroups, List<String> currentSubtree) throws GroupTreeResolveException {
|
||||
if (visitedGroups.contains(groupName)) {
|
||||
throw new GroupTreeResolveException("Recursion detected when trying to resolve group '" + groupName + "'. Whole recursion path: " + currentSubtree);
|
||||
}
|
||||
|
||||
visitedGroups.add(groupName);
|
||||
|
||||
Group group = asMap.get(groupName);
|
||||
|
||||
List<GroupTreeEntry> children = new LinkedList<>();
|
||||
GroupTreeEntry result = new GroupTreeEntry(group.getGroupName(), children);
|
||||
|
||||
for (String childrenName : group.getChildrenNames()) {
|
||||
List<String> subtreeCopy = new LinkedList<>(currentSubtree);
|
||||
subtreeCopy.add(childrenName);
|
||||
GroupTreeEntry childEntry = resolveGroupTree(childrenName, asMap, visitedGroups, subtreeCopy);
|
||||
children.add(childEntry);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// static classes
|
||||
|
||||
public static class GroupTreeResolveException extends Exception {
|
||||
|
||||
public GroupTreeResolveException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Group {
|
||||
|
||||
private final String groupName;
|
||||
private final List<String> childrenNames;
|
||||
|
||||
public Group(String groupName, String... childrenNames) {
|
||||
this(groupName, Arrays.asList(childrenNames));
|
||||
}
|
||||
|
||||
public Group(String groupName, Collection<String> childrenNames) {
|
||||
this.groupName = groupName;
|
||||
this.childrenNames = new LinkedList<>(childrenNames);
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
public List<String> getChildrenNames() {
|
||||
return childrenNames;
|
||||
}
|
||||
}
|
||||
|
||||
public static class GroupTreeEntry {
|
||||
|
||||
private final String groupName;
|
||||
private final List<GroupTreeEntry> children;
|
||||
|
||||
public GroupTreeEntry(String groupName, List<GroupTreeEntry> children) {
|
||||
this.groupName = groupName;
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
public List<GroupTreeEntry> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder("{ " + groupName + " -> [ ");
|
||||
for (GroupTreeEntry child : children) {
|
||||
builder.append(child.toString());
|
||||
}
|
||||
builder.append(" ]}");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.role;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.LDAPUtils;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.Condition;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapper;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationSyncResult;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
/**
|
||||
* Map realm roles or roles of particular client to LDAP groups
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper implements CommonLDAPGroupMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class);
|
||||
|
||||
private final RoleMapperConfig config;
|
||||
private final RoleLDAPFederationMapperFactory factory;
|
||||
|
||||
public RoleLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm, RoleLDAPFederationMapperFactory factory) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
this.config = new RoleMapperConfig(mapperModel);
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public LDAPQuery createLDAPGroupQuery() {
|
||||
return createRoleQuery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonLDAPGroupMapperConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
LDAPGroupMapperMode mode = config.getMode();
|
||||
|
||||
// For now, import LDAP role mappings just during create
|
||||
if (mode == LDAPGroupMapperMode.IMPORT && isCreate) {
|
||||
|
||||
List<LDAPObject> ldapRoles = getLDAPRoleMappings(ldapUser);
|
||||
|
||||
// Import role mappings from LDAP into Keycloak DB
|
||||
String roleNameAttr = config.getRoleNameLdapAttribute();
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(roleNameAttr);
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer();
|
||||
RoleModel role = roleContainer.getRole(roleName);
|
||||
|
||||
if (role == null) {
|
||||
role = roleContainer.addRole(roleName);
|
||||
}
|
||||
|
||||
logger.debugf("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername());
|
||||
user.grantRole(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
}
|
||||
|
||||
|
||||
// Sync roles from LDAP to Keycloak DB
|
||||
@Override
|
||||
public UserFederationSyncResult syncDataFromFederationProviderToKeycloak() {
|
||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return String.format("%d imported roles, %d roles already exists in Keycloak", getAdded(), getUpdated());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
logger.debugf("Syncing roles from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||
|
||||
// Send LDAP query
|
||||
LDAPQuery ldapQuery = createRoleQuery();
|
||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer();
|
||||
String rolesRdnAttr = config.getRoleNameLdapAttribute();
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||
|
||||
if (roleContainer.getRole(roleName) == null) {
|
||||
logger.debugf("Syncing role [%s] from LDAP to keycloak DB", roleName);
|
||||
roleContainer.addRole(roleName);
|
||||
syncResult.increaseAdded();
|
||||
} else {
|
||||
syncResult.increaseUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
|
||||
// Sync roles from Keycloak back to LDAP
|
||||
@Override
|
||||
public UserFederationSyncResult syncDataFromKeycloakToFederationProvider() {
|
||||
UserFederationSyncResult syncResult = new UserFederationSyncResult() {
|
||||
|
||||
@Override
|
||||
public String getStatus() {
|
||||
return String.format("%d roles imported to LDAP, %d roles already existed in LDAP", getAdded(), getUpdated());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (config.getMode() != LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
logger.warnf("Ignored sync for federation mapper '%s' as it's mode is '%s'", mapperModel.getName(), config.getMode().toString());
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
logger.debugf("Syncing roles from Keycloak into LDAP. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
|
||||
|
||||
// Send LDAP query to see which roles exists there
|
||||
LDAPQuery ldapQuery = createRoleQuery();
|
||||
List<LDAPObject> ldapRoles = ldapQuery.getResultList();
|
||||
|
||||
Set<String> ldapRoleNames = new HashSet<>();
|
||||
String rolesRdnAttr = config.getRoleNameLdapAttribute();
|
||||
for (LDAPObject ldapRole : ldapRoles) {
|
||||
String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
|
||||
ldapRoleNames.add(roleName);
|
||||
}
|
||||
|
||||
|
||||
RoleContainerModel roleContainer = getTargetRoleContainer();
|
||||
Set<RoleModel> keycloakRoles = roleContainer.getRoles();
|
||||
|
||||
for (RoleModel keycloakRole : keycloakRoles) {
|
||||
String roleName = keycloakRole.getName();
|
||||
if (ldapRoleNames.contains(roleName)) {
|
||||
syncResult.increaseUpdated();
|
||||
} else {
|
||||
logger.debugf("Syncing role [%s] from Keycloak to LDAP", roleName);
|
||||
createLDAPRole(roleName);
|
||||
syncResult.increaseAdded();
|
||||
}
|
||||
}
|
||||
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
// TODO: Possible to merge with GroupMapper and move to common class
|
||||
public LDAPQuery createRoleQuery() {
|
||||
LDAPQuery ldapQuery = new LDAPQuery(ldapProvider);
|
||||
|
||||
// For now, use same search scope, which is configured "globally" and used for user's search.
|
||||
ldapQuery.setSearchScope(ldapProvider.getLdapIdentityStore().getConfig().getSearchScope());
|
||||
|
||||
String rolesDn = config.getRolesDn();
|
||||
ldapQuery.setSearchDn(rolesDn);
|
||||
|
||||
Collection<String> roleObjectClasses = config.getRoleObjectClasses(ldapProvider);
|
||||
ldapQuery.addObjectClasses(roleObjectClasses);
|
||||
|
||||
String rolesRdnAttr = config.getRoleNameLdapAttribute();
|
||||
|
||||
String customFilter = config.getCustomLdapFilter();
|
||||
if (customFilter != null && customFilter.trim().length() > 0) {
|
||||
Condition customFilterCondition = new LDAPQueryConditionsBuilder().addCustomLDAPFilter(customFilter);
|
||||
ldapQuery.addWhereCondition(customFilterCondition);
|
||||
}
|
||||
|
||||
String membershipAttr = config.getMembershipLdapAttribute();
|
||||
ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
|
||||
ldapQuery.addReturningLdapAttribute(membershipAttr);
|
||||
|
||||
return ldapQuery;
|
||||
}
|
||||
|
||||
protected RoleContainerModel getTargetRoleContainer() {
|
||||
boolean realmRolesMapping = config.isRealmRolesMapping();
|
||||
if (realmRolesMapping) {
|
||||
return realm;
|
||||
} else {
|
||||
String clientId = config.getClientId();
|
||||
if (clientId == null) {
|
||||
throw new ModelException("Using client roles mapping is requested, but parameter client.id not found!");
|
||||
}
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
if (client == null) {
|
||||
throw new ModelException("Can't found requested client with clientId: " + clientId);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LDAPObject createLDAPRole(String roleName) {
|
||||
LDAPObject ldapRole = LDAPUtils.createLDAPGroup(ldapProvider, roleName, config.getRoleNameLdapAttribute(), config.getRoleObjectClasses(ldapProvider),
|
||||
config.getRolesDn(), Collections.<String, Set<String>>emptyMap());
|
||||
|
||||
logger.debugf("Creating role [%s] to LDAP with DN [%s]", roleName, ldapRole.getDn().toString());
|
||||
return ldapRole;
|
||||
}
|
||||
|
||||
public void addRoleMappingInLDAP(String roleName, LDAPObject ldapUser) {
|
||||
LDAPObject ldapRole = loadLDAPRoleByName(roleName);
|
||||
if (ldapRole == null) {
|
||||
ldapRole = createLDAPRole(roleName);
|
||||
}
|
||||
|
||||
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
|
||||
}
|
||||
|
||||
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
|
||||
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
|
||||
}
|
||||
|
||||
public LDAPObject loadLDAPRoleByName(String roleName) {
|
||||
LDAPQuery ldapQuery = createRoleQuery();
|
||||
Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(config.getRoleNameLdapAttribute(), roleName);
|
||||
ldapQuery.addWhereCondition(roleNameCondition);
|
||||
return ldapQuery.getFirstResult();
|
||||
}
|
||||
|
||||
protected List<LDAPObject> getLDAPRoleMappings(LDAPObject ldapUser) {
|
||||
String strategyKey = config.getUserRolesRetrieveStrategy();
|
||||
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
|
||||
return strategy.getLDAPRoleMappings(this, ldapUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||
final LDAPGroupMapperMode mode = config.getMode();
|
||||
|
||||
// For IMPORT mode, all operations are performed against local DB
|
||||
if (mode == LDAPGroupMapperMode.IMPORT) {
|
||||
return delegate;
|
||||
} else {
|
||||
return new LDAPRoleMappingsUserDelegate(delegate, ldapUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
String strategyKey = config.getUserRolesRetrieveStrategy();
|
||||
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
|
||||
strategy.beforeUserLDAPQuery(query);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
|
||||
|
||||
private final LDAPObject ldapUser;
|
||||
private final RoleContainerModel roleContainer;
|
||||
|
||||
// Avoid loading role mappings from LDAP more times per-request
|
||||
private Set<RoleModel> cachedLDAPRoleMappings;
|
||||
|
||||
public LDAPRoleMappingsUserDelegate(UserModel user, LDAPObject ldapUser) {
|
||||
super(user);
|
||||
this.ldapUser = ldapUser;
|
||||
this.roleContainer = getTargetRoleContainer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getRealmRoleMappings() {
|
||||
if (roleContainer.equals(realm)) {
|
||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
|
||||
|
||||
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
// Use just role mappings from LDAP
|
||||
return ldapRoleMappings;
|
||||
} else {
|
||||
// Merge mappings from both DB and LDAP
|
||||
Set<RoleModel> modelRoleMappings = super.getRealmRoleMappings();
|
||||
ldapRoleMappings.addAll(modelRoleMappings);
|
||||
return ldapRoleMappings;
|
||||
}
|
||||
} else {
|
||||
return super.getRealmRoleMappings();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getClientRoleMappings(ClientModel client) {
|
||||
if (roleContainer.equals(client)) {
|
||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
|
||||
|
||||
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
// Use just role mappings from LDAP
|
||||
return ldapRoleMappings;
|
||||
} else {
|
||||
// Merge mappings from both DB and LDAP
|
||||
Set<RoleModel> modelRoleMappings = super.getClientRoleMappings(client);
|
||||
ldapRoleMappings.addAll(modelRoleMappings);
|
||||
return ldapRoleMappings;
|
||||
}
|
||||
} else {
|
||||
return super.getClientRoleMappings(client);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRole(RoleModel role) {
|
||||
Set<RoleModel> roles = getRoleMappings();
|
||||
return KeycloakModelUtils.hasRole(roles, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantRole(RoleModel role) {
|
||||
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
|
||||
if (role.getContainer().equals(roleContainer)) {
|
||||
|
||||
// We need to create new role mappings in LDAP
|
||||
cachedLDAPRoleMappings = null;
|
||||
addRoleMappingInLDAP(role.getName(), ldapUser);
|
||||
} else {
|
||||
super.grantRole(role);
|
||||
}
|
||||
} else {
|
||||
super.grantRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<RoleModel> getRoleMappings() {
|
||||
Set<RoleModel> modelRoleMappings = super.getRoleMappings();
|
||||
|
||||
Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted();
|
||||
|
||||
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
|
||||
// For LDAP-only we want to retrieve role mappings of target container just from LDAP
|
||||
Set<RoleModel> modelRolesCopy = new HashSet<>(modelRoleMappings);
|
||||
for (RoleModel role : modelRolesCopy) {
|
||||
if (role.getContainer().equals(roleContainer)) {
|
||||
modelRoleMappings.remove(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelRoleMappings.addAll(ldapRoleMappings);
|
||||
return modelRoleMappings;
|
||||
}
|
||||
|
||||
protected Set<RoleModel> getLDAPRoleMappingsConverted() {
|
||||
if (cachedLDAPRoleMappings != null) {
|
||||
return new HashSet<>(cachedLDAPRoleMappings);
|
||||
}
|
||||
|
||||
List<LDAPObject> ldapRoles = getLDAPRoleMappings(ldapUser);
|
||||
|
||||
Set<RoleModel> roles = new HashSet<>();
|
||||
String roleNameLdapAttr = config.getRoleNameLdapAttribute();
|
||||
for (LDAPObject role : ldapRoles) {
|
||||
String roleName = role.getAttributeAsString(roleNameLdapAttr);
|
||||
RoleModel modelRole = roleContainer.getRole(roleName);
|
||||
if (modelRole == null) {
|
||||
// Add role to local DB
|
||||
modelRole = roleContainer.addRole(roleName);
|
||||
}
|
||||
roles.add(modelRole);
|
||||
}
|
||||
|
||||
cachedLDAPRoleMappings = new HashSet<>(roles);
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteRoleMapping(RoleModel role) {
|
||||
if (role.getContainer().equals(roleContainer)) {
|
||||
|
||||
LDAPQuery ldapQuery = createRoleQuery();
|
||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||
Condition roleNameCondition = conditionsBuilder.equal(config.getRoleNameLdapAttribute(), role.getName());
|
||||
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
|
||||
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
|
||||
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
|
||||
LDAPObject ldapRole = ldapQuery.getFirstResult();
|
||||
|
||||
if (ldapRole == null) {
|
||||
// Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
|
||||
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||
super.deleteRoleMapping(role);
|
||||
}
|
||||
} else {
|
||||
// Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
|
||||
if (config.getMode() == LDAPGroupMapperMode.READ_ONLY) {
|
||||
throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY");
|
||||
} else {
|
||||
// Delete ldap role mappings
|
||||
cachedLDAPRoleMappings = null;
|
||||
deleteRoleMappingInLDAP(ldapUser, ldapRole);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.deleteRoleMapping(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.role;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPConfig;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
|
||||
import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
|
||||
import org.keycloak.federation.ldap.mappers.membership.MembershipType;
|
||||
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "role-ldap-mapper";
|
||||
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
protected static final Map<String, UserRolesRetrieveStrategy> userRolesStrategies = new LinkedHashMap<>();
|
||||
|
||||
|
||||
static {
|
||||
userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
|
||||
userRolesStrategies.put(RoleMapperConfig.GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
|
||||
userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
|
||||
|
||||
ProviderConfigProperty rolesDn = createConfigProperty(RoleMapperConfig.ROLES_DN, "LDAP Roles DN",
|
||||
"LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(rolesDn);
|
||||
|
||||
ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute",
|
||||
"Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(roleNameLDAPAttribute);
|
||||
|
||||
ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleMapperConfig.ROLE_OBJECT_CLASSES, "Role Object Classes",
|
||||
"Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(roleObjectClasses);
|
||||
|
||||
ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
|
||||
"Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(membershipLDAPAttribute);
|
||||
|
||||
|
||||
List<String> membershipTypes = new LinkedList<>();
|
||||
for (MembershipType membershipType : MembershipType.values()) {
|
||||
membershipTypes.add(membershipType.toString());
|
||||
}
|
||||
ProviderConfigProperty membershipType = createConfigProperty(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, "Membership Attribute Type",
|
||||
"DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
|
||||
"UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .",
|
||||
ProviderConfigProperty.LIST_TYPE, membershipTypes);
|
||||
configProperties.add(membershipType);
|
||||
|
||||
|
||||
ProviderConfigProperty ldapFilter = createConfigProperty(RoleMapperConfig.ROLES_LDAP_FILTER,
|
||||
"LDAP Filter",
|
||||
"LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'",
|
||||
ProviderConfigProperty.STRING_TYPE, null);
|
||||
configProperties.add(ldapFilter);
|
||||
|
||||
|
||||
List<String> modes = new LinkedList<>();
|
||||
for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
|
||||
modes.add(mode.toString());
|
||||
}
|
||||
ProviderConfigProperty mode = createConfigProperty(RoleMapperConfig.MODE, "Mode",
|
||||
"LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " +
|
||||
"retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " +
|
||||
"they are saved to local keycloak DB.",
|
||||
ProviderConfigProperty.LIST_TYPE, modes);
|
||||
configProperties.add(mode);
|
||||
|
||||
|
||||
List<String> roleRetrievers = new LinkedList<>(userRolesStrategies.keySet());
|
||||
ProviderConfigProperty retriever = createConfigProperty(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, "User Roles Retrieve Strategy",
|
||||
"Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
|
||||
"GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
|
||||
"LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension."
|
||||
,
|
||||
ProviderConfigProperty.LIST_TYPE, roleRetrievers);
|
||||
configProperties.add(retriever);
|
||||
|
||||
|
||||
ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleMapperConfig.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping",
|
||||
"If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, null);
|
||||
configProperties.add(useRealmRolesMappings);
|
||||
|
||||
ProviderConfigProperty clientIdProperty = createConfigProperty(RoleMapperConfig.CLIENT_ID, "Client ID",
|
||||
"Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false",
|
||||
ProviderConfigProperty.CLIENT_LIST_TYPE, null);
|
||||
configProperties.add(clientIdProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Used to map role mappings of roles from some LDAP DN to Keycloak role mappings of either realm roles or client roles of particular client";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return ROLE_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Role mappings";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
Map<String, String> defaultValues = new HashMap<>();
|
||||
LDAPConfig config = new LDAPConfig(providerModel.getConfig());
|
||||
|
||||
defaultValues.put(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE, LDAPConstants.CN);
|
||||
|
||||
String roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
defaultValues.put(RoleMapperConfig.ROLE_OBJECT_CLASSES, roleObjectClasses);
|
||||
|
||||
defaultValues.put(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE, LDAPConstants.MEMBER);
|
||||
defaultValues.put(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE, MembershipType.DN.toString());
|
||||
|
||||
String mode = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
|
||||
defaultValues.put(RoleMapperConfig.MODE, mode);
|
||||
|
||||
defaultValues.put(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY, RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE);
|
||||
defaultValues.put(RoleMapperConfig.USE_REALM_ROLES_MAPPING, "true");
|
||||
return defaultValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserFederationMapperSyncConfigRepresentation getSyncConfig() {
|
||||
return new UserFederationMapperSyncConfigRepresentation(true, "sync-ldap-roles-to-keycloak", true, "sync-keycloak-roles-to-ldap");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", mapperModel);
|
||||
checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", mapperModel);
|
||||
|
||||
String realmMappings = mapperModel.getConfig().get(RoleMapperConfig.USE_REALM_ROLES_MAPPING);
|
||||
boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
|
||||
if (!useRealmMappings) {
|
||||
String clientId = mapperModel.getConfig().get(RoleMapperConfig.CLIENT_ID);
|
||||
if (clientId == null || clientId.trim().isEmpty()) {
|
||||
throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used");
|
||||
}
|
||||
}
|
||||
|
||||
String customLdapFilter = mapperModel.getConfig().get(RoleMapperConfig.ROLES_LDAP_FILTER);
|
||||
if ((customLdapFilter != null && customLdapFilter.trim().length() > 0) && (!customLdapFilter.startsWith("(") || !customLdapFilter.endsWith(")"))) {
|
||||
throw new MapperConfigValidationException("Custom Roles LDAP filter must starts with '(' and ends with ')'");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new RoleLDAPFederationMapper(mapperModel, federationProvider, realm, this);
|
||||
}
|
||||
|
||||
protected UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(String strategyKey) {
|
||||
return userRolesStrategies.get(strategyKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package org.keycloak.federation.ldap.mappers.membership.role;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
|
||||
import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class RoleMapperConfig extends CommonLDAPGroupMapperConfig {
|
||||
|
||||
// LDAP DN where are roles of this tree saved.
|
||||
public static final String ROLES_DN = "roles.dn";
|
||||
|
||||
// Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
|
||||
public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
|
||||
|
||||
// Object classes of the role object.
|
||||
public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
|
||||
|
||||
// Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID)
|
||||
public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping";
|
||||
|
||||
// ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false
|
||||
public static final String CLIENT_ID = "client.id";
|
||||
|
||||
// Customized LDAP filter which is added to the whole LDAP query
|
||||
public static final String ROLES_LDAP_FILTER = "roles.ldap.filter";
|
||||
|
||||
// See UserRolesRetrieveStrategy
|
||||
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE";
|
||||
public static final String GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE = "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE";
|
||||
public static final String LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY = "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY";
|
||||
|
||||
|
||||
public RoleMapperConfig(UserFederationMapperModel mapperModel) {
|
||||
super(mapperModel);
|
||||
}
|
||||
|
||||
public String getRolesDn() {
|
||||
String rolesDn = mapperModel.getConfig().get(ROLES_DN);
|
||||
if (rolesDn == null) {
|
||||
throw new ModelException("Roles DN is null! Check your configuration");
|
||||
}
|
||||
return rolesDn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLDAPGroupsDn() {
|
||||
return getRolesDn();
|
||||
}
|
||||
|
||||
public String getRoleNameLdapAttribute() {
|
||||
String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE);
|
||||
return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLDAPGroupNameLdapAttribute() {
|
||||
return getRoleNameLdapAttribute();
|
||||
}
|
||||
|
||||
public Collection<String> getRoleObjectClasses(LDAPFederationProvider ldapProvider) {
|
||||
String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES);
|
||||
if (objectClasses == null) {
|
||||
// For Active directory, the default is 'group' . For other servers 'groupOfNames'
|
||||
objectClasses = ldapProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
|
||||
}
|
||||
|
||||
return getConfigValues(objectClasses);
|
||||
}
|
||||
|
||||
public String getCustomLdapFilter() {
|
||||
return mapperModel.getConfig().get(ROLES_LDAP_FILTER);
|
||||
}
|
||||
|
||||
public boolean isRealmRolesMapping() {
|
||||
String realmRolesMapping = mapperModel.getConfig().get(USE_REALM_ROLES_MAPPING);
|
||||
return realmRolesMapping==null || Boolean.parseBoolean(realmRolesMapping);
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return mapperModel.getConfig().get(CLIENT_ID);
|
||||
}
|
||||
|
||||
|
||||
public String getUserRolesRetrieveStrategy() {
|
||||
String strategyString = mapperModel.getConfig().get(USER_ROLES_RETRIEVE_STRATEGY);
|
||||
return strategyString!=null ? strategyString : LOAD_ROLES_BY_MEMBER_ATTRIBUTE;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
package org.keycloak.federation.ldap.mappers.msad;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.naming.AuthenticationException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.federation.ldap.idm.query.internal.LDAPQuery;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
|
||||
/**
|
||||
* Mapper specific to MSAD. It's able to read the userAccountControl and pwdLastSet attributes and set actions in Keycloak based on that.
|
||||
* It's also able to handle exception code from LDAP user authentication (See http://www-01.ibm.com/support/docview.wss?uid=swg21290631 )
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MSADUserAccountControlMapper extends AbstractLDAPFederationMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(MSADUserAccountControlMapper.class);
|
||||
|
||||
private static final Pattern AUTH_EXCEPTION_REGEX = Pattern.compile(".*AcceptSecurityContext error, data ([0-9a-f]*), v.*");
|
||||
|
||||
public MSADUserAccountControlMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
|
||||
super(mapperModel, ldapProvider, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeLDAPQuery(LDAPQuery query) {
|
||||
query.addReturningLdapAttribute(LDAPConstants.PWD_LAST_SET);
|
||||
query.addReturningLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
|
||||
|
||||
// This needs to be read-only and can be set to writable just on demand
|
||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
if (ldapProvider.getEditMode() != UserFederationProvider.EditMode.WRITABLE) {
|
||||
query.addReturningReadOnlyLdapAttribute(LDAPConstants.USER_ACCOUNT_CONTROL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel proxy(LDAPObject ldapUser, UserModel delegate) {
|
||||
return new MSADUserModelDelegate(delegate, ldapUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAuthenticationFailure(LDAPObject ldapUser, UserModel user, AuthenticationException ldapException) {
|
||||
String exceptionMessage = ldapException.getMessage();
|
||||
Matcher m = AUTH_EXCEPTION_REGEX.matcher(exceptionMessage);
|
||||
if (m.matches()) {
|
||||
String errorCode = m.group(1);
|
||||
return processAuthErrorCode(errorCode, user);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean processAuthErrorCode(String errorCode, UserModel user) {
|
||||
logger.debugf("MSAD Error code is '%s' after failed LDAP login of user", errorCode, user.getUsername());
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) {
|
||||
if (errorCode.equals("532") || errorCode.equals("773")) {
|
||||
// User needs to change his MSAD password. Allow him to login, but add UPDATE_PASSWORD required action
|
||||
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
return true;
|
||||
} else if (errorCode.equals("533")) {
|
||||
// User is disabled in MSAD. Set him to disabled in KC as well
|
||||
user.setEnabled(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public class MSADUserModelDelegate extends UserModelDelegate {
|
||||
|
||||
private final LDAPObject ldapUser;
|
||||
|
||||
public MSADUserModelDelegate(UserModel delegate, LDAPObject ldapUser) {
|
||||
super(delegate);
|
||||
this.ldapUser = ldapUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
boolean kcEnabled = super.isEnabled();
|
||||
|
||||
if (getPwdLastSet() > 0) {
|
||||
// Merge KC and MSAD
|
||||
return kcEnabled && !getUserAccountControl().has(UserAccountControl.ACCOUNTDISABLE);
|
||||
} else {
|
||||
// If new MSAD user is created and pwdLastSet is still 0, MSAD account is in disabled state. So read just from Keycloak DB. User is not able to login via MSAD anyway
|
||||
return kcEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
// Always update DB
|
||||
super.setEnabled(enabled);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && getPwdLastSet() > 0) {
|
||||
logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString());
|
||||
|
||||
UserAccountControl control = getUserAccountControl();
|
||||
if (enabled) {
|
||||
control.remove(UserAccountControl.ACCOUNTDISABLE);
|
||||
} else {
|
||||
control.add(UserAccountControl.ACCOUNTDISABLE);
|
||||
}
|
||||
|
||||
updateUserAccountControl(control);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCredential(UserCredentialModel cred) {
|
||||
// Update LDAP password first
|
||||
super.updateCredential(cred);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
logger.debugf("Going to update userAccountControl for ldap user '%s' after successful password update", ldapUser.getDn().toString());
|
||||
|
||||
// Normally it's read-only
|
||||
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
|
||||
|
||||
UserAccountControl control = getUserAccountControl();
|
||||
control.remove(UserAccountControl.PASSWD_NOTREQD);
|
||||
control.remove(UserAccountControl.PASSWORD_EXPIRED);
|
||||
|
||||
if (super.isEnabled()) {
|
||||
control.remove(UserAccountControl.ACCOUNTDISABLE);
|
||||
}
|
||||
|
||||
updateUserAccountControl(control);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(RequiredAction action) {
|
||||
String actionName = action.name();
|
||||
addRequiredAction(actionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(String action) {
|
||||
// Always update DB
|
||||
super.addRequiredAction(action);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
|
||||
logger.debugf("Going to propagate required action UPDATE_PASSWORD to MSAD for ldap user '%s' ", ldapUser.getDn().toString());
|
||||
|
||||
// Normally it's read-only
|
||||
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "0");
|
||||
ldapProvider.getLdapIdentityStore().update(ldapUser);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(RequiredAction action) {
|
||||
String actionName = action.name();
|
||||
removeRequiredAction(actionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(String action) {
|
||||
// Always update DB
|
||||
super.removeRequiredAction(action);
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && RequiredAction.UPDATE_PASSWORD.toString().equals(action)) {
|
||||
|
||||
// Don't set pwdLastSet in MSAD when it is new user
|
||||
UserAccountControl accountControl = getUserAccountControl();
|
||||
if (accountControl.getValue() != 0 && !accountControl.has(UserAccountControl.PASSWD_NOTREQD)) {
|
||||
logger.debugf("Going to remove required action UPDATE_PASSWORD from MSAD for ldap user '%s' ", ldapUser.getDn().toString());
|
||||
|
||||
// Normally it's read-only
|
||||
ldapUser.removeReadOnlyAttributeName(LDAPConstants.PWD_LAST_SET);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.PWD_LAST_SET, "-1");
|
||||
ldapProvider.getLdapIdentityStore().update(ldapUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRequiredActions() {
|
||||
Set<String> requiredActions = super.getRequiredActions();
|
||||
|
||||
if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE) {
|
||||
if (getPwdLastSet() == 0 || getUserAccountControl().has(UserAccountControl.PASSWORD_EXPIRED)) {
|
||||
requiredActions = new HashSet<>(requiredActions);
|
||||
requiredActions.add(RequiredAction.UPDATE_PASSWORD.toString());
|
||||
return requiredActions;
|
||||
}
|
||||
}
|
||||
|
||||
return requiredActions;
|
||||
}
|
||||
|
||||
protected long getPwdLastSet() {
|
||||
String pwdLastSet = ldapUser.getAttributeAsString(LDAPConstants.PWD_LAST_SET);
|
||||
return pwdLastSet == null ? 0 : Long.parseLong(pwdLastSet);
|
||||
}
|
||||
|
||||
protected UserAccountControl getUserAccountControl() {
|
||||
String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL);
|
||||
long longValue = userAccountControl == null ? 0 : Long.parseLong(userAccountControl);
|
||||
return new UserAccountControl(longValue);
|
||||
}
|
||||
|
||||
// Update user in LDAP
|
||||
protected void updateUserAccountControl(UserAccountControl accountControl) {
|
||||
String userAccountControlValue = String.valueOf(accountControl.getValue());
|
||||
logger.debugf("Updating userAccountControl of user '%s' to value '%s'", ldapUser.getDn().toString(), userAccountControlValue);
|
||||
|
||||
ldapUser.setSingleAttribute(LDAPConstants.USER_ACCOUNT_CONTROL, userAccountControlValue);
|
||||
ldapProvider.getLdapIdentityStore().update(ldapUser);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package org.keycloak.federation.ldap.mappers.msad;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.federation.ldap.LDAPFederationProvider;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
|
||||
import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
|
||||
import org.keycloak.mappers.MapperConfigValidationException;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class MSADUserAccountControlMapperFactory extends AbstractLDAPFederationMapperFactory {
|
||||
|
||||
public static final String PROVIDER_ID = LDAPConstants.MSAD_USER_ACCOUNT_CONTROL_MAPPER;
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
static {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Mapper specific to MSAD. It's able to integrate the MSAD user account state into Keycloak account state (account enabled, password is expired etc). It's using userAccountControl and pwdLastSet MSAD attributes for that. " +
|
||||
"For example if pwdLastSet is 0, the Keycloak user is required to update password, if userAccountControl is 514 (disabled account) the Keycloak user is disabled as well etc. Mapper is also able to handle exception code from LDAP user authentication.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return ATTRIBUTE_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "MSAD User Account Control";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultConfig(UserFederationProviderModel providerModel) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractLDAPFederationMapper createMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider federationProvider, RealmModel realm) {
|
||||
return new MSADUserAccountControlMapper(mapperModel, federationProvider, realm);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package org.keycloak.federation.ldap.mappers.msad;
|
||||
|
||||
/**
|
||||
* See https://support.microsoft.com/en-us/kb/305144
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserAccountControl {
|
||||
|
||||
public static final long SCRIPT = 0x0001l;
|
||||
public static final long ACCOUNTDISABLE = 0x0002l;
|
||||
public static final long HOMEDIR_REQUIRED = 0x0008l;
|
||||
public static final long LOCKOUT = 0x0010l;
|
||||
public static final long PASSWD_NOTREQD = 0x0020l;
|
||||
public static final long PASSWD_CANT_CHANGE = 0x0040l;
|
||||
public static final long ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080l;
|
||||
public static final long TEMP_DUPLICATE_ACCOUNT = 0x0100l;
|
||||
public static final long NORMAL_ACCOUNT = 0x0200l;
|
||||
public static final long INTERDOMAIN_TRUST_ACCOUNT = 0x0800l;
|
||||
public static final long WORKSTATION_TRUST_ACCOUNT = 0x1000l;
|
||||
public static final long SERVER_TRUST_ACCOUNT = 0x2000l;
|
||||
public static final long DONT_EXPIRE_PASSWORD = 0x10000l;
|
||||
public static final long MNS_LOGON_ACCOUNT = 0x20000l;
|
||||
public static final long SMARTCARD_REQUIRED = 0x40000l;
|
||||
public static final long TRUSTED_FOR_DELEGATION = 0x80000l;
|
||||
public static final long NOT_DELEGATED = 0x100000l;
|
||||
public static final long USE_DES_KEY_ONLY = 0x200000l;
|
||||
public static final long DONT_REQ_PREAUTH = 0x400000l;
|
||||
public static final long PASSWORD_EXPIRED = 0x800000l;
|
||||
public static final long TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000l;
|
||||
public static final long PARTIAL_SECRETS_ACCOUNT = 0x04000000l;
|
||||
|
||||
private long value;
|
||||
|
||||
public UserAccountControl(long value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean has(long feature) {
|
||||
return (this.value & feature) > 0;
|
||||
}
|
||||
|
||||
public void add(long feature) {
|
||||
if (!has(feature)) {
|
||||
this.value += feature;
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(long feature) {
|
||||
if (has(feature)) {
|
||||
this.value -= feature;
|
||||
}
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.HardcodedLDAPRoleMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.membership.role.RoleLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.membership.group.GroupLDAPFederationMapperFactory
|
||||
org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory
|
|
@ -0,0 +1,108 @@
|
|||
package org.keycloak.federation.ldap.idm.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.federation.ldap.mappers.membership.group.GroupTreeResolver;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class GroupTreeResolverTest {
|
||||
|
||||
@Test
|
||||
public void testGroupResolvingCorrect() throws GroupTreeResolver.GroupTreeResolveException {
|
||||
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3");
|
||||
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group4", "group5");
|
||||
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group6");
|
||||
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4");
|
||||
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5");
|
||||
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7");
|
||||
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
|
||||
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7);
|
||||
|
||||
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||
List<GroupTreeResolver.GroupTreeEntry> groupTree = resolver.resolveGroupTree(groups);
|
||||
Assert.assertEquals(1, groupTree.size());
|
||||
Assert.assertEquals("{ group1 -> [ { group2 -> [ { group4 -> [ ]}{ group5 -> [ ]} ]}{ group3 -> [ { group6 -> [ { group7 -> [ ]} ]} ]} ]}", groupTree.get(0).toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupResolvingCorrect2_multipleRootGroups() throws GroupTreeResolver.GroupTreeResolveException {
|
||||
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group8");
|
||||
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
|
||||
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2");
|
||||
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5");
|
||||
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group6", "group7");
|
||||
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6");
|
||||
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
|
||||
GroupTreeResolver.Group group8 = new GroupTreeResolver.Group("group8", "group9");
|
||||
GroupTreeResolver.Group group9 = new GroupTreeResolver.Group("group9");
|
||||
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7, group8, group9);
|
||||
|
||||
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||
List<GroupTreeResolver.GroupTreeEntry> groupTree = resolver.resolveGroupTree(groups);
|
||||
|
||||
Assert.assertEquals(2, groupTree.size());
|
||||
Assert.assertEquals("{ group3 -> [ { group2 -> [ ]} ]}", groupTree.get(0).toString());
|
||||
Assert.assertEquals("{ group4 -> [ { group1 -> [ { group8 -> [ { group9 -> [ ]} ]} ]}{ group5 -> [ { group6 -> [ ]}{ group7 -> [ ]} ]} ]}", groupTree.get(1).toString());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGroupResolvingRecursion() {
|
||||
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2", "group3");
|
||||
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
|
||||
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group4");
|
||||
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group5");
|
||||
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group1");
|
||||
GroupTreeResolver.Group group6 = new GroupTreeResolver.Group("group6", "group7");
|
||||
GroupTreeResolver.Group group7 = new GroupTreeResolver.Group("group7");
|
||||
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5, group6, group7);
|
||||
|
||||
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||
try {
|
||||
resolver.resolveGroupTree(groups);
|
||||
Assert.fail("Exception expected because of recursion");
|
||||
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||
Assert.assertTrue(gre.getMessage().startsWith("Recursion detected"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupResolvingMultipleParents() {
|
||||
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2");
|
||||
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2");
|
||||
GroupTreeResolver.Group group3 = new GroupTreeResolver.Group("group3", "group2");
|
||||
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4", "group1", "group5");
|
||||
GroupTreeResolver.Group group5 = new GroupTreeResolver.Group("group5", "group4");
|
||||
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group3, group4, group5);
|
||||
|
||||
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||
try {
|
||||
resolver.resolveGroupTree(groups);
|
||||
Assert.fail("Exception expected because of some groups have multiple parents");
|
||||
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||
Assert.assertTrue(gre.getMessage().contains("detected to have multiple parents"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGroupResolvingMissingGroup() {
|
||||
GroupTreeResolver.Group group1 = new GroupTreeResolver.Group("group1", "group2");
|
||||
GroupTreeResolver.Group group2 = new GroupTreeResolver.Group("group2", "group3");
|
||||
GroupTreeResolver.Group group4 = new GroupTreeResolver.Group("group4");
|
||||
List<GroupTreeResolver.Group> groups = Arrays.asList(group1, group2, group4);
|
||||
|
||||
GroupTreeResolver resolver = new GroupTreeResolver();
|
||||
try {
|
||||
resolver.resolveGroupTree(groups);
|
||||
Assert.fail("Exception expected because of missing referenced group");
|
||||
} catch (GroupTreeResolver.GroupTreeResolveException gre) {
|
||||
Assert.assertEquals("Group 'group3' referenced as member of group 'group2' doesn't exists", gre.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,17 @@ public class LDAPDnTest {
|
|||
|
||||
dn.addFirst("uid", "Johny,Depp");
|
||||
Assert.assertEquals("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org", dn.toString());
|
||||
Assert.assertEquals(LDAPDn.fromString("uid=Johny\\,Depp,ou=People,dc=keycloak,dc=org"), dn);
|
||||
|
||||
Assert.assertEquals("ou=People,dc=keycloak,dc=org", dn.getParentDn());
|
||||
|
||||
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=keycloak, dc=org")));
|
||||
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("dc=org")));
|
||||
Assert.assertTrue(dn.isDescendantOf(LDAPDn.fromString("DC=keycloak, DC=org")));
|
||||
Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org")));
|
||||
Assert.assertFalse(dn.isDescendantOf(dn));
|
||||
|
||||
Assert.assertEquals("uid", dn.getFirstRdnAttrName());
|
||||
Assert.assertEquals("Johny\\,Depp", dn.getFirstRdnAttrValue());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,11 +41,9 @@ public class TotpBean {
|
|||
private final String totpSecretEncoded;
|
||||
private final boolean enabled;
|
||||
private final String contextUrl;
|
||||
private final String realmName;
|
||||
private final String keyUri;
|
||||
|
||||
public TotpBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri) {
|
||||
this.realmName = realm.getName();
|
||||
this.enabled = session.users().configuredForCredentialType(realm.getOTPPolicy().getType(), realm, user);
|
||||
this.contextUrl = baseUri.getPath();
|
||||
|
||||
|
|
|
@ -52,8 +52,10 @@ role_manage-events=Gerencia eventos
|
|||
role_view-profile=Visualiza perfil
|
||||
role_manage-account=Gerencia conta
|
||||
role_read-token=L\u00EA token
|
||||
role_offline-access=Acesso Offline
|
||||
client_account=Conta
|
||||
client_security-admin-console=Console de Administra\u00E7\u00E3o de Seguran\u00E7a
|
||||
client_admin-cli=Admin CLI
|
||||
client_realm-management=Gerenciamento de Realm
|
||||
client_broker=Broker
|
||||
|
||||
|
@ -85,9 +87,11 @@ application=Aplicativo
|
|||
availablePermissions=Permiss\u00F5es Dispon\u00EDveis
|
||||
grantedPermissions=Permiss\u00F5es Concedidas
|
||||
grantedPersonalInfo=Informa\u00E7\u00F5es Pessoais Concedidas
|
||||
additionalGrants=Concess\u00F5es Adicionais
|
||||
action=A\u00E7\u00E3o
|
||||
inResource=em
|
||||
fullAccess=Acesso Completo
|
||||
offlineToken=Offline Token
|
||||
revoke=Revogar Concess\u00F5es
|
||||
|
||||
configureAuthenticators=Autenticadores Configurados
|
||||
|
@ -130,6 +134,7 @@ federatedIdentityLinkNotActiveMessage=Esta identidade n\u00E3o est\u00E1 mais em
|
|||
federatedIdentityRemovingLastProviderMessage=Voc\u00EA n\u00E3o pode remover a \u00FAltima identidade federada como voc\u00EA n\u00E3o tem senha
|
||||
identityProviderRedirectErrorMessage=Falha ao redirecionar para o provedor de identidade
|
||||
identityProviderRemovedMessage=Provedor de identidade removido com sucesso
|
||||
identityProviderAlreadyLinkedMessage=Identidade federada retornado por {0} j\u00E1 est\u00E1 ligado a outro usu\u00E1rio.
|
||||
|
||||
accountDisabledMessage=Conta desativada, contate o administrador
|
||||
|
||||
|
@ -147,4 +152,4 @@ locale_de=Deutsch
|
|||
locale_en=English
|
||||
locale_it=Italian
|
||||
locale_pt-BR=Portugu\u00EAs (BR)
|
||||
locale_fr=Fran\u00e7ais
|
||||
locale_fr=Fran\u00E7ais
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Keycloak Admin Console</title>
|
||||
<title></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="${resourceUrl}/img/favicon.ico">
|
||||
<#if properties.styles?has_content>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
consoleTitle=Keycloak Admin Console
|
||||
|
||||
# Common messages
|
||||
enabled=Enabled
|
||||
name=Name
|
||||
displayName=Display name
|
||||
displayNameHtml=HTML Display name
|
||||
save=Save
|
||||
cancel=Cancel
|
||||
onText=ON
|
||||
|
@ -472,6 +476,12 @@ social.default-scopes.tooltip=The scopes to be sent when asking for authorizatio
|
|||
key=Key
|
||||
stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration.
|
||||
|
||||
# User federation
|
||||
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
|
||||
sync-keycloak-roles-to-ldap=Sync Keycloak Roles To LDAP
|
||||
sync-ldap-groups-to-keycloak=Sync LDAP Groups To Keycloak
|
||||
sync-keycloak-groups-to-ldap=Sync Keycloak Groups To LDAP
|
||||
|
||||
realms=Realms
|
||||
realm=Realm
|
||||
|
||||
|
@ -482,7 +492,9 @@ client.description.tooltip=Specifies description of the client. For example 'My
|
|||
|
||||
expires=Expires
|
||||
expiration=Expiration
|
||||
expiration.tooltip=Specifies how long the token should be valid
|
||||
count=Count
|
||||
count.tooltip=Specifies how many clients can be created using the token
|
||||
remainingCount=Remaining count
|
||||
created=Created
|
||||
back=Back
|
||||
|
|
|
@ -61,7 +61,11 @@ angular.element(document).ready(function () {
|
|||
module.factory('Auth', function() {
|
||||
return auth;
|
||||
});
|
||||
angular.bootstrap(document, ["keycloak"]);
|
||||
var injector = angular.bootstrap(document, ["keycloak"]);
|
||||
|
||||
injector.get('$translate')('consoleTitle').then(function(consoleTitle) {
|
||||
document.title=consoleTitle;
|
||||
});
|
||||
}, function() {
|
||||
window.location.reload();
|
||||
});
|
||||
|
@ -1088,6 +1092,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
client : function(ClientLoader) {
|
||||
return ClientLoader();
|
||||
},
|
||||
templates : function(ClientTemplateListLoader) {
|
||||
return ClientTemplateListLoader();
|
||||
},
|
||||
clients : function(ClientListLoader) {
|
||||
return ClientListLoader();
|
||||
}
|
||||
|
@ -1102,6 +1109,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
client : function(ClientLoader) {
|
||||
return ClientLoader();
|
||||
},
|
||||
serverInfo : function(ServerInfoLoader) {
|
||||
return ServerInfoLoader();
|
||||
}
|
||||
},
|
||||
controller : 'ClientInstallationCtrl'
|
||||
|
@ -1125,7 +1135,7 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
controller : 'UserRoleMappingCtrl'
|
||||
})
|
||||
.when('/create/client/:realm', {
|
||||
templateUrl : resourceUrl + '/partials/client-detail.html',
|
||||
templateUrl : resourceUrl + '/partials/create-client.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
|
@ -1143,7 +1153,7 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
return ServerInfoLoader();
|
||||
}
|
||||
},
|
||||
controller : 'ClientDetailCtrl'
|
||||
controller : 'CreateClientCtrl'
|
||||
})
|
||||
.when('/realms/:realm/clients/:client', {
|
||||
templateUrl : resourceUrl + '/partials/client-detail.html',
|
||||
|
@ -1202,6 +1212,21 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
controller : 'ClientTemplateDetailCtrl'
|
||||
})
|
||||
.when('/realms/:realm/client-templates/:template/scope-mappings', {
|
||||
templateUrl : resourceUrl + '/partials/client-template-scope-mappings.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
template : function(ClientTemplateLoader) {
|
||||
return ClientTemplateLoader();
|
||||
},
|
||||
clients : function(ClientListLoader) {
|
||||
return ClientListLoader();
|
||||
}
|
||||
},
|
||||
controller : 'ClientTemplateScopeMappingCtrl'
|
||||
})
|
||||
.when('/realms/:realm/clients', {
|
||||
templateUrl : resourceUrl + '/partials/client-list.html',
|
||||
resolve : {
|
||||
|
@ -2350,6 +2375,34 @@ module.filter('capitalize', function() {
|
|||
};
|
||||
});
|
||||
|
||||
/*
|
||||
* Guarantees a deterministic property iteration order.
|
||||
* See: http://www.2ality.com/2015/10/property-traversal-order-es6.html
|
||||
*/
|
||||
module.filter('toOrderedMapSortedByKey', function(){
|
||||
return function(input){
|
||||
|
||||
if(!input){
|
||||
return input;
|
||||
}
|
||||
|
||||
var keys = Object.keys(input);
|
||||
|
||||
if(keys.length <= 1){
|
||||
return input;
|
||||
}
|
||||
|
||||
keys.sort();
|
||||
|
||||
var result = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
result[keys[i]] = input[keys[i]];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('kcSidebarResize', function ($window) {
|
||||
return function (scope, element) {
|
||||
function resize() {
|
||||
|
|
|
@ -685,7 +685,7 @@ module.controller('ClientListCtrl', function($scope, realm, clients, Client, ser
|
|||
};
|
||||
});
|
||||
|
||||
module.controller('ClientInstallationCtrl', function($scope, realm, client, ClientInstallation,ClientInstallationJBoss, $http, $routeParams) {
|
||||
module.controller('ClientInstallationCtrl', function($scope, realm, client, serverInfo, ClientInstallation,$http, $routeParams) {
|
||||
$scope.realm = realm;
|
||||
$scope.client = client;
|
||||
$scope.installation = null;
|
||||
|
@ -693,36 +693,24 @@ module.controller('ClientInstallationCtrl', function($scope, realm, client, Clie
|
|||
$scope.configFormat = null;
|
||||
$scope.filename = null;
|
||||
|
||||
$scope.configFormats = [
|
||||
"Keycloak JSON",
|
||||
"Wildfly/EAP Subsystem XML"
|
||||
];
|
||||
var protocol = client.protocol;
|
||||
if (!protocol) protocol = 'openid-connect';
|
||||
$scope.configFormats = serverInfo.clientInstallations[protocol];
|
||||
console.log('configFormats.length: ' + $scope.configFormats.length);
|
||||
|
||||
$scope.changeFormat = function() {
|
||||
if ($scope.configFormat == "Keycloak JSON") {
|
||||
$scope.filename = 'keycloak.json';
|
||||
|
||||
var url = ClientInstallation.url({ realm: $routeParams.realm, client: $routeParams.client });
|
||||
var url = ClientInstallation.url({ realm: $routeParams.realm, client: $routeParams.client, provider: $scope.configFormat.id });
|
||||
$http.get(url).success(function(data) {
|
||||
var tmp = angular.fromJson(data);
|
||||
$scope.installation = angular.toJson(tmp, true);
|
||||
$scope.type = 'application/json';
|
||||
})
|
||||
} else if ($scope.configFormat == "Wildfly/EAP Subsystem XML") {
|
||||
$scope.filename = 'keycloak.xml';
|
||||
|
||||
var url = ClientInstallationJBoss.url({ realm: $routeParams.realm, client: $routeParams.client });
|
||||
$http.get(url).success(function(data) {
|
||||
$scope.installation = data;
|
||||
$scope.type = 'text/xml';
|
||||
})
|
||||
var installation = data;
|
||||
if ($scope.configFormat.mediaType == 'application/json') {
|
||||
installation = angular.fromJson(data);
|
||||
installation = angular.toJson(installation, true);
|
||||
}
|
||||
|
||||
console.debug($scope.filename);
|
||||
$scope.installation = installation;
|
||||
})
|
||||
};
|
||||
|
||||
$scope.download = function() {
|
||||
saveAs(new Blob([$scope.installation], { type: $scope.type }), $scope.filename);
|
||||
saveAs(new Blob([$scope.installation], { type: $scope.configFormat.mediaType }), $scope.configFormat.filename);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -736,7 +724,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
|||
"bearer-only"
|
||||
];
|
||||
|
||||
$scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
|
||||
$scope.protocols = ['openid-connect',
|
||||
'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort();
|
||||
|
||||
$scope.templates = [ {name:'NONE'}];
|
||||
for (var i = 0; i < templates.length; i++) {
|
||||
|
@ -765,7 +754,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
|||
];
|
||||
|
||||
$scope.realm = realm;
|
||||
$scope.create = !client.clientId;
|
||||
$scope.samlAuthnStatement = false;
|
||||
$scope.samlMultiValuedRoles = false;
|
||||
$scope.samlServerSignature = false;
|
||||
|
@ -870,20 +858,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
|||
if (!$scope.create) {
|
||||
$scope.client = angular.copy(client);
|
||||
updateProperties();
|
||||
} else {
|
||||
$scope.client = {
|
||||
enabled: true,
|
||||
standardFlowEnabled: true,
|
||||
attributes: {}
|
||||
};
|
||||
$scope.client.attributes['saml_signature_canonicalization_method'] = $scope.canonicalization[0].value;
|
||||
$scope.client.redirectUris = [];
|
||||
$scope.accessType = $scope.accessTypes[0];
|
||||
$scope.protocol = $scope.protocols[0];
|
||||
$scope.signatureAlgorithm = $scope.signatureAlgorithms[1];
|
||||
$scope.nameIdFormat = $scope.nameIdFormats[0];
|
||||
$scope.samlAuthnStatement = true;
|
||||
$scope.samlForceNameIdFormat = false;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1054,18 +1028,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
|||
|
||||
if ($scope.client.protocol != 'saml' && !$scope.client.bearerOnly && ($scope.client.standardFlowEnabled || $scope.client.implicitFlowEnabled) && (!$scope.client.redirectUris || $scope.client.redirectUris.length == 0)) {
|
||||
Notifications.error("You must specify at least one redirect uri");
|
||||
} else {
|
||||
if ($scope.create) {
|
||||
Client.save({
|
||||
realm: realm.realm,
|
||||
client: ''
|
||||
}, $scope.client, function (data, headers) {
|
||||
$scope.changed = false;
|
||||
var l = headers().location;
|
||||
var id = l.substring(l.lastIndexOf("/") + 1);
|
||||
$location.url("/realms/" + realm.realm + "/clients/" + id);
|
||||
Notifications.success("The client has been created.");
|
||||
});
|
||||
} else {
|
||||
Client.update({
|
||||
realm : realm.realm,
|
||||
|
@ -1077,7 +1039,6 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
|||
Notifications.success("Your changes have been saved to the client.");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
|
@ -1089,8 +1050,113 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
|||
};
|
||||
});
|
||||
|
||||
module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, Notifications,
|
||||
Client,
|
||||
module.controller('CreateClientCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
|
||||
$scope.protocols = ['openid-connect',
|
||||
'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort();
|
||||
$scope.create = true;
|
||||
$scope.templates = [ {name:'NONE'}];
|
||||
for (var i = 0; i < templates.length; i++) {
|
||||
var template = templates[i];
|
||||
$scope.templates.push(template);
|
||||
}
|
||||
|
||||
$scope.realm = realm;
|
||||
|
||||
$scope.client = {
|
||||
enabled: true,
|
||||
attributes: {}
|
||||
};
|
||||
$scope.client.redirectUris = [];
|
||||
$scope.protocol = $scope.protocols[0];
|
||||
|
||||
|
||||
$scope.importFile = function(fileContent){
|
||||
console.debug(fileContent);
|
||||
ClientDescriptionConverter.save({
|
||||
realm: realm.realm
|
||||
}, fileContent, function (data) {
|
||||
$scope.client = data;
|
||||
$scope.importing = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.viewImportDetails = function() {
|
||||
$modal.open({
|
||||
templateUrl: resourceUrl + '/partials/modal/view-object.html',
|
||||
controller: 'ObjectModalCtrl',
|
||||
resolve: {
|
||||
object: function () {
|
||||
return $scope.client;
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
$scope.switchChange = function() {
|
||||
$scope.changed = true;
|
||||
}
|
||||
|
||||
$scope.changeProtocol = function() {
|
||||
if ($scope.protocol == "openid-connect") {
|
||||
$scope.client.protocol = "openid-connect";
|
||||
} else if ($scope.protocol == "saml") {
|
||||
$scope.client.protocol = "saml";
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch(function() {
|
||||
return $location.path();
|
||||
}, function() {
|
||||
$scope.path = $location.path().substring(1).split("/");
|
||||
});
|
||||
|
||||
function isChanged() {
|
||||
if (!angular.equals($scope.client, client)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$scope.$watch('client', function() {
|
||||
$scope.changed = isChanged();
|
||||
}, true);
|
||||
|
||||
|
||||
$scope.save = function() {
|
||||
|
||||
$scope.client.protocol = $scope.protocol;
|
||||
|
||||
if ($scope.client.protocol == 'openid-connect' && !$scope.client.rootUrl) {
|
||||
Notifications.error("You must specify the root URL of application");
|
||||
}
|
||||
|
||||
if ($scope.client.protocol == 'saml' && !$scope.client.adminUrl) {
|
||||
Notifications.error("You must specify the SAML Endpoint URL");
|
||||
}
|
||||
|
||||
Client.save({
|
||||
realm: realm.realm,
|
||||
client: ''
|
||||
}, $scope.client, function (data, headers) {
|
||||
$scope.changed = false;
|
||||
var l = headers().location;
|
||||
var id = l.substring(l.lastIndexOf("/") + 1);
|
||||
$location.url("/realms/" + realm.realm + "/clients/" + id);
|
||||
Notifications.success("The client has been created.");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$route.reload();
|
||||
};
|
||||
|
||||
$scope.cancel = function() {
|
||||
$location.url("/realms/" + realm.realm + "/clients");
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, client, clients, templates, Notifications,
|
||||
Client, ClientTemplate,
|
||||
ClientRealmScopeMapping, ClientClientScopeMapping, ClientRole,
|
||||
ClientAvailableRealmScopeMapping, ClientAvailableClientScopeMapping,
|
||||
ClientCompositeRealmScopeMapping, ClientCompositeClientScopeMapping) {
|
||||
|
@ -1107,8 +1173,20 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
|
|||
$scope.clientMappings = [];
|
||||
$scope.dummymodel = [];
|
||||
|
||||
if (client.clientTemplate) {
|
||||
for (var i = 0; i < templates.length; i++) {
|
||||
if (templates[i].name == client.clientTemplate) {
|
||||
ClientTemplate.get({realm: realm.realm, template: templates[i].id}, function(data) {
|
||||
$scope.template = data;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.changeFullScopeAllowed = function() {
|
||||
}
|
||||
|
||||
|
||||
$scope.changeFlag = function() {
|
||||
Client.update({
|
||||
realm : realm.realm,
|
||||
client : client.id
|
||||
|
@ -1122,6 +1200,7 @@ module.controller('ClientScopeMappingCtrl', function($scope, $http, realm, clien
|
|||
|
||||
|
||||
|
||||
|
||||
function updateRealmRoles() {
|
||||
$scope.realmRoles = ClientAvailableRealmScopeMapping.query({realm : realm.realm, client : client.id});
|
||||
$scope.realmMappings = ClientRealmScopeMapping.query({realm : realm.realm, client : client.id});
|
||||
|
@ -1420,6 +1499,7 @@ module.controller('AddBuiltinProtocolMapperCtrl', function($scope, realm, client
|
|||
});
|
||||
|
||||
module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client, templates, serverInfo,
|
||||
Client,
|
||||
ClientProtocolMappersByProtocol, ClientProtocolMapper,
|
||||
$route, Dialog, Notifications) {
|
||||
$scope.realm = realm;
|
||||
|
@ -1435,6 +1515,16 @@ module.controller('ClientProtocolMapperListCtrl', function($scope, realm, client
|
|||
}
|
||||
}
|
||||
}
|
||||
$scope.changeFlag = function() {
|
||||
Client.update({
|
||||
realm : realm.realm,
|
||||
client : client.id
|
||||
}, $scope.client, function() {
|
||||
$scope.changed = false;
|
||||
client = angular.copy($scope.client);
|
||||
Notifications.success("Client updated.");
|
||||
});
|
||||
}
|
||||
|
||||
var protocolMappers = serverInfo.protocolMapperTypes[client.protocol];
|
||||
var mapperTypes = {};
|
||||
|
@ -1525,7 +1615,7 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo
|
|||
};
|
||||
|
||||
$scope.remove = function() {
|
||||
Dialog.confirmDelete($scope.mapper.name, 'mapper', function() {
|
||||
Dialog.confirmDelete($scope.model.mapper.name, 'mapper', function() {
|
||||
ClientProtocolMapper.remove({ realm: realm.realm, client: client.id, id : $scope.model.mapper.id }, function() {
|
||||
Notifications.success("The mapper has been deleted.");
|
||||
$location.url("/realms/" + realm.realm + '/clients/' + client.id + "/mappers");
|
||||
|
@ -1910,6 +2000,104 @@ module.controller('ClientTemplateAddBuiltinProtocolMapperCtrl', function($scope,
|
|||
});
|
||||
|
||||
|
||||
module.controller('ClientTemplateScopeMappingCtrl', function($scope, $http, realm, template, clients, Notifications,
|
||||
ClientTemplate,
|
||||
ClientTemplateRealmScopeMapping, ClientTemplateClientScopeMapping, ClientRole,
|
||||
ClientTemplateAvailableRealmScopeMapping, ClientTemplateAvailableClientScopeMapping,
|
||||
ClientTemplateCompositeRealmScopeMapping, ClientTemplateCompositeClientScopeMapping) {
|
||||
$scope.realm = realm;
|
||||
$scope.template = angular.copy(template);
|
||||
$scope.selectedRealmRoles = [];
|
||||
$scope.selectedRealmMappings = [];
|
||||
$scope.realmMappings = [];
|
||||
$scope.clients = clients;
|
||||
$scope.clientRoles = [];
|
||||
$scope.clientComposite = [];
|
||||
$scope.selectedClientRoles = [];
|
||||
$scope.selectedClientMappings = [];
|
||||
$scope.clientMappings = [];
|
||||
$scope.dummymodel = [];
|
||||
|
||||
|
||||
$scope.changeFullScopeAllowed = function() {
|
||||
ClientTemplate.update({
|
||||
realm : realm.realm,
|
||||
template : template.id
|
||||
}, $scope.template, function() {
|
||||
$scope.changed = false;
|
||||
template = angular.copy($scope.template);
|
||||
updateTemplateRealmRoles();
|
||||
Notifications.success("Scope mappings updated.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updateTemplateRealmRoles() {
|
||||
$scope.realmRoles = ClientTemplateAvailableRealmScopeMapping.query({realm : realm.realm, template : template.id});
|
||||
$scope.realmMappings = ClientTemplateRealmScopeMapping.query({realm : realm.realm, template : template.id});
|
||||
$scope.realmComposite = ClientTemplateCompositeRealmScopeMapping.query({realm : realm.realm, template : template.id});
|
||||
}
|
||||
|
||||
function updateTemplateClientRoles() {
|
||||
if ($scope.targetClient) {
|
||||
$scope.clientRoles = ClientTemplateAvailableClientScopeMapping.query({realm : realm.realm, template : template.id, targetClient : $scope.targetClient.id});
|
||||
$scope.clientMappings = ClientTemplateClientScopeMapping.query({realm : realm.realm, template : template.id, targetClient : $scope.targetClient.id});
|
||||
$scope.clientComposite = ClientTemplateCompositeClientScopeMapping.query({realm : realm.realm, template : template.id, targetClient : $scope.targetClient.id});
|
||||
} else {
|
||||
$scope.clientRoles = null;
|
||||
$scope.clientMappings = null;
|
||||
$scope.clientComposite = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.changeClient = function() {
|
||||
updateTemplateClientRoles();
|
||||
};
|
||||
|
||||
$scope.addRealmRole = function() {
|
||||
var roles = $scope.selectedRealmRoles;
|
||||
$scope.selectedRealmRoles = [];
|
||||
$http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm',
|
||||
roles).success(function() {
|
||||
updateTemplateRealmRoles();
|
||||
Notifications.success("Scope mappings updated.");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteRealmRole = function() {
|
||||
var roles = $scope.selectedRealmMappings;
|
||||
$scope.selectedRealmMappings = [];
|
||||
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/realm',
|
||||
{data : roles, headers : {"content-type" : "application/json"}}).success(function () {
|
||||
updateTemplateRealmRoles();
|
||||
Notifications.success("Scope mappings updated.");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addClientRole = function() {
|
||||
var roles = $scope.selectedClientRoles;
|
||||
$scope.selectedClientRoles = [];
|
||||
$http.post(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/clients/' + $scope.targetClient.id,
|
||||
roles).success(function () {
|
||||
updateTemplateClientRoles();
|
||||
Notifications.success("Scope mappings updated.");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteClientRole = function() {
|
||||
var roles = $scope.selectedClientMappings;
|
||||
$scope.selectedClientMappings = [];
|
||||
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/client-templates/' + template.id + '/scope-mappings/clients/' + $scope.targetClient.id,
|
||||
{data : roles, headers : {"content-type" : "application/json"}}).success(function () {
|
||||
updateTemplateClientRoles();
|
||||
Notifications.success("Scope mappings updated.");
|
||||
});
|
||||
};
|
||||
|
||||
updateTemplateRealmRoles();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -644,6 +644,15 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
|
|||
instance.config.updateProfileFirstLogin = true;
|
||||
instance.config.allowKerberosAuthentication = true;
|
||||
}
|
||||
|
||||
if (providerFactory.properties) {
|
||||
|
||||
for (var i = 0; i < providerFactory.properties.length; i++) {
|
||||
var configProperty = providerFactory.properties[i];
|
||||
instance.config[configProperty.name] = configProperty.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$scope.fullSyncEnabled = (instance.fullSyncPeriod && instance.fullSyncPeriod > 0);
|
||||
$scope.changedSyncEnabled = (instance.changedSyncPeriod && instance.changedSyncPeriod > 0);
|
||||
|
@ -986,7 +995,7 @@ module.controller('UserFederationMapperListCtrl', function($scope, $location, No
|
|||
|
||||
});
|
||||
|
||||
module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, clients, UserFederationMapper, Notifications, Dialog, $location) {
|
||||
module.controller('UserFederationMapperCtrl', function($scope, realm, provider, mapperTypes, mapper, clients, UserFederationMapper, UserFederationMapperSync, Notifications, Dialog, $location) {
|
||||
console.log('UserFederationMapperCtrl');
|
||||
$scope.realm = realm;
|
||||
$scope.provider = provider;
|
||||
|
@ -1035,6 +1044,22 @@ module.controller('UserFederationMapperCtrl', function($scope, realm, provider,
|
|||
});
|
||||
};
|
||||
|
||||
$scope.triggerFedToKeycloakSync = function() {
|
||||
triggerMapperSync("fedToKeycloak")
|
||||
}
|
||||
|
||||
$scope.triggerKeycloakToFedSync = function() {
|
||||
triggerMapperSync("keycloakToFed");
|
||||
}
|
||||
|
||||
function triggerMapperSync(direction) {
|
||||
UserFederationMapperSync.save({ direction: direction, realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) {
|
||||
Notifications.success("Data synced successfully. " + syncResult.status);
|
||||
}, function(error) {
|
||||
Notifications.error(error.data.errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
module.controller('UserFederationMapperCreateCtrl', function($scope, realm, provider, mapperTypes, clients, UserFederationMapper, Notifications, Dialog, $location) {
|
||||
|
@ -1050,13 +1075,7 @@ module.controller('UserFederationMapperCreateCtrl', function($scope, realm, prov
|
|||
|
||||
$scope.$watch('mapperType', function() {
|
||||
if ($scope.mapperType != null) {
|
||||
$scope.mapper.config = {};
|
||||
for ( var i = 0; i < $scope.mapperType.properties.length; i++) {
|
||||
var property = $scope.mapperType.properties[i];
|
||||
if (property.type === 'String' || property.type === 'boolean') {
|
||||
$scope.mapper.config[ property.name ] = property.defaultValue;
|
||||
}
|
||||
}
|
||||
$scope.mapper.config = $scope.mapperType.defaultConfig;
|
||||
}
|
||||
}, true);
|
||||
|
||||
|
|
|
@ -284,15 +284,6 @@ module.factory('ClientClaimsLoader', function(Loader, ClientClaims, $route, $q)
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('ClientInstallationLoader', function(Loader, ClientInstallation, $route, $q) {
|
||||
return Loader.get(ClientInstallation, function() {
|
||||
return {
|
||||
realm : $route.current.params.realm,
|
||||
client : $route.current.params.client
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientRoleListLoader', function(Loader, ClientRole, $route, $q) {
|
||||
return Loader.query(ClientRole, function() {
|
||||
return {
|
||||
|
|
|
@ -380,6 +380,10 @@ module.factory('UserFederationMapper', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('UserFederationMapperSync', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers/:mapperId/sync');
|
||||
});
|
||||
|
||||
|
||||
module.factory('UserSessionStats', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/users/:user/session-stats', {
|
||||
|
@ -848,6 +852,52 @@ module.factory('ClientTemplateProtocolMappersByProtocol', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('ClientTemplateRealmScopeMapping', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/realm', {
|
||||
realm : '@realm',
|
||||
template : '@template'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientTemplateAvailableRealmScopeMapping', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/realm/available', {
|
||||
realm : '@realm',
|
||||
template : '@template'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientTemplateCompositeRealmScopeMapping', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/realm/composite', {
|
||||
realm : '@realm',
|
||||
template : '@template'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientTemplateClientScopeMapping', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/clients/:targetClient', {
|
||||
realm : '@realm',
|
||||
template : '@template',
|
||||
targetClient : '@targetClient'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientTemplateAvailableClientScopeMapping', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/clients/:targetClient/available', {
|
||||
realm : '@realm',
|
||||
template : '@template',
|
||||
targetClient : '@targetClient'
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientTemplateCompositeClientScopeMapping', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/client-templates/:template/scope-mappings/clients/:targetClient/composite', {
|
||||
realm : '@realm',
|
||||
template : '@template',
|
||||
targetClient : '@targetClient'
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.factory('ClientSessionStats', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/clients/:client/session-stats', {
|
||||
realm : '@realm',
|
||||
|
@ -994,16 +1044,28 @@ module.factory('ClientDescriptionConverter', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
/*
|
||||
module.factory('ClientInstallation', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/clients/:client/installation/providers/:provider', {
|
||||
realm : '@realm',
|
||||
client : '@client',
|
||||
provider : '@provider'
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
|
||||
module.factory('ClientInstallation', function($resource) {
|
||||
var url = authUrl + '/admin/realms/:realm/clients/:client/installation/json';
|
||||
var url = authUrl + '/admin/realms/:realm/clients/:client/installation/providers/:provider';
|
||||
return {
|
||||
url : function(parameters)
|
||||
{
|
||||
return url.replace(':realm', parameters.realm).replace(':client', parameters.client);
|
||||
return url.replace(':realm', parameters.realm).replace(':client', parameters.client).replace(':provider', parameters.provider);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.factory('ClientInstallationJBoss', function($resource) {
|
||||
var url = authUrl + '/admin/realms/:realm/clients/:client/installation/jboss';
|
||||
return {
|
||||
|
|
|
@ -2,30 +2,15 @@
|
|||
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
|
||||
<li data-ng-show="create">{{:: 'add-client' | translate}}</li>
|
||||
<li data-ng-hide="create">{{client.clientId}}</li>
|
||||
<li>{{client.clientId}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-client></kc-tabs-client>
|
||||
|
||||
<form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group" data-ng-show="create">
|
||||
<label for="name" class="col-sm-2 control-label">{{:: 'import' | translate}}</label>
|
||||
|
||||
<div class="col-md-6" data-ng-hide="importing">
|
||||
<label for="import-file" class="btn btn-default">{{:: 'select-file' | translate}} <i class="pficon pficon-import"></i></label>
|
||||
<input id="import-file" type="file" class="hidden" kc-on-read-file="importFile($fileContent)">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" data-ng-show="importing">
|
||||
<button class="btn btn-default" data-ng-click="viewImportDetails()">{{:: 'view-details' | translate}}</button>
|
||||
<button class="btn btn-default" data-ng-click="reset()">{{:: 'clear-import' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="clientId">{{:: 'client-id' | translate}} <span class="required" data-ng-show="create">*</span></label>
|
||||
<label class="col-md-2 control-label" for="clientId">{{:: 'client-id' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="clientId" name="clientId" data-ng-model="client.clientId" autofocus required>
|
||||
</div>
|
||||
|
@ -250,14 +235,14 @@
|
|||
<kc-tooltip>{{:: 'valid-redirect-uris.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-ng-show="!client.bearerOnly && !create">
|
||||
<div class="form-group" data-ng-show="!client.bearerOnly">
|
||||
<label class="col-md-2 control-label" for="baseUrl">{{:: 'base-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="baseUrl" id="baseUrl" data-ng-model="client.baseUrl">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'base-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-hide="create || protocol == 'saml'">
|
||||
<div class="form-group" data-ng-hide="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="adminUrl">{{:: 'admin-url' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="adminUrl" id="adminUrl"
|
||||
|
@ -287,7 +272,7 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'idp-sso-relay-state.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="!client.bearerOnly && !create && protocol == 'openid-connect' && (client.standardFlowEnabled || client.implicitFlowEnabled)">
|
||||
<div class="form-group" data-ng-show="!client.bearerOnly && protocol == 'openid-connect' && (client.standardFlowEnabled || client.implicitFlowEnabled)">
|
||||
<label class="col-md-2 control-label" for="newWebOrigin">{{:: 'web-origins' | translate}}</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
|
@ -342,11 +327,7 @@
|
|||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageClients">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<div>
|
||||
<select class="form-control" id="configFormats" name="configFormats" ng-change="changeFormat()" ng-model="configFormat" ng-options="a for a in configFormats">
|
||||
<select class="form-control" id="configFormats" name="configFormats" ng-change="changeFormat()" ng-model="configFormat" ng-options="a.displayType for a in configFormats">
|
||||
<option value="" selected> {{:: 'select-a-format' | translate}} </option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,20 @@
|
|||
|
||||
<kc-tabs-client></kc-tabs-client>
|
||||
|
||||
<form class="form-horizontal" name="allowScope" novalidate kc-read-only="!access.manageClients">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group" ng-show="client.clientTemplate">
|
||||
<label class="col-md-2 control-label" for="useTemplateScope">Inherit Template Mappers</label>
|
||||
<kc-tooltip>Inherit mappers from client template</kc-tooltip>
|
||||
<div class="col-md-1">
|
||||
<input ng-model="client.useTemplateMappers" ng-click="changeFlag()" name="useTemplateScope" id="useTemplateScope" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">view template mappers</a>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -24,7 +38,6 @@
|
|||
<div class="pull-right" data-ng-show="access.manageClients">
|
||||
<a class="btn btn-default" href="#/create/client/{{realm.realm}}/{{client.id}}/mappers">{{:: 'create' | translate}}</a>
|
||||
<a class="btn btn-default" href="#/realms/{{realm.realm}}/clients/{{client.id}}/add-mappers">{{:: 'add-builtin' | translate}}</a>
|
||||
<a ng-show="template" class="btn btn-default" href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">Inherited Template Mappers</a>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
|
|
|
@ -11,17 +11,37 @@
|
|||
<p class="subtitle"></p>
|
||||
<form class="form-horizontal" name="allowScope" novalidate kc-read-only="!access.manageClients">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<div class="form-group" ng-show="client.clientTemplate">
|
||||
<label class="col-md-2 control-label" for="useTemplateScope">Inherit Template Scope</label>
|
||||
<kc-tooltip>Inherit scope from client template</kc-tooltip>
|
||||
<div class="col-md-1">
|
||||
<input ng-model="client.useTemplateScope" ng-click="changeFlag()" name="useTemplateScope" id="useTemplateScope" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/scope-mappings">view template scope</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="client.useTemplateScope && template && template.fullScopeAllowed">
|
||||
<label class="col-md-2 control-label" for="fullScopeAllowed">{{:: 'full-scope-allowed' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'full-scope-allowed.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="client.fullScopeAllowed" ng-click="changeFullScopeAllowed()" name="fullScopeAllowed" id="fullScopeAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
<input ng-model="client.fullScopeAllowed" ng-click="changeFlag()" name="fullScopeAllowed" id="fullScopeAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="client.useTemplateScope && template && template.fullScopeAllowed">
|
||||
<label class="col-md-2 control-label" for="fullScopeAllowed">{{:: 'full-scope-allowed' | translate}}</label>
|
||||
<kc-tooltip>Client template has full scope allowed, which means this client will have the full scope of all roles.</kc-tooltip>
|
||||
<div class="col-md-1">
|
||||
<input ng-model="template.fullScopeAllowed" name="fullScopeAllowed" id="fullScopeAllowed" ng-disabled="true" onoffswitch />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<i>inherited</i>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!client.fullScopeAllowed">
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!client.fullScopeAllowed" data-ng-hide="client.useTemplateScope && template && template.fullScopeAllowed">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" class="control-label">{{:: 'realm-roles' | translate}}</label>
|
||||
<div class="col-md-10">
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/client-templates">Client Templates</a></li>
|
||||
<li>{{template.name}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-client-template></kc-tabs-client-template>
|
||||
|
||||
<h2><span>{{template.name}}</span> {{:: 'scope-mappings' | translate}} </h2>
|
||||
<p class="subtitle"></p>
|
||||
<form class="form-horizontal" name="allowScope" novalidate kc-read-only="!access.manageClients">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="fullScopeAllowed">{{:: 'full-scope-allowed' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'full-scope-allowed.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="template.fullScopeAllowed" ng-click="changeFullScopeAllowed()" name="fullScopeAllowed" id="fullScopeAllowed" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients" data-ng-show="!template.fullScopeAllowed">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" class="control-label">{{:: 'realm-roles' | translate}}</label>
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="control-label" for="available">{{:: 'available-roles' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'scope.available-roles.tooltip' | translate}}</kc-tooltip>
|
||||
|
||||
<select id="available" class="form-control" multiple size="5"
|
||||
ng-multiple="true"
|
||||
ng-model="selectedRealmRoles"
|
||||
ng-options="r.name for r in realmRoles">
|
||||
</select>
|
||||
<button ng-disabled="selectedRealmRoles.length == 0" class="btn btn-default" type="submit" ng-click="addRealmRole()">
|
||||
{{:: 'add-selected' | translate}} <i class="fa fa-angle-double-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="control-label" for="assigned">{{:: 'assigned-roles' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'assigned-roles.tooltip' | translate}}</kc-tooltip>
|
||||
<select id="assigned" class="form-control" multiple size=5
|
||||
ng-multiple="true"
|
||||
ng-model="selectedRealmMappings"
|
||||
ng-options="r.name for r in realmMappings">
|
||||
</select>
|
||||
<button ng-disabled="selectedRealmMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteRealmRole()">
|
||||
<i class="fa fa-angle-double-left"></i> {{:: 'remove-selected' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="control-label" for="realm-composite">{{:: 'effective-roles' | translate}} </label>
|
||||
<kc-tooltip>{{:: 'realm.effective-roles.tooltip' | translate}}</kc-tooltip>
|
||||
<select id="realm-composite" class="form-control" multiple size=5
|
||||
disabled="true"
|
||||
ng-model="dummymodel"
|
||||
ng-options="r.name for r in realmComposite">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" class="control-label">
|
||||
<span>{{:: 'client-roles' | translate}}</span>
|
||||
<select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="targetClient" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
|
||||
</label>
|
||||
|
||||
<div class="col-md-10">
|
||||
<div class="row" data-ng-hide="targetClient">
|
||||
<div class="col-md-4"><span class="text-muted">{{:: 'select-client-roles.tooltip' | translate}}</span></div>
|
||||
</div>
|
||||
<div class="row" data-ng-show="targetClient">
|
||||
<div class="col-md-3">
|
||||
<label class="control-label" for="available-client">{{:: 'available-roles' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'assign.available-roles.tooltip' | translate}}</kc-tooltip>
|
||||
<select id="available-client" class="form-control" multiple size="5"
|
||||
ng-multiple="true"
|
||||
ng-model="selectedClientRoles"
|
||||
ng-options="r.name for r in clientRoles">
|
||||
</select>
|
||||
<button ng-disabled="selectedClientRoles.length == 0" class="btn btn-default" type="submit" ng-click="addClientRole()">
|
||||
{{:: 'add-selected' | translate}} <i class="fa fa-angle-double-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="control-label" for="assigned-client">{{:: 'assigned-roles' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'client.assigned-roles.tooltip' | translate}}</kc-tooltip>
|
||||
<select id="assigned-client" class="form-control" multiple size=5
|
||||
ng-multiple="true"
|
||||
ng-model="selectedClientMappings"
|
||||
ng-options="r.name for r in clientMappings">
|
||||
</select>
|
||||
<button ng-disabled="selectedClientMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteClientRole()">
|
||||
<i class="fa fa-angle-double-left"></i> {{:: 'remove-selected' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="control-label" for="client-composite">{{:: 'effective-roles' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'client.effective-roles.tooltip' | translate}}</kc-tooltip>
|
||||
<select id="client-composite" class="form-control" multiple size=5
|
||||
disabled="true"
|
||||
ng-model="dummymodel"
|
||||
ng-options="r.name for r in clientComposite">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -0,0 +1,83 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
|
||||
<li>{{:: 'add-client' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-client></kc-tabs-client>
|
||||
|
||||
<form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{{:: 'import' | translate}}</label>
|
||||
|
||||
<div class="col-md-6" data-ng-hide="importing">
|
||||
<label for="import-file" class="btn btn-default">{{:: 'select-file' | translate}} <i class="pficon pficon-import"></i></label>
|
||||
<input id="import-file" type="file" class="hidden" kc-on-read-file="importFile($fileContent)">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6" data-ng-show="importing">
|
||||
<button class="btn btn-default" data-ng-click="viewImportDetails()">{{:: 'view-details' | translate}}</button>
|
||||
<button class="btn btn-default" data-ng-click="reset()">{{:: 'clear-import' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="clientId">{{:: 'client-id' | translate}} <span class="required">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="clientId" name="clientId" data-ng-model="client.clientId" autofocus required>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-id.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="protocol">{{:: 'client-protocol' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<div>
|
||||
<select class="form-control" id="protocol"
|
||||
ng-change="changeProtocol()"
|
||||
ng-model="protocol"
|
||||
ng-options="aProtocol for aProtocol in protocols">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-protocol.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="protocol">Client Template</label>
|
||||
<div class="col-sm-6">
|
||||
<div>
|
||||
<select class="form-control" id="template"
|
||||
ng-model="client.clientTemplate"
|
||||
ng-options="template.name as template.name for template in templates">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-tooltip>Client template this client inherits configuration from</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-hide="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="rootUrl">{{:: 'root-url' | translate}} <span class="required">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="client.rootUrl">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="masterSamlUrl">Client SAML Endpoint <span class="required">*</span></label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="masterSamlUrl" id="masterSamlUrl"
|
||||
data-ng-model="client.adminUrl">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'master-saml-processing-url.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -37,6 +37,9 @@
|
|||
<input class="form-control" type="text" data-ng-model="instance.config[ option ]" >
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<kc-provider-config realm="realm" config="instance.config" properties="providerFactory.properties"></kc-provider-config>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -183,14 +183,6 @@
|
|||
</div>
|
||||
<kc-tooltip>Does the LDAP server support pagination.</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix" data-ng-show="instance.config.vendor === 'ad' ">
|
||||
<label class="col-md-2 control-label" for="userAccountControlsAfterPasswordUpdate">Enable Account After Password Update</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="instance.config.userAccountControlsAfterPasswordUpdate" name="userAccountControlsAfterPasswordUpdate" id="userAccountControlsAfterPasswordUpdate" onoffswitch />
|
||||
</div>
|
||||
<kc-tooltip>Useful just for Active Directory. If enabled, then Keycloak will always set
|
||||
Active Directory userAccountControl attribute to 512 after password update. This would mean that particular user will be enabled in Active Directory</kc-tooltip>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||
<button kc-save data-ng-disabled="!changed">Save</button>
|
||||
<button kc-reset data-ng-disabled="!changed">Cancel</button>
|
||||
<button class="btn btn-primary" data-ng-click="triggerFedToKeycloakSync()" data-ng-hide="!mapperType.syncConfig.fedToKeycloakSyncSupported" data-ng-disabled="changed">{{:: mapperType.syncConfig.fedToKeycloakSyncMessage | translate}}</button>
|
||||
<button class="btn btn-primary" data-ng-click="triggerKeycloakToFedSync()" data-ng-hide="!mapperType.syncConfig.keycloakToFedSyncSupported" data-ng-disabled="changed">{{:: mapperType.syncConfig.keycloakToFedSyncMessage | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -9,6 +9,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="name">{{:: 'displayName' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" id="displayName" name="displayName" data-ng-model="realm.displayName">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="name">{{:: 'displayNameHtml' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" id="displayNameHtml" name="displayNameHtml" data-ng-model="realm.displayNameHtml">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, value) in user.attributes">
|
||||
<tr ng-repeat="(key, value) in user.attributes | toOrderedMapSortedByKey">
|
||||
<td>{{key}}</td>
|
||||
<td><input ng-model="user.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
|
||||
<td class="kc-action-cell">
|
||||
|
|
|
@ -12,5 +12,9 @@
|
|||
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">{{:: 'mappers' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
<li ng-class="{active: path[4] == 'scope-mappings'}" >
|
||||
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -33,7 +33,7 @@
|
|||
|
||||
<li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">{{:: 'clustering' | translate}}</a></li>
|
||||
|
||||
<li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
|
||||
<li ng-class="{active: path[4] == 'installation'}">
|
||||
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/installation">{{:: 'installation' | translate}}</a>
|
||||
<kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
|
||||
</li>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
${msg("emailLinkIdpTitle", idpAlias)}
|
||||
<#elseif section = "form">
|
||||
<p id="instruction1" class="instruction">
|
||||
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.name)}
|
||||
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
||||
</p>
|
||||
<p id="instruction2" class="instruction">
|
||||
${msg("emailLinkIdp2")} <a href="${url.firstBrokerLoginUrl}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<#if section = "title">
|
||||
${msg("oauthGrantTitle")}
|
||||
<#elseif section = "header">
|
||||
${msg("oauthGrantTitleHtml",(realm.name!''))} <strong><#if client.name??>${advancedMsg(client.name)}<#else>${client.clientId}</#if></strong>.
|
||||
${msg("oauthGrantTitleHtml",(realm.displayNameHtml!''))} <strong><#if client.name??>${advancedMsg(client.name)}<#else>${client.clientId}</#if></strong>.
|
||||
<#elseif section = "form">
|
||||
<div id="kc-oauth" class="content-area">
|
||||
<h3>${msg("oauthGrantRequest")}</h3>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue