mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
Disable the Account UI when the ACCOUNT feature is disabled (#48807)
Closes #48806 Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
@@ -14,12 +14,18 @@ import { useRealm } from "./context/realm-context/RealmContext";
|
|||||||
import { toDashboard } from "./dashboard/routes/Dashboard";
|
import { toDashboard } from "./dashboard/routes/Dashboard";
|
||||||
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
|
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
|
||||||
import { joinPath } from "./utils/joinPath";
|
import { joinPath } from "./utils/joinPath";
|
||||||
|
import { useIsFeatureDisabled, Feature } from "./utils/useIsFeatureEnabled";
|
||||||
import useToggle from "./utils/useToggle";
|
import useToggle from "./utils/useToggle";
|
||||||
|
|
||||||
const ManageAccountDropdownItem = () => {
|
const ManageAccountDropdownItem = () => {
|
||||||
const { keycloak } = useEnvironment();
|
const { keycloak } = useEnvironment();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isFeatureDisabled = useIsFeatureDisabled();
|
||||||
|
|
||||||
|
if (isFeatureDisabled(Feature.AccountV3)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="manage account"
|
key="manage account"
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { unversionedName } from "./useIsFeatureEnabled";
|
||||||
|
|
||||||
|
describe("unversionedName", () => {
|
||||||
|
it("strips version suffix from feature names", () => {
|
||||||
|
expect(unversionedName("ACCOUNT_V3")).toBe("ACCOUNT");
|
||||||
|
expect(unversionedName("ADMIN_FINE_GRAINED_AUTHZ_V2")).toBe(
|
||||||
|
"ADMIN_FINE_GRAINED_AUTHZ",
|
||||||
|
);
|
||||||
|
expect(unversionedName("TOKEN_EXCHANGE_STANDARD_V2")).toBe(
|
||||||
|
"TOKEN_EXCHANGE_STANDARD",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns name unchanged when there is no version suffix", () => {
|
||||||
|
expect(unversionedName("ACCOUNT_API")).toBe("ACCOUNT_API");
|
||||||
|
expect(unversionedName("ORGANIZATION")).toBe("ORGANIZATION");
|
||||||
|
expect(unversionedName("DPOP")).toBe("DPOP");
|
||||||
|
expect(unversionedName("CLIENT_POLICIES")).toBe("CLIENT_POLICIES");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only strips trailing version suffix", () => {
|
||||||
|
expect(unversionedName("V2_SOMETHING")).toBe("V2_SOMETHING");
|
||||||
|
expect(unversionedName("FEATURE_V2_EXTRA")).toBe("FEATURE_V2_EXTRA");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
|||||||
import { useAccess } from "../context/access/Access";
|
import { useAccess } from "../context/access/Access";
|
||||||
|
|
||||||
export enum Feature {
|
export enum Feature {
|
||||||
|
AccountV3 = "ACCOUNT_V3",
|
||||||
AdminFineGrainedAuthz = "ADMIN_FINE_GRAINED_AUTHZ",
|
AdminFineGrainedAuthz = "ADMIN_FINE_GRAINED_AUTHZ",
|
||||||
AdminFineGrainedAuthzV2 = "ADMIN_FINE_GRAINED_AUTHZ_V2",
|
AdminFineGrainedAuthzV2 = "ADMIN_FINE_GRAINED_AUTHZ_V2",
|
||||||
ClientPolicies = "CLIENT_POLICIES",
|
ClientPolicies = "CLIENT_POLICIES",
|
||||||
@@ -26,6 +27,8 @@ export enum Feature {
|
|||||||
IdentityBrokeringAPIV2 = "IDENTITY_BROKERING_API_V2",
|
IdentityBrokeringAPIV2 = "IDENTITY_BROKERING_API_V2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const unversionedName = (name: string) => name.replace(/_V\d+$/, "");
|
||||||
|
|
||||||
export default function useIsFeatureEnabled() {
|
export default function useIsFeatureEnabled() {
|
||||||
const { features } = useServerInfo();
|
const { features } = useServerInfo();
|
||||||
const { hasAccess } = useAccess();
|
const { hasAccess } = useAccess();
|
||||||
@@ -51,3 +54,16 @@ export default function useIsFeatureEnabled() {
|
|||||||
.includes(feature);
|
.includes(feature);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useIsFeatureDisabled() {
|
||||||
|
const { features } = useServerInfo();
|
||||||
|
|
||||||
|
return function isFeatureDisabled(feature: Feature) {
|
||||||
|
if (!features) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !features.some(
|
||||||
|
(f) => f.enabled && unversionedName(f.name!) === unversionedName(feature),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+9
-1
@@ -1,9 +1,17 @@
|
|||||||
package org.keycloak.services.resource;
|
package org.keycloak.services.resource;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>A factory that creates {@link AccountResourceProvider} instances.
|
* <p>A factory that creates {@link AccountResourceProvider} instances.
|
||||||
*/
|
*/
|
||||||
public interface AccountResourceProviderFactory extends ProviderFactory<AccountResourceProvider> {
|
public interface AccountResourceProviderFactory extends ProviderFactory<AccountResourceProvider>, EnvironmentDependentProviderFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default boolean isSupported(Config.Scope config) {
|
||||||
|
return Profile.isAnyVersionOfFeatureEnabled(Profile.Feature.ACCOUNT_V3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,11 +168,12 @@ public class AccountLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AccountResourceProvider getAccountResourceProvider(Theme theme) {
|
private AccountResourceProvider getAccountResourceProvider(Theme theme) {
|
||||||
try {
|
try {
|
||||||
if (theme.getProperties().containsKey(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY)) {
|
if (theme != null && theme.getProperties().containsKey(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY)) {
|
||||||
return session.getProvider(AccountResourceProvider.class, theme.getProperties().getProperty(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY));
|
return session.getProvider(AccountResourceProvider.class, theme.getProperties().getProperty(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY));
|
||||||
|
}
|
||||||
|
} catch (IOException ignore) {
|
||||||
}
|
}
|
||||||
} catch (IOException ignore) {}
|
return session.getProvider(AccountResourceProvider.class);
|
||||||
return session.getProvider(AccountResourceProvider.class);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ public class DefaultThemeManager implements ThemeManager {
|
|||||||
if (theme == null) {
|
if (theme == null) {
|
||||||
String defaultThemeName = session.getProvider(ThemeSelectorProvider.class).getDefaultThemeName(type);
|
String defaultThemeName = session.getProvider(ThemeSelectorProvider.class).getDefaultThemeName(type);
|
||||||
theme = loadTheme(defaultThemeName, type, realm);
|
theme = loadTheme(defaultThemeName, type, realm);
|
||||||
log.errorv("Failed to find {0} theme {1}, using built-in themes", type, name);
|
if (theme == null) {
|
||||||
|
log.warnv("Failed to find {0} theme {1}, as it might be unavailable because a required feature is disabled", type, name);
|
||||||
|
} else {
|
||||||
|
log.errorv("Failed to find {0} theme {1}, using built-in themes", type, name);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
theme = factory.addCachedTheme(name, type, theme);
|
theme = factory.addCachedTheme(name, type, theme);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -132,7 +132,9 @@ public class KeycloakServerConfigBuilder {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public KeycloakServerConfigBuilder featuresDisabled(Profile.Feature... features) {
|
public KeycloakServerConfigBuilder featuresDisabled(Profile.Feature... features) {
|
||||||
this.featuresDisabled.addAll(toFeatureStrings(features));
|
this.featuresDisabled.addAll(Arrays.stream(features)
|
||||||
|
.map(Profile.Feature::getUnversionedKey)
|
||||||
|
.collect(Collectors.toSet()));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.keycloak.tests.account;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.testframework.annotations.InjectHttpClient;
|
||||||
|
import org.keycloak.testframework.annotations.InjectRealm;
|
||||||
|
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||||
|
import org.keycloak.testframework.realm.ManagedRealm;
|
||||||
|
import org.keycloak.testframework.server.KeycloakServerConfig;
|
||||||
|
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||||
|
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
@KeycloakIntegrationTest(config = AccountConsoleDisabledTest.ServerConfig.class)
|
||||||
|
public class AccountConsoleDisabledTest {
|
||||||
|
|
||||||
|
@InjectRealm
|
||||||
|
ManagedRealm realm;
|
||||||
|
|
||||||
|
@InjectHttpClient
|
||||||
|
CloseableHttpClient httpClient;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accountConsoleReturns404WhenDisabled() throws IOException {
|
||||||
|
HttpGet request = new HttpGet(realm.getBaseUrl() + "/account/");
|
||||||
|
|
||||||
|
try (CloseableHttpResponse response = httpClient.execute(request)) {
|
||||||
|
assertEquals(404, response.getStatusLine().getStatusCode(),
|
||||||
|
"Account console should return 404 when ACCOUNT_V3 feature is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServerConfig implements KeycloakServerConfig {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
|
||||||
|
return config.featuresDisabled(Profile.Feature.ACCOUNT_V3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user