mirror of https://github.com/halo-dev/halo
Add support for sec:authorize attribute of Thymeleaf (#7322)
#### What type of PR is this? /kind improvement /area core /area theme /milestone 2.20.x #### What this PR does / why we need it: This PR adds support for sec:authorize attribute of Thymeleaf which is not supported yet. See https://github.com/halo-dev/halo/issues/7316 for more. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/7316 #### Does this PR introduce a user-facing change? ```release-note 完善主题模板判断用户角色等功能 ```pull/7341/head
parent
629a0f893e
commit
067e3d58e1
|
@ -10,6 +10,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||||
|
@ -44,6 +45,7 @@ import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableSpringWebSession
|
@EnableSpringWebSession
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
|
@EnableReactiveMethodSecurity
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WebServerSecurityConfig {
|
public class WebServerSecurityConfig {
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.info.BuildProperties;
|
import org.springframework.boot.info.BuildProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
||||||
import run.halo.app.theme.dialect.GeneratorMetaProcessor;
|
import run.halo.app.theme.dialect.GeneratorMetaProcessor;
|
||||||
|
@ -26,8 +27,9 @@ public class ThemeConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SpringSecurityDialect springSecurityDialect(
|
SpringSecurityDialect springSecurityDialect(
|
||||||
ServerSecurityContextRepository securityContextRepository) {
|
ServerSecurityContextRepository securityContextRepository,
|
||||||
return new HaloSpringSecurityDialect(securityContextRepository);
|
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {
|
||||||
|
return new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|
|
@ -1,18 +1,36 @@
|
||||||
package run.halo.app.theme.dialect;
|
package run.halo.app.theme.dialect;
|
||||||
|
|
||||||
import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;
|
import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;
|
||||||
|
import static org.thymeleaf.extras.springsecurity6.dialect.processor.AuthorizeAttrProcessor.ATTR_NAME;
|
||||||
|
import static org.thymeleaf.extras.springsecurity6.dialect.processor.AuthorizeAttrProcessor.ATTR_PRECEDENCE;
|
||||||
import static run.halo.app.infra.AnonymousUserConst.PRINCIPAL;
|
import static run.halo.app.infra.AnonymousUserConst.PRINCIPAL;
|
||||||
import static run.halo.app.infra.AnonymousUserConst.Role;
|
import static run.halo.app.infra.AnonymousUserConst.Role;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.security.access.expression.ExpressionUtils;
|
||||||
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextImpl;
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.util.MethodInvocationUtils;
|
||||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.thymeleaf.context.ITemplateContext;
|
||||||
|
import org.thymeleaf.engine.AttributeName;
|
||||||
|
import org.thymeleaf.extras.springsecurity6.auth.AuthUtils;
|
||||||
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
||||||
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
|
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
|
||||||
|
import org.thymeleaf.extras.springsecurity6.util.SpringVersionSpecificUtils;
|
||||||
import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils;
|
import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils;
|
||||||
|
import org.thymeleaf.model.IProcessableElementTag;
|
||||||
|
import org.thymeleaf.processor.IProcessor;
|
||||||
|
import org.thymeleaf.standard.processor.AbstractStandardConditionalVisibilityTagProcessor;
|
||||||
|
import org.thymeleaf.templatemode.TemplateMode;
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,8 +46,12 @@ public class HaloSpringSecurityDialect extends SpringSecurityDialect implements
|
||||||
|
|
||||||
private final ServerSecurityContextRepository securityContextRepository;
|
private final ServerSecurityContextRepository securityContextRepository;
|
||||||
|
|
||||||
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository) {
|
private final ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;
|
||||||
|
|
||||||
|
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository,
|
||||||
|
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {
|
||||||
this.securityContextRepository = securityContextRepository;
|
this.securityContextRepository = securityContextRepository;
|
||||||
|
this.expressionHandler = expressionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -53,4 +75,81 @@ public class HaloSpringSecurityDialect extends SpringSecurityDialect implements
|
||||||
// Just overwrite the value of the attribute
|
// Just overwrite the value of the attribute
|
||||||
getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer);
|
getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<IProcessor> getProcessors(String dialectPrefix) {
|
||||||
|
LinkedHashSet<IProcessor> processors = new LinkedHashSet<>();
|
||||||
|
processors.add(
|
||||||
|
new HaloAuthorizeAttrProcessor(TemplateMode.HTML, dialectPrefix, ATTR_NAME)
|
||||||
|
);
|
||||||
|
processors.addAll(super.getProcessors(dialectPrefix));
|
||||||
|
return processors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HaloAuthorizeAttrProcessor
|
||||||
|
extends AbstractStandardConditionalVisibilityTagProcessor {
|
||||||
|
|
||||||
|
protected HaloAuthorizeAttrProcessor(TemplateMode templateMode, String dialectPrefix,
|
||||||
|
String attrName) {
|
||||||
|
super(templateMode, dialectPrefix, attrName, ATTR_PRECEDENCE - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isVisible(ITemplateContext context, IProcessableElementTag tag,
|
||||||
|
AttributeName attributeName, String attributeValue) {
|
||||||
|
|
||||||
|
final String attrValue = (attributeValue == null ? null : attributeValue.trim());
|
||||||
|
|
||||||
|
if (attrValue == null || attrValue.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Authentication authentication = AuthUtils.getAuthenticationObject(context);
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve expr
|
||||||
|
var expr = Optional.of(attributeValue)
|
||||||
|
.filter(v -> v.startsWith("${") && v.endsWith("}"))
|
||||||
|
.map(v -> v.substring(2, v.length() - 1))
|
||||||
|
.orElse(attributeValue);
|
||||||
|
|
||||||
|
var expressionHandler = HaloSpringSecurityDialect.this.expressionHandler.getIfUnique();
|
||||||
|
if (expressionHandler == null) {
|
||||||
|
// no expression handler found
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expression = expressionHandler.getExpressionParser().parseExpression(expr);
|
||||||
|
|
||||||
|
var methodInvocation = MethodInvocationUtils.createFromClass(this,
|
||||||
|
HaloAuthorizeAttrProcessor.class,
|
||||||
|
"dummyAuthorize",
|
||||||
|
new Class[] {Authentication.class},
|
||||||
|
new Object[] {authentication}
|
||||||
|
);
|
||||||
|
var evaluationContext =
|
||||||
|
expressionHandler.createEvaluationContext(authentication, methodInvocation);
|
||||||
|
|
||||||
|
var expressionObjects = context.getExpressionObjects();
|
||||||
|
var wrappedEvolutionContext = SpringVersionSpecificUtils.wrapEvaluationContext(
|
||||||
|
evaluationContext, expressionObjects
|
||||||
|
);
|
||||||
|
|
||||||
|
return ExpressionUtils.evaluateAsBoolean(expression, wrappedEvolutionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is only used to create a method invocation for the expression parser.
|
||||||
|
*
|
||||||
|
* @param authentication authentication object
|
||||||
|
* @return result of authorization expression evaluation
|
||||||
|
*/
|
||||||
|
public Boolean dummyAuthorize(Authentication authentication) {
|
||||||
|
throw new UnsupportedOperationException("Should not be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package run.halo.app.theme.dialect;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||||
|
import static org.springframework.http.MediaType.TEXT_HTML;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
|
import org.thymeleaf.TemplateEngine;
|
||||||
|
import org.thymeleaf.context.WebContext;
|
||||||
|
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
|
||||||
|
import org.thymeleaf.spring6.SpringWebFluxTemplateEngine;
|
||||||
|
import org.thymeleaf.spring6.web.webflux.SpringWebFluxWebApplication;
|
||||||
|
import org.thymeleaf.templateresolver.StringTemplateResolver;
|
||||||
|
|
||||||
|
// @ExtendWith(MockitoExtension.class)
|
||||||
|
@SpringBootTest
|
||||||
|
class HaloSpringSecurityDialectTest {
|
||||||
|
|
||||||
|
TemplateEngine templateEngine;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ServerSecurityContextRepository securityContextRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
var haloSpringSecurityDialect =
|
||||||
|
new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);
|
||||||
|
templateEngine = new SpringWebFluxTemplateEngine();
|
||||||
|
templateEngine.addTemplateResolver(new StringTemplateResolver());
|
||||||
|
templateEngine.addDialect(haloSpringSecurityDialect);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> shouldEvaluateSecAuthorizeAttr() {
|
||||||
|
return Stream.of(
|
||||||
|
arguments(
|
||||||
|
"Evaluate sec:authorize to true when role match",
|
||||||
|
List.of("ROLE_ADMIN"),
|
||||||
|
"""
|
||||||
|
<p sec:authorize="hasRole('ROLE_ADMIN')">Admin</p>\
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
<p>Admin</p>\
|
||||||
|
"""),
|
||||||
|
arguments(
|
||||||
|
"Evaluate sec:authorize to false when role not match",
|
||||||
|
List.of("ROLE_USER"),
|
||||||
|
"""
|
||||||
|
<p sec:authorize="hasRole('ROLE_ADMIN')"></p>\
|
||||||
|
""",
|
||||||
|
"")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@MethodSource
|
||||||
|
void shouldEvaluateSecAuthorizeAttr(String name, List<String> authorities, String template,
|
||||||
|
String expected) {
|
||||||
|
var request = MockServerHttpRequest.get("/halo-sec-authorize").build();
|
||||||
|
var exchange = new MockServerWebExchange.Builder(request).build();
|
||||||
|
var webExchange = SpringWebFluxWebApplication.buildApplication(null)
|
||||||
|
.buildExchange(exchange, Locale.getDefault(), TEXT_HTML, UTF_8);
|
||||||
|
var context = new WebContext(webExchange);
|
||||||
|
var authentication = new UsernamePasswordAuthenticationToken("fake-user", "fake-credential",
|
||||||
|
AuthorityUtils.createAuthorityList(authorities));
|
||||||
|
var securityContext = new SecurityContextImpl(authentication);
|
||||||
|
context.setVariable(SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME,
|
||||||
|
securityContext);
|
||||||
|
var result = templateEngine.process(template, context);
|
||||||
|
assertEquals(expected, result);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue