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.Configuration;
|
||||
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.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
|
@ -44,6 +45,7 @@ import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
|||
@Configuration
|
||||
@EnableSpringWebSession
|
||||
@EnableWebFluxSecurity
|
||||
@EnableReactiveMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class WebServerSecurityConfig {
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
||||
import run.halo.app.theme.dialect.GeneratorMetaProcessor;
|
||||
|
@ -26,8 +27,9 @@ public class ThemeConfiguration {
|
|||
|
||||
@Bean
|
||||
SpringSecurityDialect springSecurityDialect(
|
||||
ServerSecurityContextRepository securityContextRepository) {
|
||||
return new HaloSpringSecurityDialect(securityContextRepository);
|
||||
ServerSecurityContextRepository securityContextRepository,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {
|
||||
return new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
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.Role;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
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.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.util.MethodInvocationUtils;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
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.util.SpringSecurityContextUtils;
|
||||
import org.thymeleaf.extras.springsecurity6.util.SpringVersionSpecificUtils;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -28,8 +46,12 @@ public class HaloSpringSecurityDialect extends SpringSecurityDialect implements
|
|||
|
||||
private final ServerSecurityContextRepository securityContextRepository;
|
||||
|
||||
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository) {
|
||||
private final ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;
|
||||
|
||||
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository,
|
||||
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
this.expressionHandler = expressionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,4 +75,81 @@ public class HaloSpringSecurityDialect extends SpringSecurityDialect implements
|
|||
// Just overwrite the value of the attribute
|
||||
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