KEYCLOAK-3666 Dynamic client registration policies
This commit is contained in:
parent
1c0abbd722
commit
18e0c0277f
86 changed files with 3401 additions and 564 deletions
|
@ -27,6 +27,7 @@ public class ComponentExportRepresentation {
|
||||||
private String id;
|
private String id;
|
||||||
private String name;
|
private String name;
|
||||||
private String providerId;
|
private String providerId;
|
||||||
|
private String subType;
|
||||||
private MultivaluedHashMap<String, ComponentExportRepresentation> subComponents = new MultivaluedHashMap<>();
|
private MultivaluedHashMap<String, ComponentExportRepresentation> subComponents = new MultivaluedHashMap<>();
|
||||||
private MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
private MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||||
|
|
||||||
|
@ -54,6 +55,14 @@ public class ComponentExportRepresentation {
|
||||||
this.providerId = providerId;
|
this.providerId = providerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSubType() {
|
||||||
|
return subType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubType(String subType) {
|
||||||
|
this.subType = subType;
|
||||||
|
}
|
||||||
|
|
||||||
public MultivaluedHashMap<String, String> getConfig() {
|
public MultivaluedHashMap<String, String> getConfig() {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ public class ComponentRepresentation {
|
||||||
private String providerId;
|
private String providerId;
|
||||||
private String providerType;
|
private String providerType;
|
||||||
private String parentId;
|
private String parentId;
|
||||||
|
private String subType;
|
||||||
private MultivaluedHashMap<String, String> config;
|
private MultivaluedHashMap<String, String> config;
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
@ -73,6 +74,14 @@ public class ComponentRepresentation {
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSubType() {
|
||||||
|
return subType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubType(String subType) {
|
||||||
|
this.subType = subType;
|
||||||
|
}
|
||||||
|
|
||||||
public MultivaluedHashMap<String, String> getConfig() {
|
public MultivaluedHashMap<String, String> getConfig() {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.admin.client.resource;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
|
import org.keycloak.representations.idm.ComponentTypeRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface ClientRegistrationPolicyResource {
|
||||||
|
|
||||||
|
@Path("providers")
|
||||||
|
@GET
|
||||||
|
@NoCache
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
List<ComponentTypeRepresentation> getProviders();
|
||||||
|
}
|
|
@ -156,8 +156,8 @@ public interface RealmResource {
|
||||||
@Path("clients-initial-access")
|
@Path("clients-initial-access")
|
||||||
ClientInitialAccessResource clientInitialAccess();
|
ClientInitialAccessResource clientInitialAccess();
|
||||||
|
|
||||||
@Path("clients-trusted-hosts")
|
@Path("client-registration-policy")
|
||||||
public ClientRegistrationTrustedHostResource clientRegistrationTrustedHost();
|
ClientRegistrationPolicyResource clientRegistrationPolicy();
|
||||||
|
|
||||||
@Path("partialImport")
|
@Path("partialImport")
|
||||||
@POST
|
@POST
|
||||||
|
|
|
@ -2053,6 +2053,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
c.setParentId(model.getParentId());
|
c.setParentId(model.getParentId());
|
||||||
c.setProviderType(model.getProviderType());
|
c.setProviderType(model.getProviderType());
|
||||||
c.setProviderId(model.getProviderId());
|
c.setProviderId(model.getProviderId());
|
||||||
|
c.setSubType(model.getSubType());
|
||||||
c.setRealm(realm);
|
c.setRealm(realm);
|
||||||
em.persist(c);
|
em.persist(c);
|
||||||
setConfig(model, c);
|
setConfig(model, c);
|
||||||
|
@ -2087,6 +2088,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
c.setProviderId(component.getProviderId());
|
c.setProviderId(component.getProviderId());
|
||||||
c.setProviderType(component.getProviderType());
|
c.setProviderType(component.getProviderType());
|
||||||
c.setParentId(component.getParentId());
|
c.setParentId(component.getParentId());
|
||||||
|
c.setSubType(component.getSubType());
|
||||||
em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
|
em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
|
||||||
em.flush();
|
em.flush();
|
||||||
setConfig(component, c);
|
setConfig(component, c);
|
||||||
|
@ -2156,6 +2158,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
||||||
model.setName(c.getName());
|
model.setName(c.getName());
|
||||||
model.setProviderType(c.getProviderType());
|
model.setProviderType(c.getProviderType());
|
||||||
model.setProviderId(c.getProviderId());
|
model.setProviderId(c.getProviderId());
|
||||||
|
model.setSubType(c.getSubType());
|
||||||
model.setParentId(c.getParentId());
|
model.setParentId(c.getParentId());
|
||||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||||
TypedQuery<ComponentConfigEntity> configQuery = em.createNamedQuery("getComponentConfig", ComponentConfigEntity.class)
|
TypedQuery<ComponentConfigEntity> configQuery = em.createNamedQuery("getComponentConfig", ComponentConfigEntity.class)
|
||||||
|
|
|
@ -65,6 +65,9 @@ public class ComponentEntity {
|
||||||
@Column(name="PARENT_ID")
|
@Column(name="PARENT_ID")
|
||||||
protected String parentId;
|
protected String parentId;
|
||||||
|
|
||||||
|
@Column(name="SUB_TYPE")
|
||||||
|
protected String subType;
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +108,14 @@ public class ComponentEntity {
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSubType() {
|
||||||
|
return subType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubType(String subType) {
|
||||||
|
this.subType = subType;
|
||||||
|
}
|
||||||
|
|
||||||
public RealmEntity getRealm() {
|
public RealmEntity getRealm() {
|
||||||
return realm;
|
return realm;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@
|
||||||
<column name="PROVIDER_DISPLAY_NAME" type="VARCHAR(255)"></column>
|
<column name="PROVIDER_DISPLAY_NAME" type="VARCHAR(255)"></column>
|
||||||
</addColumn>
|
</addColumn>
|
||||||
|
|
||||||
|
<addColumn tableName="COMPONENT">
|
||||||
|
<column name="SUB_TYPE" type="VARCHAR(255)"></column>
|
||||||
|
</addColumn>
|
||||||
|
|
||||||
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.ExtractRealmKeysFromRealmTable"/>
|
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.ExtractRealmKeysFromRealmTable"/>
|
||||||
<dropColumn tableName="REALM" columnName="CODE_SECRET" />
|
<dropColumn tableName="REALM" columnName="CODE_SECRET" />
|
||||||
<dropColumn tableName="REALM" columnName="PRIVATE_KEY" />
|
<dropColumn tableName="REALM" columnName="PRIVATE_KEY" />
|
||||||
|
|
|
@ -1962,11 +1962,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
} else {
|
} else {
|
||||||
entity.setId(model.getId());
|
entity.setId(model.getId());
|
||||||
}
|
}
|
||||||
entity.setConfig(model.getConfig());
|
updateComponentEntity(entity, model);
|
||||||
entity.setParentId(model.getParentId());
|
|
||||||
entity.setProviderType(model.getProviderType());
|
|
||||||
entity.setProviderId(model.getProviderId());
|
|
||||||
entity.setName(model.getName());
|
|
||||||
model.setId(entity.getId());
|
model.setId(entity.getId());
|
||||||
realm.getComponentEntities().add(entity);
|
realm.getComponentEntities().add(entity);
|
||||||
updateRealm();
|
updateRealm();
|
||||||
|
@ -1980,11 +1976,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
|
|
||||||
for (ComponentEntity entity : realm.getComponentEntities()) {
|
for (ComponentEntity entity : realm.getComponentEntities()) {
|
||||||
if (entity.getId().equals(model.getId())) {
|
if (entity.getId().equals(model.getId())) {
|
||||||
entity.setConfig(model.getConfig());
|
updateComponentEntity(entity, model);
|
||||||
entity.setParentId(model.getParentId());
|
|
||||||
entity.setProviderType(model.getProviderType());
|
|
||||||
entity.setProviderId(model.getProviderId());
|
|
||||||
entity.setName(model.getName());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1992,6 +1984,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateComponentEntity(ComponentEntity entity, ComponentModel model) {
|
||||||
|
entity.setConfig(model.getConfig());
|
||||||
|
entity.setParentId(model.getParentId());
|
||||||
|
entity.setProviderType(model.getProviderType());
|
||||||
|
entity.setSubType(model.getSubType());
|
||||||
|
entity.setProviderId(model.getProviderId());
|
||||||
|
entity.setName(model.getName());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeComponent(ComponentModel component) {
|
public void removeComponent(ComponentModel component) {
|
||||||
Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
|
Iterator<ComponentEntity> it = realm.getComponentEntities().iterator();
|
||||||
|
@ -2053,6 +2054,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
model.setParentId(entity.getParentId());
|
model.setParentId(entity.getParentId());
|
||||||
model.setProviderId(entity.getProviderId());
|
model.setProviderId(entity.getProviderId());
|
||||||
model.setProviderType(entity.getProviderType());
|
model.setProviderType(entity.getProviderType());
|
||||||
|
model.setSubType(entity.getSubType());
|
||||||
MultivaluedHashMap<String, String> map = new MultivaluedHashMap<>();
|
MultivaluedHashMap<String, String> map = new MultivaluedHashMap<>();
|
||||||
map.putAll(entity.getConfig());
|
map.putAll(entity.getConfig());
|
||||||
model.setConfig(map);
|
model.setConfig(map);
|
||||||
|
|
|
@ -31,6 +31,7 @@ public class ComponentEntity extends AbstractIdentifiableEntity {
|
||||||
protected String providerType;
|
protected String providerType;
|
||||||
protected String providerId;
|
protected String providerId;
|
||||||
protected String parentId;
|
protected String parentId;
|
||||||
|
protected String subType;
|
||||||
protected Map<String, List<String>> config = new MultivaluedHashMap<>();
|
protected Map<String, List<String>> config = new MultivaluedHashMap<>();
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
@ -65,6 +66,14 @@ public class ComponentEntity extends AbstractIdentifiableEntity {
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSubType() {
|
||||||
|
return subType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubType(String subType) {
|
||||||
|
this.subType = subType;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, List<String>> getConfig() {
|
public Map<String, List<String>> getConfig() {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ public class ComponentModel implements Serializable {
|
||||||
private String providerId;
|
private String providerId;
|
||||||
private String providerType;
|
private String providerType;
|
||||||
private String parentId;
|
private String parentId;
|
||||||
|
private String subType;
|
||||||
private MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
private MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||||
private transient ConcurrentHashMap<String, Object> notes = new ConcurrentHashMap<>();
|
private transient ConcurrentHashMap<String, Object> notes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@ public class ComponentModel implements Serializable {
|
||||||
this.providerId = copy.providerId;
|
this.providerId = copy.providerId;
|
||||||
this.providerType = copy.providerType;
|
this.providerType = copy.providerType;
|
||||||
this.parentId = copy.parentId;
|
this.parentId = copy.parentId;
|
||||||
|
this.subType = copy.subType;
|
||||||
this.config = copy.config;
|
this.config = copy.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,4 +150,12 @@ public class ComponentModel implements Serializable {
|
||||||
public void setParentId(String parentId) {
|
public void setParentId(String parentId) {
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSubType() {
|
||||||
|
return subType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubType(String subType) {
|
||||||
|
this.subType = subType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.models;
|
package org.keycloak.models;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.cache.UserCache;
|
import org.keycloak.models.cache.UserCache;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.scripting.ScriptingProvider;
|
import org.keycloak.scripting.ScriptingProvider;
|
||||||
|
@ -59,6 +60,8 @@ public interface KeycloakSession {
|
||||||
*/
|
*/
|
||||||
<T extends Provider> T getProvider(Class<T> clazz, String id);
|
<T extends Provider> T getProvider(Class<T> clazz, String id);
|
||||||
|
|
||||||
|
<T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all provider factories that manage provider instances of class.
|
* Get all provider factories that manage provider instances of class.
|
||||||
*
|
*
|
||||||
|
|
|
@ -768,6 +768,13 @@ public class ModelToRepresentation {
|
||||||
public static List<ConfigPropertyRepresentation> toRepresentation(List<ProviderConfigProperty> configProperties) {
|
public static List<ConfigPropertyRepresentation> toRepresentation(List<ProviderConfigProperty> configProperties) {
|
||||||
List<ConfigPropertyRepresentation> propertiesRep = new LinkedList<>();
|
List<ConfigPropertyRepresentation> propertiesRep = new LinkedList<>();
|
||||||
for (ProviderConfigProperty prop : configProperties) {
|
for (ProviderConfigProperty prop : configProperties) {
|
||||||
|
ConfigPropertyRepresentation propRep = toRepresentation(prop);
|
||||||
|
propertiesRep.add(propRep);
|
||||||
|
}
|
||||||
|
return propertiesRep;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConfigPropertyRepresentation toRepresentation(ProviderConfigProperty prop) {
|
||||||
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
|
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
|
||||||
propRep.setName(prop.getName());
|
propRep.setName(prop.getName());
|
||||||
propRep.setLabel(prop.getLabel());
|
propRep.setLabel(prop.getLabel());
|
||||||
|
@ -775,9 +782,7 @@ public class ModelToRepresentation {
|
||||||
propRep.setDefaultValue(prop.getDefaultValue());
|
propRep.setDefaultValue(prop.getDefaultValue());
|
||||||
propRep.setHelpText(prop.getHelpText());
|
propRep.setHelpText(prop.getHelpText());
|
||||||
propRep.setSecret(prop.isSecret());
|
propRep.setSecret(prop.isSecret());
|
||||||
propertiesRep.add(propRep);
|
return propRep;
|
||||||
}
|
|
||||||
return propertiesRep;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ComponentRepresentation toRepresentation(KeycloakSession session, ComponentModel component, boolean internal) {
|
public static ComponentRepresentation toRepresentation(KeycloakSession session, ComponentModel component, boolean internal) {
|
||||||
|
@ -786,6 +791,7 @@ public class ModelToRepresentation {
|
||||||
rep.setName(component.getName());
|
rep.setName(component.getName());
|
||||||
rep.setProviderId(component.getProviderId());
|
rep.setProviderId(component.getProviderId());
|
||||||
rep.setProviderType(component.getProviderType());
|
rep.setProviderType(component.getProviderType());
|
||||||
|
rep.setSubType(component.getSubType());
|
||||||
rep.setParentId(component.getParentId());
|
rep.setParentId(component.getParentId());
|
||||||
if (internal) {
|
if (internal) {
|
||||||
rep.setConfig(component.getConfig());
|
rep.setConfig(component.getConfig());
|
||||||
|
|
|
@ -392,6 +392,7 @@ public class RepresentationToModel {
|
||||||
component.setConfig(compRep.getConfig());
|
component.setConfig(compRep.getConfig());
|
||||||
component.setProviderType(providerType);
|
component.setProviderType(providerType);
|
||||||
component.setProviderId(compRep.getProviderId());
|
component.setProviderId(compRep.getProviderId());
|
||||||
|
component.setSubType(compRep.getSubType());
|
||||||
component.setParentId(parentId);
|
component.setParentId(parentId);
|
||||||
component = newRealm.addComponentModel(component);
|
component = newRealm.addComponentModel(component);
|
||||||
if (compRep.getSubComponents() != null) {
|
if (compRep.getSubComponents() != null) {
|
||||||
|
@ -1684,6 +1685,7 @@ public class RepresentationToModel {
|
||||||
model.setProviderId(rep.getProviderId());
|
model.setProviderId(rep.getProviderId());
|
||||||
model.setConfig(new MultivaluedHashMap<>());
|
model.setConfig(new MultivaluedHashMap<>());
|
||||||
model.setName(rep.getName());
|
model.setName(rep.getName());
|
||||||
|
model.setSubType(rep.getSubType());
|
||||||
|
|
||||||
if (rep.getConfig() != null) {
|
if (rep.getConfig() != null) {
|
||||||
Set<String> keys = new HashSet<>(rep.getConfig().keySet());
|
Set<String> keys = new HashSet<>(rep.getConfig().keySet());
|
||||||
|
@ -1721,6 +1723,10 @@ public class RepresentationToModel {
|
||||||
component.setProviderId(rep.getProviderId());
|
component.setProviderId(rep.getProviderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rep.getSubType() != null) {
|
||||||
|
component.setSubType(rep.getSubType());
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, ProviderConfigProperty> providerConfiguration = null;
|
Map<String, ProviderConfigProperty> providerConfiguration = null;
|
||||||
if (!internal) {
|
if (!internal) {
|
||||||
providerConfiguration = ComponentUtil.getComponentConfigProperties(session, component);
|
providerConfiguration = ComponentUtil.getComponentConfigProperties(session, component);
|
||||||
|
|
|
@ -24,17 +24,27 @@ package org.keycloak.provider;
|
||||||
public class ProviderConfigProperty {
|
public class ProviderConfigProperty {
|
||||||
public static final String BOOLEAN_TYPE="boolean";
|
public static final String BOOLEAN_TYPE="boolean";
|
||||||
public static final String STRING_TYPE="String";
|
public static final String STRING_TYPE="String";
|
||||||
|
|
||||||
|
// Possibility to configure multiple String values of any value (something like "redirect_uris" for clients)
|
||||||
|
public static final String MULTIVALUED_STRING_TYPE="MultivaluedString";
|
||||||
|
|
||||||
public static final String SCRIPT_TYPE="Script";
|
public static final String SCRIPT_TYPE="Script";
|
||||||
public static final String FILE_TYPE="File";
|
public static final String FILE_TYPE="File";
|
||||||
public static final String ROLE_TYPE="Role";
|
public static final String ROLE_TYPE="Role";
|
||||||
|
|
||||||
|
// Possibility to configure single String value, which needs to be chosen from the list of predefined values (HTML select)
|
||||||
public static final String LIST_TYPE="List";
|
public static final String LIST_TYPE="List";
|
||||||
|
|
||||||
|
// Possibility to configure multiple String values, which needs to be chosen from the list of predefined values (HTML select with multiple)
|
||||||
|
public static final String MULTIVALUED_LIST_TYPE="MultivaluedList";
|
||||||
|
|
||||||
public static final String CLIENT_LIST_TYPE="ClientList";
|
public static final String CLIENT_LIST_TYPE="ClientList";
|
||||||
public static final String PASSWORD="Password";
|
public static final String PASSWORD="Password";
|
||||||
|
|
||||||
protected String name;
|
protected String name;
|
||||||
protected String label;
|
protected String label;
|
||||||
protected String helpText;
|
protected String helpText;
|
||||||
protected String type;
|
protected String type = STRING_TYPE;
|
||||||
protected Object defaultValue;
|
protected Object defaultValue;
|
||||||
protected boolean secret;
|
protected boolean secret;
|
||||||
|
|
||||||
|
|
|
@ -240,6 +240,7 @@ public class ExportUtils {
|
||||||
compRep.setProviderId(component.getProviderId());
|
compRep.setProviderId(component.getProviderId());
|
||||||
compRep.setConfig(component.getConfig());
|
compRep.setConfig(component.getConfig());
|
||||||
compRep.setName(component.getName());
|
compRep.setName(component.getName());
|
||||||
|
compRep.setSubType(component.getSubType());
|
||||||
compRep.setSubComponents(exportComponents(realm, component.getId()));
|
compRep.setSubComponents(exportComponents(realm, component.getId()));
|
||||||
components.add(component.getProviderType(), compRep);
|
components.add(component.getProviderType(), compRep);
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return "openid-connect";
|
return OIDCLoginProtocol.LOGIN_PROTOCOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -25,10 +25,6 @@ import java.util.List;
|
||||||
public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
|
public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
|
||||||
public static final String PROVIDER_ID_SUFFIX = "-pairwise-sub-mapper";
|
public static final String PROVIDER_ID_SUFFIX = "-pairwise-sub-mapper";
|
||||||
|
|
||||||
public static String getId(String prefix) {
|
|
||||||
return prefix + PROVIDER_ID_SUFFIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract String getIdPrefix();
|
public abstract String getIdPrefix();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,7 +113,7 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final String getId() {
|
public final String getId() {
|
||||||
return getIdPrefix() + PROVIDER_ID_SUFFIX;
|
return "oidc-" + getIdPrefix() + PROVIDER_ID_SUFFIX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class SHA256PairwiseSubMapper extends AbstractPairwiseSubMapper {
|
||||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||||
private final Charset charset;
|
private final Charset charset;
|
||||||
|
|
||||||
public SHA256PairwiseSubMapper() throws NoSuchAlgorithmException {
|
public SHA256PairwiseSubMapper() {
|
||||||
charset = Charset.forName("UTF-8");
|
charset = Charset.forName("UTF-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ public class SHA256PairwiseSubMapper extends AbstractPairwiseSubMapper {
|
||||||
Map<String, String> config;
|
Map<String, String> config;
|
||||||
ProtocolMapperRepresentation pairwise = new ProtocolMapperRepresentation();
|
ProtocolMapperRepresentation pairwise = new ProtocolMapperRepresentation();
|
||||||
pairwise.setName("pairwise subject identifier");
|
pairwise.setName("pairwise subject identifier");
|
||||||
pairwise.setProtocolMapper(AbstractPairwiseSubMapper.getId(PROVIDER_ID));
|
pairwise.setProtocolMapper(new SHA256PairwiseSubMapper().getId());
|
||||||
pairwise.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
pairwise.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
pairwise.setConsentRequired(false);
|
pairwise.setConsentRequired(false);
|
||||||
config = new HashMap<>();
|
config = new HashMap<>();
|
||||||
|
|
|
@ -7,6 +7,8 @@ import org.keycloak.services.ServicesLogger;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -23,7 +25,11 @@ public class PairwiseSubMapperUtils {
|
||||||
* @param clientRedirectUris
|
* @param clientRedirectUris
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static Set<String> resolveValidRedirectUris(String clientRootUrl, Set<String> clientRedirectUris) {
|
public static Set<String> resolveValidRedirectUris(String clientRootUrl, Collection<String> clientRedirectUris) {
|
||||||
|
if (clientRedirectUris == null) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> validRedirects = new HashSet<String>();
|
Set<String> validRedirects = new HashSet<String>();
|
||||||
for (String redirectUri : clientRedirectUris) {
|
for (String redirectUri : clientRedirectUris) {
|
||||||
if (redirectUri.startsWith("/")) {
|
if (redirectUri.startsWith("/")) {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.protocol.saml.clientregistration;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.AbstractClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class EntityDescriptorClientRegistrationContext extends AbstractClientRegistrationContext {
|
||||||
|
|
||||||
|
public EntityDescriptorClientRegistrationContext(KeycloakSession session, ClientRepresentation client, ClientRegistrationProvider provider) {
|
||||||
|
super(session, client, provider);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
|
import org.keycloak.protocol.saml.EntityDescriptorDescriptionConverter;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
|
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
import org.keycloak.services.clientregistration.DefaultClientRegistrationContext;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
|
@ -45,10 +45,12 @@ public class EntityDescriptorClientRegistrationProvider extends AbstractClientRe
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response createSaml(String descriptor) {
|
public Response createSaml(String descriptor) {
|
||||||
ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
|
ClientRepresentation client = session.getProvider(ClientDescriptionConverter.class, EntityDescriptorDescriptionConverter.ID).convertToInternal(descriptor);
|
||||||
ClientRegistrationContext context = new ClientRegistrationContext(client);
|
EntityDescriptorClientRegistrationContext context = new EntityDescriptorClientRegistrationContext(session, client, this);
|
||||||
client = create(context);
|
client = create(context);
|
||||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
||||||
return Response.created(uri).entity(client).build();
|
return Response.created(uri).entity(client).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services;
|
package org.keycloak.services;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentFactory;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.credential.UserCredentialStoreManager;
|
import org.keycloak.credential.UserCredentialStoreManager;
|
||||||
import org.keycloak.keys.DefaultKeyManager;
|
import org.keycloak.keys.DefaultKeyManager;
|
||||||
import org.keycloak.models.KeycloakContext;
|
import org.keycloak.models.KeycloakContext;
|
||||||
|
@ -190,6 +192,28 @@ public class DefaultKeycloakSession implements KeycloakSession {
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends Provider> T getProvider(Class<T> clazz, ComponentModel componentModel) {
|
||||||
|
String modelId = componentModel.getProviderType() + "::" + componentModel.getId();
|
||||||
|
|
||||||
|
Object found = getAttribute(modelId);
|
||||||
|
if (found != null) {
|
||||||
|
return clazz.cast(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProviderFactory<T> providerFactory = factory.getProviderFactory(clazz, componentModel.getProviderId());
|
||||||
|
if (providerFactory == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComponentFactory<T, T> componentFactory = (ComponentFactory<T, T>) providerFactory;
|
||||||
|
T provider = componentFactory.create(this, componentModel);
|
||||||
|
enlistForClose(provider);
|
||||||
|
setAttribute(modelId, provider);
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
public <T extends Provider> Set<String> listProviderIds(Class<T> clazz) {
|
public <T extends Provider> Set<String> listProviderIds(Class<T> clazz) {
|
||||||
return factory.getAllProviderIds(clazz);
|
return factory.getAllProviderIds(clazz);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.validation.ClientValidator;
|
||||||
|
import org.keycloak.services.validation.ValidationMessages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractClientRegistrationContext implements ClientRegistrationContext {
|
||||||
|
|
||||||
|
protected final KeycloakSession session;
|
||||||
|
protected final ClientRepresentation client;
|
||||||
|
protected final ClientRegistrationProvider provider;
|
||||||
|
|
||||||
|
public AbstractClientRegistrationContext(KeycloakSession session, ClientRepresentation client, ClientRegistrationProvider provider) {
|
||||||
|
this.session = session;
|
||||||
|
this.client = client;
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRepresentation getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSession getSession() {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationProvider getProvider() {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateClient(ValidationMessages validationMessages) {
|
||||||
|
return ClientValidator.validate(client, validationMessages);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.ClientInitialAccessModel;
|
import org.keycloak.models.ClientInitialAccessModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelDuplicateException;
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
@ -29,7 +28,8 @@ import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.ForbiddenException;
|
import org.keycloak.services.ForbiddenException;
|
||||||
import org.keycloak.services.validation.ClientValidator;
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
||||||
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
import org.keycloak.services.validation.ValidationMessages;
|
import org.keycloak.services.validation.ValidationMessages;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -52,10 +52,10 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
|
|
||||||
event.event(EventType.CLIENT_REGISTER);
|
event.event(EventType.CLIENT_REGISTER);
|
||||||
|
|
||||||
auth.requireCreate();
|
RegistrationAuth registrationAuth = auth.requireCreate(context);
|
||||||
|
|
||||||
ValidationMessages validationMessages = new ValidationMessages();
|
ValidationMessages validationMessages = new ValidationMessages();
|
||||||
if (!validateClient(context, validationMessages)) {
|
if (!context.validateClient(validationMessages)) {
|
||||||
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
||||||
throw new ErrorResponseException(
|
throw new ErrorResponseException(
|
||||||
errorCode,
|
errorCode,
|
||||||
|
@ -67,11 +67,13 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
try {
|
try {
|
||||||
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
|
ClientModel clientModel = RepresentationToModel.createClient(session, session.getContext().getRealm(), client, true);
|
||||||
|
|
||||||
|
ClientRegistrationPolicyManager.triggerAfterRegister(context, registrationAuth, clientModel);
|
||||||
|
|
||||||
client = ModelToRepresentation.toRepresentation(clientModel);
|
client = ModelToRepresentation.toRepresentation(clientModel);
|
||||||
|
|
||||||
client.setSecret(clientModel.getSecret());
|
client.setSecret(clientModel.getSecret());
|
||||||
|
|
||||||
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel);
|
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, clientModel, registrationAuth);
|
||||||
client.setRegistrationAccessToken(registrationAccessToken);
|
client.setRegistrationAccessToken(registrationAccessToken);
|
||||||
|
|
||||||
if (auth.isInitialAccessToken()) {
|
if (auth.isInitialAccessToken()) {
|
||||||
|
@ -79,11 +81,6 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
initialAccessModel.decreaseRemainingCount();
|
initialAccessModel.decreaseRemainingCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.isRegistrationHostTrusted()) {
|
|
||||||
ClientRegistrationTrustedHostModel trustedHost = auth.getTrustedHostModel();
|
|
||||||
trustedHost.decreaseRemainingCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.client(client.getClientId()).success();
|
event.client(client.getClientId()).success();
|
||||||
return client;
|
return client;
|
||||||
} catch (ModelDuplicateException e) {
|
} catch (ModelDuplicateException e) {
|
||||||
|
@ -100,7 +97,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
|
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
|
||||||
|
|
||||||
if (auth.isRegistrationAccessToken()) {
|
if (auth.isRegistrationAccessToken()) {
|
||||||
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
|
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth());
|
||||||
rep.setRegistrationAccessToken(registrationAccessToken);
|
rep.setRegistrationAccessToken(registrationAccessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,14 +111,14 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
event.event(EventType.CLIENT_UPDATE).client(clientId);
|
event.event(EventType.CLIENT_UPDATE).client(clientId);
|
||||||
|
|
||||||
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
|
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
|
||||||
auth.requireUpdate(client);
|
RegistrationAuth registrationAuth = auth.requireUpdate(context, client);
|
||||||
|
|
||||||
if (!client.getClientId().equals(rep.getClientId())) {
|
if (!client.getClientId().equals(rep.getClientId())) {
|
||||||
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(ErrorCodes.INVALID_CLIENT_METADATA, "Client Identifier modified", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidationMessages validationMessages = new ValidationMessages();
|
ValidationMessages validationMessages = new ValidationMessages();
|
||||||
if (!validateClient(context, validationMessages)) {
|
if (!context.validateClient(validationMessages)) {
|
||||||
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
String errorCode = validationMessages.fieldHasError("redirectUris") ? ErrorCodes.INVALID_REDIRECT_URI : ErrorCodes.INVALID_CLIENT_METADATA;
|
||||||
throw new ErrorResponseException(
|
throw new ErrorResponseException(
|
||||||
errorCode,
|
errorCode,
|
||||||
|
@ -134,10 +131,12 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
rep = ModelToRepresentation.toRepresentation(client);
|
rep = ModelToRepresentation.toRepresentation(client);
|
||||||
|
|
||||||
if (auth.isRegistrationAccessToken()) {
|
if (auth.isRegistrationAccessToken()) {
|
||||||
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client);
|
String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth());
|
||||||
rep.setRegistrationAccessToken(registrationAccessToken);
|
rep.setRegistrationAccessToken(registrationAccessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientRegistrationPolicyManager.triggerAfterUpdate(context, registrationAuth, client);
|
||||||
|
|
||||||
event.client(client.getClientId()).success();
|
event.client(client.getClientId()).success();
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
@ -147,7 +146,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
event.event(EventType.CLIENT_DELETE).client(clientId);
|
event.event(EventType.CLIENT_DELETE).client(clientId);
|
||||||
|
|
||||||
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
|
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
|
||||||
auth.requireUpdate(client);
|
auth.requireDelete(client);
|
||||||
|
|
||||||
if (session.getContext().getRealm().removeClient(client.getId())) {
|
if (session.getContext().getRealm().removeClient(client.getId())) {
|
||||||
event.client(client.getClientId()).success();
|
event.client(client.getClientId()).success();
|
||||||
|
@ -161,6 +160,11 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationAuth getAuth() {
|
||||||
|
return this.auth;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setEvent(EventBuilder event) {
|
public void setEvent(EventBuilder event) {
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
@ -170,9 +174,4 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean validateClient(ClientRegistrationContext context, ValidationMessages validationMessages) {
|
|
||||||
ClientRepresentation client = context.getClient();
|
|
||||||
return ClientValidator.validate(client, validationMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,11 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationAuth getAuth() {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setEvent(EventBuilder event) {
|
public void setEvent(EventBuilder event) {
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
|
|
@ -28,13 +28,15 @@ import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.models.AdminRoles;
|
import org.keycloak.models.AdminRoles;
|
||||||
import org.keycloak.models.ClientInitialAccessModel;
|
import org.keycloak.models.ClientInitialAccessModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
@ -48,17 +50,17 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public class ClientRegistrationAuth {
|
public class ClientRegistrationAuth {
|
||||||
|
|
||||||
private KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private EventBuilder event;
|
private final ClientRegistrationProvider provider;
|
||||||
|
private final EventBuilder event;
|
||||||
|
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private JsonWebToken jwt;
|
private JsonWebToken jwt;
|
||||||
private ClientInitialAccessModel initialAccessModel;
|
private ClientInitialAccessModel initialAccessModel;
|
||||||
|
|
||||||
private ClientRegistrationTrustedHostModel trustedHostModel;
|
public ClientRegistrationAuth(KeycloakSession session, ClientRegistrationProvider provider, EventBuilder event) {
|
||||||
|
|
||||||
public ClientRegistrationAuth(KeycloakSession session, EventBuilder event) {
|
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
this.provider = provider;
|
||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +70,6 @@ public class ClientRegistrationAuth {
|
||||||
|
|
||||||
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||||
if (authorizationHeader == null) {
|
if (authorizationHeader == null) {
|
||||||
|
|
||||||
// Try trusted hosts
|
|
||||||
trustedHostModel = ClientRegistrationHostUtils.getTrustedHost(session.getContext().getConnection().getRemoteAddr(), session, realm);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,10 +92,6 @@ public class ClientRegistrationAuth {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRegistrationHostTrusted() {
|
|
||||||
return trustedHostModel != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBearerToken() {
|
private boolean isBearerToken() {
|
||||||
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
|
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
|
||||||
}
|
}
|
||||||
|
@ -109,30 +104,42 @@ public class ClientRegistrationAuth {
|
||||||
return jwt != null && ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType());
|
return jwt != null && ClientRegistrationTokenUtils.TYPE_REGISTRATION_ACCESS_TOKEN.equals(jwt.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requireCreate() {
|
public RegistrationAuth requireCreate(ClientRegistrationContext context) {
|
||||||
init();
|
init();
|
||||||
|
|
||||||
if (isRegistrationHostTrusted()) {
|
RegistrationAuth registrationAuth = RegistrationAuth.ANONYMOUS;
|
||||||
// Client registrations from trusted hosts
|
|
||||||
return;
|
if (isBearerToken()) {
|
||||||
} else if (isBearerToken()) {
|
|
||||||
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
|
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
|
||||||
return;
|
registrationAuth = RegistrationAuth.AUTHENTICATED;
|
||||||
} else {
|
} else {
|
||||||
throw forbidden();
|
throw forbidden();
|
||||||
}
|
}
|
||||||
} else if (isInitialAccessToken()) {
|
} else if (isInitialAccessToken()) {
|
||||||
if (initialAccessModel.getRemainingCount() > 0) {
|
if (initialAccessModel.getRemainingCount() > 0) {
|
||||||
if (initialAccessModel.getExpiration() == 0 || (initialAccessModel.getTimestamp() + initialAccessModel.getExpiration()) > Time.currentTime()) {
|
if (initialAccessModel.getExpiration() == 0 || (initialAccessModel.getTimestamp() + initialAccessModel.getExpiration()) > Time.currentTime()) {
|
||||||
return;
|
registrationAuth = RegistrationAuth.AUTHENTICATED;
|
||||||
|
} else {
|
||||||
|
throw unauthorized("Expired initial access token");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw unauthorized("No remaining count on initial access token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw unauthorized("Not authenticated to view client. Host not trusted and Token is missing or invalid.");
|
try {
|
||||||
|
ClientRegistrationPolicyManager.triggerBeforeRegister(context, registrationAuth);
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
throw forbidden(crpe.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return registrationAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requireView(ClientModel client) {
|
public void requireView(ClientModel client) {
|
||||||
|
RegistrationAuth authType = null;
|
||||||
|
boolean authenticated = false;
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
if (isBearerToken()) {
|
if (isBearerToken()) {
|
||||||
|
@ -140,26 +147,65 @@ public class ClientRegistrationAuth {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
throw notFound();
|
throw notFound();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
|
authenticated = true;
|
||||||
|
authType = RegistrationAuth.AUTHENTICATED;
|
||||||
} else {
|
} else {
|
||||||
throw forbidden();
|
throw forbidden();
|
||||||
}
|
}
|
||||||
} else if (isRegistrationAccessToken()) {
|
} else if (isRegistrationAccessToken()) {
|
||||||
if (client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
|
if (client != null && client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
|
||||||
return;
|
authenticated = true;
|
||||||
|
authType = getRegistrationAuth();
|
||||||
}
|
}
|
||||||
} else if (isInitialAccessToken()) {
|
} else if (isInitialAccessToken()) {
|
||||||
throw unauthorized("Not initial access token");
|
throw unauthorized("Not initial access token allowed");
|
||||||
} else {
|
} else {
|
||||||
if (authenticateClient(client)) {
|
if (authenticateClient(client)) {
|
||||||
return;
|
authenticated = true;
|
||||||
|
authType = RegistrationAuth.AUTHENTICATED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw unauthorized("Not authorized to view client. Missing or invalid token or bad client credentials.");
|
if (authenticated) {
|
||||||
|
try {
|
||||||
|
ClientRegistrationPolicyManager.triggerBeforeView(session, provider, authType, client);
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
throw forbidden(crpe.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw unauthorized("Not authorized to view client. Not valid token or client credentials provided.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requireUpdate(ClientModel client) {
|
public RegistrationAuth getRegistrationAuth() {
|
||||||
|
String str = (String) jwt.getOtherClaims().get(RegistrationAccessToken.REGISTRATION_AUTH);
|
||||||
|
return RegistrationAuth.fromString(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegistrationAuth requireUpdate(ClientRegistrationContext context, ClientModel client) {
|
||||||
|
RegistrationAuth regAuth = requireUpdateAuth(client);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientRegistrationPolicyManager.triggerBeforeUpdate(context, regAuth, client);
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
throw forbidden(crpe.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return regAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requireDelete(ClientModel client) {
|
||||||
|
RegistrationAuth chainType = requireUpdateAuth(client);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientRegistrationPolicyManager.triggerBeforeRemove(session, provider, chainType, client);
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
throw forbidden(crpe.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegistrationAuth requireUpdateAuth(ClientModel client) {
|
||||||
init();
|
init();
|
||||||
|
|
||||||
if (isBearerToken()) {
|
if (isBearerToken()) {
|
||||||
|
@ -167,27 +213,24 @@ public class ClientRegistrationAuth {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
throw notFound();
|
throw notFound();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
|
return RegistrationAuth.AUTHENTICATED;
|
||||||
} else {
|
} else {
|
||||||
throw forbidden();
|
throw forbidden();
|
||||||
}
|
}
|
||||||
} else if (isRegistrationAccessToken()) {
|
} else if (isRegistrationAccessToken()) {
|
||||||
if (client.getRegistrationToken() != null && client != null && client.getRegistrationToken().equals(jwt.getId())) {
|
if (client != null && client.getRegistrationToken() != null && client.getRegistrationToken().equals(jwt.getId())) {
|
||||||
return;
|
return getRegistrationAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw unauthorized("Not authorized to update client. Missing or invalid token.");
|
throw unauthorized("Not authorized to update client. Maybe missing token or bad token type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientInitialAccessModel getInitialAccessModel() {
|
public ClientInitialAccessModel getInitialAccessModel() {
|
||||||
return initialAccessModel;
|
return initialAccessModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientRegistrationTrustedHostModel getTrustedHostModel() {
|
|
||||||
return trustedHostModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasRole(String... role) {
|
private boolean hasRole(String... role) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> otherClaims = jwt.getOtherClaims();
|
Map<String, Object> otherClaims = jwt.getOtherClaims();
|
||||||
|
@ -227,6 +270,10 @@ public class ClientRegistrationAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean authenticateClient(ClientModel client) {
|
private boolean authenticateClient(ClientModel client) {
|
||||||
|
if (client == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (client.isPublicClient()) {
|
if (client.isPublicClient()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -259,8 +306,12 @@ public class ClientRegistrationAuth {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Failure forbidden() {
|
private Failure forbidden() {
|
||||||
|
return forbidden("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Failure forbidden(String errorDescription) {
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INSUFFICIENT_SCOPE, "Forbidden", Response.Status.FORBIDDEN);
|
throw new ErrorResponseException(OAuthErrorException.INSUFFICIENT_SCOPE, errorDescription, Response.Status.FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Failure notFound() {
|
private Failure notFound() {
|
||||||
|
|
|
@ -17,20 +17,21 @@
|
||||||
|
|
||||||
package org.keycloak.services.clientregistration;
|
package org.keycloak.services.clientregistration;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.validation.ValidationMessages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class ClientRegistrationContext {
|
public interface ClientRegistrationContext {
|
||||||
|
|
||||||
private final ClientRepresentation client;
|
ClientRepresentation getClient();
|
||||||
|
|
||||||
public ClientRegistrationContext(ClientRepresentation client) {
|
KeycloakSession getSession();
|
||||||
this.client = client;
|
|
||||||
}
|
ClientRegistrationProvider getProvider();
|
||||||
|
|
||||||
|
boolean validateClient(ValidationMessages validationMessages);
|
||||||
|
|
||||||
public ClientRepresentation getClient() {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ public class ClientRegistrationHostUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return null if host from request is not trusted. Otherwise return trusted host model
|
* @return null if host from request is not trusted. Otherwise return trusted host model
|
||||||
|
*
|
||||||
|
* TODO: Remove
|
||||||
*/
|
*/
|
||||||
public static ClientRegistrationTrustedHostModel getTrustedHost(String hostAddress, KeycloakSession session, RealmModel realm) {
|
public static ClientRegistrationTrustedHostModel getTrustedHost(String hostAddress, KeycloakSession session, RealmModel realm) {
|
||||||
logger.debugf("Verifying remote host : %s", hostAddress);
|
logger.debugf("Verifying remote host : %s", hostAddress);
|
||||||
|
|
|
@ -27,6 +27,8 @@ public interface ClientRegistrationProvider extends Provider {
|
||||||
|
|
||||||
void setAuth(ClientRegistrationAuth auth);
|
void setAuth(ClientRegistrationAuth auth);
|
||||||
|
|
||||||
|
ClientRegistrationAuth getAuth();
|
||||||
|
|
||||||
void setEvent(EventBuilder event);
|
void setEvent(EventBuilder event);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ public class ClientRegistrationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.setEvent(event);
|
provider.setEvent(event);
|
||||||
provider.setAuth(new ClientRegistrationAuth(session, event));
|
provider.setAuth(new ClientRegistrationAuth(session, provider, event));
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -43,19 +44,23 @@ public class ClientRegistrationTokenUtils {
|
||||||
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
|
public static final String TYPE_INITIAL_ACCESS_TOKEN = "InitialAccessToken";
|
||||||
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
|
public static final String TYPE_REGISTRATION_ACCESS_TOKEN = "RegistrationAccessToken";
|
||||||
|
|
||||||
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client) {
|
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client, RegistrationAuth registrationAuth) {
|
||||||
return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client);
|
return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client, registrationAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String updateRegistrationAccessToken(KeycloakSession session, RealmModel realm, UriInfo uri, ClientModel client) {
|
public static String updateRegistrationAccessToken(KeycloakSession session, RealmModel realm, UriInfo uri, ClientModel client, RegistrationAuth registrationAuth) {
|
||||||
String id = KeycloakModelUtils.generateId();
|
String id = KeycloakModelUtils.generateId();
|
||||||
client.setRegistrationToken(id);
|
client.setRegistrationToken(id);
|
||||||
String token = createToken(session, realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
|
|
||||||
return token;
|
RegistrationAccessToken regToken = new RegistrationAccessToken();
|
||||||
|
regToken.setRegistrationAuth(registrationAuth.toString().toLowerCase());
|
||||||
|
|
||||||
|
return setupToken(regToken, session, realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String createInitialAccessToken(KeycloakSession session, RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
|
public static String createInitialAccessToken(KeycloakSession session, RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
|
||||||
return createToken(session, realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
|
JsonWebToken initialToken = new JsonWebToken();
|
||||||
|
return setupToken(initialToken, session, realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TokenVerification verifyToken(KeycloakSession session, RealmModel realm, UriInfo uri, String token) {
|
public static TokenVerification verifyToken(KeycloakSession session, RealmModel realm, UriInfo uri, String token) {
|
||||||
|
@ -100,9 +105,7 @@ public class ClientRegistrationTokenUtils {
|
||||||
return TokenVerification.success(jwt);
|
return TokenVerification.success(jwt);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String createToken(KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) {
|
private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) {
|
||||||
JsonWebToken jwt = new JsonWebToken();
|
|
||||||
|
|
||||||
String issuer = getIssuer(realm, uri);
|
String issuer = getIssuer(realm, uri);
|
||||||
|
|
||||||
jwt.type(type);
|
jwt.type(type);
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||||
|
import org.keycloak.services.validation.ValidationMessages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class DefaultClientRegistrationContext extends AbstractClientRegistrationContext {
|
||||||
|
|
||||||
|
public DefaultClientRegistrationContext(KeycloakSession session, ClientRepresentation client, ClientRegistrationProvider provider) {
|
||||||
|
super(session, client, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateClient(ValidationMessages validationMessages) {
|
||||||
|
return super.validateClient(validationMessages) && PairwiseClientValidator.validate(session, client, validationMessages);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,11 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.services.clientregistration;
|
package org.keycloak.services.clientregistration;
|
||||||
|
|
||||||
import org.keycloak.events.EventBuilder;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
|
||||||
import org.keycloak.services.validation.ValidationMessages;
|
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
@ -48,7 +45,7 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response createDefault(ClientRepresentation client) {
|
public Response createDefault(ClientRepresentation client) {
|
||||||
ClientRegistrationContext context = new ClientRegistrationContext(client);
|
DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this);
|
||||||
client = create(context);
|
client = create(context);
|
||||||
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
URI uri = session.getContext().getUri().getAbsolutePathBuilder().path(client.getClientId()).build();
|
||||||
return Response.created(uri).entity(client).build();
|
return Response.created(uri).entity(client).build();
|
||||||
|
@ -66,7 +63,7 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
|
||||||
@Path("{clientId}")
|
@Path("{clientId}")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) {
|
public Response updateDefault(@PathParam("clientId") String clientId, ClientRepresentation client) {
|
||||||
ClientRegistrationContext context = new ClientRegistrationContext(client);
|
DefaultClientRegistrationContext context = new DefaultClientRegistrationContext(session, client, this);
|
||||||
client = update(clientId, context);
|
client = update(clientId, context);
|
||||||
return Response.ok(client).build();
|
return Response.ok(client).build();
|
||||||
}
|
}
|
||||||
|
@ -76,24 +73,4 @@ public class DefaultClientRegistrationProvider extends AbstractClientRegistratio
|
||||||
public void deleteDefault(@PathParam("clientId") String clientId) {
|
public void deleteDefault(@PathParam("clientId") String clientId) {
|
||||||
delete(clientId);
|
delete(clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setAuth(ClientRegistrationAuth auth) {
|
|
||||||
this.auth = auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEvent(EventBuilder event) {
|
|
||||||
this.event = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean validateClient(ClientRegistrationContext context, ValidationMessages validationMessages) {
|
|
||||||
ClientRepresentation client = context.getClient();
|
|
||||||
return super.validateClient(context, validationMessages) && PairwiseClientValidator.validate(session, client, validationMessages);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.keycloak.representations.JsonWebToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class RegistrationAccessToken extends JsonWebToken {
|
||||||
|
|
||||||
|
public static final String REGISTRATION_AUTH = "registration_auth";
|
||||||
|
|
||||||
|
@JsonProperty(REGISTRATION_AUTH)
|
||||||
|
protected String registrationAuth;
|
||||||
|
|
||||||
|
public String getRegistrationAuth() {
|
||||||
|
return registrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegistrationAuth(String registrationAuth) {
|
||||||
|
this.registrationAuth = registrationAuth;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,23 +17,46 @@
|
||||||
|
|
||||||
package org.keycloak.services.clientregistration.oidc;
|
package org.keycloak.services.clientregistration.oidc;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.utils.SubjectType;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
import org.keycloak.services.clientregistration.AbstractClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
import org.keycloak.services.clientregistration.DefaultClientRegistrationContext;
|
||||||
|
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||||
|
import org.keycloak.services.validation.ValidationMessages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class OIDCClientRegistrationContext extends ClientRegistrationContext {
|
public class OIDCClientRegistrationContext extends AbstractClientRegistrationContext {
|
||||||
|
|
||||||
private final OIDCClientRepresentation oidcRep;
|
private final OIDCClientRepresentation oidcRep;
|
||||||
|
|
||||||
public OIDCClientRegistrationContext(ClientRepresentation client, OIDCClientRepresentation oidcRep) {
|
public OIDCClientRegistrationContext(KeycloakSession session, ClientRepresentation client, ClientRegistrationProvider provider, OIDCClientRepresentation oidcRep) {
|
||||||
super(client);
|
super(session, client, provider);
|
||||||
this.oidcRep = oidcRep;
|
this.oidcRep = oidcRep;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OIDCClientRepresentation getOidcRep() {
|
@Override
|
||||||
return oidcRep;
|
public boolean validateClient(ValidationMessages validationMessages) {
|
||||||
|
boolean valid = super.validateClient(validationMessages);
|
||||||
|
|
||||||
|
String rootUrl = client.getRootUrl();
|
||||||
|
Set<String> redirectUris = new HashSet<>();
|
||||||
|
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
|
||||||
|
|
||||||
|
SubjectType subjectType = SubjectType.parse(oidcRep.getSubjectType());
|
||||||
|
String sectorIdentifierUri = oidcRep.getSectorIdentifierUri();
|
||||||
|
|
||||||
|
// If sector_identifier_uri is in oidc config, then always validate it
|
||||||
|
if (SubjectType.PAIRWISE == subjectType || (sectorIdentifierUri != null && !sectorIdentifierUri.isEmpty())) {
|
||||||
|
valid = valid && PairwiseClientValidator.validate(session, rootUrl, redirectUris, oidcRep.getSectorIdentifierUri(), validationMessages);
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
|
import org.keycloak.services.clientregistration.AbstractClientRegistrationProvider;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
|
import org.keycloak.services.clientregistration.ClientRegistrationAuth;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.DefaultClientRegistrationContext;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationException;
|
import org.keycloak.services.clientregistration.ClientRegistrationException;
|
||||||
import org.keycloak.services.clientregistration.ErrorCodes;
|
import org.keycloak.services.clientregistration.ErrorCodes;
|
||||||
import org.keycloak.services.validation.PairwiseClientValidator;
|
import org.keycloak.services.validation.PairwiseClientValidator;
|
||||||
|
@ -78,7 +79,7 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
|
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
|
||||||
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(client, clientOIDC);
|
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(session, client, this, clientOIDC);
|
||||||
client = create(oidcContext);
|
client = create(oidcContext);
|
||||||
|
|
||||||
ClientModel clientModel = session.getContext().getRealm().getClientByClientId(client.getClientId());
|
ClientModel clientModel = session.getContext().getRealm().getClientByClientId(client.getClientId());
|
||||||
|
@ -110,7 +111,7 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
|
public Response updateOIDC(@PathParam("clientId") String clientId, OIDCClientRepresentation clientOIDC) {
|
||||||
try {
|
try {
|
||||||
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
|
ClientRepresentation client = DescriptionConverter.toInternal(session, clientOIDC);
|
||||||
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(client, clientOIDC);
|
OIDCClientRegistrationContext oidcContext = new OIDCClientRegistrationContext(session, client, this, clientOIDC);
|
||||||
client = update(clientId, oidcContext);
|
client = update(clientId, oidcContext);
|
||||||
|
|
||||||
ClientModel clientModel = session.getContext().getRealm().getClientByClientId(client.getClientId());
|
ClientModel clientModel = session.getContext().getRealm().getClientByClientId(client.getClientId());
|
||||||
|
@ -132,21 +133,6 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
delete(clientId);
|
delete(clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setAuth(ClientRegistrationAuth auth) {
|
|
||||||
this.auth = auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setEvent(EventBuilder event) {
|
|
||||||
this.event = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void updatePairwiseSubMappers(ClientModel clientModel, SubjectType subjectType, String sectorIdentifierUri) {
|
private void updatePairwiseSubMappers(ClientModel clientModel, SubjectType subjectType, String sectorIdentifierUri) {
|
||||||
if (subjectType == SubjectType.PAIRWISE) {
|
if (subjectType == SubjectType.PAIRWISE) {
|
||||||
|
|
||||||
|
@ -181,29 +167,6 @@ public class OIDCClientRegistrationProvider extends AbstractClientRegistrationPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean validateClient(ClientRegistrationContext context, ValidationMessages validationMessages) {
|
|
||||||
OIDCClientRegistrationContext oidcContext = (OIDCClientRegistrationContext) context;
|
|
||||||
OIDCClientRepresentation oidcRep = oidcContext.getOidcRep();
|
|
||||||
|
|
||||||
boolean valid = super.validateClient(context, validationMessages);
|
|
||||||
|
|
||||||
ClientRepresentation client = oidcContext.getClient();
|
|
||||||
|
|
||||||
String rootUrl = client.getRootUrl();
|
|
||||||
Set<String> redirectUris = new HashSet<>();
|
|
||||||
if (client.getRedirectUris() != null) redirectUris.addAll(client.getRedirectUris());
|
|
||||||
|
|
||||||
SubjectType subjectType = SubjectType.parse(oidcRep.getSubjectType());
|
|
||||||
String sectorIdentifierUri = oidcRep.getSectorIdentifierUri();
|
|
||||||
|
|
||||||
// If sector_identifier_uri is in oidc config, then always validate it
|
|
||||||
if (SubjectType.PAIRWISE == subjectType || (sectorIdentifierUri != null && !sectorIdentifierUri.isEmpty())) {
|
|
||||||
valid = valid && PairwiseClientValidator.validate(session, rootUrl, redirectUris, oidcRep.getSectorIdentifierUri(), validationMessages);
|
|
||||||
}
|
|
||||||
return valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateClientRepWithProtocolMappers(ClientModel clientModel, ClientRepresentation rep) {
|
private void updateClientRepWithProtocolMappers(ClientModel clientModel, ClientRepresentation rep) {
|
||||||
List<ProtocolMapperRepresentation> mappings = new LinkedList<>();
|
List<ProtocolMapperRepresentation> mappings = new LinkedList<>();
|
||||||
for (ProtocolMapperModel model : clientModel.getProtocolMappers()) {
|
for (ProtocolMapperModel model : clientModel.getProtocolMappers()) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.component.ComponentValidationException;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public abstract class AbstractClientRegistrationPolicyFactory implements ClientRegistrationPolicyFactory {
|
||||||
|
|
||||||
|
protected KeycloakSessionFactory sessionFactory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
this.sessionFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties(KeycloakSession session) {
|
||||||
|
return getConfigProperties();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface ClientRegistrationPolicy extends Provider {
|
||||||
|
|
||||||
|
void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
void afterRegister(ClientRegistrationContext context, ClientModel clientModel);
|
||||||
|
|
||||||
|
void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
void afterUpdate(ClientRegistrationContext context, ClientModel clientModel);
|
||||||
|
|
||||||
|
void beforeView(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
void beforeDelete(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationPolicyException extends Exception {
|
||||||
|
|
||||||
|
private ComponentModel policyModel;
|
||||||
|
|
||||||
|
public ClientRegistrationPolicyException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientRegistrationPolicyException(String message, Throwable throwable) {
|
||||||
|
super(message, throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentModel getPolicyModel() {
|
||||||
|
return policyModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPolicyModel(ComponentModel policyModel) {
|
||||||
|
this.policyModel = policyModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return policyModel==null ? super.getMessage() : String.format("Policy '%s' rejected request to client-registration service. Details: %s", policyModel.getName(), super.getMessage());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentFactory;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface ClientRegistrationPolicyFactory extends ComponentFactory<ClientRegistrationPolicy, ClientRegistrationPolicy> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get config properties filled for particular session. It assumes the session.getContext() has set realm
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<ProviderConfigProperty> getConfigProperties(KeycloakSession session);
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationPolicyManager {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ClientRegistrationPolicyManager.class);
|
||||||
|
|
||||||
|
public static void triggerBeforeRegister(ClientRegistrationContext context, RegistrationAuth authType) throws ClientRegistrationPolicyException {
|
||||||
|
triggerPolicies(context.getSession(), authType, "before register client", (ClientRegistrationPolicy policy) -> {
|
||||||
|
|
||||||
|
policy.beforeRegister(context);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void triggerAfterRegister(ClientRegistrationContext context, RegistrationAuth authType, ClientModel client) {
|
||||||
|
try {
|
||||||
|
triggerPolicies(context.getSession(), authType, "after register client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
|
||||||
|
|
||||||
|
policy.afterRegister(context, client);
|
||||||
|
|
||||||
|
});
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
throw new IllegalStateException(crpe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void triggerBeforeUpdate(ClientRegistrationContext context, RegistrationAuth authType, ClientModel client) throws ClientRegistrationPolicyException {
|
||||||
|
triggerPolicies(context.getSession(), authType, "before update client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
|
||||||
|
|
||||||
|
policy.beforeUpdate(context, client);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void triggerAfterUpdate(ClientRegistrationContext context, RegistrationAuth authType, ClientModel client) {
|
||||||
|
try {
|
||||||
|
triggerPolicies(context.getSession(), authType, "after update client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
|
||||||
|
|
||||||
|
policy.afterUpdate(context, client);
|
||||||
|
|
||||||
|
});
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
throw new IllegalStateException(crpe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void triggerBeforeView(KeycloakSession session, ClientRegistrationProvider provider, RegistrationAuth authType, ClientModel client) throws ClientRegistrationPolicyException {
|
||||||
|
triggerPolicies(session, authType, "before view client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
|
||||||
|
|
||||||
|
policy.beforeView(provider, client);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void triggerBeforeRemove(KeycloakSession session, ClientRegistrationProvider provider, RegistrationAuth authType, ClientModel client) throws ClientRegistrationPolicyException {
|
||||||
|
triggerPolicies(session, authType, "before delete client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
|
||||||
|
|
||||||
|
policy.beforeDelete(provider, client);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static void triggerPolicies(KeycloakSession session, RegistrationAuth authType, String opDescription, ClientRegOperation op) throws ClientRegistrationPolicyException {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
String policyTypeKey = getComponentTypeKey(authType);
|
||||||
|
List<ComponentModel> policyModels = realm.getComponents(realm.getId(), ClientRegistrationPolicy.class.getName());
|
||||||
|
|
||||||
|
policyModels = policyModels.stream().filter((ComponentModel model) -> {
|
||||||
|
|
||||||
|
return policyTypeKey.equals(model.getSubType());
|
||||||
|
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (ComponentModel policyModel : policyModels) {
|
||||||
|
ClientRegistrationPolicy policy = session.getProvider(ClientRegistrationPolicy.class, policyModel);
|
||||||
|
if (policy == null) {
|
||||||
|
throw new ClientRegistrationPolicyException("Policy of type '" + policyModel.getProviderId() + "' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: trace
|
||||||
|
logger.infof("Running policy '%s' %s", policyModel.getName(), opDescription);
|
||||||
|
|
||||||
|
try {
|
||||||
|
op.run(policy);
|
||||||
|
} catch (ClientRegistrationPolicyException crpe) {
|
||||||
|
crpe.setPolicyModel(policyModel);
|
||||||
|
logger.warnf("Operation '%s' rejected. %s", opDescription, crpe.getMessage());
|
||||||
|
throw crpe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ClientRegOperation {
|
||||||
|
|
||||||
|
void run(ClientRegistrationPolicy policy) throws ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getComponentTypeKey(RegistrationAuth authType) {
|
||||||
|
return authType.toString().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import org.keycloak.provider.Provider;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
import org.keycloak.provider.Spi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationPolicySpi implements Spi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInternal() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "client-registration-policy";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends Provider> getProviderClass() {
|
||||||
|
return ClientRegistrationPolicy.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||||
|
return ClientRegistrationPolicyFactory.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.AddressMapper;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
|
||||||
|
import org.keycloak.protocol.saml.mappers.RoleListMapper;
|
||||||
|
import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
|
||||||
|
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.ClientTemplatesClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.ConsentRequiredClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.ProtocolMappersClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.ScopeClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class DefaultClientRegistrationPolicies {
|
||||||
|
|
||||||
|
private static String[] DEFAULT_ALLOWED_PROTOCOL_MAPPERS = {
|
||||||
|
UserAttributeStatementMapper.PROVIDER_ID,
|
||||||
|
UserAttributeMapper.PROVIDER_ID,
|
||||||
|
UserPropertyAttributeStatementMapper.PROVIDER_ID,
|
||||||
|
UserPropertyMapper.PROVIDER_ID,
|
||||||
|
FullNameMapper.PROVIDER_ID,
|
||||||
|
AddressMapper.PROVIDER_ID,
|
||||||
|
new SHA256PairwiseSubMapper().getId(),
|
||||||
|
RoleListMapper.PROVIDER_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void addDefaultPolicies(RealmModel realm) {
|
||||||
|
String anonPolicyType = ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.ANONYMOUS);
|
||||||
|
String authPolicyType = ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.AUTHENTICATED);
|
||||||
|
List<ComponentModel> policies = realm.getComponents(realm.getId(), ClientRegistrationPolicy.class.getName());
|
||||||
|
|
||||||
|
// Probably an issue if admin removes all policies intentionally...
|
||||||
|
if (policies == null ||policies.isEmpty()) {
|
||||||
|
addAnonymousPolicies(realm, anonPolicyType);
|
||||||
|
addAuthPolicies(realm, authPolicyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ComponentModel createModelInstance(String name, RealmModel realm, String providerId, String policyType) {
|
||||||
|
ComponentModel model = new ComponentModel();
|
||||||
|
model.setName(name);
|
||||||
|
model.setParentId(realm.getId());
|
||||||
|
model.setProviderId(providerId);
|
||||||
|
model.setProviderType(ClientRegistrationPolicy.class.getName());
|
||||||
|
model.setSubType(policyType);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addAnonymousPolicies(RealmModel realm, String policyTypeKey) {
|
||||||
|
ComponentModel trustedHostModel = createModelInstance("Trusted Hosts", realm, TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
|
||||||
|
|
||||||
|
// Not any trusted hosts by default
|
||||||
|
trustedHostModel.getConfig().put(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, Collections.emptyList());
|
||||||
|
trustedHostModel.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "true");
|
||||||
|
trustedHostModel.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.CLIENT_URIS_MUST_MATCH, "true");
|
||||||
|
realm.addComponentModel(trustedHostModel);
|
||||||
|
|
||||||
|
ComponentModel consentRequiredModel = createModelInstance("Consent Required", realm, ConsentRequiredClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
|
||||||
|
realm.addComponentModel(consentRequiredModel);
|
||||||
|
|
||||||
|
ComponentModel scopeModel =createModelInstance("Full Scope Disabled", realm, ScopeClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
|
||||||
|
realm.addComponentModel(scopeModel);
|
||||||
|
|
||||||
|
addGenericPolicies(realm, policyTypeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void addAuthPolicies(RealmModel realm, String policyTypeKey) {
|
||||||
|
addGenericPolicies(realm, policyTypeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addGenericPolicies(RealmModel realm, String policyTypeKey) {
|
||||||
|
ComponentModel protMapperModel = createModelInstance("Allowed Protocol Mapper Types", realm, ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
|
||||||
|
protMapperModel.getConfig().put(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, Arrays.asList(DEFAULT_ALLOWED_PROTOCOL_MAPPERS));
|
||||||
|
protMapperModel.getConfig().putSingle(ProtocolMappersClientRegistrationPolicyFactory.CONSENT_REQUIRED_FOR_ALL_MAPPERS, "true");
|
||||||
|
realm.addComponentModel(protMapperModel);
|
||||||
|
|
||||||
|
ComponentModel clientTemplatesModel = createModelInstance("Allowed Client Templates", realm, ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
|
||||||
|
clientTemplatesModel.getConfig().put(ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES, Collections.emptyList());
|
||||||
|
realm.addComponentModel(clientTemplatesModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy;
|
||||||
|
|
||||||
|
import org.keycloak.services.clientregistration.RegistrationAccessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public enum RegistrationAuth {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case when client is registered without token (either initialAccessToken or BearerToken).
|
||||||
|
*
|
||||||
|
* Note this will be the case also for update + view + remove with registrationToken, which was created during anonymous registration
|
||||||
|
*/
|
||||||
|
ANONYMOUS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case when client is registered with token (either initialAccessToken or BearerToken).
|
||||||
|
*
|
||||||
|
* Note this will be the case also update + view + remove with registrationToken, which was created during authenticated registration or via admin console
|
||||||
|
*/
|
||||||
|
AUTHENTICATED;
|
||||||
|
|
||||||
|
public static RegistrationAuth fromString(String regAuth) {
|
||||||
|
return Enum.valueOf(RegistrationAuth.class, regAuth.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.ClientTemplateModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ClientTemplatesClientRegistrationPolicy implements ClientRegistrationPolicy {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ClientTemplatesClientRegistrationPolicy.class);
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final ComponentModel componentModel;
|
||||||
|
|
||||||
|
public ClientTemplatesClientRegistrationPolicy(KeycloakSession session, ComponentModel componentModel) {
|
||||||
|
this.session = session;
|
||||||
|
this.componentModel = componentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
String clientTemplate = context.getClient().getClientTemplate();
|
||||||
|
if (!isTemplateAllowed(clientTemplate)) {
|
||||||
|
throw new ClientRegistrationPolicyException("Not permitted to use specified clientTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
String newTemplate = context.getClient().getClientTemplate();
|
||||||
|
|
||||||
|
// Check if template was already set before. Then we allow update
|
||||||
|
ClientTemplateModel currentTemplate = clientModel.getClientTemplate();
|
||||||
|
if (currentTemplate == null || !currentTemplate.getName().equals(newTemplate)) {
|
||||||
|
if (!isTemplateAllowed(newTemplate)) {
|
||||||
|
throw new ClientRegistrationPolicyException("Not permitted to use specified clientTemplate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterUpdate(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeView(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeDelete(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTemplateAllowed(String template) {
|
||||||
|
if (template == null) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
List<String> allowedTemplates = componentModel.getConfig().getList(ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES);
|
||||||
|
return allowedTemplates.contains(template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.ClientTemplateModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ClientTemplatesClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
|
||||||
|
|
||||||
|
private List<ProviderConfigProperty> configProperties;
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "allowed-client-templates";
|
||||||
|
|
||||||
|
public static final String ALLOWED_CLIENT_TEMPLATES = "allowed-client-templates";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new ClientTemplatesClientRegistrationPolicy(session, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "When present, it allows to specify whitelist of client templates, which will be allowed in representation of registered (or updated) client";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties(KeycloakSession session) {
|
||||||
|
ProviderConfigProperty property;
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(ALLOWED_CLIENT_TEMPLATES);
|
||||||
|
property.setLabel("allowed-client-templates.label");
|
||||||
|
property.setHelpText("allowed-client-templates.tooltip");
|
||||||
|
property.setType(ProviderConfigProperty.MULTIVALUED_LIST_TYPE);
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
property.setDefaultValue(getClientTemplates(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
configProperties = Collections.singletonList(property);
|
||||||
|
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getClientTemplates(KeycloakSession session) {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
if (realm == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
List<ClientTemplateModel> clientTemplates = realm.getClientTemplates();
|
||||||
|
|
||||||
|
return clientTemplates.stream().map((ClientTemplateModel clientTemplate) -> {
|
||||||
|
|
||||||
|
return clientTemplate.getName();
|
||||||
|
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return getConfigProperties(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ConsentRequiredClientRegistrationPolicy implements ClientRegistrationPolicy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
clientModel.setConsentRequired(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
if (context.getClient().isConsentRequired() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (clientModel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEnabled = clientModel.isConsentRequired();
|
||||||
|
boolean newEnabled = context.getClient().isConsentRequired();
|
||||||
|
|
||||||
|
if (isEnabled && !newEnabled) {
|
||||||
|
throw new ClientRegistrationPolicyException("Not permitted to update consentRequired to false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterUpdate(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeView(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeDelete(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ConsentRequiredClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "consent-required";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new ConsentRequiredClientRegistrationPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "When present, then newly registered client will always have 'consentRequired' switch enabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ProtocolMappersClientRegistrationPolicy implements ClientRegistrationPolicy {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ProtocolMappersClientRegistrationPolicy.class);
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final ComponentModel componentModel;
|
||||||
|
|
||||||
|
public ProtocolMappersClientRegistrationPolicy(KeycloakSession session, ComponentModel componentModel) {
|
||||||
|
this.session = session;
|
||||||
|
this.componentModel = componentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
testMappers(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void testMappers(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
List<ProtocolMapperRepresentation> protocolMappers = context.getClient().getProtocolMappers();
|
||||||
|
if (protocolMappers == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> allowedMapperProviders = getAllowedMapperProviders();
|
||||||
|
|
||||||
|
for (ProtocolMapperRepresentation mapper : protocolMappers) {
|
||||||
|
String mapperType = mapper.getProtocolMapper();
|
||||||
|
|
||||||
|
if (!allowedMapperProviders.contains(mapperType)) {
|
||||||
|
logger.warnf("ProtocolMapper '%s' of type '%s' not allowed", mapper.getName(), mapperType);
|
||||||
|
throw new ClientRegistrationPolicyException("ProtocolMapper type not allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void enableConsentRequiredForAll(ClientModel clientModel) {
|
||||||
|
if (isConsentRequiredForMappers()) {
|
||||||
|
// TODO: Debug
|
||||||
|
logger.infof("Enable consentRequired for all protocol mappers of client %s", clientModel.getClientId());
|
||||||
|
|
||||||
|
Set<ProtocolMapperModel> mappers = clientModel.getProtocolMappers();
|
||||||
|
|
||||||
|
for (ProtocolMapperModel mapper : mappers) {
|
||||||
|
mapper.setConsentRequired(true);
|
||||||
|
|
||||||
|
if (mapper.getConsentText() == null) {
|
||||||
|
mapper.setConsentText(mapper.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
clientModel.updateProtocolMapper(mapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove builtin mappers of unsupported types too
|
||||||
|
@Override
|
||||||
|
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
// Remove mappers of unsupported type, which were added "automatically"
|
||||||
|
List<String> allowedMapperProviders = getAllowedMapperProviders();
|
||||||
|
Set<ProtocolMapperModel> createdMappers = clientModel.getProtocolMappers();
|
||||||
|
|
||||||
|
createdMappers.stream().filter((ProtocolMapperModel mapper) -> {
|
||||||
|
|
||||||
|
return !allowedMapperProviders.contains(mapper.getProtocolMapper());
|
||||||
|
|
||||||
|
}).forEach((ProtocolMapperModel mapperToRemove) -> {
|
||||||
|
|
||||||
|
// TODO: debug
|
||||||
|
logger.infof("Removing builtin mapper '%s' of type '%s' as type is not permitted", mapperToRemove.getName(), mapperToRemove.getProtocolMapper());
|
||||||
|
clientModel.removeProtocolMapper(mapperToRemove);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable consentRequired for all protocolMappers
|
||||||
|
enableConsentRequiredForAll(clientModel);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't take already existing protocolMappers into consideration for now
|
||||||
|
@Override
|
||||||
|
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
testMappers(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterUpdate(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
// Enable consentRequired for all protocolMappers
|
||||||
|
enableConsentRequiredForAll(clientModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeView(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeDelete(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getAllowedMapperProviders() {
|
||||||
|
return componentModel.getConfig().getList(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isConsentRequiredForMappers() {
|
||||||
|
String s = componentModel.getConfig().getFirst(ProtocolMappersClientRegistrationPolicyFactory.CONSENT_REQUIRED_FOR_ALL_MAPPERS);
|
||||||
|
return s==null || Boolean.parseBoolean(s);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.protocol.ProtocolMapper;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ProtocolMappersClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
|
||||||
|
|
||||||
|
private List<ProviderConfigProperty> configProperties = new LinkedList<>();
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "allowed-protocol-mappers";
|
||||||
|
|
||||||
|
public static final String ALLOWED_PROTOCOL_MAPPER_TYPES = "allowed-protocol-mapper-types";
|
||||||
|
|
||||||
|
public static final String CONSENT_REQUIRED_FOR_ALL_MAPPERS = "consent-required-for-all-mappers";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new ProtocolMappersClientRegistrationPolicy(session, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
super.postInit(factory);
|
||||||
|
|
||||||
|
ProviderConfigProperty property;
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(ALLOWED_PROTOCOL_MAPPER_TYPES);
|
||||||
|
property.setLabel("allowed-protocol-mappers.label");
|
||||||
|
property.setHelpText("allowed-protocol-mappers.tooltip");
|
||||||
|
property.setType(ProviderConfigProperty.MULTIVALUED_LIST_TYPE);
|
||||||
|
property.setDefaultValue(getProtocolMapperFactoryIds());
|
||||||
|
configProperties.add(property);
|
||||||
|
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(CONSENT_REQUIRED_FOR_ALL_MAPPERS);
|
||||||
|
property.setLabel("consent-required-for-all-mappers.label");
|
||||||
|
property.setHelpText("consent-required-for-all-mappers.tooltip");
|
||||||
|
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||||
|
property.setDefaultValue("true");
|
||||||
|
configProperties.add(property);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getProtocolMapperFactoryIds() {
|
||||||
|
List<ProviderFactory> protocolMapperFactories = sessionFactory.getProviderFactories(ProtocolMapper.class);
|
||||||
|
return protocolMapperFactories.stream().map((ProviderFactory factory) -> {
|
||||||
|
|
||||||
|
return factory.getId();
|
||||||
|
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "When present, it allows to specify whitelist of protocol mapper types, which will be allowed in representation of registered (or updated) client";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ScopeClientRegistrationPolicy implements ClientRegistrationPolicy {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ScopeClientRegistrationPolicy.class);
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final ComponentModel componentModel;
|
||||||
|
|
||||||
|
public ScopeClientRegistrationPolicy(KeycloakSession session, ComponentModel componentModel) {
|
||||||
|
this.session = session;
|
||||||
|
this.componentModel = componentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
clientModel.setFullScopeAllowed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
if (context.getClient().isFullScopeAllowed() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (clientModel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isAllowed = clientModel.isFullScopeAllowed();
|
||||||
|
boolean newAllowed = context.getClient().isFullScopeAllowed();
|
||||||
|
|
||||||
|
if (!isAllowed && newAllowed) {
|
||||||
|
throw new ClientRegistrationPolicyException("Not permitted to enable fullScopeAllowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterUpdate(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeView(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeDelete(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ScopeClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "scope";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new ScopeClientRegistrationPolicy(session, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "When present, then newly registered client won't have full scope allowed";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,276 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationContext;
|
||||||
|
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPolicy {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(TrustedHostClientRegistrationPolicy.class);
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final ComponentModel componentModel;
|
||||||
|
|
||||||
|
public TrustedHostClientRegistrationPolicy(KeycloakSession session, ComponentModel componentModel) {
|
||||||
|
this.session = session;
|
||||||
|
this.componentModel = componentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
verifyHost();
|
||||||
|
verifyClientUrls(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
verifyHost();
|
||||||
|
verifyClientUrls(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterUpdate(ClientRegistrationContext context, ClientModel clientModel) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeView(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
verifyHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeDelete(ClientRegistrationProvider provider, ClientModel clientModel) throws ClientRegistrationPolicyException {
|
||||||
|
verifyHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPL
|
||||||
|
|
||||||
|
protected void verifyHost() throws ClientRegistrationPolicyException {
|
||||||
|
boolean hostMustMatch = isHostMustMatch();
|
||||||
|
if (!hostMustMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String hostAddress = session.getContext().getConnection().getRemoteAddr();
|
||||||
|
|
||||||
|
// TODO: debug
|
||||||
|
logger.infof("Verifying remote host : %s", hostAddress);
|
||||||
|
|
||||||
|
List<String> trustedHosts = getTrustedHosts();
|
||||||
|
List<String> trustedDomains = getTrustedDomains();
|
||||||
|
|
||||||
|
// Verify trustedHosts by their IP addresses
|
||||||
|
String verifiedHost = verifyHostInTrustedHosts(hostAddress, trustedHosts);
|
||||||
|
if (verifiedHost != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify domains if hostAddress hostname belongs to the domain. This assumes proper DNS setup
|
||||||
|
verifiedHost = verifyHostInTrustedDomains(hostAddress, trustedDomains);
|
||||||
|
if (verifiedHost != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warnf("Failed to verify remote host : %s", hostAddress);
|
||||||
|
throw new ClientRegistrationPolicyException("Host not trusted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected List<String> getTrustedHosts() {
|
||||||
|
List<String> trustedHostsConfig = componentModel.getConfig().getList(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS);
|
||||||
|
return trustedHostsConfig.stream().filter((String hostname) -> {
|
||||||
|
|
||||||
|
return !hostname.startsWith("*.");
|
||||||
|
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected List<String> getTrustedDomains() {
|
||||||
|
List<String> trustedHostsConfig = componentModel.getConfig().getList(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS);
|
||||||
|
List<String> domains = new LinkedList<>();
|
||||||
|
|
||||||
|
for (String hostname : trustedHostsConfig) {
|
||||||
|
if (hostname.startsWith("*.")) {
|
||||||
|
hostname = hostname.substring(2);
|
||||||
|
domains.add(hostname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected String verifyHostInTrustedHosts(String hostAddress, List<String> trustedHosts) {
|
||||||
|
for (String confHostName : trustedHosts) {
|
||||||
|
try {
|
||||||
|
String hostIPAddress = InetAddress.getByName(confHostName).getHostAddress();
|
||||||
|
|
||||||
|
logger.tracef("Trying host '%s' of address '%s'", confHostName, hostIPAddress);
|
||||||
|
if (hostIPAddress.equals(hostAddress)) {
|
||||||
|
logger.debugf("Successfully verified host : %s", confHostName);
|
||||||
|
return confHostName;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException uhe) {
|
||||||
|
logger.warnf("Unknown host from realm configuration: %s", confHostName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected String verifyHostInTrustedDomains(String hostAddress, List<String> trustedDomains) {
|
||||||
|
if (!trustedDomains.isEmpty()) {
|
||||||
|
try {
|
||||||
|
String hostname = InetAddress.getByName(hostAddress).getHostName();
|
||||||
|
|
||||||
|
// TODO: Debug
|
||||||
|
logger.infof("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname);
|
||||||
|
|
||||||
|
for (String confDomain : trustedDomains) {
|
||||||
|
if (hostname.endsWith(confDomain)) {
|
||||||
|
logger.debugf("Successfully verified host '%s' by trusted domain '%s'", hostname, confDomain);
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException uhe) {
|
||||||
|
logger.warnf("Request of address '%s' came from unknown host. Skip verification by domains", hostAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void verifyClientUrls(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
|
||||||
|
boolean redirectUriMustMatch = isClientUrisMustMatch();
|
||||||
|
if (!redirectUriMustMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> trustedHosts = getTrustedHosts();
|
||||||
|
List<String> trustedDomains = getTrustedDomains();
|
||||||
|
|
||||||
|
ClientRepresentation client = context.getClient();
|
||||||
|
String rootUrl = client.getRootUrl();
|
||||||
|
String baseUrl = client.getBaseUrl();
|
||||||
|
String adminUrl = client.getAdminUrl();
|
||||||
|
List<String> redirectUris = client.getRedirectUris();
|
||||||
|
|
||||||
|
baseUrl = relativeToAbsoluteURI(rootUrl, baseUrl);
|
||||||
|
adminUrl = relativeToAbsoluteURI(rootUrl, adminUrl);
|
||||||
|
Set<String> resolvedRedirects = PairwiseSubMapperUtils.resolveValidRedirectUris(rootUrl, redirectUris);
|
||||||
|
|
||||||
|
if (rootUrl != null) {
|
||||||
|
checkURLTrusted(rootUrl, trustedHosts, trustedDomains);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl != null) {
|
||||||
|
checkURLTrusted(baseUrl, trustedHosts, trustedDomains);
|
||||||
|
}
|
||||||
|
if (adminUrl != null) {
|
||||||
|
checkURLTrusted(adminUrl, trustedHosts, trustedDomains);
|
||||||
|
}
|
||||||
|
for (String redirect : resolvedRedirects) {
|
||||||
|
checkURLTrusted(redirect, trustedHosts, trustedDomains);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void checkURLTrusted(String url, List<String> trustedHosts, List<String> trustedDomains) throws ClientRegistrationPolicyException {
|
||||||
|
try {
|
||||||
|
String host = new URL(url).getHost();
|
||||||
|
|
||||||
|
for (String trustedHost : trustedHosts) {
|
||||||
|
if (host.equals(trustedHost)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String trustedDomain : trustedDomains) {
|
||||||
|
if (host.endsWith(trustedDomain)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (MalformedURLException mfe) {
|
||||||
|
logger.warnf("URL '%s' is malformed", url);
|
||||||
|
throw new ClientRegistrationPolicyException("URL is malformed");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warnf("URL '%s' doesn't match any trustedHost or trustedDomain", url);
|
||||||
|
throw new ClientRegistrationPolicyException("URL doesn't match any trusted host or trusted domain");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static String relativeToAbsoluteURI(String rootUrl, String relative) {
|
||||||
|
if (relative == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relative.startsWith("/")) {
|
||||||
|
return relative;
|
||||||
|
} else if (rootUrl == null || rootUrl.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootUrl + relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isHostMustMatch() {
|
||||||
|
return parseBoolean(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isClientUrisMustMatch() {
|
||||||
|
return parseBoolean(TrustedHostClientRegistrationPolicyFactory.CLIENT_URIS_MUST_MATCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// True by default
|
||||||
|
private boolean parseBoolean(String propertyKey) {
|
||||||
|
String val = componentModel.getConfig().getFirst(propertyKey);
|
||||||
|
return val==null || Boolean.parseBoolean(val);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policy.impl;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.component.ComponentValidationException;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ConfigurationValidationHelper;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class TrustedHostClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "trusted-hosts";
|
||||||
|
|
||||||
|
public static final String TRUSTED_HOSTS = "trusted-hosts";
|
||||||
|
|
||||||
|
public static final String HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH = "host-sending-registration-request-must-match";
|
||||||
|
public static final String CLIENT_URIS_MUST_MATCH = "client-uris-must-match";
|
||||||
|
|
||||||
|
private static final ProviderConfigProperty TRUSTED_HOSTS_PROPERTY = new ProviderConfigProperty(TRUSTED_HOSTS, "trusted-hosts.label", "trusted-hosts.tooltip", ProviderConfigProperty.MULTIVALUED_STRING_TYPE, null);
|
||||||
|
|
||||||
|
private static final ProviderConfigProperty HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH_PROPERTY = new ProviderConfigProperty(HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "host-sending-registration-request-must-match.label",
|
||||||
|
"host-sending-registration-request-must-match.tooltip", ProviderConfigProperty.BOOLEAN_TYPE, "true");
|
||||||
|
|
||||||
|
private static final ProviderConfigProperty CLIENT_URIS_MUST_MATCH_PROPERTY = new ProviderConfigProperty(CLIENT_URIS_MUST_MATCH, "client-uris-must-match.label",
|
||||||
|
"client-uris-must-match.tooltip", ProviderConfigProperty.BOOLEAN_TYPE, "true");
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
|
||||||
|
return new TrustedHostClientRegistrationPolicy(session, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Allows to specify from which hosts is user able to register and which redirect URIs can client use in it's configuration";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Arrays.asList(TRUSTED_HOSTS_PROPERTY, HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH_PROPERTY, CLIENT_URIS_MUST_MATCH_PROPERTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
|
||||||
|
ConfigurationValidationHelper.check(config)
|
||||||
|
.checkBoolean(HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH_PROPERTY, true)
|
||||||
|
.checkBoolean(CLIENT_URIS_MUST_MATCH_PROPERTY, true);
|
||||||
|
|
||||||
|
TrustedHostClientRegistrationPolicy policy = new TrustedHostClientRegistrationPolicy(session, config);
|
||||||
|
if (!policy.isHostMustMatch() && !policy.isClientUrisMustMatch()) {
|
||||||
|
throw new ComponentValidationException("At least one of hosts verification or client URIs validation must be enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
import org.keycloak.storage.UserStorageProviderModel;
|
import org.keycloak.storage.UserStorageProviderModel;
|
||||||
|
import org.keycloak.services.clientregistration.policy.DefaultClientRegistrationPolicies;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -120,6 +121,7 @@ public class RealmManager implements RealmImporter {
|
||||||
setupRequiredActions(realm);
|
setupRequiredActions(realm);
|
||||||
setupOfflineTokens(realm);
|
setupOfflineTokens(realm);
|
||||||
setupAuthorizationServices(realm);
|
setupAuthorizationServices(realm);
|
||||||
|
setupClientRegistrations(realm);
|
||||||
|
|
||||||
fireRealmPostCreate(realm);
|
fireRealmPostCreate(realm);
|
||||||
|
|
||||||
|
@ -500,6 +502,8 @@ public class RealmManager implements RealmImporter {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAuthorizationServices(realm);
|
setupAuthorizationServices(realm);
|
||||||
|
setupClientRegistrations(realm);
|
||||||
|
|
||||||
fireRealmPostCreate(realm);
|
fireRealmPostCreate(realm);
|
||||||
|
|
||||||
return realm;
|
return realm;
|
||||||
|
@ -598,6 +602,10 @@ public class RealmManager implements RealmImporter {
|
||||||
KeycloakModelUtils.setupAuthorizationServices(realm);
|
KeycloakModelUtils.setupAuthorizationServices(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupClientRegistrations(RealmModel realm) {
|
||||||
|
DefaultClientRegistrationPolicies.addDefaultPolicies(realm);
|
||||||
|
}
|
||||||
|
|
||||||
private void fireRealmPostCreate(RealmModel realm) {
|
private void fireRealmPostCreate(RealmModel realm) {
|
||||||
session.getKeycloakSessionFactory().publish(new RealmModel.RealmPostCreateEvent() {
|
session.getKeycloakSessionFactory().publish(new RealmModel.RealmPostCreateEvent() {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -85,6 +85,8 @@ public class KeycloakApplication extends Application {
|
||||||
// two places to avoid dependency between Keycloak Subsystem and Keycloak Services module.
|
// two places to avoid dependency between Keycloak Subsystem and Keycloak Services module.
|
||||||
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
|
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
|
||||||
|
|
||||||
|
public static final String KEYCLOAK_EMBEDDED = "keycloak.embedded";
|
||||||
|
|
||||||
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
|
||||||
|
|
||||||
protected boolean embedded = false;
|
protected boolean embedded = false;
|
||||||
|
@ -97,7 +99,7 @@ public class KeycloakApplication extends Application {
|
||||||
|
|
||||||
public KeycloakApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
|
public KeycloakApplication(@Context ServletContext context, @Context Dispatcher dispatcher) {
|
||||||
try {
|
try {
|
||||||
if ("true".equals(context.getInitParameter("keycloak.embedded"))) {
|
if ("true".equals(context.getInitParameter(KEYCLOAK_EMBEDDED))) {
|
||||||
embedded = true;
|
embedded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -930,7 +930,7 @@ public class AuthenticationManagementResource {
|
||||||
rep.setProviderId(providerId);
|
rep.setProviderId(providerId);
|
||||||
rep.setName(factory.getDisplayType());
|
rep.setName(factory.getDisplayType());
|
||||||
rep.setHelpText(factory.getHelpText());
|
rep.setHelpText(factory.getHelpText());
|
||||||
rep.setProperties(new LinkedList<ConfigPropertyRepresentation>());
|
rep.setProperties(new LinkedList<>());
|
||||||
List<ProviderConfigProperty> configProperties = factory.getConfigProperties();
|
List<ProviderConfigProperty> configProperties = factory.getConfigProperties();
|
||||||
for (ProviderConfigProperty prop : configProperties) {
|
for (ProviderConfigProperty prop : configProperties) {
|
||||||
ConfigPropertyRepresentation propRep = getConfigPropertyRep(prop);
|
ConfigPropertyRepresentation propRep = getConfigPropertyRep(prop);
|
||||||
|
@ -940,14 +940,7 @@ public class AuthenticationManagementResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConfigPropertyRepresentation getConfigPropertyRep(ProviderConfigProperty prop) {
|
private ConfigPropertyRepresentation getConfigPropertyRep(ProviderConfigProperty prop) {
|
||||||
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
|
return ModelToRepresentation.toRepresentation(prop);
|
||||||
propRep.setName(prop.getName());
|
|
||||||
propRep.setLabel(prop.getLabel());
|
|
||||||
propRep.setType(prop.getType());
|
|
||||||
propRep.setDefaultValue(prop.getDefaultValue());
|
|
||||||
propRep.setHelpText(prop.getHelpText());
|
|
||||||
propRep.setSecret(prop.isSecret());
|
|
||||||
return propRep;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
|
import org.keycloak.events.admin.ResourceType;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
import org.keycloak.representations.idm.ComponentTypeRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ClientRegistrationPolicyResource {
|
||||||
|
|
||||||
|
private final RealmAuth auth;
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
protected KeycloakSession session;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
protected UriInfo uriInfo;
|
||||||
|
|
||||||
|
public ClientRegistrationPolicyResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
|
||||||
|
this.auth = auth;
|
||||||
|
this.realm = realm;
|
||||||
|
this.adminEvent = adminEvent.resource(ResourceType.CLIENT_INITIAL_ACCESS_MODEL);
|
||||||
|
|
||||||
|
auth.init(RealmAuth.Resource.CLIENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base path for retrieve providers with the configProperties properly filled
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Path("providers")
|
||||||
|
@GET
|
||||||
|
@NoCache
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public List<ComponentTypeRepresentation> getProviders() {
|
||||||
|
List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(ClientRegistrationPolicy.class);
|
||||||
|
|
||||||
|
return providerFactories.stream().map((ProviderFactory factory) -> {
|
||||||
|
|
||||||
|
ClientRegistrationPolicyFactory clientRegFactory = (ClientRegistrationPolicyFactory) factory;
|
||||||
|
List<ProviderConfigProperty> configProps = clientRegFactory.getConfigProperties(session);
|
||||||
|
|
||||||
|
ComponentTypeRepresentation rep = new ComponentTypeRepresentation();
|
||||||
|
rep.setId(clientRegFactory.getId());
|
||||||
|
rep.setHelpText(clientRegFactory.getHelpText());
|
||||||
|
rep.setProperties(ModelToRepresentation.toRepresentation(configProps));
|
||||||
|
return rep;
|
||||||
|
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -45,6 +45,8 @@ import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
|
import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils;
|
||||||
|
import org.keycloak.services.clientregistration.RegistrationAccessToken;
|
||||||
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
import org.keycloak.services.managers.ClientManager;
|
import org.keycloak.services.managers.ClientManager;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.managers.ResourceAdminManager;
|
import org.keycloak.services.managers.ResourceAdminManager;
|
||||||
|
@ -274,7 +276,7 @@ public class ClientResource {
|
||||||
throw new NotFoundException("Could not find client");
|
throw new NotFoundException("Could not find client");
|
||||||
}
|
}
|
||||||
|
|
||||||
String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, realm, uriInfo, client);
|
String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, realm, uriInfo, client, RegistrationAuth.AUTHENTICATED);
|
||||||
|
|
||||||
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
|
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
|
||||||
rep.setRegistrationAccessToken(token);
|
rep.setRegistrationAccessToken(token);
|
||||||
|
|
|
@ -83,13 +83,15 @@ public class ComponentResource {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public List<ComponentRepresentation> getComponents(@QueryParam("parent") String parent, @QueryParam("type") String type) {
|
public List<ComponentRepresentation> getComponents(@QueryParam("parent") String parent, @QueryParam("type") String type) {
|
||||||
auth.requireManage();
|
auth.requireView();
|
||||||
List<ComponentModel> components = Collections.EMPTY_LIST;
|
List<ComponentModel> components = Collections.EMPTY_LIST;
|
||||||
if (parent == null) {
|
if (parent == null && type == null) {
|
||||||
components = realm.getComponents();
|
components = realm.getComponents();
|
||||||
|
|
||||||
} else if (type == null) {
|
} else if (type == null) {
|
||||||
components = realm.getComponents(parent);
|
components = realm.getComponents(parent);
|
||||||
|
} else if (parent == null) {
|
||||||
|
components = realm.getComponents(realm.getId(), type);
|
||||||
} else {
|
} else {
|
||||||
components = realm.getComponents(parent, type);
|
components = realm.getComponents(parent, type);
|
||||||
}
|
}
|
||||||
|
@ -108,9 +110,10 @@ public class ComponentResource {
|
||||||
try {
|
try {
|
||||||
ComponentModel model = RepresentationToModel.toModel(session, rep);
|
ComponentModel model = RepresentationToModel.toModel(session, rep);
|
||||||
if (model.getParentId() == null) model.setParentId(realm.getId());
|
if (model.getParentId() == null) model.setParentId(realm.getId());
|
||||||
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
|
|
||||||
|
|
||||||
model = realm.addComponentModel(model);
|
model = realm.addComponentModel(model);
|
||||||
|
|
||||||
|
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
|
||||||
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
|
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
|
||||||
} catch (ComponentValidationException e) {
|
} catch (ComponentValidationException e) {
|
||||||
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
|
@ -132,8 +135,9 @@ public class ComponentResource {
|
||||||
@PUT
|
@PUT
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public void updateComponent(@PathParam("id") String id, ComponentRepresentation rep) {
|
public Response updateComponent(@PathParam("id") String id, ComponentRepresentation rep) {
|
||||||
auth.requireManage();
|
auth.requireManage();
|
||||||
|
try {
|
||||||
ComponentModel model = realm.getComponent(id);
|
ComponentModel model = realm.getComponent(id);
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
throw new NotFoundException("Could not find component");
|
throw new NotFoundException("Could not find component");
|
||||||
|
@ -141,6 +145,10 @@ public class ComponentResource {
|
||||||
RepresentationToModel.updateComponent(session, rep, model, false);
|
RepresentationToModel.updateComponent(session, rep, model, false);
|
||||||
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
|
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
|
||||||
realm.updateComponent(model);
|
realm.updateComponent(model);
|
||||||
|
return Response.noContent().build();
|
||||||
|
} catch (ComponentValidationException e) {
|
||||||
|
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@DELETE
|
@DELETE
|
||||||
|
|
|
@ -261,13 +261,7 @@ public class IdentityProviderResource {
|
||||||
rep.setHelpText(mapper.getHelpText());
|
rep.setHelpText(mapper.getHelpText());
|
||||||
List<ProviderConfigProperty> configProperties = mapper.getConfigProperties();
|
List<ProviderConfigProperty> configProperties = mapper.getConfigProperties();
|
||||||
for (ProviderConfigProperty prop : configProperties) {
|
for (ProviderConfigProperty prop : configProperties) {
|
||||||
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
|
ConfigPropertyRepresentation propRep = ModelToRepresentation.toRepresentation(prop);
|
||||||
propRep.setName(prop.getName());
|
|
||||||
propRep.setLabel(prop.getLabel());
|
|
||||||
propRep.setType(prop.getType());
|
|
||||||
propRep.setDefaultValue(prop.getDefaultValue());
|
|
||||||
propRep.setHelpText(prop.getHelpText());
|
|
||||||
propRep.setSecret(prop.isSecret());
|
|
||||||
rep.getProperties().add(propRep);
|
rep.getProperties().add(propRep);
|
||||||
}
|
}
|
||||||
types.put(rep.getId(), rep);
|
types.put(rep.getId(), rep);
|
||||||
|
|
|
@ -54,19 +54,24 @@ import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.representations.adapters.action.GlobalRequestResult;
|
import org.keycloak.representations.adapters.action.GlobalRequestResult;
|
||||||
import org.keycloak.representations.idm.AdminEventRepresentation;
|
import org.keycloak.representations.idm.AdminEventRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ComponentTypeRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.PartialImportRepresentation;
|
import org.keycloak.representations.idm.PartialImportRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
|
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.info.SpiInfoRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.LDAPConnectionTestManager;
|
import org.keycloak.services.managers.LDAPConnectionTestManager;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.managers.ResourceAdminManager;
|
import org.keycloak.services.managers.ResourceAdminManager;
|
||||||
import org.keycloak.services.managers.UsersSyncManager;
|
import org.keycloak.services.managers.UsersSyncManager;
|
||||||
import org.keycloak.services.resources.admin.RealmAuth.Resource;
|
import org.keycloak.services.resources.admin.RealmAuth.Resource;
|
||||||
|
import org.keycloak.services.resources.admin.info.ServerInfoAdminResource;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
@ -202,15 +207,9 @@ public class RealmAdminResource {
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("client-registration-policy")
|
||||||
/**
|
public ClientRegistrationPolicyResource getClientRegistrationPolicy() {
|
||||||
* Base path for managing client initial access tokens
|
ClientRegistrationPolicyResource resource = new ClientRegistrationPolicyResource(realm, auth, adminEvent);
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Path("clients-trusted-hosts")
|
|
||||||
public ClientRegistrationTrustedHostResource getClientRegistrationTrustedHost() {
|
|
||||||
ClientRegistrationTrustedHostResource resource = new ClientRegistrationTrustedHostResource(realm, auth, adminEvent);
|
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,13 +230,7 @@ public class UserFederationProviderResource {
|
||||||
rep.setSyncConfig(mapperFactory.getSyncConfig());
|
rep.setSyncConfig(mapperFactory.getSyncConfig());
|
||||||
List<ProviderConfigProperty> configProperties = mapperFactory.getConfigProperties();
|
List<ProviderConfigProperty> configProperties = mapperFactory.getConfigProperties();
|
||||||
for (ProviderConfigProperty prop : configProperties) {
|
for (ProviderConfigProperty prop : configProperties) {
|
||||||
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
|
ConfigPropertyRepresentation propRep = ModelToRepresentation.toRepresentation(prop);
|
||||||
propRep.setName(prop.getName());
|
|
||||||
propRep.setLabel(prop.getLabel());
|
|
||||||
propRep.setType(prop.getType());
|
|
||||||
propRep.setDefaultValue(prop.getDefaultValue());
|
|
||||||
propRep.setHelpText(prop.getHelpText());
|
|
||||||
propRep.setSecret(prop.isSecret());
|
|
||||||
rep.getProperties().add(propRep);
|
rep.getProperties().add(propRep);
|
||||||
}
|
}
|
||||||
rep.setDefaultConfig(mapperFactory.getDefaultConfig(this.federationProviderModel));
|
rep.setDefaultConfig(mapperFactory.getDefaultConfig(this.federationProviderModel));
|
||||||
|
|
|
@ -249,15 +249,7 @@ public class UserFederationProvidersResource {
|
||||||
|
|
||||||
|
|
||||||
private ConfigPropertyRepresentation toConfigPropertyRepresentation(ProviderConfigProperty prop) {
|
private ConfigPropertyRepresentation toConfigPropertyRepresentation(ProviderConfigProperty prop) {
|
||||||
|
return ModelToRepresentation.toRepresentation(prop);
|
||||||
ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
|
|
||||||
propRep.setName(prop.getName());
|
|
||||||
propRep.setLabel(prop.getLabel());
|
|
||||||
propRep.setType(prop.getType());
|
|
||||||
propRep.setDefaultValue(prop.getDefaultValue());
|
|
||||||
propRep.setHelpText(prop.getHelpText());
|
|
||||||
propRep.setSecret(prop.isSecret());
|
|
||||||
return propRep;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ConfigPropertyRepresentation> toConfigPropertyRepresentationList(List<ProviderConfigProperty> props) {
|
private List<ConfigPropertyRepresentation> toConfigPropertyRepresentationList(List<ProviderConfigProperty> props) {
|
||||||
|
|
|
@ -18,4 +18,5 @@
|
||||||
org.keycloak.exportimport.ClientDescriptionConverterSpi
|
org.keycloak.exportimport.ClientDescriptionConverterSpi
|
||||||
org.keycloak.wellknown.WellKnownSpi
|
org.keycloak.wellknown.WellKnownSpi
|
||||||
org.keycloak.services.clientregistration.ClientRegistrationSpi
|
org.keycloak.services.clientregistration.ClientRegistrationSpi
|
||||||
|
org.keycloak.services.clientregistration.policy.ClientRegistrationPolicySpi
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
#
|
||||||
|
# Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
# and other contributors as indicated by the @author tags.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory
|
||||||
|
org.keycloak.services.clientregistration.policy.impl.ConsentRequiredClientRegistrationPolicyFactory
|
||||||
|
org.keycloak.services.clientregistration.policy.impl.ProtocolMappersClientRegistrationPolicyFactory
|
||||||
|
org.keycloak.services.clientregistration.policy.impl.ClientTemplatesClientRegistrationPolicyFactory
|
||||||
|
org.keycloak.services.clientregistration.policy.impl.ScopeClientRegistrationPolicyFactory
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientregistration.policies.impl;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class HostsTest {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
//Security.setProperty("networkaddress.cache.ttl", "5");
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (int i=0 ; i<100 ; i++) {
|
||||||
|
//Foo f = test1();
|
||||||
|
//Foo f = test2();
|
||||||
|
//Foo f = test3();
|
||||||
|
Foo f = test4();
|
||||||
|
//Foo f = test5();
|
||||||
|
//Foo f = test6();
|
||||||
|
|
||||||
|
long end = System.currentTimeMillis();
|
||||||
|
System.out.println("IPAddr=" + f.ipAddr + ", Hostname=" + f.hostname + ", Took: " + (end-start) + " ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 ms
|
||||||
|
private static Foo test1() throws Exception {
|
||||||
|
Foo f = new Foo();
|
||||||
|
InetAddress addr = InetAddress.getByName("10.40.2.225");
|
||||||
|
f.ipAddr = addr.getHostAddress();
|
||||||
|
f.hostname = "";
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 231 ms - increasing linearly
|
||||||
|
private static Foo test2() throws Exception {
|
||||||
|
Foo f = new Foo();
|
||||||
|
InetAddress addr = InetAddress.getByName("10.40.2.225");
|
||||||
|
f.ipAddr = addr.getHostAddress();
|
||||||
|
f.hostname = addr.getHostName();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 240 ms - increasing linearly
|
||||||
|
private static Foo test3() throws Exception {
|
||||||
|
Foo f = new Foo();
|
||||||
|
InetAddress addr = InetAddress.getByName("10.40.2.225");
|
||||||
|
f.ipAddr = addr.getHostAddress();
|
||||||
|
for (int i=0 ; i<10 ; i++) {
|
||||||
|
f.hostname = addr.getHostName();
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 27 ms (Everything at 1st call)
|
||||||
|
private static Foo test4() throws Exception {
|
||||||
|
Foo f = new Foo();
|
||||||
|
InetAddress addr = InetAddress.getByName("www.seznam.cz");
|
||||||
|
f.ipAddr = addr.getHostAddress();
|
||||||
|
f.hostname = addr.getHostName();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 257 ms - increasing
|
||||||
|
private static Foo test5() throws Exception {
|
||||||
|
Foo f = new Foo();
|
||||||
|
InetAddress addr = InetAddress.getByName("77.75.77.53");
|
||||||
|
f.ipAddr = addr.getHostAddress();
|
||||||
|
f.hostname = addr.getHostName();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test DNS caching
|
||||||
|
private static Foo test6() throws Exception {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
return test4();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class Foo {
|
||||||
|
|
||||||
|
private String hostname;
|
||||||
|
private String ipAddr;
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
|
||||||
di.setClassLoader(getClass().getClassLoader());
|
di.setClassLoader(getClass().getClassLoader());
|
||||||
di.setContextPath("/auth");
|
di.setContextPath("/auth");
|
||||||
di.setDeploymentName("Keycloak");
|
di.setDeploymentName("Keycloak");
|
||||||
|
di.addInitParameter(KeycloakApplication.KEYCLOAK_EMBEDDED, "true");
|
||||||
|
|
||||||
di.setDefaultServletConfig(new DefaultServletConfig(true));
|
di.setDefaultServletConfig(new DefaultServletConfig(true));
|
||||||
di.addWelcomePage("theme/keycloak/welcome/resources/index.html");
|
di.addWelcomePage("theme/keycloak/welcome/resources/index.html");
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.keycloak.testsuite.admin;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.keycloak.admin.client.resource.ClientRegistrationTrustedHostResource;
|
|
||||||
import org.keycloak.events.admin.OperationType;
|
|
||||||
import org.keycloak.events.admin.ResourceType;
|
|
||||||
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
|
||||||
import org.keycloak.testsuite.Assert;
|
|
||||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
public class ClientRegTrustedHostTest extends AbstractAdminTest {
|
|
||||||
|
|
||||||
|
|
||||||
private ClientRegistrationTrustedHostResource resource;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void before() {
|
|
||||||
resource = realm.clientRegistrationTrustedHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInitialAccessTokens() {
|
|
||||||
|
|
||||||
// Successfully create "localhost1" rep
|
|
||||||
ClientRegistrationTrustedHostRepresentation rep = new ClientRegistrationTrustedHostRepresentation();
|
|
||||||
rep.setHostName("localhost1");
|
|
||||||
rep.setCount(5);
|
|
||||||
|
|
||||||
Response res = resource.create(rep);
|
|
||||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost1"), rep, ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
|
||||||
res.close();
|
|
||||||
|
|
||||||
// Failed to create conflicting rep "localhost1" again
|
|
||||||
res = resource.create(rep);
|
|
||||||
Assert.assertEquals(409, res.getStatus());
|
|
||||||
assertAdminEvents.assertEmpty();
|
|
||||||
res.close();
|
|
||||||
|
|
||||||
// Successfully create "localhost2" rep
|
|
||||||
rep = new ClientRegistrationTrustedHostRepresentation();
|
|
||||||
rep.setHostName("localhost2");
|
|
||||||
rep.setCount(10);
|
|
||||||
|
|
||||||
res = resource.create(rep);
|
|
||||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost2"), rep, ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
|
||||||
res.close();
|
|
||||||
|
|
||||||
// Get "localhost1"
|
|
||||||
rep = resource.get("localhost1");
|
|
||||||
assertRep(rep, "localhost1", 5, 5);
|
|
||||||
|
|
||||||
// Update "localhost1"
|
|
||||||
rep.setCount(7);
|
|
||||||
rep.setRemainingCount(7);
|
|
||||||
resource.update("localhost1", rep);
|
|
||||||
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost1"), rep, ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
|
||||||
|
|
||||||
// Get all
|
|
||||||
List<ClientRegistrationTrustedHostRepresentation> alls = resource.list();
|
|
||||||
Assert.assertEquals(2, alls.size());
|
|
||||||
assertRep(findByHost(alls, "localhost1"), "localhost1", 7, 7);
|
|
||||||
assertRep(findByHost(alls, "localhost2"), "localhost2", 10, 10);
|
|
||||||
|
|
||||||
// Delete "localhost1"
|
|
||||||
resource.delete("localhost1");
|
|
||||||
assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost1"), ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
|
||||||
|
|
||||||
// Get all and check just "localhost2" available
|
|
||||||
alls = resource.list();
|
|
||||||
Assert.assertEquals(1, alls.size());
|
|
||||||
assertRep(alls.get(0), "localhost2", 10, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClientRegistrationTrustedHostRepresentation findByHost(List<ClientRegistrationTrustedHostRepresentation> list, String hostName) {
|
|
||||||
for (ClientRegistrationTrustedHostRepresentation rep : list) {
|
|
||||||
if (hostName.equals(rep.getHostName())) {
|
|
||||||
return rep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertRep(ClientRegistrationTrustedHostRepresentation rep, String expectedHost, int expectedCount, int expectedRemaining) {
|
|
||||||
Assert.assertEquals(expectedHost, rep.getHostName());
|
|
||||||
Assert.assertEquals(expectedCount, rep.getCount().intValue());
|
|
||||||
Assert.assertEquals(expectedRemaining, rep.getRemainingCount().intValue());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -102,6 +102,8 @@ public class ComponentsTest extends AbstractAdminTest {
|
||||||
|
|
||||||
String id = createComponent(rep);
|
String id = createComponent(rep);
|
||||||
ComponentRepresentation returned = components.component(id).toRepresentation();
|
ComponentRepresentation returned = components.component(id).toRepresentation();
|
||||||
|
assertEquals( "foo", returned.getSubType());
|
||||||
|
|
||||||
assertEquals(1, returned.getConfig().size());
|
assertEquals(1, returned.getConfig().size());
|
||||||
assertTrue(returned.getConfig().containsKey("required"));
|
assertTrue(returned.getConfig().containsKey("required"));
|
||||||
}
|
}
|
||||||
|
@ -211,6 +213,7 @@ public class ComponentsTest extends AbstractAdminTest {
|
||||||
rep.setParentId(realmId);
|
rep.setParentId(realmId);
|
||||||
rep.setProviderId("test");
|
rep.setProviderId("test");
|
||||||
rep.setProviderType(TestProvider.class.getName());
|
rep.setProviderType(TestProvider.class.getName());
|
||||||
|
rep.setSubType("foo");
|
||||||
|
|
||||||
MultivaluedHashMap config = new MultivaluedHashMap();
|
MultivaluedHashMap config = new MultivaluedHashMap();
|
||||||
rep.setConfig(config);
|
rep.setConfig(config);
|
||||||
|
|
|
@ -0,0 +1,562 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.client;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.client.registration.Auth;
|
||||||
|
import org.keycloak.client.registration.ClientRegistrationException;
|
||||||
|
import org.keycloak.client.registration.HttpErrorException;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.HardcodedRole;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
|
||||||
|
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
|
||||||
|
import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
|
||||||
|
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
|
||||||
|
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientTemplateRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ComponentTypeRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.services.clientregistration.RegistrationAccessToken;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
|
||||||
|
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyManager;
|
||||||
|
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.ClientTemplatesClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.ProtocolMappersClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class OIDCClientRegistrationPoliciesTest extends AbstractClientRegistrationTest {
|
||||||
|
|
||||||
|
private static final String PRIVATE_KEY = "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=";
|
||||||
|
private static final String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
super.addTestRealms(testRealms);
|
||||||
|
testRealms.get(0).setId(REALM_NAME);
|
||||||
|
testRealms.get(0).setPrivateKey(PRIVATE_KEY);
|
||||||
|
testRealms.get(0).setPublicKey(PUBLIC_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() throws Exception {
|
||||||
|
super.before();
|
||||||
|
//
|
||||||
|
// ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
|
||||||
|
// reg.auth(Auth.token(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RealmResource realmResource() {
|
||||||
|
return adminClient.realm(REALM_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRepresentation createRep(String clientId) {
|
||||||
|
ClientRepresentation client = new ClientRepresentation();
|
||||||
|
client.setClientId(clientId);
|
||||||
|
client.setSecret("test-secret");
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OIDCClientRepresentation createRepOidc() {
|
||||||
|
return createRepOidc("http://localhost:8080/foo", "http://localhost:8080/foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
private OIDCClientRepresentation createRepOidc(String clientBaseUri, String clientRedirectUri) {
|
||||||
|
OIDCClientRepresentation client = new OIDCClientRepresentation();
|
||||||
|
client.setClientName("RegistrationAccessTokenTest");
|
||||||
|
client.setClientUri(clientBaseUri);
|
||||||
|
client.setRedirectUris(Collections.singletonList(clientRedirectUri));
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OIDCClientRepresentation create() throws ClientRegistrationException {
|
||||||
|
OIDCClientRepresentation client = createRepOidc();
|
||||||
|
|
||||||
|
OIDCClientRepresentation response = reg.oidc().create(client);
|
||||||
|
|
||||||
|
reg.auth(Auth.token(response));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOidcFail(ClientRegOp operation, OIDCClientRepresentation client, int expectedStatusCode) {
|
||||||
|
assertOidcFail(operation, client, expectedStatusCode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOidcFail(ClientRegOp operation, OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
|
||||||
|
try {
|
||||||
|
switch (operation) {
|
||||||
|
case CREATE: reg.oidc().create(client);
|
||||||
|
break;
|
||||||
|
case UPDATE: reg.oidc().update(client);
|
||||||
|
break;
|
||||||
|
case DELETE: reg.oidc().delete(client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.fail("Not expected to successfuly run operation " + operation.toString() + " on client");
|
||||||
|
} catch (ClientRegistrationException expected) {
|
||||||
|
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
|
||||||
|
Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
|
||||||
|
if (expectedErrorContains != null) {
|
||||||
|
assertTrue("Error response doesn't contain expected text. The error response text is: " + httpEx.getErrorResponse(), httpEx.getErrorResponse().contains(expectedErrorContains));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertFail(ClientRegOp operation, ClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
|
||||||
|
try {
|
||||||
|
switch (operation) {
|
||||||
|
case CREATE: reg.create(client);
|
||||||
|
break;
|
||||||
|
case UPDATE: reg.update(client);
|
||||||
|
break;
|
||||||
|
case DELETE: reg.delete(client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.fail("Not expected to successfuly run operation " + operation.toString() + " on client");
|
||||||
|
} catch (ClientRegistrationException expected) {
|
||||||
|
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
|
||||||
|
Assert.assertEquals(expectedStatusCode, httpEx.getStatusLine().getStatusCode());
|
||||||
|
if (expectedErrorContains != null) {
|
||||||
|
assertTrue("Error response doesn't contain expected text. The error response text is: " + httpEx.getErrorResponse(), httpEx.getErrorResponse().contains(expectedErrorContains));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAnonCreateWithTrustedHost() throws Exception {
|
||||||
|
// Failed to create client (untrusted host)
|
||||||
|
OIDCClientRepresentation client = createRepOidc("http://root", "http://redirect");
|
||||||
|
assertOidcFail(ClientRegOp.CREATE, client, 403, "Host not trusted");
|
||||||
|
|
||||||
|
// Should still fail (bad redirect_uri)
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
assertOidcFail(ClientRegOp.CREATE, client, 403, "URL doesn't match");
|
||||||
|
|
||||||
|
// Should still fail (bad base_uri)
|
||||||
|
client.setRedirectUris(Collections.singletonList("http://localhost:8080/foo"));
|
||||||
|
assertOidcFail(ClientRegOp.CREATE, client, 403, "URL doesn't match");
|
||||||
|
|
||||||
|
// Success create client
|
||||||
|
client.setClientUri("http://localhost:8080/foo");
|
||||||
|
OIDCClientRepresentation oidcClientRep = reg.oidc().create(client);
|
||||||
|
|
||||||
|
|
||||||
|
// Test registration access token
|
||||||
|
assertRegAccessToken(oidcClientRep.getRegistrationAccessToken(), RegistrationAuth.ANONYMOUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAnonUpdateWithTrustedHost() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
OIDCClientRepresentation client = create();
|
||||||
|
|
||||||
|
// Fail update client
|
||||||
|
client.setRedirectUris(Collections.singletonList("http://bad:8080/foo"));
|
||||||
|
assertOidcFail(ClientRegOp.UPDATE, client, 403, "URL doesn't match");
|
||||||
|
|
||||||
|
// Should be fine now
|
||||||
|
client.setRedirectUris(Collections.singletonList("http://localhost:8080/foo"));
|
||||||
|
reg.oidc().update(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRedirectUriWithDomain() throws Exception {
|
||||||
|
// Change the policy to avoid checking hosts
|
||||||
|
ComponentRepresentation trustedHostPolicyRep = findPolicyByProviderAndAuth(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
|
||||||
|
trustedHostPolicyRep.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "false");
|
||||||
|
|
||||||
|
// Configure some trusted host and domain
|
||||||
|
trustedHostPolicyRep.getConfig().put(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, Arrays.asList("www.host.com", "*.example.com"));
|
||||||
|
realmResource().components().component(trustedHostPolicyRep.getId()).update(trustedHostPolicyRep);
|
||||||
|
|
||||||
|
// Verify client can be created with the redirectUri from trusted host and domain
|
||||||
|
OIDCClientRepresentation oidcClientRep = createRepOidc("http://www.host.com", "http://www.example.com");
|
||||||
|
reg.oidc().create(oidcClientRep);
|
||||||
|
|
||||||
|
// Remove domain from the config
|
||||||
|
trustedHostPolicyRep.getConfig().put(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, Arrays.asList("www.host.com", "www1.example.com"));
|
||||||
|
realmResource().components().component(trustedHostPolicyRep.getId()).update(trustedHostPolicyRep);
|
||||||
|
|
||||||
|
// Check new client can't be created anymore
|
||||||
|
oidcClientRep = createRepOidc("http://www.host.com", "http://www.example.com");
|
||||||
|
assertOidcFail(ClientRegOp.CREATE, oidcClientRep, 403, "URL doesn't match");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAnonConsentRequired() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
OIDCClientRepresentation client = create();
|
||||||
|
|
||||||
|
// Assert new client has consent required
|
||||||
|
String clientId = client.getClientId();
|
||||||
|
ClientRepresentation clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
|
||||||
|
Assert.assertTrue(clientRep.isConsentRequired());
|
||||||
|
|
||||||
|
// Try update with disabled consent required. Should fail
|
||||||
|
clientRep.setConsentRequired(false);
|
||||||
|
assertFail(ClientRegOp.UPDATE, clientRep, 403, "Not permitted to update consentRequired to false");
|
||||||
|
|
||||||
|
// Try update with enabled consent required. Should pass
|
||||||
|
clientRep.setConsentRequired(true);
|
||||||
|
reg.update(clientRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAnonFullScopeAllowed() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
OIDCClientRepresentation client = create();
|
||||||
|
|
||||||
|
// Assert new client has fullScopeAllowed disabled
|
||||||
|
String clientId = client.getClientId();
|
||||||
|
ClientRepresentation clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
|
||||||
|
Assert.assertFalse(clientRep.isFullScopeAllowed());
|
||||||
|
|
||||||
|
// Try update with disabled consent required. Should fail
|
||||||
|
clientRep.setFullScopeAllowed(true);
|
||||||
|
assertFail(ClientRegOp.UPDATE, clientRep, 403, "Not permitted to enable fullScopeAllowed");
|
||||||
|
|
||||||
|
// Try update with enabled consent required. Should pass
|
||||||
|
clientRep.setFullScopeAllowed(false);
|
||||||
|
reg.update(clientRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProviders() throws Exception {
|
||||||
|
List<ComponentTypeRepresentation> reps = realmResource().clientRegistrationPolicy().getProviders();
|
||||||
|
Map<String, ComponentTypeRepresentation> providersMap = reps.stream().collect(Collectors.toMap((ComponentTypeRepresentation rep) -> {
|
||||||
|
return rep.getId();
|
||||||
|
}, (ComponentTypeRepresentation rep) -> {
|
||||||
|
return rep;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// test that ProtocolMappersClientRegistrationPolicy provider contains available protocol mappers
|
||||||
|
ComponentTypeRepresentation protMappersRep = providersMap.get(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID);
|
||||||
|
List<String> availableMappers = getProviderConfigProperty(protMappersRep, ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES);
|
||||||
|
|
||||||
|
List<String> someExpectedMappers = Arrays.asList(UserAttributeStatementMapper.PROVIDER_ID,
|
||||||
|
UserAttributeMapper.PROVIDER_ID,
|
||||||
|
UserPropertyAttributeStatementMapper.PROVIDER_ID,
|
||||||
|
UserPropertyMapper.PROVIDER_ID, HardcodedRole.PROVIDER_ID);
|
||||||
|
availableMappers.containsAll(someExpectedMappers);
|
||||||
|
|
||||||
|
// test that clientTemplate provider doesn't contain any client templates yet
|
||||||
|
ComponentTypeRepresentation clientTemplateRep = providersMap.get(ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID);
|
||||||
|
List<String> clientTemplates = getProviderConfigProperty(clientTemplateRep, ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES);
|
||||||
|
Assert.assertTrue(clientTemplates.isEmpty());
|
||||||
|
|
||||||
|
// Add some clientTemplates
|
||||||
|
ClientTemplateRepresentation clientTemplate = new ClientTemplateRepresentation();
|
||||||
|
clientTemplate.setName("foo");
|
||||||
|
realmResource().clientTemplates().create(clientTemplate);
|
||||||
|
|
||||||
|
clientTemplate = new ClientTemplateRepresentation();
|
||||||
|
clientTemplate.setName("bar");
|
||||||
|
realmResource().clientTemplates().create(clientTemplate);
|
||||||
|
|
||||||
|
// send request again and test that clientTemplate provider contains added client templates
|
||||||
|
reps = realmResource().clientRegistrationPolicy().getProviders();
|
||||||
|
clientTemplateRep = reps.stream().filter((ComponentTypeRepresentation rep1) -> {
|
||||||
|
|
||||||
|
return rep1.getId().equals(ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID);
|
||||||
|
|
||||||
|
}).findFirst().get();
|
||||||
|
|
||||||
|
clientTemplates = getProviderConfigProperty(clientTemplateRep, ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES);
|
||||||
|
Assert.assertNames(clientTemplates, "foo", "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getProviderConfigProperty(ComponentTypeRepresentation provider, String expectedConfigPropName) {
|
||||||
|
Assert.assertNotNull(provider);
|
||||||
|
|
||||||
|
List<ConfigPropertyRepresentation> list = provider.getProperties();
|
||||||
|
|
||||||
|
list = list.stream().filter((ConfigPropertyRepresentation rep) -> {
|
||||||
|
|
||||||
|
return rep.getName().equals(expectedConfigPropName);
|
||||||
|
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
Assert.assertEquals(list.size(), 1);
|
||||||
|
ConfigPropertyRepresentation allowedProtocolMappers = list.get(0);
|
||||||
|
|
||||||
|
Assert.assertEquals(allowedProtocolMappers.getName(), expectedConfigPropName);
|
||||||
|
return (List<String>) allowedProtocolMappers.getDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClientTemplatesPolicy() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
|
||||||
|
// Add some clientTemplate through Admin REST
|
||||||
|
ClientTemplateRepresentation clientTemplate = new ClientTemplateRepresentation();
|
||||||
|
clientTemplate.setName("foo");
|
||||||
|
realmResource().clientTemplates().create(clientTemplate);
|
||||||
|
|
||||||
|
// I can't register new client with this template
|
||||||
|
ClientRepresentation clientRep = createRep("test-app");
|
||||||
|
clientRep.setClientTemplate("foo");
|
||||||
|
assertFail(ClientRegOp.CREATE, clientRep, 403, "Not permitted to use specified clientTemplate");
|
||||||
|
|
||||||
|
// Register client without template - should success
|
||||||
|
clientRep.setClientTemplate(null);
|
||||||
|
ClientRepresentation registeredClient = reg.create(clientRep);
|
||||||
|
reg.auth(Auth.token(registeredClient));
|
||||||
|
|
||||||
|
// Try to update client with template - should fail
|
||||||
|
registeredClient.setClientTemplate("foo");
|
||||||
|
assertFail(ClientRegOp.UPDATE, registeredClient, 403, "Not permitted to use specified clientTemplate");
|
||||||
|
|
||||||
|
// Update client with the clientTemplate via Admin REST
|
||||||
|
ClientRepresentation client = ApiUtil.findClientByClientId(realmResource(), "test-app").toRepresentation();
|
||||||
|
client.setClientTemplate("foo");
|
||||||
|
realmResource().clients().get(client.getId()).update(client);
|
||||||
|
|
||||||
|
// Now the update via clientRegistration is permitted too as template was already set
|
||||||
|
reg.update(registeredClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClientTemplatesPolicyWithPermittedTemplate() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
|
||||||
|
// Add some clientTemplate through Admin REST
|
||||||
|
ClientTemplateRepresentation clientTemplate = new ClientTemplateRepresentation();
|
||||||
|
clientTemplate.setName("foo");
|
||||||
|
realmResource().clientTemplates().create(clientTemplate);
|
||||||
|
|
||||||
|
// I can't register new client with this template
|
||||||
|
ClientRepresentation clientRep = createRep("test-app");
|
||||||
|
clientRep.setClientTemplate("foo");
|
||||||
|
assertFail(ClientRegOp.CREATE, clientRep, 403, "Not permitted to use specified clientTemplate");
|
||||||
|
|
||||||
|
// Update the policy to allow the "foo" template
|
||||||
|
ComponentRepresentation clientTemplatesPolicyRep = findPolicyByProviderAndAuth(ClientTemplatesClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
|
||||||
|
clientTemplatesPolicyRep.getConfig().putSingle(ClientTemplatesClientRegistrationPolicyFactory.ALLOWED_CLIENT_TEMPLATES, "foo");
|
||||||
|
realmResource().components().component(clientTemplatesPolicyRep.getId()).update(clientTemplatesPolicyRep);
|
||||||
|
|
||||||
|
// Check that I can register client now
|
||||||
|
ClientRepresentation registeredClient = reg.create(clientRep);
|
||||||
|
Assert.assertNotNull(registeredClient.getRegistrationAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// PROTOCOL MAPPERS
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProtocolMappersCreate() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
|
||||||
|
// Try to add client with some "hardcoded role" mapper. Should fail
|
||||||
|
ClientRepresentation clientRep = createRep("test-app");
|
||||||
|
clientRep.setProtocolMappers(Collections.singletonList(createHardcodedMapperRep()));
|
||||||
|
assertFail(ClientRegOp.CREATE, clientRep, 403, "ProtocolMapper type not allowed");
|
||||||
|
|
||||||
|
// Try the same authenticated. Should still fail.
|
||||||
|
ClientInitialAccessPresentation token = adminClient.realm(REALM_NAME).clientInitialAccess().create(new ClientInitialAccessCreatePresentation(0, 10));
|
||||||
|
reg.auth(Auth.token(token));
|
||||||
|
assertFail(ClientRegOp.CREATE, clientRep, 403, "ProtocolMapper type not allowed");
|
||||||
|
|
||||||
|
// Update the "authenticated" policy and allow hardcoded role mapper
|
||||||
|
ComponentRepresentation protocolMapperPolicyRep = findPolicyByProviderAndAuth(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAuth());
|
||||||
|
protocolMapperPolicyRep.getConfig().add(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, HardcodedRole.PROVIDER_ID);
|
||||||
|
realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);
|
||||||
|
|
||||||
|
// Check authenticated registration is permitted
|
||||||
|
ClientRepresentation registeredClient = reg.create(clientRep);
|
||||||
|
Assert.assertNotNull(registeredClient.getRegistrationAccessToken());
|
||||||
|
|
||||||
|
// Check "anonymous" registration still fails
|
||||||
|
clientRep = createRep("test-app-2");
|
||||||
|
clientRep.setProtocolMappers(Collections.singletonList(createHardcodedMapperRep()));
|
||||||
|
reg.auth(null);
|
||||||
|
assertFail(ClientRegOp.CREATE, clientRep, 403, "ProtocolMapper type not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ProtocolMapperRepresentation createHardcodedMapperRep() {
|
||||||
|
ProtocolMapperRepresentation protocolMapper = new ProtocolMapperRepresentation();
|
||||||
|
protocolMapper.setName("Hardcoded foo role");
|
||||||
|
protocolMapper.setProtocolMapper(HardcodedRole.PROVIDER_ID);
|
||||||
|
protocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
protocolMapper.setConsentRequired(false);
|
||||||
|
protocolMapper.setConsentText(null);
|
||||||
|
protocolMapper.getConfig().put(HardcodedRole.ROLE_CONFIG, "foo-role");
|
||||||
|
return protocolMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProtocolMappersUpdate() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
|
||||||
|
// Check I can add client with allowed protocolMappers
|
||||||
|
ProtocolMapperRepresentation protocolMapper = new ProtocolMapperRepresentation();
|
||||||
|
protocolMapper.setName("Full name");
|
||||||
|
protocolMapper.setProtocolMapper(FullNameMapper.PROVIDER_ID);
|
||||||
|
protocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
protocolMapper.setConsentRequired(true);
|
||||||
|
protocolMapper.setConsentText("Full name");
|
||||||
|
|
||||||
|
ClientRepresentation clientRep = createRep("test-app");
|
||||||
|
clientRep.setProtocolMappers(Collections.singletonList(protocolMapper));
|
||||||
|
|
||||||
|
ClientRepresentation registeredClient = reg.create(clientRep);
|
||||||
|
reg.auth(Auth.token(registeredClient));
|
||||||
|
|
||||||
|
// Add some disallowed protocolMapper
|
||||||
|
registeredClient.getProtocolMappers().add(createHardcodedMapperRep());
|
||||||
|
|
||||||
|
// Check I can't update client because of protocolMapper
|
||||||
|
assertFail(ClientRegOp.UPDATE, registeredClient, 403, "ProtocolMapper type not allowed");
|
||||||
|
|
||||||
|
// Remove "bad" protocolMapper
|
||||||
|
registeredClient.getProtocolMappers().removeIf((ProtocolMapperRepresentation mapper) -> {
|
||||||
|
return mapper.getProtocolMapper().equals(HardcodedRole.PROVIDER_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check I can update client now
|
||||||
|
reg.update(registeredClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProtocolMappersConsentRequired() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
|
||||||
|
// Register client and assert it has builtin protocol mappers
|
||||||
|
ClientRepresentation clientRep = createRep("test-app");
|
||||||
|
ClientRepresentation registeredClient = reg.create(clientRep);
|
||||||
|
|
||||||
|
long usernamePropMappersCount = registeredClient.getProtocolMappers().stream().filter((ProtocolMapperRepresentation protocolMapper) -> {
|
||||||
|
return protocolMapper.getProtocolMapper().equals(UserPropertyMapper.PROVIDER_ID);
|
||||||
|
}).count();
|
||||||
|
Assert.assertTrue(usernamePropMappersCount > 0);
|
||||||
|
|
||||||
|
// Remove USernamePropertyMapper from the policy configuration
|
||||||
|
ComponentRepresentation protocolMapperPolicyRep = findPolicyByProviderAndAuth(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
|
||||||
|
protocolMapperPolicyRep.getConfig().getList(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES).remove(UserPropertyMapper.PROVIDER_ID);
|
||||||
|
realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);
|
||||||
|
|
||||||
|
// Register another client. Assert it doesn't have builtin mappers anymore
|
||||||
|
clientRep = createRep("test-app-2");
|
||||||
|
registeredClient = reg.create(clientRep);
|
||||||
|
|
||||||
|
usernamePropMappersCount = registeredClient.getProtocolMappers().stream().filter((ProtocolMapperRepresentation protocolMapper) -> {
|
||||||
|
return protocolMapper.getProtocolMapper().equals(UserPropertyMapper.PROVIDER_ID);
|
||||||
|
}).count();
|
||||||
|
Assert.assertEquals(0, usernamePropMappersCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProtocolMappersRemoveBuiltins() throws Exception {
|
||||||
|
setTrustedHost("localhost", getPolicyAnon());
|
||||||
|
|
||||||
|
// Change policy to allow hardcoded mapper
|
||||||
|
|
||||||
|
ComponentRepresentation protocolMapperPolicyRep = findPolicyByProviderAndAuth(ProtocolMappersClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
|
||||||
|
protocolMapperPolicyRep.getConfig().add(ProtocolMappersClientRegistrationPolicyFactory.ALLOWED_PROTOCOL_MAPPER_TYPES, HardcodedRole.PROVIDER_ID);
|
||||||
|
realmResource().components().component(protocolMapperPolicyRep.getId()).update(protocolMapperPolicyRep);
|
||||||
|
|
||||||
|
// Create client with hardcoded mapper
|
||||||
|
ClientRepresentation clientRep = createRep("test-app");
|
||||||
|
clientRep.setProtocolMappers(Collections.singletonList(createHardcodedMapperRep()));
|
||||||
|
ClientRepresentation registeredClient = reg.create(clientRep);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, registeredClient.getProtocolMappers().size());
|
||||||
|
ProtocolMapperRepresentation hardcodedMapper = registeredClient.getProtocolMappers().get(0);
|
||||||
|
Assert.assertTrue(hardcodedMapper.isConsentRequired());
|
||||||
|
Assert.assertEquals("Hardcoded foo role", hardcodedMapper.getConsentText());
|
||||||
|
}
|
||||||
|
|
||||||
|
// HELPER METHODS
|
||||||
|
|
||||||
|
|
||||||
|
private String getPolicyAnon() {
|
||||||
|
return ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.ANONYMOUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPolicyAuth() {
|
||||||
|
return ClientRegistrationPolicyManager.getComponentTypeKey(RegistrationAuth.AUTHENTICATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComponentRepresentation findPolicyByProviderAndAuth(String providerId, String authType) {
|
||||||
|
// Change the policy to avoid checking hosts
|
||||||
|
List<ComponentRepresentation> reps = realmResource().components().query(REALM_NAME, ClientRegistrationPolicy.class.getName());
|
||||||
|
for (ComponentRepresentation rep : reps) {
|
||||||
|
if (rep.getSubType().equals(authType) && rep.getProviderId().equals(providerId)) {
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTrustedHost(String hostname, String policyType) {
|
||||||
|
List<ComponentRepresentation> reps = realmResource().components().query(REALM_NAME, getPolicyAnon());
|
||||||
|
ComponentRepresentation trustedHostRep = findPolicyByProviderAndAuth(TrustedHostClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
|
||||||
|
trustedHostRep.getConfig().putSingle(TrustedHostClientRegistrationPolicyFactory.TRUSTED_HOSTS, hostname);
|
||||||
|
realmResource().components().component(trustedHostRep.getId()).update(trustedHostRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertRegAccessToken(String registrationAccessToken, RegistrationAuth expectedRegAuth) throws Exception {
|
||||||
|
byte[] content = new JWSInput(registrationAccessToken).getContent();
|
||||||
|
RegistrationAccessToken regAccessToken = JsonSerialization.readValue(content, RegistrationAccessToken.class);
|
||||||
|
Assert.assertEquals(regAccessToken.getRegistrationAuth(), expectedRegAuth.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ClientRegOp {
|
||||||
|
CREATE, READ, UPDATE, DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -103,39 +103,9 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCreateWithTrustedHost() throws Exception {
|
|
||||||
reg.auth(null);
|
|
||||||
|
|
||||||
OIDCClientRepresentation client = createRep();
|
|
||||||
|
|
||||||
// Failed to create client
|
|
||||||
assertCreateFail(client, 401);
|
|
||||||
|
|
||||||
// Create trusted host entry
|
|
||||||
createTrustedHost("localhost", 2);
|
|
||||||
|
|
||||||
// Successfully register client
|
|
||||||
reg.oidc().create(client);
|
|
||||||
|
|
||||||
// Just one remaining available
|
|
||||||
ClientRegistrationTrustedHostRepresentation rep = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().get("localhost");
|
|
||||||
Assert.assertEquals(1, rep.getRemainingCount().intValue());
|
|
||||||
|
|
||||||
// Successfully register client2
|
|
||||||
reg.oidc().create(client);
|
|
||||||
|
|
||||||
// Failed to create 3rd client
|
|
||||||
assertCreateFail(client, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
// KEYCLOAK-3421
|
// KEYCLOAK-3421
|
||||||
@Test
|
@Test
|
||||||
public void createClientWithUriFragment() {
|
public void createClientWithUriFragment() {
|
||||||
reg.auth(null);
|
|
||||||
|
|
||||||
createTrustedHost("localhost", 1);
|
|
||||||
|
|
||||||
OIDCClientRepresentation client = createRep();
|
OIDCClientRepresentation client = createRep();
|
||||||
client.setRedirectUris(Arrays.asList("http://localhost/auth", "http://localhost/auth#fragment", "http://localhost/auth*"));
|
client.setRedirectUris(Arrays.asList("http://localhost/auth", "http://localhost/auth#fragment", "http://localhost/auth*"));
|
||||||
|
|
||||||
|
@ -231,9 +201,4 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
|
Assert.assertEquals(config.getRequestObjectSignatureAlg(), Algorithm.RS256);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createTrustedHost(String name, int count) {
|
|
||||||
Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create(name, count, count));
|
|
||||||
Assert.assertEquals(201, response.getStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,8 @@ package org.keycloak.testsuite.exportimport;
|
||||||
|
|
||||||
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
|
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.ComponentsResource;
|
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
|
||||||
import org.keycloak.exportimport.ExportImportConfig;
|
import org.keycloak.exportimport.ExportImportConfig;
|
||||||
import org.keycloak.exportimport.dir.DirExportProvider;
|
import org.keycloak.exportimport.dir.DirExportProvider;
|
||||||
import org.keycloak.exportimport.dir.DirExportProviderFactory;
|
import org.keycloak.exportimport.dir.DirExportProviderFactory;
|
||||||
|
@ -32,16 +29,14 @@ import org.keycloak.representations.idm.ComponentRepresentation;
|
||||||
import org.keycloak.representations.idm.KeysMetadataRepresentation;
|
import org.keycloak.representations.idm.KeysMetadataRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -245,7 +240,15 @@ public class ExportImportTest extends AbstractExportImportTest {
|
||||||
assertEquals(e.getProviderId(), a.getProviderId());
|
assertEquals(e.getProviderId(), a.getProviderId());
|
||||||
assertEquals(e.getProviderType(), a.getProviderType());
|
assertEquals(e.getProviderType(), a.getProviderType());
|
||||||
assertEquals(e.getParentId(), a.getParentId());
|
assertEquals(e.getParentId(), a.getParentId());
|
||||||
assertEquals(e.getConfig(), a.getConfig());
|
assertEquals(e.getSubType(), a.getSubType());
|
||||||
|
Assert.assertNames(e.getConfig().keySet(), a.getConfig().keySet().toArray(new String[] {}));
|
||||||
|
|
||||||
|
// Compare config values without take order into account
|
||||||
|
for (Map.Entry<String, List<String>> entry : e.getConfig().entrySet()) {
|
||||||
|
List<String> eList = entry.getValue();
|
||||||
|
List<String> aList = a.getConfig().getList(entry.getKey());
|
||||||
|
Assert.assertNames(eList, aList.toArray(new String[] {}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,3 +67,4 @@ log4j.logger.org.apache.directory.server.core=warn
|
||||||
|
|
||||||
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
|
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
|
||||||
# log4j.logger.org.keycloak.keys.infinispan=trace
|
# log4j.logger.org.keycloak.keys.infinispan=trace
|
||||||
|
log4j.logger.org.keycloak.services.clientregistration.policy=trace
|
|
@ -309,7 +309,7 @@ public class KeycloakServer {
|
||||||
di.setDeploymentName("Keycloak");
|
di.setDeploymentName("Keycloak");
|
||||||
di.setDefaultEncoding("UTF-8");
|
di.setDefaultEncoding("UTF-8");
|
||||||
|
|
||||||
di.addInitParameter("keycloak.embedded", "true");
|
di.addInitParameter(KeycloakApplication.KEYCLOAK_EMBEDDED, "true");
|
||||||
|
|
||||||
di.setDefaultServletConfig(new DefaultServletConfig(true));
|
di.setDefaultServletConfig(new DefaultServletConfig(true));
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ realm-tab-email=Email
|
||||||
realm-tab-themes=Themes
|
realm-tab-themes=Themes
|
||||||
realm-tab-cache=Cache
|
realm-tab-cache=Cache
|
||||||
realm-tab-tokens=Tokens
|
realm-tab-tokens=Tokens
|
||||||
realm-tab-client-initial-access=Initial Access Tokens
|
realm-tab-client-registration=Client Registration
|
||||||
realm-tab-security-defenses=Security Defenses
|
realm-tab-security-defenses=Security Defenses
|
||||||
realm-tab-general=General
|
realm-tab-general=General
|
||||||
add-realm=Add realm
|
add-realm=Add realm
|
||||||
|
@ -534,7 +534,6 @@ remainingCount=Remaining Count
|
||||||
created=Created
|
created=Created
|
||||||
back=Back
|
back=Back
|
||||||
initial-access-tokens=Initial Access Tokens
|
initial-access-tokens=Initial Access Tokens
|
||||||
initial-access-tokens.tooltip=Initial Access Tokens for dynamic registrations of clients. Request with those tokens can be sent from any host.
|
|
||||||
add-initial-access-tokens=Add Initial Access Token
|
add-initial-access-tokens=Add Initial Access Token
|
||||||
initial-access-token=Initial Access Token
|
initial-access-token=Initial Access Token
|
||||||
initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later
|
initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later
|
||||||
|
@ -543,16 +542,25 @@ initial-access-token.confirm.title=Copy Initial Access Token
|
||||||
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later
|
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later
|
||||||
no-initial-access-available=No Initial Access Tokens available
|
no-initial-access-available=No Initial Access Tokens available
|
||||||
|
|
||||||
trusted-hosts-legend=Trusted Hosts For Client Registrations
|
client-reg-policies=Client Registration Policies
|
||||||
trusted-hosts-legend.tooltip=Hosts, which are trusted for client registrations. Client registration requests from those hosts can be sent even without initial access token. The amount of client registrations from particular host can be limited to specified count.
|
anonymous-policies=Anonymous Access Policies
|
||||||
no-client-trusted-hosts-available=No Trusted Hosts available
|
anonymous-policies.tooltip=Those Policies are used when Client Registration Service is invoked by unauthenticated request. This means request doesn't contain Initial Access Token nor Bearer Token.
|
||||||
add-client-reg-trusted-host=Add Trusted Host
|
auth-policies=Authenticated Access Policies
|
||||||
hostname=Hostname
|
auth-policies.tooltip=Those Policies are used when Client Registration Service is invoked by authenticated request. This means request contains Initial Access Token or Bearer Token.
|
||||||
client-reg-hostname.tooltip=Fully-Qualified Hostname or IP Address. Client registration requests from this host/address will be trusted and allowed to register new client.
|
policy-name=Policy Name
|
||||||
client-reg-count.tooltip=Allowed count of client registration requests from particular host. You need to restart this once the limit is reached.
|
no-client-reg-policies-configured=No Client Registration Policies
|
||||||
client-reg-remainingCount.tooltip=Remaining count of client registration requests from this host. You need to restart this once the limit is reached.
|
trusted-hosts.label=Trusted Hosts
|
||||||
reset-remaining-count=Reset Remaining Count
|
trusted-hosts.tooltip=List of Hosts, which are trusted and are allowed to invoke Client Registration Service and/or be used as values of Client URIs. You can use hostnames or IP addresses. If you use star at the beginning (for example '*.example.com' ) then whole domain example.com will be trusted.
|
||||||
|
host-sending-registration-request-must-match.label=Host Sending Client Registration Request Must Match
|
||||||
|
host-sending-registration-request-must-match.tooltip=If on, then any request to Client Registration Service is allowed just if it was sent from some trusted host or domain.
|
||||||
|
client-uris-must-match.label=Client URIs Must Match
|
||||||
|
client-uris-must-match.tooltip=If on, then all Client URIs (Redirect URIs and others) are allowed just if they match some trusted host or domain.
|
||||||
|
allowed-protocol-mappers.label=Allowed Protocol Mappers
|
||||||
|
allowed-protocol-mappers.tooltip=Whitelist of allowed protocol mapper providers. If there is an attempt to register client, which contains some protocol mappers, which were not whitelisted, then registration request will be rejected.
|
||||||
|
consent-required-for-all-mappers.label=Consent Required For Mappers
|
||||||
|
consent-required-for-all-mappers.tooltip=If on, then all newly registered protocol mappers will automatically have consentRequired switch on. This means that user will need to approve consent screen. NOTE: Consent screen is shown just if client has consentRequired switch on. So it's usually good to use this switch together with consent-required policy.
|
||||||
|
allowed-client-templates.label=Allowed Client Templates
|
||||||
|
allowed-client-templates.tooltip=Whitelist of the client templates, which can be used on newly registered client. Attempt to register client with some client template, which is not whitelisted, will be rejected. By default, the whitelist is empty, so there are not any client templates are allowed.
|
||||||
|
|
||||||
client-templates=Client Templates
|
client-templates=Client Templates
|
||||||
client-templates.tooltip=Client templates allow you to define common configuration that is shared between multiple clients
|
client-templates.tooltip=Client templates allow you to define common configuration that is shared between multiple clients
|
||||||
|
|
|
@ -200,7 +200,7 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmTokenDetailCtrl'
|
controller : 'RealmTokenDetailCtrl'
|
||||||
})
|
})
|
||||||
.when('/realms/:realm/client-initial-access', {
|
.when('/realms/:realm/client-registration/client-initial-access', {
|
||||||
templateUrl : resourceUrl + '/partials/client-initial-access.html',
|
templateUrl : resourceUrl + '/partials/client-initial-access.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
realm : function(RealmLoader) {
|
realm : function(RealmLoader) {
|
||||||
|
@ -208,14 +208,11 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
clientInitialAccess : function(ClientInitialAccessLoader) {
|
clientInitialAccess : function(ClientInitialAccessLoader) {
|
||||||
return ClientInitialAccessLoader();
|
return ClientInitialAccessLoader();
|
||||||
},
|
|
||||||
clientRegTrustedHosts : function(ClientRegistrationTrustedHostListLoader) {
|
|
||||||
return ClientRegistrationTrustedHostListLoader();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller : 'ClientInitialAccessCtrl'
|
controller : 'ClientInitialAccessCtrl'
|
||||||
})
|
})
|
||||||
.when('/realms/:realm/client-initial-access/create', {
|
.when('/realms/:realm/client-registration/client-initial-access/create', {
|
||||||
templateUrl : resourceUrl + '/partials/client-initial-access-create.html',
|
templateUrl : resourceUrl + '/partials/client-initial-access-create.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
realm : function(RealmLoader) {
|
realm : function(RealmLoader) {
|
||||||
|
@ -224,29 +221,54 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'ClientInitialAccessCreateCtrl'
|
controller : 'ClientInitialAccessCreateCtrl'
|
||||||
})
|
})
|
||||||
.when('/realms/:realm/client-reg-trusted-hosts/create', {
|
.when('/realms/:realm/client-registration/client-reg-policies', {
|
||||||
templateUrl : resourceUrl + '/partials/client-reg-trusted-host-create.html',
|
templateUrl : resourceUrl + '/partials/client-reg-policies.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
realm : function(RealmLoader) {
|
realm : function(RealmLoader) {
|
||||||
return RealmLoader();
|
return RealmLoader();
|
||||||
},
|
},
|
||||||
clientRegTrustedHost : function() {
|
policies : function(ComponentsLoader) {
|
||||||
return {};
|
return ComponentsLoader.loadComponents(null, 'org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy');
|
||||||
|
},
|
||||||
|
clientRegistrationPolicyProviders : function(ClientRegistrationPolicyProvidersLoader) {
|
||||||
|
return ClientRegistrationPolicyProvidersLoader();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller : 'ClientRegistrationTrustedHostDetailCtrl'
|
controller : 'ClientRegPoliciesCtrl'
|
||||||
})
|
})
|
||||||
.when('/realms/:realm/client-reg-trusted-hosts/:hostname', {
|
.when('/realms/:realm/client-registration/client-reg-policies/create/:componentType/:providerId', {
|
||||||
templateUrl : resourceUrl + '/partials/client-reg-trusted-host-detail.html',
|
templateUrl : resourceUrl + '/partials/client-reg-policy-detail.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
realm : function(RealmLoader) {
|
realm : function(RealmLoader) {
|
||||||
return RealmLoader();
|
return RealmLoader();
|
||||||
},
|
},
|
||||||
clientRegTrustedHost : function(ClientRegistrationTrustedHostLoader) {
|
instance : function($route) {
|
||||||
return ClientRegistrationTrustedHostLoader();
|
return {
|
||||||
|
providerType: 'org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy',
|
||||||
|
subType: $route.current.params.componentType,
|
||||||
|
providerId: $route.current.params.providerId
|
||||||
|
};
|
||||||
|
},
|
||||||
|
clientRegistrationPolicyProviders : function(ClientRegistrationPolicyProvidersLoader) {
|
||||||
|
return ClientRegistrationPolicyProvidersLoader();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller : 'ClientRegistrationTrustedHostDetailCtrl'
|
controller : 'ClientRegPolicyDetailCtrl'
|
||||||
|
})
|
||||||
|
.when('/realms/:realm/client-registration/client-reg-policies/:provider/:componentId', {
|
||||||
|
templateUrl : resourceUrl + '/partials/client-reg-policy-detail.html',
|
||||||
|
resolve : {
|
||||||
|
realm : function(RealmLoader) {
|
||||||
|
return RealmLoader();
|
||||||
|
},
|
||||||
|
instance : function(ComponentLoader) {
|
||||||
|
return ComponentLoader();
|
||||||
|
},
|
||||||
|
clientRegistrationPolicyProviders : function(ClientRegistrationPolicyProvidersLoader) {
|
||||||
|
return ClientRegistrationPolicyProvidersLoader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller : 'ClientRegPolicyDetailCtrl'
|
||||||
})
|
})
|
||||||
.when('/realms/:realm/keys', {
|
.when('/realms/:realm/keys', {
|
||||||
templateUrl : resourceUrl + '/partials/realm-keys.html',
|
templateUrl : resourceUrl + '/partials/realm-keys.html',
|
||||||
|
@ -2456,6 +2478,24 @@ module.controller('ProviderConfigCtrl', function ($modal, $scope) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.newValues = [];
|
||||||
|
|
||||||
|
$scope.addValueToMultivalued = function(optionName) {
|
||||||
|
var valueToPush = $scope.newValues[optionName];
|
||||||
|
|
||||||
|
console.log("New value to multivalued: optionName=" + optionName + ", valueToPush=" + valueToPush);
|
||||||
|
|
||||||
|
if (!$scope.config[optionName]) {
|
||||||
|
$scope.config[optionName] = [];
|
||||||
|
}
|
||||||
|
$scope.config[optionName].push(valueToPush);
|
||||||
|
$scope.newValues[optionName] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.deleteValueFromMultivalued = function(optionName, index) {
|
||||||
|
$scope.config[optionName].splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
$scope.uploadFile = function($files, optionName, config) {
|
$scope.uploadFile = function($files, optionName, config) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
|
|
|
@ -2256,23 +2256,9 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, clientRegTrustedHosts, ClientInitialAccess, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
|
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, ClientInitialAccess, Dialog, Notifications, $route, $location) {
|
||||||
$scope.realm = realm;
|
$scope.realm = realm;
|
||||||
$scope.clientInitialAccess = clientInitialAccess;
|
$scope.clientInitialAccess = clientInitialAccess;
|
||||||
$scope.clientRegTrustedHosts = clientRegTrustedHosts;
|
|
||||||
|
|
||||||
$scope.updateHost = function(hostname) {
|
|
||||||
$location.url('/realms/' + realm.realm + '/client-reg-trusted-hosts/' + hostname);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeHost = function(hostname) {
|
|
||||||
Dialog.confirmDelete(hostname, 'trusted host for client registration', function() {
|
|
||||||
ClientRegistrationTrustedHost.remove({ realm: realm.realm, hostname: hostname }, function() {
|
|
||||||
Notifications.success("The trusted host for client registration was deleted.");
|
|
||||||
$route.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.remove = function(id) {
|
$scope.remove = function(id) {
|
||||||
Dialog.confirmDelete(id, 'initial access token', function() {
|
Dialog.confirmDelete(id, 'initial access token', function() {
|
||||||
|
@ -2284,57 +2270,6 @@ module.controller('ClientInitialAccessCtrl', function($scope, realm, clientIniti
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('ClientRegistrationTrustedHostDetailCtrl', function($scope, realm, clientRegTrustedHost, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
|
|
||||||
$scope.realm = realm;
|
|
||||||
|
|
||||||
$scope.create = !clientRegTrustedHost.hostName;
|
|
||||||
$scope.changed = false;
|
|
||||||
|
|
||||||
if ($scope.create) {
|
|
||||||
$scope.count = 5;
|
|
||||||
} else {
|
|
||||||
$scope.hostName = clientRegTrustedHost.hostName;
|
|
||||||
$scope.count = clientRegTrustedHost.count;
|
|
||||||
$scope.remainingCount = clientRegTrustedHost.remainingCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.save = function() {
|
|
||||||
if ($scope.create) {
|
|
||||||
ClientRegistrationTrustedHost.save({
|
|
||||||
realm: realm.realm
|
|
||||||
}, { hostName: $scope.hostName, count: $scope.count, remainingCount: $scope.count }, function (data) {
|
|
||||||
Notifications.success("The trusted host was created.");
|
|
||||||
$location.url('/realms/' + realm.realm + '/client-reg-trusted-hosts/' + $scope.hostName);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ClientRegistrationTrustedHost.update({
|
|
||||||
realm: realm.realm, hostname: $scope.hostName
|
|
||||||
}, { hostName: $scope.hostName, count: $scope.count, remainingCount: $scope.count }, function (data) {
|
|
||||||
Notifications.success("The trusted host was updated.");
|
|
||||||
$route.reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.cancel = function() {
|
|
||||||
$location.url('/realms/' + realm.realm + '/client-initial-access');
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.resetRemainingCount = function() {
|
|
||||||
$scope.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$watch('count', function(newVal, oldVal) {
|
|
||||||
if (oldVal == newVal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.changed = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) {
|
module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) {
|
||||||
$scope.expirationUnit = 'Days';
|
$scope.expirationUnit = 'Days';
|
||||||
$scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit);
|
$scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit);
|
||||||
|
@ -2357,7 +2292,7 @@ module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, Clien
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.cancel = function() {
|
$scope.cancel = function() {
|
||||||
$location.url('/realms/' + realm.realm + '/client-initial-access');
|
$location.url('/realms/' + realm.realm + '/client-registration/client-initial-access');
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.done = function() {
|
$scope.done = function() {
|
||||||
|
@ -2375,11 +2310,134 @@ module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, Clien
|
||||||
var title = $translate.instant('initial-access-token.confirm.title');
|
var title = $translate.instant('initial-access-token.confirm.title');
|
||||||
var message = $translate.instant('initial-access-token.confirm.text');
|
var message = $translate.instant('initial-access-token.confirm.text');
|
||||||
Dialog.open(title, message, btns, function() {
|
Dialog.open(title, message, btns, function() {
|
||||||
$location.url('/realms/' + realm.realm + '/client-initial-access');
|
$location.url('/realms/' + realm.realm + '/client-registration/client-initial-access');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.controller('ClientRegPoliciesCtrl', function($scope, realm, clientRegistrationPolicyProviders, policies, Dialog, Notifications, Components, $route, $location) {
|
||||||
|
$scope.realm = realm;
|
||||||
|
$scope.providers = clientRegistrationPolicyProviders;
|
||||||
|
$scope.anonPolicies = [];
|
||||||
|
$scope.authPolicies = [];
|
||||||
|
for (var i=0 ; i<policies.length ; i++) {
|
||||||
|
var policy = policies[i];
|
||||||
|
if (policy.subType === 'anonymous') {
|
||||||
|
$scope.anonPolicies.push(policy);
|
||||||
|
} else if (policy.subType === 'authenticated') {
|
||||||
|
$scope.authPolicies.push(policy);
|
||||||
|
} else {
|
||||||
|
throw 'subType is required for clientRegistration policy component!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.addProvider = function(authType, provider) {
|
||||||
|
console.log('Add provider: authType ' + authType + ', providerId: ' + provider.id);
|
||||||
|
$location.url("/realms/" + realm.realm + "/client-registration/client-reg-policies/create/" + authType + '/' + provider.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.getInstanceLink = function(instance) {
|
||||||
|
return "/realms/" + realm.realm + "/client-registration/client-reg-policies/" + instance.providerId + "/" + instance.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.removeInstance = function(instance) {
|
||||||
|
Dialog.confirmDelete(instance.name, 'client registration policy', function() {
|
||||||
|
Components.remove({
|
||||||
|
realm : realm.realm,
|
||||||
|
componentId : instance.id
|
||||||
|
}, function() {
|
||||||
|
$route.reload();
|
||||||
|
Notifications.success("The policy has been deleted.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
module.controller('ClientRegPolicyDetailCtrl', function($scope, realm, clientRegistrationPolicyProviders, instance, Dialog, Notifications, Components, $route, $location) {
|
||||||
|
$scope.realm = realm;
|
||||||
|
$scope.instance = instance;
|
||||||
|
$scope.providerTypes = clientRegistrationPolicyProviders;
|
||||||
|
|
||||||
|
for (var i=0 ; i<$scope.providerTypes.length ; i++) {
|
||||||
|
var providerType = $scope.providerTypes[i];
|
||||||
|
if (providerType.id === instance.providerId) {
|
||||||
|
$scope.providerType = providerType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.create = !$scope.instance.name;
|
||||||
|
|
||||||
|
function toDefaultValue(configProperty) {
|
||||||
|
if (configProperty.type === 'MultivaluedString' || configProperty.type === 'MultivaluedList') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configProperty.defaultValue) {
|
||||||
|
return [ configProperty.defaultValue ];
|
||||||
|
} else {
|
||||||
|
return [ '' ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.create) {
|
||||||
|
$scope.instance.name = $scope.instance.providerId;
|
||||||
|
$scope.instance.parentId = realm.id;
|
||||||
|
$scope.instance.config = {};
|
||||||
|
|
||||||
|
if ($scope.providerType.properties) {
|
||||||
|
|
||||||
|
for (var i = 0; i < $scope.providerType.properties.length; i++) {
|
||||||
|
var configProperty = $scope.providerType.properties[i];
|
||||||
|
$scope.instance.config[configProperty.name] = toDefaultValue(configProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldCopy = angular.copy($scope.instance);
|
||||||
|
$scope.changed = false;
|
||||||
|
|
||||||
|
$scope.$watch('instance', function() {
|
||||||
|
if (!angular.equals($scope.instance, oldCopy)) {
|
||||||
|
$scope.changed = true;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.reset = function() {
|
||||||
|
$route.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.save = function() {
|
||||||
|
$scope.changed = false;
|
||||||
|
if ($scope.create) {
|
||||||
|
Components.save({realm: realm.realm}, $scope.instance, function (data, headers) {
|
||||||
|
var l = headers().location;
|
||||||
|
var id = l.substring(l.lastIndexOf("/") + 1);
|
||||||
|
$location.url("/realms/" + realm.realm + "/client-registration/client-reg-policies/" + $scope.instance.providerId + "/" + id);
|
||||||
|
Notifications.success("The policy has been created.");
|
||||||
|
}, function (errorResponse) {
|
||||||
|
if (errorResponse.data && errorResponse.data['error_description']) {
|
||||||
|
Notifications.error(errorResponse.data['error_description']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Components.update({realm: realm.realm,
|
||||||
|
componentId: instance.id
|
||||||
|
},
|
||||||
|
$scope.instance, function () {
|
||||||
|
$route.reload();
|
||||||
|
Notifications.success("The policy has been updated.");
|
||||||
|
}, function (errorResponse) {
|
||||||
|
if (errorResponse.data && errorResponse.data['error_description']) {
|
||||||
|
Notifications.error(errorResponse.data['error_description']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
module.controller('RealmImportCtrl', function($scope, realm, $route,
|
module.controller('RealmImportCtrl', function($scope, realm, $route,
|
||||||
Notifications, $modal, $resource) {
|
Notifications, $modal, $resource) {
|
||||||
$scope.rawContent = {};
|
$scope.rawContent = {};
|
||||||
|
|
|
@ -143,6 +143,22 @@ module.factory('ComponentLoader', function(Loader, Components, $route, $q) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.factory('ComponentsLoader', function(Loader, Components, $route, $q) {
|
||||||
|
var componentsLoader = {};
|
||||||
|
|
||||||
|
componentsLoader.loadComponents = function(parent, componentType) {
|
||||||
|
return Loader.query(Components, function() {
|
||||||
|
return {
|
||||||
|
realm : $route.current.params.realm,
|
||||||
|
parent : parent,
|
||||||
|
type: componentType
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
return componentsLoader;
|
||||||
|
});
|
||||||
|
|
||||||
module.factory('UserFederationInstanceLoader', function(Loader, UserFederationInstances, $route, $q) {
|
module.factory('UserFederationInstanceLoader', function(Loader, UserFederationInstances, $route, $q) {
|
||||||
return Loader.get(UserFederationInstances, function() {
|
return Loader.get(UserFederationInstances, function() {
|
||||||
return {
|
return {
|
||||||
|
@ -518,22 +534,14 @@ module.factory('ClientInitialAccessLoader', function(Loader, ClientInitialAccess
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.factory('ClientRegistrationTrustedHostListLoader', function(Loader, ClientRegistrationTrustedHost, $route) {
|
module.factory('ClientRegistrationPolicyProvidersLoader', function(Loader, ClientRegistrationPolicyProviders, $route) {
|
||||||
return Loader.query(ClientRegistrationTrustedHost, function() {
|
return Loader.query(ClientRegistrationPolicyProviders, function() {
|
||||||
return {
|
return {
|
||||||
realm: $route.current.params.realm
|
realm: $route.current.params.realm
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.factory('ClientRegistrationTrustedHostLoader', function(Loader, ClientRegistrationTrustedHost, $route) {
|
|
||||||
return Loader.get(ClientRegistrationTrustedHost, function() {
|
|
||||||
return {
|
|
||||||
realm: $route.current.params.realm,
|
|
||||||
hostname : $route.current.params.hostname
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -304,19 +304,6 @@ module.factory('ClientInitialAccess', function($resource) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.factory('ClientRegistrationTrustedHost', function($resource) {
|
|
||||||
return $resource(authUrl + '/admin/realms/:realm/clients-trusted-hosts/:hostname', {
|
|
||||||
realm : '@realm',
|
|
||||||
hostname : '@hostname'
|
|
||||||
}, {
|
|
||||||
update : {
|
|
||||||
method : 'PUT'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
module.factory('ClientProtocolMapper', function($resource) {
|
module.factory('ClientProtocolMapper', function($resource) {
|
||||||
return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/models/:id', {
|
return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/models/:id', {
|
||||||
realm : '@realm',
|
realm : '@realm',
|
||||||
|
@ -1674,4 +1661,10 @@ module.factory('UserStorageSync', function($resource) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.factory('ClientRegistrationPolicyProviders', function($resource) {
|
||||||
|
return $resource(authUrl + '/admin/realms/:realm/client-registration-policy/providers', {
|
||||||
|
realm : '@realm',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
|
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
|
<li><a href="#/realms/{{realm.realm}}/client-registration/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
|
||||||
<li>{{:: 'add-initial-access-tokens' | translate}}</li>
|
<li>{{:: 'add-initial-access-tokens' | translate}}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
|
|
@ -1,59 +1,11 @@
|
||||||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
<kc-tabs-realm></kc-tabs-realm>
|
<kc-tabs-realm></kc-tabs-realm>
|
||||||
|
|
||||||
<div class="form-group">
|
<ul class="nav nav-tabs nav-tabs-pf">
|
||||||
<legend>{{:: 'trusted-hosts-legend' | translate}}</legend>
|
<li class="active"><a href="#/realms/{{realm.realm}}/client-registration/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
|
||||||
<kc-tooltip>{{:: 'trusted-hosts-legend.tooltip' | translate}}</kc-tooltip>
|
<li><a href="#/realms/{{realm.realm}}/client-registration/client-reg-policies">{{:: 'client-reg-policies' | translate}}</a></li>
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
<table class="table table-striped table-bordered">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="kc-table-actions" colspan="6">
|
|
||||||
<div class="form-inline">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search1.host" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
|
|
||||||
<div class="input-group-addon">
|
|
||||||
<i class="fa fa-search" type="submit"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pull-right" data-ng-show="access.manageClients">
|
|
||||||
<a id="createClientTrustedHost" class="btn btn-default" href="#/realms/{{realm.realm}}/client-reg-trusted-hosts/create">{{:: 'create' | translate}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr data-ng-hide="clientRegTrustedHosts.length == 0">
|
|
||||||
<th>{{:: 'hostname' | translate}}</th>
|
|
||||||
<th>{{:: 'count' | translate}}</th>
|
|
||||||
<th>{{:: 'remainingCount' | translate}}</th>
|
|
||||||
<th colspan="2">{{:: 'actions' | translate}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr ng-repeat="host in clientRegTrustedHosts | filter:search1 | orderBy:'timestamp'">
|
|
||||||
<td>{{host.hostName}}</td>
|
|
||||||
<td>{{host.count}}</td>
|
|
||||||
<td>{{host.remainingCount}}</td>
|
|
||||||
<td class="kc-action-cell" data-ng-click="updateHost(host.hostName)">{{:: 'update' | translate}}</td>
|
|
||||||
<td class="kc-action-cell" data-ng-click="removeHost(host.hostName)">{{:: 'delete' | translate}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr data-ng-show="(clientRegTrustedHosts | filter:search1).length == 0">
|
|
||||||
<td class="text-muted" colspan="3" data-ng-show="search1.host">{{:: 'no-results' | translate}}</td>
|
|
||||||
<td class="text-muted" colspan="3" data-ng-hide="search1.host">{{:: 'no-client-trusted-hosts-available' | translate}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<legend>{{:: 'initial-access-tokens' | translate}}</legend>
|
|
||||||
<kc-tooltip>{{:: 'initial-access-tokens.tooltip' | translate}}</kc-tooltip>
|
|
||||||
</div>
|
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -69,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pull-right" data-ng-show="access.manageClients">
|
<div class="pull-right" data-ng-show="access.manageClients">
|
||||||
<a id="createClient" class="btn btn-default" href="#/realms/{{realm.realm}}/client-initial-access/create">{{:: 'create' | translate}}</a>
|
<a id="createClient" class="btn btn-default" href="#/realms/{{realm.realm}}/client-registration/client-initial-access/create">{{:: 'create' | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
|
<kc-tabs-realm></kc-tabs-realm>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs nav-tabs-pf">
|
||||||
|
<li><a href="#/realms/{{realm.realm}}/client-registration/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
|
||||||
|
<li class="active"><a href="#/realms/{{realm.realm}}/client-registration/client-reg-policies">{{:: 'client-reg-policies' | translate}}</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend><span class="text">{{:: 'anonymous-policies' | translate}}</span></legend><kc-tooltip>{{:: 'anonymous-policies.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr ng-show="providers.length > 0 && access.manageClients">
|
||||||
|
<th colspan="5" class="kc-table-actions">
|
||||||
|
<div class="pull-right">
|
||||||
|
<div>
|
||||||
|
<select class="form-control" ng-model="selectedProvider"
|
||||||
|
ng-options="p.id for p in providers"
|
||||||
|
data-ng-change="addProvider('anonymous', selectedProvider); selectedProvider = null">
|
||||||
|
<option value="" disabled selected>{{:: 'add-provider.placeholder' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr data-ng-show="anonPolicies && anonPolicies.length > 0">
|
||||||
|
<th>{{:: 'policy-name' | translate}}</th>
|
||||||
|
<th>{{:: 'provider-id' | translate}}</th>
|
||||||
|
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="instance in anonPolicies">
|
||||||
|
<td><a href="#{{getInstanceLink(instance)}}">{{instance.name}}</a></td>
|
||||||
|
<td>{{instance.providerId}}</td>
|
||||||
|
<td class="kc-action-cell" kc-open="{{getInstanceLink(instance)}}">{{:: 'edit' | translate}}</td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="removeInstance(instance)">{{:: 'delete' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-ng-show="!anonPolicies || anonPolicies.length == 0">
|
||||||
|
<td class="text-muted">{{:: 'no-client-reg-policies-configured' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend><span class="text">{{:: 'auth-policies' | translate}}</span></legend><kc-tooltip>{{:: 'auth-policies.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr ng-show="providers.length > 0 && access.manageClients">
|
||||||
|
<th colspan="5" class="kc-table-actions">
|
||||||
|
<div class="pull-right">
|
||||||
|
<div>
|
||||||
|
<select class="form-control" ng-model="selectedProvider"
|
||||||
|
ng-options="p.id for p in providers"
|
||||||
|
data-ng-change="addProvider('authenticated', selectedProvider); selectedProvider = null">
|
||||||
|
<option value="" disabled selected>{{:: 'add-provider.placeholder' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr data-ng-show="authPolicies && authPolicies.length > 0">
|
||||||
|
<th>{{:: 'policy-name' | translate}}</th>
|
||||||
|
<th>{{:: 'provider-id' | translate}}</th>
|
||||||
|
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="instance in authPolicies">
|
||||||
|
<td><a href="#{{getInstanceLink(instance)}}">{{instance.name}}</a></td>
|
||||||
|
<td>{{instance.providerId}}</td>
|
||||||
|
<td class="kc-action-cell" kc-open="{{getInstanceLink(instance)}}">{{:: 'edit' | translate}}</td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="removeInstance(instance)">{{:: 'delete' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-ng-show="!authPolicies || authPolicies.length == 0">
|
||||||
|
<td class="text-muted">{{:: 'no-client-reg-policies-configured' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-menu></kc-menu>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
~ and other contributors as indicated by the @author tags.
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<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-registration/client-reg-policies">{{:: 'client-reg-policies' | translate}}</a></li>
|
||||||
|
<li>{{instance.name}}</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageClients ">
|
||||||
|
<fieldset>
|
||||||
|
<legend><span class="text">{{instance.name}}</span></legend><kc-tooltip>{{:: providerType.helpText | translate}}</kc-tooltip>
|
||||||
|
<div class="form-group clearfix" data-ng-show="!create">
|
||||||
|
<label class="col-md-2 control-label" for="instanceId">{{:: 'id' | translate}} </label>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input class="form-control" id="instanceId" type="text" ng-model="instance.id" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input class="form-control" id="name" type="text" ng-model="instance.name" required>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'client-reg-policy.name.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="form-group clearfix">
|
||||||
|
<label class="col-md-2 control-label" for="policyType">{{:: 'provider' | translate}}</label>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input class="form-control" id="policyType" type="text" ng-model="providerType.id" data-ng-readonly="true">
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{providerType.helpText}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
<kc-component-config config="instance.config" properties="providerType.properties" realm="realm"></kc-component-config>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="form-group" data-ng-show="create && access.manageClients">
|
||||||
|
<div class="col-md-10 col-md-offset-2">
|
||||||
|
<button kc-save>{{:: 'save' | translate}}</button>
|
||||||
|
<button kc-reset>{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" data-ng-show="!create && access.manageClients">
|
||||||
|
<div class="col-md-10 col-md-offset-2">
|
||||||
|
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||||
|
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-menu></kc-menu>
|
|
@ -2,20 +2,25 @@
|
||||||
<div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl">
|
<div data-ng-repeat="option in properties" class="form-group" data-ng-controller="ProviderConfigCtrl">
|
||||||
<label class="col-md-2 control-label">{{:: option.label | translate}}</label>
|
<label class="col-md-2 control-label">{{:: option.label | translate}}</label>
|
||||||
|
|
||||||
<div class="col-md-6" data-ng-hide="option.type == 'boolean' || option.type == 'List' || option.type == 'Role' || option.type == 'ClientList' || option.type == 'Password' || option.type=='Script' || option.type=='File'">
|
<div class="col-md-6" data-ng-show="option.type == 'String'">
|
||||||
<input class="form-control" type="text" data-ng-model="config[ option.name ][0]" >
|
<input class="form-control" type="text" data-ng-model="config[ option.name ][0]" >
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6" data-ng-show="option.type == 'Password'">
|
<div class="col-md-6" data-ng-show="option.type == 'Password'">
|
||||||
<input class="form-control" type="password" data-ng-model="config[ option.name ][0]" >
|
<input class="form-control" type="password" data-ng-model="config[ option.name ][0]" >
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6" data-ng-show="option.type == 'boolean'">
|
<div class="col-md-6" data-ng-show="option.type == 'boolean'">
|
||||||
<input ng-model="config[ option.name ][0]" value="'true'" name="option.name" onoffswitchstring on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
<input ng-model="config[ option.name ][0]" value="'true'" id="option.name" name="option.name" onoffswitchstring on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6" data-ng-show="option.type == 'List'">
|
<div class="col-md-6" data-ng-show="option.type == 'List'">
|
||||||
<select ng-model="config[ option.name ][0]" ng-options="data for data in option.defaultValue">
|
<select ng-model="config[ option.name ][0]" ng-options="data for data in option.defaultValue">
|
||||||
<option value="" selected> {{:: 'selectOne' | translate}} </option>
|
<option value="" selected> {{:: 'selectOne' | translate}} </option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6" data-ng-show="option.type == 'MultivaluedList'">
|
||||||
|
<select ui-select2 data-ng-model="config[ option.name ]" data-placeholder="{{:: 'selectMultiple' | translate}}..." multiple>
|
||||||
|
<option ng-repeat="val in option.defaultValue" value="{{val}}" ng-selected="true">{{val}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="col-md-6" data-ng-show="option.type == 'Role'">
|
<div class="col-md-6" data-ng-show="option.type == 'Role'">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
|
@ -41,11 +46,27 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6" data-ng-if="option.type == 'Script'">
|
<div class="col-md-6" data-ng-if="option.type == 'Script'">
|
||||||
<div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
|
<div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
|
||||||
{{config[option.name]}}
|
{{config[option.name]}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6" data-ng-if="option.type == 'MultivaluedString'">
|
||||||
|
<div class="input-group" ng-repeat="(i, currentOption) in config[option.name] track by $index">
|
||||||
|
<input class="form-control" ng-model="config[option.name][i]">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-ng-click="deleteValueFromMultivalued(option.name, $index)"><span class="fa fa-minus"></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control" ng-model="newValues[option.name]">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" data-ng-click="newValues[option.name].length > 0 && addValueToMultivalued(option.name)"><span class="fa fa-plus"></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: option.helpText | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -13,7 +13,7 @@
|
||||||
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">{{:: 'realm-tab-themes' | translate}}</a></li>
|
<li ng-class="{active: path[2] == 'theme-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/theme-settings">{{:: 'realm-tab-themes' | translate}}</a></li>
|
||||||
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">{{:: 'realm-tab-cache' | translate}}</a></li>
|
<li ng-class="{active: path[2] == 'cache-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/cache-settings">{{:: 'realm-tab-cache' | translate}}</a></li>
|
||||||
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">{{:: 'realm-tab-tokens' | translate}}</a></li>
|
<li ng-class="{active: path[2] == 'token-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/token-settings">{{:: 'realm-tab-tokens' | translate}}</a></li>
|
||||||
<li ng-class="{active: path[2] == 'client-initial-access'}" data-ng-show="access.viewClients"><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'realm-tab-client-initial-access' | translate}}</a></li>
|
<li ng-class="{active: path[2] == 'client-registration'}" data-ng-show="access.viewClients"><a href="#/realms/{{realm.realm}}/client-registration/client-initial-access">{{:: 'realm-tab-client-registration' | translate}}</a></li>
|
||||||
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
|
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
Loading…
Reference in a new issue