mirror of
https://github.com/keycloak/keycloak.git
synced 2026-10-23 19:29:23 +03:00
Require manage-realm to change realm-level default client scopes
Closes #54151 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
@@ -4,6 +4,14 @@
|
||||
Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
|
||||
In minor or patch releases, {project_name} will only introduce breaking changes to fix bugs.
|
||||
|
||||
=== Realm-level default client scopes require `manage-realm`
|
||||
// https://github.com/keycloak/keycloak/issues/50588
|
||||
|
||||
Changing the realm-level default and optional client scopes through the Admin REST API endpoints `default-default-client-scopes/{clientScopeId}` and `default-optional-client-scopes/{clientScopeId}` now requires the `manage-realm` role.
|
||||
Previously `manage-clients` was sufficient, even though these lists determine which client scopes every newly created client in the realm receives.
|
||||
Administrators who hold only `manage-clients` can still create, update, and delete individual client scopes, and can still read the realm-level lists; they can no longer change which of them are realm defaults.
|
||||
If you have automation that manages realm default client scopes with a `manage-clients` account, grant it `manage-realm` or run it with an account that already has it.
|
||||
|
||||
=== Deleting required client scopes
|
||||
// https://github.com/keycloak/keycloak/issues/51248
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import type { Row } from "../clients/scopes/ClientScopes";
|
||||
import { useAlerts } from "@keycloak/keycloak-ui-shared";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
import {
|
||||
ClientScope,
|
||||
allClientScopeTypes,
|
||||
@@ -34,6 +35,7 @@ export const ChangeTypeDropdown = ({
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
return (
|
||||
<Select
|
||||
@@ -43,7 +45,10 @@ export const ChangeTypeDropdown = ({
|
||||
toggle={(ref) => (
|
||||
<MenuToggle
|
||||
id="change-type-dropdown"
|
||||
isDisabled={selectedRows.length === 0}
|
||||
isDisabled={
|
||||
selectedRows.length === 0 ||
|
||||
(!clientId && !hasAccess("manage-realm"))
|
||||
}
|
||||
ref={ref}
|
||||
onClick={() => setOpen(!open)}
|
||||
isExpanded={open}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { Action, KeycloakDataTable } from "@keycloak/keycloak-ui-shared";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import helpUrls from "../help-urls";
|
||||
import { emptyFormatter } from "../util";
|
||||
@@ -58,12 +59,14 @@ const TypeSelector = (scope: TypeSelectorProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
return (
|
||||
<CellDropdown
|
||||
clientScope={scope}
|
||||
type={scope.type}
|
||||
all
|
||||
isDisabled={!hasAccess("manage-realm")}
|
||||
onSelect={async (value) => {
|
||||
try {
|
||||
await changeScope(adminClient, scope, value as AllClientScopeType);
|
||||
|
||||
@@ -109,14 +109,21 @@ export default function EditClientScope() {
|
||||
const eventsTab = useRoutableTab(toClientScope({ realm, id, tab: "events" }));
|
||||
|
||||
const onSubmit = async (formData: ClientScopeDefaultOptionalType) => {
|
||||
const clientScope = convertFormValuesToObject({
|
||||
const updatedScope = convertFormValuesToObject({
|
||||
...formData,
|
||||
name: formData.name?.trim().replace(/ /g, "_"),
|
||||
});
|
||||
|
||||
try {
|
||||
await adminClient.clientScopes.update({ id }, clientScope);
|
||||
await changeScope(adminClient, { ...clientScope, id }, clientScope.type);
|
||||
await adminClient.clientScopes.update({ id }, updatedScope);
|
||||
if (updatedScope.type !== clientScope?.type) {
|
||||
await changeScope(
|
||||
adminClient,
|
||||
{ ...updatedScope, id, type: clientScope!.type },
|
||||
updatedScope.type,
|
||||
);
|
||||
setClientScope((prev) => (prev ? { ...prev, ...updatedScope } : prev));
|
||||
}
|
||||
|
||||
addAlert(t("updateSuccessClientScope"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ClientScopeDefaultOptionalType,
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
import { FormAccess } from "../../components/form/FormAccess";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import {
|
||||
useLoginProviders,
|
||||
@@ -198,9 +199,13 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
const isParameterizedScopeWithFeatureDisabled =
|
||||
!isParameterizedScopesEnabled &&
|
||||
clientScope?.attributes?.["is.parameterized.scope"] === "true";
|
||||
const scopeTypeOptions = isParameterized
|
||||
? allClientScopeTypes.filter((key) => key !== "default")
|
||||
: allClientScopeTypes;
|
||||
const { hasAccess } = useAccess();
|
||||
const canManageRealm = hasAccess("manage-realm");
|
||||
const scopeTypeOptions = allClientScopeTypes.filter((key) => {
|
||||
if (isParameterized && key === "default") return false;
|
||||
if (!canManageRealm && !clientScope) return key === "none";
|
||||
return true;
|
||||
});
|
||||
|
||||
const selectedProtocol = useWatch({
|
||||
control,
|
||||
@@ -537,6 +542,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
name="type"
|
||||
label={t("type")}
|
||||
labelIcon={t("scopeTypeHelp")}
|
||||
isDisabled={!canManageRealm && !!clientScope}
|
||||
controller={{ defaultValue: allClientScopeTypes[0] }}
|
||||
options={scopeTypeOptions.map((key) => ({
|
||||
key,
|
||||
|
||||
@@ -287,7 +287,9 @@ public class RealmAdminResource {
|
||||
}
|
||||
|
||||
private void addDefaultClientScope(String clientScopeId, boolean defaultScope) {
|
||||
auth.clients().requireManageClientScopes();
|
||||
// Realm-wide configuration affecting every newly created client, so manage-realm is required
|
||||
// rather than the manage-clients level needed to manage an individual client scope.
|
||||
auth.realm().requireManageRealm();
|
||||
|
||||
ClientScopeModel clientScope = realm.getClientScopeById(clientScopeId);
|
||||
if (clientScope == null) {
|
||||
@@ -318,7 +320,8 @@ public class RealmAdminResource {
|
||||
@APIResponse(responseCode = "404", description = "Not Found")
|
||||
})
|
||||
public void removeDefaultDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) {
|
||||
auth.clients().requireManageClientScopes();
|
||||
// See addDefaultClientScope - realm-wide configuration requires manage-realm.
|
||||
auth.realm().requireManageRealm();
|
||||
|
||||
ClientScopeModel clientScope = realm.getClientScopeById(clientScopeId);
|
||||
if (clientScope == null) {
|
||||
|
||||
@@ -294,6 +294,49 @@ public class PermissionsTest extends AbstractPermissionsTest {
|
||||
clients.get(AdminRoles.QUERY_USERS), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The realm-level default and optional client scopes determine which client scopes every newly created
|
||||
* client receives, so changing them requires manage-realm. Managing an individual client scope still only
|
||||
* requires manage-clients, and reading the realm-level lists still only requires view access on clients.
|
||||
*/
|
||||
@Test
|
||||
public void realmDefaultClientScopes() {
|
||||
ClientScopeRepresentation scope = new ClientScopeRepresentation();
|
||||
scope.setName("realm-default-scope-permissions-test");
|
||||
scope.setProtocol("openid-connect");
|
||||
String scopeId = ApiUtil.getCreatedId(managedRealm1.admin().clientScopes().create(scope));
|
||||
managedRealm1.cleanup().add(r -> r.clientScopes().get(scopeId).remove());
|
||||
|
||||
Invocation addDefault = realm -> realm.addDefaultDefaultClientScope(scopeId);
|
||||
Invocation removeDefault = realm -> realm.removeDefaultDefaultClientScope(scopeId);
|
||||
Invocation addOptional = realm -> realm.addDefaultOptionalClientScope(scopeId);
|
||||
Invocation removeOptional = realm -> realm.removeDefaultOptionalClientScope(scopeId);
|
||||
|
||||
// manage-clients is enough to manage the client scope itself, but not to make it a realm default
|
||||
invoke(addDefault, clients.get("none"), false);
|
||||
invoke(addDefault, clients.get("master-none"), false);
|
||||
invoke(addDefault, clients.get(AdminRoles.VIEW_REALM), false);
|
||||
invoke(addDefault, clients.get(AdminRoles.MANAGE_CLIENTS), false);
|
||||
invoke(removeDefault, clients.get(AdminRoles.MANAGE_CLIENTS), false);
|
||||
invoke(addOptional, clients.get(AdminRoles.MANAGE_CLIENTS), false);
|
||||
invoke(removeOptional, clients.get(AdminRoles.MANAGE_CLIENTS), false);
|
||||
|
||||
invoke(addDefault, clients.get(AdminRoles.MANAGE_REALM), true);
|
||||
invoke(removeDefault, clients.get(AdminRoles.MANAGE_REALM), true);
|
||||
invoke(addOptional, clients.get(AdminRoles.MANAGE_REALM), true);
|
||||
invoke(removeOptional, clients.get(AdminRoles.MANAGE_REALM), true);
|
||||
|
||||
invoke(addDefault, clients.get(AdminRoles.REALM_ADMIN), true);
|
||||
invoke(removeDefault, clients.get(AdminRoles.REALM_ADMIN), true);
|
||||
invoke(addDefault, clients.get("master-admin"), true);
|
||||
invoke(removeDefault, clients.get("master-admin"), true);
|
||||
|
||||
// reading the realm-level lists is unchanged
|
||||
invoke(realm -> realm.getDefaultDefaultClientScopes(), clients.get(AdminRoles.VIEW_CLIENTS), true);
|
||||
invoke(realm -> realm.getDefaultOptionalClientScopes(), clients.get(AdminRoles.VIEW_CLIENTS), true);
|
||||
invoke(realm -> realm.getDefaultDefaultClientScopes(), clients.get(AdminRoles.MANAGE_CLIENTS), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientInitialAccess() {
|
||||
invoke(realm -> realm.clientInitialAccess().list(), Resource.CLIENT, false);
|
||||
|
||||
Reference in New Issue
Block a user