Merge pull request #3370 from mposolda/master

Client registration policies - polishing
This commit is contained in:
Marek Posolda 2016-10-19 20:06:29 +02:00 committed by GitHub
commit 2acea2b2ee
39 changed files with 515 additions and 744 deletions

View file

@ -1,60 +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.representations.idm;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationTrustedHostRepresentation {
String hostName;
Integer count;
Integer remainingCount;
public static ClientRegistrationTrustedHostRepresentation create(String hostName, int count, int remainingCount) {
ClientRegistrationTrustedHostRepresentation rep = new ClientRegistrationTrustedHostRepresentation();
rep.setHostName(hostName);
rep.setCount(count);
rep.setRemainingCount(remainingCount);
return rep;
}
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(Integer remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -1,63 +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.admin.client.resource;
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientRegistrationTrustedHostResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
Response create(ClientRegistrationTrustedHostRepresentation config);
@PUT
@Path("{hostname}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
Response update(final @PathParam("hostname") String hostName, ClientRegistrationTrustedHostRepresentation config);
@GET
@Path("{hostname}")
@Produces(MediaType.APPLICATION_JSON)
ClientRegistrationTrustedHostRepresentation get(final @PathParam("hostname") String hostName);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<ClientRegistrationTrustedHostRepresentation> list();
@DELETE
@Path("{hostname}")
void delete(final @PathParam("hostname") String hostName);
}

View file

@ -1,87 +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.models.sessions.infinispan;
import org.infinispan.Cache;
import org.keycloak.models.ClientRegistrationTrustedHostModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.ClientRegistrationTrustedHostEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationTrustedHostAdapter implements ClientRegistrationTrustedHostModel {
private final KeycloakSession session;
private final InfinispanUserSessionProvider provider;
private final Cache<String, SessionEntity> cache;
private final RealmModel realm;
private final ClientRegistrationTrustedHostEntity entity;
public ClientRegistrationTrustedHostAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientRegistrationTrustedHostEntity entity) {
this.session = session;
this.provider = provider;
this.cache = cache;
this.realm = realm;
this.entity = entity;
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public String getHostName() {
return entity.getHostName();
}
@Override
public int getCount() {
return entity.getCount();
}
@Override
public void setCount(int count) {
entity.setCount(count);
update();
}
@Override
public int getRemainingCount() {
return entity.getRemainingCount();
}
@Override
public void setRemainingCount(int remainingCount) {
entity.setRemainingCount(remainingCount);
update();
}
@Override
public void decreaseRemainingCount() {
entity.setRemainingCount(entity.getRemainingCount() - 1);
update();
}
void update() {
provider.getTx().replace(cache, entity.getId(), entity);
}
}

View file

@ -23,11 +23,9 @@ import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientRegistrationTrustedHostModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
@ -35,14 +33,12 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
import org.keycloak.models.sessions.infinispan.entities.ClientRegistrationTrustedHostEntity;
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.stream.ClientInitialAccessPredicate;
import org.keycloak.models.sessions.infinispan.stream.ClientRegistrationTrustedHostPredicate;
import org.keycloak.models.sessions.infinispan.stream.ClientSessionPredicate;
import org.keycloak.models.sessions.infinispan.stream.Comparators;
import org.keycloak.models.sessions.infinispan.stream.Mappers;
@ -540,12 +536,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
}
ClientRegistrationTrustedHostAdapter wrap(RealmModel realm, ClientRegistrationTrustedHostEntity entity) {
Cache<String, SessionEntity> cache = getCache(false);
return entity != null ? new ClientRegistrationTrustedHostAdapter(session, this, cache, realm, entity) : null;
}
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
}
@ -737,62 +727,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return list;
}
@Override
public ClientRegistrationTrustedHostModel createClientRegistrationTrustedHostModel(RealmModel realm, String hostName, int count) {
if (getClientRegistrationTrustedHostModel(realm, hostName) != null) {
throw new ModelDuplicateException("Client registration already exists for this realm and hostName");
}
String id = computeClientRegistrationTrustedHostEntityId(realm, hostName);
ClientRegistrationTrustedHostEntity entity = new ClientRegistrationTrustedHostEntity();
entity.setId(id);
entity.setHostName(hostName);
entity.setRealm(realm.getId());
entity.setCount(count);
entity.setRemainingCount(count);
tx.put(sessionCache, id, entity);
return wrap(realm, entity);
}
@Override
public ClientRegistrationTrustedHostModel getClientRegistrationTrustedHostModel(RealmModel realm, String hostName) {
String id = computeClientRegistrationTrustedHostEntityId(realm, hostName);
Cache<String, SessionEntity> cache = getCache(false);
ClientRegistrationTrustedHostEntity entity = (ClientRegistrationTrustedHostEntity) cache.get(id);
// If created in this transaction
if (entity == null) {
entity = (ClientRegistrationTrustedHostEntity) tx.get(cache, id);
}
return wrap(realm, entity);
}
@Override
public void removeClientRegistrationTrustedHostModel(RealmModel realm, String hostName) {
String id = computeClientRegistrationTrustedHostEntityId(realm, hostName);
tx.remove(getCache(false), id);
}
@Override
public List<ClientRegistrationTrustedHostModel> listClientRegistrationTrustedHosts(RealmModel realm) {
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(ClientRegistrationTrustedHostPredicate.create(realm.getId())).iterator();
List<ClientRegistrationTrustedHostModel> list = new LinkedList<>();
while (itr.hasNext()) {
list.add(wrap(realm, (ClientRegistrationTrustedHostEntity) itr.next().getValue()));
}
return list;
}
private static final String CLIENT_REG_TRUSTED_HOST_ID_PREFIX = "reg:::";
private String computeClientRegistrationTrustedHostEntityId(RealmModel realm, String hostName) {
return CLIENT_REG_TRUSTED_HOST_ID_PREFIX + realm.getId() + ":::" + hostName;
}
class InfinispanKeycloakTransaction implements KeycloakTransaction {

View file

@ -1,54 +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.models.sessions.infinispan.entities;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationTrustedHostEntity extends SessionEntity {
private String hostName;
private int count;
private int remainingCount;
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(int remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -1,58 +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.models.sessions.infinispan.stream;
import org.keycloak.models.sessions.infinispan.entities.ClientRegistrationTrustedHostEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Predicate;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationTrustedHostPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable {
public static ClientRegistrationTrustedHostPredicate create(String realm) {
return new ClientRegistrationTrustedHostPredicate(realm);
}
private ClientRegistrationTrustedHostPredicate(String realm) {
this.realm = realm;
}
private String realm;
@Override
public boolean test(Map.Entry<String, SessionEntity> entry) {
SessionEntity e = entry.getValue();
if (!realm.equals(e.getRealm())) {
return false;
}
if (!(e instanceof ClientRegistrationTrustedHostEntity)) {
return false;
}
return true;
}
}

View file

@ -21,11 +21,15 @@ package org.keycloak.component;
* @version $Revision: 1 $
*/
public class ComponentValidationException extends RuntimeException {
private Object[] parameters;
public ComponentValidationException() {
}
public ComponentValidationException(String message) {
public ComponentValidationException(String message, Object... parameters) {
super(message);
this.parameters = parameters;
}
public ComponentValidationException(String message, Throwable cause) {
@ -39,4 +43,12 @@ public class ComponentValidationException extends RuntimeException {
public ComponentValidationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}

View file

@ -61,4 +61,6 @@ public interface Details {
String SIGNATURE_REQUIRED = "signature_required";
String SIGNATURE_ALGORITHM = "signature_algorithm";
String CLIENT_REGISTRATION_POLICY = "client_registration_policy";
}

View file

@ -128,11 +128,6 @@ public enum ResourceType {
*/
, CLIENT_INITIAL_ACCESS_MODEL
/**
*
*/
, CLIENT_REGISTRATION_TRUSTED_HOST_MODEL
/**
*
*/

View file

@ -1,36 +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.models;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientRegistrationTrustedHostModel {
RealmModel getRealm();
String getHostName();
int getCount();
void setCount(int count);
int getRemainingCount();
void setRemainingCount(int remainingCount);
void decreaseRemainingCount();
}

View file

@ -57,7 +57,7 @@ public interface UserModel extends RoleMapperModel {
void setEnabled(boolean enabled);
/**
* Set single value of specified attribute. Remove all other existing values
* Set single value of specified attribute. Remove all other existing values of this attribute
*
* @param name
* @param value

View file

@ -82,11 +82,6 @@ public interface UserSessionProvider extends Provider {
void removeClientInitialAccessModel(RealmModel realm, String id);
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
ClientRegistrationTrustedHostModel createClientRegistrationTrustedHostModel(RealmModel realm, String hostName, int count);
ClientRegistrationTrustedHostModel getClientRegistrationTrustedHostModel(RealmModel realm, String hostName);
void removeClientRegistrationTrustedHostModel(RealmModel realm, String hostName);
List<ClientRegistrationTrustedHostModel> listClientRegistrationTrustedHosts(RealmModel realm);
void close();
}

View file

@ -49,7 +49,7 @@ public class ConfigurationValidationHelper {
try {
Integer.parseInt(val);
} catch (NumberFormatException e) {
throw new ComponentValidationException(label + " should be a number");
throw new ComponentValidationException("''{0}'' should be a number", label);
}
}
@ -68,7 +68,7 @@ public class ConfigurationValidationHelper {
try {
Long.parseLong(val);
} catch (NumberFormatException e) {
throw new ComponentValidationException(label + " should be a number");
throw new ComponentValidationException("''{0}'' should be a number", label);
}
}
@ -81,7 +81,7 @@ public class ConfigurationValidationHelper {
public ConfigurationValidationHelper checkSingle(String key, String label, boolean required) throws ComponentValidationException {
if (model.getConfig().containsKey(key) && model.getConfig().get(key).size() > 1) {
throw new ComponentValidationException(label + " should be a single entry");
throw new ComponentValidationException("''{0}'' should be a single entry", label);
}
if (required) {
@ -98,7 +98,7 @@ public class ConfigurationValidationHelper {
public ConfigurationValidationHelper checkRequired(String key, String label) throws ComponentValidationException {
List<String> values = model.getConfig().get(key);
if (values == null) {
throw new ComponentValidationException(label + " is required");
throw new ComponentValidationException("''{0}'' is required", label);
}
return this;
@ -113,7 +113,7 @@ public class ConfigurationValidationHelper {
String val = model.getConfig().getFirst(key);
if (val != null && !(val.equals("true") || val.equals("false"))) {
throw new ComponentValidationException(label + " should be 'true' or 'false'");
throw new ComponentValidationException("''{0}'' should be ''true'' or ''false''", label);
}
return this;

View file

@ -434,8 +434,21 @@ public interface ServicesLogger extends BasicLogger {
@Message(id=97, value="Invalid request")
void invalidRequest(@Cause Throwable t);
@LogMessage(level = ERROR)
@Message(id=98, value="Failed to get redirect uris from sector identifier URI: %s")
void failedToGetRedirectUrisFromSectorIdentifierUri(@Cause Throwable t, String sectorIdentifierUri);
@LogMessage(level = WARN)
@Message(id=99, value="Operation '%s' rejected. %s")
void clientRegistrationRequestRejected(String opDescription, String detailedMessage);
@LogMessage(level = WARN)
@Message(id=100, value= "ProtocolMapper '%s' of type '%s' not allowed")
void clientRegistrationMapperNotAllowed(String mapperName, String mapperType);
@LogMessage(level = WARN)
@Message(id=101, value= "Failed to verify remote host : %s")
void failedToVerifyRemoteHost(String hostname);
@LogMessage(level = WARN)
@Message(id=102, value= "URL '%s' doesn't match any trustedHost or trustedDomain")
void urlDoesntMatch(String url);
}

View file

@ -170,6 +170,11 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
this.event = event;
}
@Override
public EventBuilder getEvent() {
return event;
}
@Override
public void close() {
}

View file

@ -75,6 +75,11 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi
this.event = event;
}
@Override
public EventBuilder getEvent() {
return event;
}
@Override
public void close() {
}

View file

@ -1,67 +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.services.clientregistration;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientRegistrationTrustedHostModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationHostUtils {
private static final Logger logger = Logger.getLogger(ClientRegistrationHostUtils.class);
/**
* @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) {
logger.debugf("Verifying remote host : %s", hostAddress);
List<ClientRegistrationTrustedHostModel> trustedHosts = session.sessions().listClientRegistrationTrustedHosts(realm);
for (ClientRegistrationTrustedHostModel realmTrustedHost : trustedHosts) {
try {
if (realmTrustedHost.getRemainingCount() <= 0) {
continue;
}
String realmHostIPAddress = InetAddress.getByName(realmTrustedHost.getHostName()).getHostAddress();
logger.debugf("Trying host '%s' of address '%s'", realmTrustedHost.getHostName(), realmHostIPAddress);
if (realmHostIPAddress.equals(hostAddress)) {
logger.debugf("Successfully verified host : %s", realmTrustedHost.getHostName());
return realmTrustedHost;
}
} catch (UnknownHostException uhe) {
logger.debugf("Unknown host from realm configuration: %s", realmTrustedHost.getHostName());
}
}
logger.debugf("Failed to verify remote host : %s", hostAddress);
return null;
}
}

View file

@ -31,4 +31,6 @@ public interface ClientRegistrationProvider extends Provider {
void setEvent(EventBuilder event);
EventBuilder getEvent();
}

View file

@ -18,13 +18,16 @@
package org.keycloak.services.clientregistration.policy;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.events.Details;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.clientregistration.ClientRegistrationContext;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
@ -36,7 +39,7 @@ 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) -> {
triggerPolicies(context.getSession(), context.getProvider(), authType, "before register client", (ClientRegistrationPolicy policy) -> {
policy.beforeRegister(context);
@ -46,7 +49,7 @@ public class ClientRegistrationPolicyManager {
public static void triggerAfterRegister(ClientRegistrationContext context, RegistrationAuth authType, ClientModel client) {
try {
triggerPolicies(context.getSession(), authType, "after register client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
triggerPolicies(context.getSession(), context.getProvider(), authType, "after register client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
policy.afterRegister(context, client);
@ -58,7 +61,7 @@ public class ClientRegistrationPolicyManager {
public static void triggerBeforeUpdate(ClientRegistrationContext context, RegistrationAuth authType, ClientModel client) throws ClientRegistrationPolicyException {
triggerPolicies(context.getSession(), authType, "before update client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
triggerPolicies(context.getSession(), context.getProvider(), authType, "before update client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
policy.beforeUpdate(context, client);
@ -67,7 +70,7 @@ public class ClientRegistrationPolicyManager {
public static void triggerAfterUpdate(ClientRegistrationContext context, RegistrationAuth authType, ClientModel client) {
try {
triggerPolicies(context.getSession(), authType, "after update client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
triggerPolicies(context.getSession(), context.getProvider(), authType, "after update client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
policy.afterUpdate(context, client);
@ -78,7 +81,7 @@ public class ClientRegistrationPolicyManager {
}
public static void triggerBeforeView(KeycloakSession session, ClientRegistrationProvider provider, RegistrationAuth authType, ClientModel client) throws ClientRegistrationPolicyException {
triggerPolicies(session, authType, "before view client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
triggerPolicies(session, provider, authType, "before view client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
policy.beforeView(provider, client);
@ -86,7 +89,7 @@ public class ClientRegistrationPolicyManager {
}
public static void triggerBeforeRemove(KeycloakSession session, ClientRegistrationProvider provider, RegistrationAuth authType, ClientModel client) throws ClientRegistrationPolicyException {
triggerPolicies(session, authType, "before delete client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
triggerPolicies(session, provider, authType, "before delete client " + client.getClientId(), (ClientRegistrationPolicy policy) -> {
policy.beforeDelete(provider, client);
@ -95,7 +98,7 @@ public class ClientRegistrationPolicyManager {
private static void triggerPolicies(KeycloakSession session, RegistrationAuth authType, String opDescription, ClientRegOperation op) throws ClientRegistrationPolicyException {
private static void triggerPolicies(KeycloakSession session, ClientRegistrationProvider provider, RegistrationAuth authType, String opDescription, ClientRegOperation op) throws ClientRegistrationPolicyException {
RealmModel realm = session.getContext().getRealm();
String policyTypeKey = getComponentTypeKey(authType);
@ -113,14 +116,16 @@ public class ClientRegistrationPolicyManager {
throw new ClientRegistrationPolicyException("Policy of type '" + policyModel.getProviderId() + "' not found");
}
// TODO: trace
logger.infof("Running policy '%s' %s", policyModel.getName(), opDescription);
if (logger.isTraceEnabled()) {
logger.tracef("Running policy '%s' %s", policyModel.getName(), opDescription);
}
try {
op.run(policy);
} catch (ClientRegistrationPolicyException crpe) {
provider.getEvent().detail(Details.CLIENT_REGISTRATION_POLICY, policyModel.getName());
crpe.setPolicyModel(policyModel);
logger.warnf("Operation '%s' rejected. %s", opDescription, crpe.getMessage());
ServicesLogger.LOGGER.clientRegistrationRequestRejected(opDescription, crpe.getMessage());
throw crpe;
}
}

View file

@ -33,6 +33,8 @@ 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.MaxClientsClientRegistrationPolicy;
import org.keycloak.services.clientregistration.policy.impl.MaxClientsClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.ProtocolMappersClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.ScopeClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
@ -87,9 +89,13 @@ public class DefaultClientRegistrationPolicies {
ComponentModel consentRequiredModel = createModelInstance("Consent Required", realm, ConsentRequiredClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
realm.addComponentModel(consentRequiredModel);
ComponentModel scopeModel =createModelInstance("Full Scope Disabled", realm, ScopeClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
ComponentModel scopeModel = createModelInstance("Full Scope Disabled", realm, ScopeClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
realm.addComponentModel(scopeModel);
ComponentModel maxClientsModel = createModelInstance("Max Clients Limit", realm, MaxClientsClientRegistrationPolicyFactory.PROVIDER_ID, policyTypeKey);
maxClientsModel.put(MaxClientsClientRegistrationPolicyFactory.MAX_CLIENTS, MaxClientsClientRegistrationPolicyFactory.DEFAULT_MAX_CLIENTS);
realm.addComponentModel(maxClientsModel);
addGenericPolicies(realm, policyTypeKey);
}

View file

@ -0,0 +1,72 @@
/*
* 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 ClientDisabledClientRegistrationPolicy implements ClientRegistrationPolicy {
@Override
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
}
@Override
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
clientModel.setEnabled(false);
}
@Override
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
if (context.getClient().isEnabled() == null) {
return;
}
if (clientModel == null) {
return;
}
boolean isEnabled = clientModel.isEnabled();
boolean newEnabled = context.getClient().isEnabled();
if (!isEnabled && newEnabled) {
throw new ClientRegistrationPolicyException("Not permitted to enable client");
}
}
@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,56 @@
/*
* 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 ClientDisabledClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
public static final String PROVIDER_ID = "client-disabled";
@Override
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
return new ClientDisabledClientRegistrationPolicy();
}
@Override
public String getHelpText() {
return "When present, then newly registered client will be disabled and admin needs to manually enable them";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -35,8 +35,6 @@ import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyE
*/
public class ClientTemplatesClientRegistrationPolicy implements ClientRegistrationPolicy {
private static final Logger logger = Logger.getLogger(ClientTemplatesClientRegistrationPolicy.class);
private final KeycloakSession session;
private final ComponentModel componentModel;

View file

@ -0,0 +1,77 @@
/*
* 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.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;
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 MaxClientsClientRegistrationPolicy implements ClientRegistrationPolicy {
private final KeycloakSession session;
private final ComponentModel componentModel;
public MaxClientsClientRegistrationPolicy(KeycloakSession session, ComponentModel componentModel) {
this.session = session;
this.componentModel = componentModel;
}
@Override
public void beforeRegister(ClientRegistrationContext context) throws ClientRegistrationPolicyException {
RealmModel realm = session.getContext().getRealm();
int currentCount = realm.getClients().size();
int maxCount = componentModel.get(MaxClientsClientRegistrationPolicyFactory.MAX_CLIENTS, MaxClientsClientRegistrationPolicyFactory.DEFAULT_MAX_CLIENTS);
if (currentCount >= maxCount) {
throw new ClientRegistrationPolicyException("It's allowed to have max " + maxCount + " clients per realm");
}
}
@Override
public void afterRegister(ClientRegistrationContext context, ClientModel clientModel) {
}
@Override
public void beforeUpdate(ClientRegistrationContext context, ClientModel clientModel) throws ClientRegistrationPolicyException {
}
@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,79 @@
/*
* 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 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 MaxClientsClientRegistrationPolicyFactory extends AbstractClientRegistrationPolicyFactory {
public static final String MAX_CLIENTS = "max-clients";
public static final ProviderConfigProperty MAX_CLIENTS_PROPERTY = new ProviderConfigProperty();
public static final int DEFAULT_MAX_CLIENTS = 200;
private static List<ProviderConfigProperty> configProperties = new LinkedList<>();
static {
MAX_CLIENTS_PROPERTY.setName(MAX_CLIENTS);
MAX_CLIENTS_PROPERTY.setLabel("max-clients.label");
MAX_CLIENTS_PROPERTY.setHelpText("max-clients.tooltip");
MAX_CLIENTS_PROPERTY.setType(ProviderConfigProperty.STRING_TYPE);
MAX_CLIENTS_PROPERTY.setDefaultValue(String.valueOf(DEFAULT_MAX_CLIENTS));
configProperties.add(MAX_CLIENTS_PROPERTY);
}
public static final String PROVIDER_ID = "max-clients";
@Override
public ClientRegistrationPolicy create(KeycloakSession session, ComponentModel model) {
return new MaxClientsClientRegistrationPolicy(session, model);
}
@Override
public String getHelpText() {
return "When present, then it won't be allowed to register new client if count of existing clients in realm is same or bigger than configured limit";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
ConfigurationValidationHelper.check(config)
.checkInt(MAX_CLIENTS_PROPERTY, true);
}
}

View file

@ -18,17 +18,15 @@
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.ServicesLogger;
import org.keycloak.services.clientregistration.ClientRegistrationContext;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
@ -66,7 +64,7 @@ public class ProtocolMappersClientRegistrationPolicy implements ClientRegistrati
String mapperType = mapper.getProtocolMapper();
if (!allowedMapperProviders.contains(mapperType)) {
logger.warnf("ProtocolMapper '%s' of type '%s' not allowed", mapper.getName(), mapperType);
ServicesLogger.LOGGER.clientRegistrationMapperNotAllowed(mapper.getName(), mapperType);
throw new ClientRegistrationPolicyException("ProtocolMapper type not allowed");
}
}
@ -75,8 +73,7 @@ public class ProtocolMappersClientRegistrationPolicy implements ClientRegistrati
protected void enableConsentRequiredForAll(ClientModel clientModel) {
if (isConsentRequiredForMappers()) {
// TODO: Debug
logger.infof("Enable consentRequired for all protocol mappers of client %s", clientModel.getClientId());
logger.debugf("Enable consentRequired for all protocol mappers of client %s", clientModel.getClientId());
Set<ProtocolMapperModel> mappers = clientModel.getProtocolMappers();
@ -105,8 +102,7 @@ public class ProtocolMappersClientRegistrationPolicy implements ClientRegistrati
}).forEach((ProtocolMapperModel mapperToRemove) -> {
// TODO: debug
logger.infof("Removing builtin mapper '%s' of type '%s' as type is not permitted", mapperToRemove.getName(), mapperToRemove.getProtocolMapper());
logger.debugf("Removing builtin mapper '%s' of type '%s' as type is not permitted", mapperToRemove.getName(), mapperToRemove.getProtocolMapper());
clientModel.removeProtocolMapper(mapperToRemove);
});

View file

@ -32,6 +32,7 @@ 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.ServicesLogger;
import org.keycloak.services.clientregistration.ClientRegistrationContext;
import org.keycloak.services.clientregistration.ClientRegistrationProvider;
import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy;
@ -95,8 +96,7 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
String hostAddress = session.getContext().getConnection().getRemoteAddr();
// TODO: debug
logger.infof("Verifying remote host : %s", hostAddress);
logger.debugf("Verifying remote host : %s", hostAddress);
List<String> trustedHosts = getTrustedHosts();
List<String> trustedDomains = getTrustedDomains();
@ -113,7 +113,7 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
return;
}
logger.warnf("Failed to verify remote host : %s", hostAddress);
ServicesLogger.LOGGER.failedToVerifyRemoteHost(hostAddress);
throw new ClientRegistrationPolicyException("Host not trusted.");
}
@ -154,7 +154,7 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
return confHostName;
}
} catch (UnknownHostException uhe) {
logger.warnf("Unknown host from realm configuration: %s", confHostName);
logger.debugf(uhe, "Unknown host from realm configuration: %s", confHostName);
}
}
@ -167,8 +167,7 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
try {
String hostname = InetAddress.getByName(hostAddress).getHostName();
// TODO: Debug
logger.infof("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname);
logger.debugf("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname);
for (String confDomain : trustedDomains) {
if (hostname.endsWith(confDomain)) {
@ -177,7 +176,7 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
}
}
} catch (UnknownHostException uhe) {
logger.warnf("Request of address '%s' came from unknown host. Skip verification by domains", hostAddress);
logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains", hostAddress);
}
}
@ -237,11 +236,11 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
}
}
} catch (MalformedURLException mfe) {
logger.warnf("URL '%s' is malformed", url);
logger.debugf(mfe, "URL '%s' is malformed", url);
throw new ClientRegistrationPolicyException("URL is malformed");
}
logger.warnf("URL '%s' doesn't match any trustedHost or trustedDomain", url);
ServicesLogger.LOGGER.urlDoesntMatch(url);
throw new ClientRegistrationPolicyException("URL doesn't match any trusted host or trusted domain");
}

View file

@ -1,180 +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.services.resources.admin;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientRegistrationTrustedHostModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
import org.keycloak.services.ErrorResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationTrustedHostResource {
private final RealmAuth auth;
private final RealmModel realm;
private final AdminEventBuilder adminEvent;
@Context
protected KeycloakSession session;
@Context
protected UriInfo uriInfo;
public ClientRegistrationTrustedHostResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
this.auth = auth;
this.realm = realm;
this.adminEvent = adminEvent.resource(ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
auth.init(RealmAuth.Resource.CLIENT);
}
/**
* Create a new initial access token.
*
* @param config
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(ClientRegistrationTrustedHostRepresentation config) {
auth.requireManage();
if (config.getHostName() == null) {
return ErrorResponse.error("hostName not provided in config", Response.Status.BAD_REQUEST);
}
int count = config.getCount() != null ? config.getCount() : 1;
try {
ClientRegistrationTrustedHostModel clientRegTrustedHostModel = session.sessions().createClientRegistrationTrustedHostModel(realm, config.getHostName(), count);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientRegTrustedHostModel.getHostName()).representation(config).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(clientRegTrustedHostModel.getHostName()).build()).build();
} catch (ModelDuplicateException mde) {
return ErrorResponse.exists(mde.getMessage());
}
}
/**
* Update a new initial access token.
*
* @param config
* @return
*/
@PUT
@Path("{hostname}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response update(final @PathParam("hostname") String hostName, ClientRegistrationTrustedHostRepresentation config) {
auth.requireManage();
if (config.getHostName() == null || !hostName.equals(config.getHostName())) {
return ErrorResponse.error("hostName not provided in config or not compatible", Response.Status.BAD_REQUEST);
}
if (config.getCount() == null) {
return ErrorResponse.error("count needs to be available", Response.Status.BAD_REQUEST);
}
if (config.getRemainingCount() != null && config.getRemainingCount() > config.getCount()) {
return ErrorResponse.error("remainingCount can't be bigger than count", Response.Status.BAD_REQUEST);
}
ClientRegistrationTrustedHostModel hostModel = session.sessions().getClientRegistrationTrustedHostModel(realm, config.getHostName());
if (hostModel == null) {
return ErrorResponse.error("hostName record not found", Response.Status.NOT_FOUND);
}
hostModel.setCount(config.getCount());
hostModel.setRemainingCount(config.getRemainingCount());
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(config).success();
return Response.noContent().build();
}
/**
* Get an initial access token.
*
* @param hostName
* @return
*/
@GET
@Path("{hostname}")
@Produces(MediaType.APPLICATION_JSON)
public ClientRegistrationTrustedHostRepresentation getConfig(final @PathParam("hostname") String hostName) {
auth.requireView();
ClientRegistrationTrustedHostModel hostModel = session.sessions().getClientRegistrationTrustedHostModel(realm, hostName);
if (hostModel == null) {
throw new NotFoundException("hostName record not found");
}
return wrap(hostModel);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<ClientRegistrationTrustedHostRepresentation> list() {
auth.requireView();
List<ClientRegistrationTrustedHostModel> models = session.sessions().listClientRegistrationTrustedHosts(realm);
List<ClientRegistrationTrustedHostRepresentation> reps = new LinkedList<>();
for (ClientRegistrationTrustedHostModel m : models) {
ClientRegistrationTrustedHostRepresentation r = wrap(m);
reps.add(r);
}
return reps;
}
@DELETE
@Path("{hostname}")
public void delete(final @PathParam("hostname") String hostName) {
auth.requireManage();
session.sessions().removeClientRegistrationTrustedHostModel(realm, hostName);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
}
private ClientRegistrationTrustedHostRepresentation wrap(ClientRegistrationTrustedHostModel model) {
return ClientRegistrationTrustedHostRepresentation.create(model.getHostName(), model.getCount(), model.getRemainingCount());
}
}

View file

@ -28,6 +28,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@ -43,9 +44,14 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -116,7 +122,7 @@ public class ComponentResource {
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(rep).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
} catch (ComponentValidationException e) {
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
return localizedErrorResponse(e);
}
}
@ -147,7 +153,7 @@ public class ComponentResource {
realm.updateComponent(model);
return Response.noContent().build();
} catch (ComponentValidationException e) {
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
return localizedErrorResponse(e);
}
}
@ -164,6 +170,22 @@ public class ComponentResource {
}
private Response localizedErrorResponse(ComponentValidationException cve) {
Properties messages = AdminRoot.getMessages(session, realm, "admin-messages", auth.getAuth().getToken().getLocale());
Object[] localizedParameters = cve.getParameters()==null ? null : Arrays.asList(cve.getParameters()).stream().map((Object parameter) -> {
if (parameter instanceof String) {
String paramStr = (String) parameter;
return messages.getProperty(paramStr, paramStr);
} else {
return parameter;
}
}).toArray();
String message = MessageFormat.format(messages.getProperty(cve.getMessage(), cve.getMessage()), localizedParameters);
return ErrorResponse.error(message, Response.Status.BAD_REQUEST);
}
}

View file

@ -19,4 +19,6 @@ org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrati
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
org.keycloak.services.clientregistration.policy.impl.ScopeClientRegistrationPolicyFactory
org.keycloak.services.clientregistration.policy.impl.ClientDisabledClientRegistrationPolicyFactory
org.keycloak.services.clientregistration.policy.impl.MaxClientsClientRegistrationPolicyFactory

View file

@ -65,7 +65,7 @@ public class ComponentsTest extends AbstractAdminTest {
try {
createComponent(rep);
} catch (WebApplicationException e) {
assertErrror(e.getResponse(), "Required is required");
assertErrror(e.getResponse(), "'Required' is required");
}
rep.getConfig().putSingle("required", "Required");
@ -75,7 +75,7 @@ public class ComponentsTest extends AbstractAdminTest {
try {
createComponent(rep);
} catch (WebApplicationException e) {
assertErrror(e.getResponse(), "Number should be a number");
assertErrror(e.getResponse(), "'Number' should be a number");
}
}

View file

@ -3,7 +3,6 @@ package org.keycloak.testsuite.cli.registration;
import org.junit.Assert;
import org.junit.Before;
import org.keycloak.admin.client.resource.ClientInitialAccessResource;
import org.keycloak.admin.client.resource.ClientRegistrationTrustedHostResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
@ -12,7 +11,6 @@ import org.keycloak.client.registration.cli.config.FileConfigHandler;
import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -27,7 +25,6 @@ import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

View file

@ -51,7 +51,9 @@ 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.ClientDisabledClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.ClientTemplatesClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.MaxClientsClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.ProtocolMappersClientRegistrationPolicyFactory;
import org.keycloak.services.clientregistration.policy.impl.TrustedHostClientRegistrationPolicyFactory;
import org.keycloak.testsuite.Assert;
@ -269,6 +271,61 @@ public class ClientRegistrationPoliciesTest extends AbstractClientRegistrationTe
}
@Test
public void testClientDisabledPolicy() throws Exception {
setTrustedHost("localhost", getPolicyAnon());
// Assert new client is enabled
OIDCClientRepresentation client = create();
String clientId = client.getClientId();
ClientRepresentation clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
Assert.assertTrue(clientRep.isEnabled());
// Add client-disabled policy
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName("Clients disabled");
rep.setParentId(REALM_NAME);
rep.setProviderId(ClientDisabledClientRegistrationPolicyFactory.PROVIDER_ID);
rep.setProviderType(ClientRegistrationPolicy.class.getName());
rep.setSubType(getPolicyAnon());
realmResource().components().add(rep);
// Assert new client is disabled
client = create();
clientId = client.getClientId();
clientRep = ApiUtil.findClientByClientId(realmResource(), clientId).toRepresentation();
Assert.assertFalse(clientRep.isEnabled());
// Try enable client. Should fail
clientRep.setEnabled(true);
assertFail(ClientRegOp.UPDATE, clientRep, 403, "Not permitted to enable client");
// Try update disabled client. Should pass
clientRep.setEnabled(false);
reg.update(clientRep);
}
@Test
public void testMaxClientsPolicy() throws Exception {
setTrustedHost("localhost", getPolicyAnon());
int clientsCount = realmResource().clients().findAll().size();
int newClientsLimit = clientsCount + 1;
// Allow to create one more client to current limit
ComponentRepresentation maxClientsPolicyRep = findPolicyByProviderAndAuth(MaxClientsClientRegistrationPolicyFactory.PROVIDER_ID, getPolicyAnon());
maxClientsPolicyRep.getConfig().putSingle(MaxClientsClientRegistrationPolicyFactory.MAX_CLIENTS, String.valueOf(newClientsLimit));
realmResource().components().component(maxClientsPolicyRep.getId()).update(maxClientsPolicyRep);
// I can register one new client
OIDCClientRepresentation client = create();
// I can't register more clients
assertOidcFail(ClientRegOp.CREATE, createRepOidc(), 403, "It's allowed to have max " + newClientsLimit + " clients per realm");
}
@Test
public void testProviders() throws Exception {
List<ComponentTypeRepresentation> reps = realmResource().clientRegistrationPolicy().getProviders();

View file

@ -0,0 +1,54 @@
/*
* 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 org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientRegistrationTester {
public static void main(String[] args) throws ClientRegistrationException {
ClientRepresentation rep = createRep1();
ClientRegistration reg = ClientRegistration.create().url("http://localhost:8081/auth", "test").build();
try {
ClientRepresentation createdRep = reg.create(rep);
System.out.println("Created client: " + createdRep.getClientId());
} catch (ClientRegistrationException ex) {
HttpErrorException httpEx = (HttpErrorException) ex.getCause();
System.err.println("HttpException when registering client. Status=" + httpEx.getStatusLine().getStatusCode() + ", Details=" + httpEx.getErrorResponse());
}
}
private static ClientRepresentation createRep1() {
ClientRepresentation rep = new ClientRepresentation();
rep.setRedirectUris(Arrays.asList("http://localhost:8080/app"));
rep.setDefaultRoles(new String[] { "foo-role" });
return rep;
}
}

View file

@ -31,7 +31,6 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;

View file

@ -144,7 +144,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.PRIORITY_KEY, "invalid");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Priority should be a number");
assertErrror(response, "'Priority' should be a number");
}
@Test
@ -156,7 +156,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.ENABLED_KEY, "invalid");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Enabled should be 'true' or 'false'");
assertErrror(response, "'Enabled' should be 'true' or 'false'");
}
@Test
@ -168,7 +168,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.ACTIVE_KEY, "invalid");
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Active should be 'true' or 'false'");
assertErrror(response, "'Active' should be 'true' or 'false'");
}
@Test
@ -178,7 +178,7 @@ public class RsaKeyProviderTest extends AbstractKeycloakTest {
ComponentRepresentation rep = createRep("invalid", RsaKeyProviderFactory.ID);
Response response = adminClient.realm("test").components().add(rep);
assertErrror(response, "Private RSA Key is required");
assertErrror(response, "'Private RSA Key' is required");
rep.getConfig().putSingle(Attributes.PRIVATE_KEY_KEY, "nonsense");
response = adminClient.realm("test").components().add(rep);

View file

@ -316,15 +316,6 @@ public class AdminEventPaths {
return uri.toString();
}
// CLIENT REGISTRATION TRUSTED HOSTS
public static String clientRegistrationTrustedHostPath(String hostName) {
URI uri = UriBuilder.fromUri("").path(RealmResource.class, "clientRegistrationTrustedHost")
.path(ClientInitialAccessResource.class, "delete")
.build(hostName);
return uri.toString();
}
// GROUPS
public static String groupsPath() {

View file

@ -67,4 +67,4 @@ log4j.logger.org.apache.directory.server.core=warn
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
# log4j.logger.org.keycloak.keys.infinispan=trace
log4j.logger.org.keycloak.services.clientregistration.policy=trace
log4j.logger.org.keycloak.services.clientregistration.policy=debug

View file

@ -543,6 +543,7 @@ initial-access-token.confirm.text=Please copy and paste the initial access token
no-initial-access-available=No Initial Access Tokens available
client-reg-policies=Client Registration Policies
client-reg-policy.name.tooltip=Display Name of the policy
anonymous-policies=Anonymous Access Policies
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.
auth-policies=Authenticated Access Policies
@ -561,6 +562,8 @@ 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.
max-clients.label=Max Clients Per Realm
max-clients.tooltip=It won't be allowed to register new client if count of existing clients in realm is same or bigger than configured limit.
client-templates=Client Templates
client-templates.tooltip=Client templates allow you to define common configuration that is shared between multiple clients