KEYCLOAK-3666 Dynamic client registration policies

This commit is contained in:
mposolda 2016-10-05 11:58:58 +02:00
parent 1c0abbd722
commit 18e0c0277f
86 changed files with 3401 additions and 564 deletions

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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();
}

View file

@ -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

View file

@ -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)

View file

@ -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;
} }

View file

@ -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" />

View file

@ -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);

View file

@ -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;
} }

View file

@ -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;
}
} }

View file

@ -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.
* *

View file

@ -768,24 +768,30 @@ 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 = new ConfigPropertyRepresentation(); ConfigPropertyRepresentation propRep = 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());
propertiesRep.add(propRep); propertiesRep.add(propRep);
} }
return propertiesRep; return propertiesRep;
} }
public static ConfigPropertyRepresentation toRepresentation(ProviderConfigProperty 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;
}
public static ComponentRepresentation toRepresentation(KeycloakSession session, ComponentModel component, boolean internal) { public static ComponentRepresentation toRepresentation(KeycloakSession session, ComponentModel component, boolean internal) {
ComponentRepresentation rep = new ComponentRepresentation(); ComponentRepresentation rep = new ComponentRepresentation();
rep.setId(component.getId()); rep.setId(component.getId());
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());

View file

@ -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);

View file

@ -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;

View file

@ -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);
} }

View file

@ -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

View file

@ -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;
} }
} }

View file

@ -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<>();

View file

@ -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("/")) {

View file

@ -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);
}
}

View file

@ -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();
} }
} }

View file

@ -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);
} }

View file

@ -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);
}
}

View file

@ -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);
}
} }

View file

@ -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;

View file

@ -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() {

View file

@ -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;
}
} }

View file

@ -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);

View file

@ -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);
} }

View file

@ -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;
} }

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
}
} }

View file

@ -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;
}
}

View file

@ -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;
} }
} }

View file

@ -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()) {

View file

@ -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();
}
}

View file

@ -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() {
}
}

View file

@ -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());
}
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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 {
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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 {
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}
}

View file

@ -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

View file

@ -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;
} }

View file

@ -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;
} }
/** /**

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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,15 +135,20 @@ 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();
ComponentModel model = realm.getComponent(id); try {
if (model == null) { ComponentModel model = realm.getComponent(id);
throw new NotFoundException("Could not find component"); if (model == null) {
throw new NotFoundException("Could not find component");
}
RepresentationToModel.updateComponent(session, rep, model, false);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
realm.updateComponent(model);
return Response.noContent().build();
} catch (ComponentValidationException e) {
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
} }
RepresentationToModel.updateComponent(session, rep, model, false);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
realm.updateComponent(model);
} }
@DELETE @DELETE

View file

@ -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);

View file

@ -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;
} }

View file

@ -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));

View file

@ -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) {

View file

@ -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

View file

@ -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

View file

@ -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;
}
}

View file

@ -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");

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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
}
}

View file

@ -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());
}
} }

View file

@ -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[] {}));
}
} }
} }

View file

@ -66,4 +66,5 @@ log4j.logger.org.apache.directory.api=warn
log4j.logger.org.apache.directory.server.core=warn 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

View file

@ -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));

View file

@ -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

View file

@ -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',
@ -2455,7 +2477,25 @@ 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) {

View file

@ -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 = {};

View file

@ -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
}
});
});

View file

@ -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',
});
});

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>