diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientUpdateSourceRolesCondition.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientUpdateSourceRolesCondition.java new file mode 100644 index 0000000000..31e73e0494 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientUpdateSourceRolesCondition.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 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.clientpolicy.condition; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; +import org.keycloak.OAuthErrorException; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.clientpolicy.AdminClientRegisterContext; +import org.keycloak.services.clientpolicy.AdminClientUpdateContext; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.ClientPolicyLogger; +import org.keycloak.services.clientpolicy.ClientPolicyVote; +import org.keycloak.services.clientpolicy.ClientUpdateContext; +import org.keycloak.services.clientpolicy.DynamicClientRegisterContext; +import org.keycloak.services.clientpolicy.DynamicClientUpdateContext; + +public class ClientUpdateSourceRolesCondition implements ClientPolicyConditionProvider { + + private static final Logger logger = Logger.getLogger(ClientUpdateSourceRolesCondition.class); + + private final KeycloakSession session; + private final ComponentModel componentModel; + + public ClientUpdateSourceRolesCondition(KeycloakSession session, ComponentModel componentModel) { + this.session = session; + this.componentModel = componentModel; + } + + @Override + public String getName() { + return componentModel.getName(); + } + + @Override + public String getProviderId() { + return componentModel.getProviderId(); + } + + @Override + public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException { + switch (context.getEvent()) { + case REGISTER: + if (context instanceof AdminClientRegisterContext) { + return getVoteForRolesMatched(((ClientUpdateContext)context).getAuthenticatedUser()); + } else if (context instanceof DynamicClientRegisterContext) { + return getVoteForRolesMatched(((ClientUpdateContext)context).getToken()); + } else { + throw new ClientPolicyException(OAuthErrorException.SERVER_ERROR, "unexpected context type."); + } + case UPDATE: + if (context instanceof AdminClientUpdateContext) { + return getVoteForRolesMatched(((ClientUpdateContext)context).getAuthenticatedUser()); + } else if (context instanceof DynamicClientUpdateContext) { + return getVoteForRolesMatched(((ClientUpdateContext)context).getToken()); + } else { + throw new ClientPolicyException(OAuthErrorException.SERVER_ERROR, "unexpected context type."); + } + default: + return ClientPolicyVote.ABSTAIN; + } + } + + private ClientPolicyVote getVoteForRolesMatched(UserModel user) { + if (isRolesMatched(user)) return ClientPolicyVote.YES; + return ClientPolicyVote.NO; + } + + private ClientPolicyVote getVoteForRolesMatched(JsonWebToken token) { + if (token == null) return ClientPolicyVote.NO; + if(isRoleMatched(token.getSubject())) return ClientPolicyVote.YES; + return ClientPolicyVote.NO; + } + + private boolean isRoleMatched(String subjectId) { + if (subjectId == null) return false; + return isRolesMatched(session.users().getUserById(subjectId, session.getContext().getRealm())); + } + + private boolean isRolesMatched(UserModel user) { + if (user == null) return false; + + Set expectedRoles = instantiateRolesForMatching(); + if (expectedRoles == null) return false; + + // user.getRoleMappingsStream() never returns null according to {@link UserModel.getRoleMappingsStream} + Set roles = user.getRoleMappingsStream().map(RoleModel::getName).collect(Collectors.toSet()); + + if (logger.isTraceEnabled()) { + roles.stream().forEach(i -> ClientPolicyLogger.log(logger, " user role = " + i)); + expectedRoles.stream().forEach(i -> ClientPolicyLogger.log(logger, "roles expected = " + i)); + } + + RealmModel realm = session.getContext().getRealm(); + boolean isMatched = expectedRoles.stream().anyMatch(i->{ + if (realm.getRole(i) != null && user.hasRole(realm.getRole(i))) { + return true; + } + return realm.getClientsStream().anyMatch(j->{ + if (j.getRole(i) != null && user.hasRole(j.getRole(i))) { + return true; + } + return false; + }); + }); + if (isMatched) { + ClientPolicyLogger.log(logger, "role matched."); + } else { + ClientPolicyLogger.log(logger, "role unmatched."); + } + return isMatched; + } + + private Set instantiateRolesForMatching() { + if (componentModel.getConfig() == null) return null; + List roles = componentModel.getConfig().get(ClientUpdateSourceRolesConditionFactory.ROLES); + if (roles == null) return null; + return new HashSet<>(roles); + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientUpdateSourceRolesConditionFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientUpdateSourceRolesConditionFactory.java new file mode 100644 index 0000000000..df0870eb1d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/ClientUpdateSourceRolesConditionFactory.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 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.clientpolicy.condition; + +import java.util.ArrayList; +import java.util.List; + +import org.keycloak.Config.Scope; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +public class ClientUpdateSourceRolesConditionFactory implements ClientPolicyConditionProviderFactory { + + public static final String PROVIDER_ID = "clientupdatesourceroles-condition"; + public static final String ROLES = "roles"; + + private static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(ROLES, PROVIDER_ID + ".label", PROVIDER_ID + ".tooltip", ProviderConfigProperty.MULTIVALUED_STRING_TYPE, "admin"); + configProperties.add(property); + } + + @Override + public ClientPolicyConditionProvider create(KeycloakSession session, ComponentModel model) { + return new ClientUpdateSourceRolesCondition(session, model); + } + + @Override + public void init(Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "The condition checks the role of the entity who tries to create/update the client to determine whether the policy is applied."; + + } + + @Override + public List getConfigProperties() { + return configProperties; + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory index ae36a5acd3..ab3910004f 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory @@ -3,4 +3,5 @@ org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory org.keycloak.services.clientpolicy.condition.ClientUpdateSourceHostsConditionFactory -org.keycloak.services.clientpolicy.condition.ClientUpdateSourceGroupsConditionFactory \ No newline at end of file +org.keycloak.services.clientpolicy.condition.ClientUpdateSourceGroupsConditionFactory +org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesConditionFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java index 31fbf1c2d2..49dd9b7a16 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java @@ -90,6 +90,7 @@ import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvide import org.keycloak.services.clientpolicy.condition.ClientUpdateContextConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceGroupsConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceHostsConditionFactory; +import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory; import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider; @@ -1038,13 +1039,13 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { registerCondition("ClientUpdateSourceGroupsCondition", policyName); logger.info("... Registered Condition : ClientUpdateSourceGroupsCondition"); - policyName = "MyPolicy-ClientUpdateContextCondition"; + policyName = "MyPolicy-ClientUpdateSourceRolesCondition"; createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null); logger.info("... Created Policy : " + policyName); - createCondition("ClientUpdateContextCondition", ClientUpdateContextConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + createCondition("ClientUpdateSourceRolesCondition", ClientUpdateSourceRolesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { }); - registerCondition("ClientUpdateContextCondition", policyName); - logger.info("... Registered Condition : ClientUpdateContextCondition"); + registerCondition("ClientUpdateSourceRolesCondition", policyName); + logger.info("... Registered Condition : ClientUpdateSourceRolesCondition"); policyName = "MyPolicy-ClientUpdateContextCondition"; createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null); @@ -1155,6 +1156,42 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { } } + @Test + public void testUpdatingClientSourceRolesCondition() throws ClientRegistrationException, ClientPolicyException { + String policyName = "MyPolicy"; + createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null); + logger.info("... Created Policy : " + policyName); + + createCondition("ClientUpdateSourceRolesCondition", ClientUpdateSourceRolesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + setConditionUpdatingClientSourceRoles(provider, new ArrayList<>(Arrays.asList(AdminRoles.CREATE_CLIENT))); + }); + registerCondition("ClientUpdateSourceRolesCondition", policyName); + logger.info("... Registered Condition : ClientUpdateSourceRolesCondition"); + + createExecutor("SecureClientAuthEnforceExecutor", SecureClientAuthEnforceExecutorFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + setExecutorAcceptedClientAuthMethods(provider, new ArrayList<>(Arrays.asList(JWTClientAuthenticator.PROVIDER_ID))); + }); + registerExecutor("SecureClientAuthEnforceExecutor", policyName); + logger.info("... Registered Executor : SecureClientAuthEnforceExecutor"); + + String cid = null; + try { + try { + authCreateClients(); + createClientDynamically("Gourmet-App", (OIDCClientRepresentation clientRep) -> {}); + fail(); + } catch (ClientRegistrationException e) { + assertEquals("Failed to send request", e.getMessage()); + } + authManageClients(); + cid = createClientDynamically("Gourmet-App", (OIDCClientRepresentation clientRep) -> { + }); + } finally { + deleteClientByAdmin(cid); + + } + } + private AuthorizationEndpointRequestObject createValidRequestObjectForSecureRequestObjectExecutor(String clientId) throws URISyntaxException { AuthorizationEndpointRequestObject requestObject = new AuthorizationEndpointRequestObject(); requestObject.id(KeycloakModelUtils.generateId()); @@ -1643,6 +1680,10 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { provider.getConfig().put(ClientUpdateSourceGroupsConditionFactory.GROUPS, groups); } + private void setConditionUpdatingClientSourceRoles(ComponentRepresentation provider, List groups) { + provider.getConfig().put(ClientUpdateSourceRolesConditionFactory.ROLES, groups); + } + private void setExecutorAugmentActivate(ComponentRepresentation provider) { provider.getConfig().putSingle("is-augment", Boolean.TRUE.toString()); }