Editing built-in client policy profiles are silently reverted

closes #27184

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2024-05-31 19:28:17 +02:00 committed by Marek Posolda
parent 2a1b79d1fc
commit 9074696382
6 changed files with 93 additions and 8 deletions

View file

@ -18,6 +18,8 @@
package org.keycloak.representations.idm;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
@ -47,4 +49,17 @@ public class ClientPolicyExecutorRepresentation {
public void setConfiguration(JsonNode configuration) {
this.configuration = configuration;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientPolicyExecutorRepresentation that = (ClientPolicyExecutorRepresentation) o;
return Objects.equals(executorProviderId, that.executorProviderId) && Objects.equals(configuration, that.configuration);
}
@Override
public int hashCode() {
return Objects.hash(executorProviderId, configuration);
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.representations.idm;
import java.util.List;
import java.util.Objects;
/**
* Client Profile's external representation class
@ -53,4 +54,17 @@ public class ClientProfileRepresentation {
public void setExecutors(List<ClientPolicyExecutorRepresentation> executors) {
this.executors = executors;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientProfileRepresentation that = (ClientProfileRepresentation) o;
return Objects.equals(name, that.name) && Objects.equals(description, that.description) && Objects.equals(executors, that.executors);
}
@Override
public int hashCode() {
return Objects.hash(name, description, executors);
}
}

View file

@ -562,7 +562,7 @@ oAuthMutual=OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled
keystore=Keystore
eventTypes.EXECUTE_ACTION_TOKEN.description=Execute action token
eventTypes.CLIENT_INFO.description=Client info
updateClientProfilesError=Provided JSON is incorrect\: Unexpected token { in JSON
updateClientProfilesError=Could not update client profiles\: {{error}}
canonicalizationHelp=Canonicalization Method for XML signatures.
authorizationHelp=Enable/Disable fine-grained authorization support for a client
sessions=Sessions
@ -1138,6 +1138,7 @@ requestObjectEncryptionHelp=JWE algorithm, which client needs to use when sendin
importSuccess=New certificate imported
attributeConsumingServiceName=Attribute Consuming Service Name
invalidJsonError=Unable to save user profile, the provided information is not valid JSON.
invalidJsonClientProfilesError=Unable to save client profiles, the provided information is not valid JSON.
promptHelp=Specifies whether the Authorization Server prompts the End-User for re-authentication and consent.
deleteBtn=Delete
defaultLocale=Default locale

View file

@ -159,7 +159,7 @@ export default function ProfilesTab() {
addError("updateClientProfilesError", error);
}
} catch (error) {
console.warn("Invalid json, ignoring value using {}");
addError("invalidJsonClientProfilesError", error);
}
};

View file

@ -294,6 +294,11 @@ public class ClientPoliciesUtil {
throw new ClientPolicyException("proposed client profile name duplicated.");
}
// validate global profiles not changed
if (isGlobalProfilesUpdated(proposedProfilesRep.getGlobalProfiles(), globalClientProfiles)) {
throw new ClientPolicyException("Global profiles cannot be updated");
}
// Conflict with any global profile is not allowed
Set<String> globalProfileNames = globalClientProfiles.stream().map(ClientProfileRepresentation::getName).collect(Collectors.toSet());
for (ClientProfileRepresentation clientProfile : proposedProfileRepList) {
@ -320,6 +325,12 @@ public class ClientPoliciesUtil {
return proposedProfilesRep;
}
private static boolean isGlobalProfilesUpdated(List<ClientProfileRepresentation> proposedGlobalProfiles, List<ClientProfileRepresentation> origGlobalProfiles) {
// if globalProfiles were not sent, we can skip this
if (proposedGlobalProfiles == null || proposedGlobalProfiles.isEmpty()) return false;
return !proposedGlobalProfiles.equals(origGlobalProfiles);
}
/**
* check whether the proposed executor's provider can be found in keycloak's ClientPolicyExecutorProvider list.
* not return null.

View file

@ -60,7 +60,7 @@ import static org.keycloak.testsuite.util.ClientPoliciesUtil.createSecureClientA
/**
* This test class is for testing loading and updating profiles and policies file of client policies.
*
*
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
@ -175,7 +175,7 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
null))
.addExecutor(PKCEEnforcerExecutorFactory.PROVIDER_ID,
createPKCEEnforceExecutorConfig(Boolean.FALSE))
.addExecutor("no-such-executor",
.addExecutor("no-such-executor",
createPKCEEnforceExecutorConfig(Boolean.TRUE))
.toRepresentation();
@ -221,6 +221,50 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
}
}
@Test
public void testUpdatingGlobalProfilesNotAllowed() throws Exception {
ClientProfilesRepresentation clientProfilesRep = adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().getProfiles(true);
List<ClientProfileRepresentation> origGlobalProfiles = clientProfilesRep.getGlobalProfiles();
// Attempt to update description of some global profile. Should fail
clientProfilesRep = adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().getProfiles(true);
clientProfilesRep.getGlobalProfiles().stream()
.filter(clientProfile -> FAPI1_BASELINE_PROFILE_NAME.equals(clientProfile.getName()))
.forEach(clientProfile -> clientProfile.setDescription("some new description"));
try {
updateProfiles(clientProfilesRep);
fail();
} catch (ClientPolicyException cpe) {
assertEquals("update profiles failed", cpe.getError());
}
// Attempt to add new global profile. Should fail
clientProfilesRep = adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().getProfiles(true);
ClientProfileRepresentation newProfile = new ClientProfileRepresentation();
newProfile.setName("new-name");
newProfile.setDescription("desc");
clientProfilesRep.getGlobalProfiles().add(newProfile);
try {
updateProfiles(clientProfilesRep);
fail();
} catch (ClientPolicyException cpe) {
assertEquals("update profiles failed", cpe.getError());
}
// Attempt to update without global profiles. Should be OK
clientProfilesRep = adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().getProfiles(true);
clientProfilesRep.setGlobalProfiles(null);
updateProfiles(clientProfilesRep);
// Attempt to update with global profiles, but not change them. Should be OK
clientProfilesRep = adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().getProfiles(true);
updateProfiles(clientProfilesRep);
// Doublecheck global profiles were not changed
clientProfilesRep = adminClient.realm(REALM_NAME).clientPoliciesProfilesResource().getProfiles(true);
Assert.assertEquals(origGlobalProfiles, clientProfilesRep.getGlobalProfiles());
}
@Test
public void testNullProfiles() throws Exception {
String beforeUpdateProfilesJson = ClientPoliciesUtil.convertClientProfilesRepresentationToJson(getProfilesWithGlobals());
@ -306,22 +350,22 @@ public class ClientPoliciesLoadUpdateTest extends AbstractClientPoliciesTest {
String beforeUpdatePoliciesJson = ClientPoliciesUtil.convertClientPoliciesRepresentationToJson(getPolicies());
// load policies
ClientPolicyRepresentation duplicatedPoliciesRep =
ClientPolicyRepresentation duplicatedPoliciesRep =
(new ClientPolicyBuilder()).createPolicy(
"builtin-duplicated-new-policy",
"builtin duplicated new policy is ignored.",
Boolean.TRUE)
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
.addCondition(ClientRolesConditionFactory.PROVIDER_ID,
createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE)))
.addProfile(FAPI1_BASELINE_PROFILE_NAME)
.toRepresentation();
ClientPolicyRepresentation loadedPolicyRep =
ClientPolicyRepresentation loadedPolicyRep =
(new ClientPolicyBuilder()).createPolicy(
"new-policy",
"duplicated profiles are ignored.",
Boolean.TRUE)
.addCondition(ClientAccessTypeConditionFactory.PROVIDER_ID,
.addCondition(ClientAccessTypeConditionFactory.PROVIDER_ID,
createClientAccessTypeConditionConfig(Arrays.asList(ClientAccessTypeConditionFactory.TYPE_PUBLIC, ClientAccessTypeConditionFactory.TYPE_BEARERONLY)))
.addProfile("lack-of-builtin-field-test-profile")
.addProfile("ordinal-test-profile")