Client Policy - Condition : Client - Client Attribute

Closes https://github.com/keycloak/keycloak/issues/31766

Signed-off-by: Yoshiyuki Tabata <yoshiyuki.tabata.jy@hitachi.com>
This commit is contained in:
Yoshiyuki Tabata 2024-07-30 21:52:01 +09:00 committed by Marek Posolda
parent 35fbcf5af8
commit cb6eb187ac
7 changed files with 269 additions and 1 deletions

View file

@ -87,6 +87,10 @@ for marking that particular client policy for particular clients.
Client Domain Name, Host or IP Address::
Applied for specific domain names of client. Or for the cases when the administrator registers/updates client from particular Host or IP Address.
Client Attribute::
Applies to clients with the client attribute of the specified name and value. If you specify multiple client attributes, they will be evaluated using AND conditions.
If you want to evaluate using OR conditions, set this condition multiple times.
Any Client::
This condition always evaluates to true. It can be used for example to ensure that all clients in the particular realm are FAPI compliant.

View file

@ -2281,6 +2281,7 @@ search=Search
validateEditMode=You must select an edit mode
copyFlowSuccess=Flow successfully duplicated
cacheSettings=Cache settings
client-attributes.label=Client Attributes
searchForClient=Search for client
permissionDeletedError=Could not delete permission due to {{error}}
eventTypes.UPDATE_PROFILE.name=Update profile
@ -2810,6 +2811,7 @@ clientSessionIdle=Client Session Idle
push=Push
targetClaimHelp=Specifies the target claim which the policy will fetch.
periodicFullSyncHelp=Whether periodic full synchronization of LDAP users to Keycloak should be enabled or not
client-attributes-condition.tooltip=Client attributes, that will be checked during this condition evaluation. Condition evaluates to true if the client has all client attributes with the name and value as the client attributes specified in the configuration.
scopePermissions.users.user-impersonated-description=Policies that decide which users can be impersonated. These policies are applied to the user being impersonated.
forceNameIdFormat=Force name ID format
noMappersInstructions=There are currently no mappers for this identity provider.

View file

@ -0,0 +1,122 @@
/*
* Copyright 2024 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 org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.ClientPolicyVote;
import org.keycloak.services.clientpolicy.context.PreAuthorizationRequestContext;
import java.util.Map;
/**
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
*/
public class ClientAttributesCondition extends AbstractClientPolicyConditionProvider<ClientAttributesCondition.Configuration> {
private static final Logger logger = Logger.getLogger(ClientAttributesCondition.class);
public ClientAttributesCondition(KeycloakSession session) {
super(session);
}
@Override
public Class<Configuration> getConditionConfigurationClass() {
return Configuration.class;
}
public static class Configuration extends ClientPolicyConditionConfigurationRepresentation {
protected Map<String, String> attributes;
public Map<String, String> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, String> attributes) {
this.attributes = attributes;
}
}
@Override
public String getProviderId() {
return ClientAttributesConditionFactory.PROVIDER_ID;
}
@Override
public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException {
switch (context.getEvent()) {
case PRE_AUTHORIZATION_REQUEST:
PreAuthorizationRequestContext paContext = (PreAuthorizationRequestContext) context;
ClientModel client = session.getContext().getRealm().getClientByClientId(paContext.getClientId());
if (isAttributesMatched(client)) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case AUTHORIZATION_REQUEST:
case TOKEN_REQUEST:
case TOKEN_RESPONSE:
case SERVICE_ACCOUNT_TOKEN_REQUEST:
case SERVICE_ACCOUNT_TOKEN_RESPONSE:
case TOKEN_REFRESH:
case TOKEN_REFRESH_RESPONSE:
case TOKEN_REVOKE:
case TOKEN_INTROSPECT:
case USERINFO_REQUEST:
case LOGOUT_REQUEST:
case BACKCHANNEL_AUTHENTICATION_REQUEST:
case BACKCHANNEL_TOKEN_REQUEST:
case BACKCHANNEL_TOKEN_RESPONSE:
case PUSHED_AUTHORIZATION_REQUEST:
case REGISTERED:
case UPDATE:
case UPDATED:
case SAML_AUTHN_REQUEST:
case SAML_LOGOUT_REQUEST:
if (isAttributesMatched(session.getContext().getClient())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
default:
return ClientPolicyVote.ABSTAIN;
}
}
private boolean isAttributesMatched(ClientModel client) {
if (client == null) return false;
Map<String, String> attributesForMatching = getAttributesForMatching();
if (attributesForMatching == null) return false;
Map<String, String> clientAttributes = client.getAttributes();
if (logger.isTraceEnabled()) {
clientAttributes.forEach((i, j) -> logger.tracev("client attribute assigned = {0}: {1}", i, j));
attributesForMatching.forEach((i, j) -> logger.tracev("client attribute for matching = {0}: {1}", i, j));
}
return attributesForMatching.entrySet().stream()
.allMatch(entry -> clientAttributes.containsKey(entry.getKey()) && clientAttributes.get(entry.getKey()).equals(entry.getValue()));
}
private Map<String, String> getAttributesForMatching() {
if (configuration.getAttributes() == null) return null;
return configuration.getAttributes();
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2024 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 org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
*/
public class ClientAttributesConditionFactory extends AbstractClientPolicyConditionProviderFactory {
public static final String PROVIDER_ID = "client-attributes";
public static final String ATTRIBUTES = "attributes";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
static {
addCommonConfigProperties(configProperties);
ProviderConfigProperty property;
property = new ProviderConfigProperty(ATTRIBUTES, PROVIDER_ID + ".label", PROVIDER_ID + "-condition.tooltip", ProviderConfigProperty.MAP_TYPE, null);
configProperties.add(property);
}
@Override
public ClientPolicyConditionProvider create(KeycloakSession session) {
return new ClientAttributesCondition(session);
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "The condition checks whether all of the specified client attributes exists on the client to determine whether the policy is applied. This effectively allows client administrator to create client attribute of specified name on the client to make sure that particular client policy will be applied on requests of this client. Condition is checked during most of OpenID Connect requests (Authorization request, token requests, introspection endpoint request etc.).";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}

View file

@ -7,3 +7,4 @@ org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceGroupsConditionF
org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceRolesConditionFactory
org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory
org.keycloak.services.clientpolicy.condition.ClientProtocolConditionFactory
org.keycloak.services.clientpolicy.condition.ClientAttributesConditionFactory

View file

@ -22,6 +22,7 @@ import static org.junit.Assert.fail;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientAccessTypeConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientAttributesConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientScopesConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientUpdateSourceGroupsConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientUpdateSourceHostsConditionConfig;
@ -60,6 +61,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientAccessTypeConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientAttributesConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceGroupsConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterSourceHostsConditionFactory;
@ -382,6 +384,71 @@ public class ClientPoliciesConditionTest extends AbstractClientPoliciesTest {
}
}
@Test
public void testClientAttributesCondition() throws Exception {
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Het Eerste Profiel")
.addExecutor(PKCEEnforcerExecutorFactory.PROVIDER_ID,
createPKCEEnforceExecutorConfig(Boolean.TRUE))
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Het Eerste Beleid", Boolean.TRUE)
.addCondition(ClientAttributesConditionFactory.PROVIDER_ID,
createClientAttributesConditionConfig(new HashMap<String, String>() {
{
put("attr1", "Apple");
put("attr2", "Orange");
}
}))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
String clientAlphaId = generateSuffixedName("Alpha-App");
String clientSecret = "secret";
createClientByAdmin(clientAlphaId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setAttributes(new HashMap<String, String>() {
{
put("attr1", "Apple");
put("attr2", "Orange");
put("attr3", "Banana");
}
});
});
String clientBetaId = generateSuffixedName("Beta-App");
createClientByAdmin(clientBetaId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientSecret);
clientRep.setAttributes(new HashMap<String, String>() {
{
put("attr1", "Apple");
put("attr2", "Peach"); // attr2 is not "Orange"
put("attr3", "Banana");
}
});
});
try {
successfulLoginAndLogout(clientBetaId, clientSecret);
} catch (Exception e) {
fail();
}
try {
failLoginByNotFollowingPKCE(clientAlphaId);
successfulLoginAndLogoutWithPKCE(clientAlphaId, clientSecret, TEST_USER_NAME, TEST_USER_PASSWORD);
} catch (Exception e) {
fail();
}
}
@Test
public void testClientAccessTypeCondition() throws Exception {
// register profiles

View file

@ -45,6 +45,7 @@ import org.keycloak.representations.idm.ClientProfileRepresentation;
import org.keycloak.representations.idm.ClientProfilesRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.condition.ClientAccessTypeCondition;
import org.keycloak.services.clientpolicy.condition.ClientAttributesCondition;
import org.keycloak.services.clientpolicy.condition.ClientRolesCondition;
import org.keycloak.services.clientpolicy.condition.ClientScopesCondition;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterContextCondition;
@ -82,6 +83,7 @@ import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.junit.Assert.fail;
@ -404,6 +406,12 @@ public final class ClientPoliciesUtil {
return config;
}
public static ClientAttributesCondition.Configuration createClientAttributesConditionConfig(Map<String, String> attributes) {
ClientAttributesCondition.Configuration config = new ClientAttributesCondition.Configuration();
config.setAttributes(attributes);
return config;
}
public static ClientUpdaterContextCondition.Configuration createClientUpdateContextConditionConfig(List<String> updateClientSource) {
ClientUpdaterContextCondition.Configuration config = new ClientUpdaterContextCondition.Configuration();
config.setUpdateClientSource(updateClientSource);