keycloak-uncached

Details

diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentValidationException.java b/server-spi/src/main/java/org/keycloak/component/ComponentValidationException.java
index d1d707c..09bbc53 100644
--- a/server-spi/src/main/java/org/keycloak/component/ComponentValidationException.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentValidationException.java
@@ -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;
+    }
 }
diff --git a/server-spi/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java b/server-spi/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java
index dd9561f..9a2eea8 100644
--- a/server-spi/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ConfigurationValidationHelper.java
@@ -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;
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/DefaultClientRegistrationPolicies.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/DefaultClientRegistrationPolicies.java
index a3d074f..6fefa93 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/policy/DefaultClientRegistrationPolicies.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/DefaultClientRegistrationPolicies.java
@@ -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);
     }
 
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/ClientDisabledClientRegistrationPolicy.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/ClientDisabledClientRegistrationPolicy.java
new file mode 100644
index 0000000..0fd53a3
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/ClientDisabledClientRegistrationPolicy.java
@@ -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 {
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/ClientDisabledClientRegistrationPolicyFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/ClientDisabledClientRegistrationPolicyFactory.java
new file mode 100644
index 0000000..5abf290
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/ClientDisabledClientRegistrationPolicyFactory.java
@@ -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;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicy.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicy.java
new file mode 100644
index 0000000..322bfaf
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicy.java
@@ -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 {
+
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java
new file mode 100644
index 0000000..00ebe05
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java
@@ -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);
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
index 39f152c..d45e26e 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
@@ -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 = 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);
+    }
 
 }
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory
index 652501a..78a8cc9 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory
@@ -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
\ No newline at end of file
+org.keycloak.services.clientregistration.policy.impl.ScopeClientRegistrationPolicyFactory
+org.keycloak.services.clientregistration.policy.impl.ClientDisabledClientRegistrationPolicyFactory
+org.keycloak.services.clientregistration.policy.impl.MaxClientsClientRegistrationPolicyFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationPoliciesTest.java
index 2a4ef8b..d5055fa 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationPoliciesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationPoliciesTest.java
@@ -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;
@@ -270,6 +272,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();
         Map<String, ComponentTypeRepresentation> providersMap = reps.stream().collect(Collectors.toMap((ComponentTypeRepresentation rep) -> {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTester.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTester.java
new file mode 100644
index 0000000..396842d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRegistrationTester.java
@@ -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;
+    }
+
+}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index ef8ef74..37055f2 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -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