KEYCLOAK-14192 Client Policy - Condition : Author of a client - User Role
This commit is contained in:
parent
8b7806dbb1
commit
200b53ed1e
4 changed files with 270 additions and 5 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue