From 6e6bb42778e71460187cc09e544f1e6cde9a41fc Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:39:04 +0800 Subject: [PATCH] feat: allow theme preview for theme admins when preview is disabled (#7277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.20.x #### What this PR does / why we need it: 支持禁用主题预览功能,但拥有主题管理权限的用户不受此功能影响 #### Which issue(s) this PR fixes: Fixes #7204 #### Does this PR introduce a user-facing change? ```release-note 支持禁用主题预览功能,但拥有主题管理权限的用户不受此功能影响 ``` --- .../run/halo/app/infra/SystemSetting.java | 1 + .../authorization/AuthorityUtils.java | 2 + .../run/halo/app/theme/ThemeResolver.java | 64 +++++++++++++++++-- .../resources/extensions/system-setting.yaml | 5 ++ 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/run/halo/app/infra/SystemSetting.java b/api/src/main/java/run/halo/app/infra/SystemSetting.java index 163b709ea..1256bf429 100644 --- a/api/src/main/java/run/halo/app/infra/SystemSetting.java +++ b/api/src/main/java/run/halo/app/infra/SystemSetting.java @@ -32,6 +32,7 @@ public class SystemSetting { public static class ThemeRouteRules { public static final String GROUP = "routeRules"; + private boolean disableThemePreview; private String categories; private String archives; private String post; diff --git a/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java b/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java index 5243fba20..8814a5985 100644 --- a/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java +++ b/application/src/main/java/run/halo/app/security/authorization/AuthorityUtils.java @@ -28,6 +28,8 @@ public enum AuthorityUtils { public static final String POST_CONTRIBUTOR_ROLE_NAME = "role-template-post-contributor"; + public static final String THEME_MANAGEMENT_ROLE_NAME = "role-template-manage-themes"; + /** * Converts an array of GrantedAuthority objects to a role set. * diff --git a/application/src/main/java/run/halo/app/theme/ThemeResolver.java b/application/src/main/java/run/halo/app/theme/ThemeResolver.java index dd9b6c4c3..6fb08b6e8 100644 --- a/application/src/main/java/run/halo/app/theme/ThemeResolver.java +++ b/application/src/main/java/run/halo/app/theme/ThemeResolver.java @@ -1,14 +1,25 @@ package run.halo.app.theme; +import static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles; + +import java.util.Collection; +import java.util.Set; import lombok.AllArgsConstructor; +import lombok.Builder; import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import run.halo.app.core.user.service.RoleService; +import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting.Theme; +import run.halo.app.infra.SystemSetting.ThemeRouteRules; import run.halo.app.infra.ThemeRootGetter; +import run.halo.app.security.authorization.AuthorityUtils; /** * @author johnniang @@ -21,6 +32,7 @@ public class ThemeResolver { private final SystemConfigurableEnvironmentFetcher environmentFetcher; private final ThemeRootGetter themeRoot; + private final RoleService roleService; public Mono getThemeContext(String themeName) { Assert.hasText(themeName, "Theme name cannot be empty"); @@ -39,17 +51,17 @@ public class ThemeResolver { public Mono getTheme(ServerWebExchange exchange) { return fetchThemeFromExchange(exchange) - .switchIfEmpty(Mono.defer(() -> environmentFetcher.fetch(Theme.GROUP, Theme.class) - .map(Theme::getActive) - .switchIfEmpty( - Mono.error(() -> new IllegalArgumentException("No theme activated"))) - .map(activatedTheme -> { + .switchIfEmpty(Mono.defer(() -> fetchActivationState() + .map(themeState -> { + var activatedTheme = themeState.activatedTheme(); var builder = ThemeContext.builder(); var themeName = exchange.getRequest().getQueryParams() .getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME); - if (StringUtils.isBlank(themeName)) { + + if (StringUtils.isBlank(themeName) || !themeState.supportsPreviewTheme()) { themeName = activatedTheme; } + boolean active = StringUtils.equals(activatedTheme, themeName); var path = themeRoot.get().resolve(themeName); return builder.name(themeName) @@ -70,4 +82,44 @@ public class ThemeResolver { .cast(ThemeContext.class); } + private Mono fetchActivationState() { + var builder = ThemeActivationState.builder(); + + var activatedMono = environmentFetcher.fetch(Theme.GROUP, Theme.class) + .map(Theme::getActive) + .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("No theme activated"))) + .doOnNext(builder::activatedTheme); + + var preivewDisabledMono = environmentFetcher.fetch(ThemeRouteRules.GROUP, + ThemeRouteRules.class) + .map(ThemeRouteRules::isDisableThemePreview) + .defaultIfEmpty(false) + .doOnNext(builder::previewDisabled); + + var themeManageMono = ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(au -> !AnonymousUserConst.isAnonymousUser(au.getName())) + .flatMap(au -> supportsPreviewTheme(authoritiesToRoles(au.getAuthorities()))) + .doOnNext(builder::hasThemeManagementRole); + + return Mono.when(activatedMono, preivewDisabledMono, themeManageMono) + .then(Mono.fromSupplier(builder::build)); + } + + private Mono supportsPreviewTheme(Collection authorities) { + return roleService.contains(authorities, Set.of(AuthorityUtils.THEME_MANAGEMENT_ROLE_NAME)) + .defaultIfEmpty(false); + } + + @Builder + record ThemeActivationState(String activatedTheme, boolean previewDisabled, + boolean hasThemeManagementRole) { + + private boolean supportsPreviewTheme() { + if (hasThemeManagementRole) { + return true; + } + return !previewDisabled; + } + } } diff --git a/application/src/main/resources/extensions/system-setting.yaml b/application/src/main/resources/extensions/system-setting.yaml index 60626a124..1985f904a 100644 --- a/application/src/main/resources/extensions/system-setting.yaml +++ b/application/src/main/resources/extensions/system-setting.yaml @@ -142,6 +142,11 @@ spec: - group: routeRules label: 主题路由设置 formSchema: + - $formkit: checkbox + label: "关闭主题预览" + value: false + name: disableThemePreview + help: "关闭后,未包含主题管理权限的用户将无法通过参数预览未激活的主题" - $formkit: text label: "分类页路由前缀" value: "categories"