KEYCLOAK-14192 Client Policy - Condition : Author of a client - User Role

This commit is contained in:
Takashi Norimatsu 2020-12-04 23:42:36 +09:00 committed by Marek Posolda
parent 8b7806dbb1
commit 200b53ed1e
4 changed files with 270 additions and 5 deletions

View file

@ -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<String> expectedRoles = instantiateRolesForMatching();
if (expectedRoles == null) return false;
// user.getRoleMappingsStream() never returns null according to {@link UserModel.getRoleMappingsStream}
Set<String> 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<String> instantiateRolesForMatching() {
if (componentModel.getConfig() == null) return null;
List<String> roles = componentModel.getConfig().get(ClientUpdateSourceRolesConditionFactory.ROLES);
if (roles == null) return null;
return new HashSet<>(roles);
}
}

View file

@ -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<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
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<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}

View file

@ -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
org.keycloak.services.clientpolicy.condition.ClientUpdateSourceGroupsConditionFactory
org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesConditionFactory

View file

@ -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<String> groups) {
provider.getConfig().put(ClientUpdateSourceRolesConditionFactory.ROLES, groups);
}
private void setExecutorAugmentActivate(ComponentRepresentation provider) {
provider.getConfig().putSingle("is-augment", Boolean.TRUE.toString());
}