Updating client policies in JSON editor is buggy. Attempt to update global client policies should throw the error

closes #30102

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2024-06-03 20:28:22 +02:00 committed by Marek Posolda
parent df4b6c871d
commit 0bf613782f
6 changed files with 116 additions and 3 deletions

View file

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

View file

@ -18,6 +18,7 @@
package org.keycloak.representations.idm; package org.keycloak.representations.idm;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* Client Policy's external representation class * Client Policy's external representation class
@ -72,4 +73,16 @@ public class ClientPolicyRepresentation {
this.profiles = profiles; this.profiles = profiles;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientPolicyRepresentation that = (ClientPolicyRepresentation) o;
return Objects.equals(name, that.name) && Objects.equals(description, that.description) && Objects.equals(enabled, that.enabled) && Objects.equals(conditions, that.conditions) && Objects.equals(profiles, that.profiles);
}
@Override
public int hashCode() {
return Objects.hash(name, description, enabled, conditions, profiles);
}
} }

View file

@ -1139,6 +1139,7 @@ importSuccess=New certificate imported
attributeConsumingServiceName=Attribute Consuming Service Name attributeConsumingServiceName=Attribute Consuming Service Name
invalidJsonError=Unable to save user profile, the provided information is not valid JSON. 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. invalidJsonClientProfilesError=Unable to save client profiles, the provided information is not valid JSON.
invalidJsonClientPoliciesError=Unable to save client policies, the provided information is not valid JSON.
promptHelp=Specifies whether the Authorization Server prompts the End-User for re-authentication and consent. promptHelp=Specifies whether the Authorization Server prompts the End-User for re-authentication and consent.
deleteBtn=Delete deleteBtn=Delete
defaultLocale=Default locale defaultLocale=Default locale
@ -1987,7 +1988,7 @@ allowRegexComparisonHelp=If OFF, then the Subject DN from given client certifica
eventTypes.UPDATE_TOTP_ERROR.description=Update totp error eventTypes.UPDATE_TOTP_ERROR.description=Update totp error
titleEvents=Events titleEvents=Events
signServiceProviderMetadata=Sign service provider metadata signServiceProviderMetadata=Sign service provider metadata
updateClientPoliciesError=Provided JSON is incorrect\: Unexpected token { in JSON updateClientPoliciesError=Could not update client policies\: {{error}}
acceptsPromptNoneHelp=This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt\=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt\=none will be forwarded to this identity provider. acceptsPromptNoneHelp=This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt\=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt\=none will be forwarded to this identity provider.
roleDetails=Role details roleDetails=Role details
eventTypes.USER_INFO_REQUEST.name=User info request eventTypes.USER_INFO_REQUEST.name=User info request

View file

@ -13,6 +13,7 @@ import {
Title, Title,
ToolbarItem, ToolbarItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { omit } from "lodash-es";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm, type UseFormReturn } from "react-hook-form"; import { Controller, useForm, type UseFormReturn } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -113,6 +114,9 @@ export const PoliciesTab = () => {
} }
}; };
const normalizePolicy = (policy: ClientPolicy): ClientPolicyRepresentation =>
omit(policy, "global");
const save = async () => { const save = async () => {
if (!code) { if (!code) {
return; return;
@ -121,9 +125,18 @@ export const PoliciesTab = () => {
try { try {
const obj: ClientPolicy[] = JSON.parse(code); const obj: ClientPolicy[] = JSON.parse(code);
const changedPolicies = obj
.filter((policy) => !policy.global)
.map((policy) => normalizePolicy(policy));
const changedGlobalPolicies = obj
.filter((policy) => policy.global)
.map((policy) => normalizePolicy(policy));
try { try {
await adminClient.clientPolicies.updatePolicy({ await adminClient.clientPolicies.updatePolicy({
policies: obj, policies: changedPolicies,
globalPolicies: changedGlobalPolicies,
}); });
addAlert(t("updateClientPoliciesSuccess"), AlertVariant.success); addAlert(t("updateClientPoliciesSuccess"), AlertVariant.success);
refresh(); refresh();
@ -132,7 +145,7 @@ export const PoliciesTab = () => {
} }
} catch (error) { } catch (error) {
console.warn("Invalid json, ignoring value using {}"); console.warn("Invalid json, ignoring value using {}");
addError("updateClientPoliciesError", error); addError("invalidJsonClientPoliciesError", error);
} }
}; };

View file

@ -331,6 +331,12 @@ public class ClientPoliciesUtil {
return !proposedGlobalProfiles.equals(origGlobalProfiles); return !proposedGlobalProfiles.equals(origGlobalProfiles);
} }
private static boolean isGlobalPoliciesUpdated(List<ClientPolicyRepresentation> proposedGlobalPolicies, List<ClientPolicyRepresentation> origGlobalPolicies) {
// if globalPolicies were not sent, we can skip this
if (proposedGlobalPolicies == null || proposedGlobalPolicies.isEmpty()) return false;
return !proposedGlobalPolicies.equals(origGlobalPolicies);
}
/** /**
* check whether the proposed executor's provider can be found in keycloak's ClientPolicyExecutorProvider list. * check whether the proposed executor's provider can be found in keycloak's ClientPolicyExecutorProvider list.
* not return null. * not return null.
@ -564,6 +570,11 @@ public class ClientPoliciesUtil {
throw new ClientPolicyException("realm not specified."); throw new ClientPolicyException("realm not specified.");
} }
// validate global profiles not changed
if (isGlobalPoliciesUpdated(proposedPoliciesRep.getGlobalPolicies(), existingGlobalPolicies)) {
throw new ClientPolicyException("Global policies cannot be updated");
}
ClientPoliciesRepresentation updatingPoliciesRep = new ClientPoliciesRepresentation(); ClientPoliciesRepresentation updatingPoliciesRep = new ClientPoliciesRepresentation();
List<ClientProfileRepresentation> allProfiles = new ArrayList<>(getClientProfilesRepresentation(session, realm).getProfiles()); List<ClientProfileRepresentation> allProfiles = new ArrayList<>(getClientProfilesRepresentation(session, realm).getProfiles());

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.securityprofile;
import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.hamcrest.MatcherAssert; import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
@ -35,12 +36,16 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider; import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.auth.page.login.OIDCLogin; import org.keycloak.testsuite.auth.page.login.OIDCLogin;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@SetDefaultProvider(spi = "security-profile", providerId = "default", config = {"name", "strict-security-profile"}) @SetDefaultProvider(spi = "security-profile", providerId = "default", config = {"name", "strict-security-profile"})
public class StrictSecurityProfileTest extends AbstractTestRealmKeycloakTest { public class StrictSecurityProfileTest extends AbstractTestRealmKeycloakTest {
@ -68,6 +73,50 @@ public class StrictSecurityProfileTest extends AbstractTestRealmKeycloakTest {
MatcherAssert.assertThat(error.getErrorMessage(), Matchers.containsString("duplicated as a global policy")); MatcherAssert.assertThat(error.getErrorMessage(), Matchers.containsString("duplicated as a global policy"));
} }
@Test
public void testUpdatingGlobalPoliciesNotAllowed() throws Exception {
ClientPoliciesRepresentation clientPoliciesRep = getClientPolicies();
List<ClientPolicyRepresentation> origGlobalPolicies = clientPoliciesRep.getGlobalPolicies();
// Attempt to update description of some global policy. Should fail
clientPoliciesRep = getClientPolicies();
clientPoliciesRep.getGlobalPolicies().stream()
.filter(clientPolicy -> "Saml secure client (signatures, post, https)".equals(clientPolicy.getName()))
.forEach(clientPolicy -> clientPolicy.setDescription("some new description"));
try {
updatePolicies(clientPoliciesRep);
fail();
} catch (ClientPolicyException cpe) {
assertEquals("update policies failed", cpe.getError());
}
// Attempt to add new global policy. Should fail
clientPoliciesRep = getClientPolicies();
ClientPolicyRepresentation newPolicy = new ClientPolicyRepresentation();
newPolicy.setName("new-name");
newPolicy.setDescription("desc");
clientPoliciesRep.getGlobalPolicies().add(newPolicy);
try {
updatePolicies(clientPoliciesRep);
fail();
} catch (ClientPolicyException cpe) {
assertEquals("update policies failed", cpe.getError());
}
// Attempt to update without global policies. Should be OK
clientPoliciesRep = getClientPolicies();
clientPoliciesRep.setGlobalPolicies(null);
updatePolicies(clientPoliciesRep);
// Attempt to update with global policies, but not change them. Should be OK
clientPoliciesRep = getClientPolicies();
updatePolicies(clientPoliciesRep);
// Doublecheck global policies were not changed
clientPoliciesRep = getClientPolicies();
org.keycloak.testsuite.Assert.assertEquals(origGlobalPolicies, clientPoliciesRep.getGlobalPolicies());
}
@Test @Test
public void testCreatePublicOpenIdConnectClientSecure() { public void testCreatePublicOpenIdConnectClientSecure() {
RealmResource realm = testRealm(); RealmResource realm = testRealm();
@ -197,4 +246,15 @@ public class StrictSecurityProfileTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus()); Assert.assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus());
getCleanup().addClientUuid(ApiUtil.getCreatedId(resp)); getCleanup().addClientUuid(ApiUtil.getCreatedId(resp));
} }
private ClientPoliciesRepresentation getClientPolicies() {
return adminClient.realm(TEST_REALM_NAME).clientPoliciesPoliciesResource().getPolicies(true);
}
protected void updatePolicies(ClientPoliciesRepresentation rep) throws ClientPolicyException {
try {
adminClient.realm(TEST_REALM_NAME).clientPoliciesPoliciesResource().updatePolicies(rep);
} catch (BadRequestException e) {
throw new ClientPolicyException("update policies failed", e.getResponse().getStatusInfo().toString());
}
}
} }