8000 Disable the Account UI when the ACCOUNT feature is disabled by mabartos · Pull Request #48807 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion js/apps/admin-ui/src/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ import { useRealm } from "./context/realm-context/RealmContext";
import { toDashboard } from "./dashboard/routes/Dashboard";
import { usePreviewLogo } from "./realm-settings/themes/LogoContext";
import { joinPath } from "./utils/joinPath";
import { useIsFeatureDisabled, Feature } from "./utils/useIsFeatureEnabled";
import useToggle from "./utils/useToggle";

const ManageAccountDropdownItem = () => {
const { keycloak } = useEnvironment();

const { t } = useTranslation();
const isFeatureDisabled = useIsFeatureDisabled();

if (isFeatureDisabled(Feature.AccountV3)) {
return null;
}

return (
<DropdownItem
key="manage account"
Expand Down
26 changes: 26 additions & 0 deletions js/apps/admin-ui/src/utils/useIsFeatureEnabled.test.ts
Original file line number Diff line number Diff line change
@@ -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 8000 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");
});
});
16 changes: 16 additions & 0 deletions js/apps/admin-ui/src/utils/useIsFeatureEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { useAccess } from "../context/access/Access";

export enum Feature {
AccountV3 = "ACCOUNT_V3",
AdminFineGrainedAuthz = "ADMIN_FINE_GRAINED_AUTHZ",
AdminFineGrainedAuthzV2 = "ADMIN_FINE_GRAINED_AUTHZ_V2",
ClientPolicies = "CLIENT_POLICIES",
Expand All @@ -25,6 +26,8 @@ export enum Feature {
IdentityBrokeringAPIV2 = "IDENTITY_BROKERING_API_V2",
}

export const unversionedName = (name: string) => name.replace(/_V\d+$/, "");

export default function useIsFeatureEnabled() {
const { features } = useServerInfo();
const { hasAccess } = useAccess();
Expand All @@ -50,3 +53,16 @@ export default function useIsFeatureEnabled() {
.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),
);
};
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
package org.keycloak.services.resource;

import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderFactory;

/**
* <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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,12 @@ private ClientModel getAccountManagementClient(RealmModel realm) {
}

private AccountResourceProvider getAccountResourceProvider(Theme theme) {
try {
if (theme.getProperties().containsKey(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY)) {
return session.getProvider(AccountResourceProvider.class, theme.getProperties().getProperty(Theme.ACCOUNT_RESOURCE_PROVIDER_KEY));
try {
if (theme != null && theme.getProperties().containsKey(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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ public Theme getTheme(String name, Theme.Type type) {
if (theme == null) {
String defaultThemeName = session.getProvider(ThemeSelectorProvider.class).getDefaultThemeName(type);
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 {
theme = factory.addCachedTheme(name, type, theme);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ public KeycloakServerConfigBuilder features(Profile.Feature... features) {
* @return
*/
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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading
0