Skip to content

Commit 067e3d5

Browse files
authored
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 #7316 for more. #### Which issue(s) this PR fixes: Fixes #7316 #### Does this PR introduce a user-facing change? ```release-note 完善主题模板判断用户角色等功能 ```
1 parent 629a0f8 commit 067e3d5

File tree

4 files changed

+198
-3
lines changed

4 files changed

+198
-3
lines changed

Diff for: application/src/main/java/run/halo/app/infra/config/WebServerSecurityConfig.java

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.context.annotation.Bean;
1111
import org.springframework.context.annotation.Configuration;
1212
import org.springframework.http.HttpMethod;
13+
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
1314
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
1415
import org.springframework.security.config.web.server.ServerHttpSecurity;
1516
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
@@ -44,6 +45,7 @@
4445
@Configuration
4546
@EnableSpringWebSession
4647
@EnableWebFluxSecurity
48+
@EnableReactiveMethodSecurity
4749
@RequiredArgsConstructor
4850
public class WebServerSecurityConfig {
4951

Diff for: application/src/main/java/run/halo/app/theme/config/ThemeConfiguration.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.boot.info.BuildProperties;
66
import org.springframework.context.annotation.Bean;
77
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
89
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
910
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
1011
import run.halo.app.theme.dialect.GeneratorMetaProcessor;
@@ -26,8 +27,9 @@ LinkExpressionObjectDialect linkExpressionObjectDialect() {
2627

2728
@Bean
2829
SpringSecurityDialect springSecurityDialect(
29-
ServerSecurityContextRepository securityContextRepository) {
30-
return new HaloSpringSecurityDialect(securityContextRepository);
30+
ServerSecurityContextRepository securityContextRepository,
31+
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {
32+
return new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);
3133
}
3234

3335
@Bean

Diff for: application/src/main/java/run/halo/app/theme/dialect/HaloSpringSecurityDialect.java

+100-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
package run.halo.app.theme.dialect;
22

33
import static org.springframework.security.core.authority.AuthorityUtils.createAuthorityList;
4+
import static org.thymeleaf.extras.springsecurity6.dialect.processor.AuthorizeAttrProcessor.ATTR_NAME;
5+
import static org.thymeleaf.extras.springsecurity6.dialect.processor.AuthorizeAttrProcessor.ATTR_PRECEDENCE;
46
import static run.halo.app.infra.AnonymousUserConst.PRINCIPAL;
57
import static run.halo.app.infra.AnonymousUserConst.Role;
68

9+
import java.util.LinkedHashSet;
10+
import java.util.Optional;
11+
import java.util.Set;
712
import java.util.function.Function;
813
import org.springframework.beans.factory.InitializingBean;
14+
import org.springframework.beans.factory.ObjectProvider;
15+
import org.springframework.security.access.expression.ExpressionUtils;
16+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
917
import org.springframework.security.authentication.AnonymousAuthenticationToken;
18+
import org.springframework.security.core.Authentication;
1019
import org.springframework.security.core.context.SecurityContextImpl;
20+
import org.springframework.security.util.MethodInvocationUtils;
1121
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
1222
import org.springframework.web.server.ServerWebExchange;
23+
import org.thymeleaf.context.ITemplateContext;
24+
import org.thymeleaf.engine.AttributeName;
25+
import org.thymeleaf.extras.springsecurity6.auth.AuthUtils;
1326
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
1427
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
28+
import org.thymeleaf.extras.springsecurity6.util.SpringVersionSpecificUtils;
1529
import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils;
30+
import org.thymeleaf.model.IProcessableElementTag;
31+
import org.thymeleaf.processor.IProcessor;
32+
import org.thymeleaf.standard.processor.AbstractStandardConditionalVisibilityTagProcessor;
33+
import org.thymeleaf.templatemode.TemplateMode;
1634
import run.halo.app.security.authorization.AuthorityUtils;
1735

1836
/**
@@ -28,8 +46,12 @@ public class HaloSpringSecurityDialect extends SpringSecurityDialect implements
2846

2947
private final ServerSecurityContextRepository securityContextRepository;
3048

31-
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository) {
49+
private final ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;
50+
51+
public HaloSpringSecurityDialect(ServerSecurityContextRepository securityContextRepository,
52+
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler) {
3253
this.securityContextRepository = securityContextRepository;
54+
this.expressionHandler = expressionHandler;
3355
}
3456

3557
@Override
@@ -53,4 +75,81 @@ public void afterPropertiesSet() {
5375
// Just overwrite the value of the attribute
5476
getExecutionAttributes().put(SECURITY_CONTEXT_EXECUTION_ATTRIBUTE_NAME, secCtxInitializer);
5577
}
78+
79+
@Override
80+
public Set<IProcessor> getProcessors(String dialectPrefix) {
81+
LinkedHashSet<IProcessor> processors = new LinkedHashSet<>();
82+
processors.add(
83+
new HaloAuthorizeAttrProcessor(TemplateMode.HTML, dialectPrefix, ATTR_NAME)
84+
);
85+
processors.addAll(super.getProcessors(dialectPrefix));
86+
return processors;
87+
}
88+
89+
public class HaloAuthorizeAttrProcessor
90+
extends AbstractStandardConditionalVisibilityTagProcessor {
91+
92+
protected HaloAuthorizeAttrProcessor(TemplateMode templateMode, String dialectPrefix,
93+
String attrName) {
94+
super(templateMode, dialectPrefix, attrName, ATTR_PRECEDENCE - 10);
95+
}
96+
97+
@Override
98+
protected boolean isVisible(ITemplateContext context, IProcessableElementTag tag,
99+
AttributeName attributeName, String attributeValue) {
100+
101+
final String attrValue = (attributeValue == null ? null : attributeValue.trim());
102+
103+
if (attrValue == null || attrValue.isEmpty()) {
104+
return false;
105+
}
106+
107+
final Authentication authentication = AuthUtils.getAuthenticationObject(context);
108+
109+
if (authentication == null) {
110+
return false;
111+
}
112+
113+
// resolve expr
114+
var expr = Optional.of(attributeValue)
115+
.filter(v -> v.startsWith("${") && v.endsWith("}"))
116+
.map(v -> v.substring(2, v.length() - 1))
117+
.orElse(attributeValue);
118+
119+
var expressionHandler = HaloSpringSecurityDialect.this.expressionHandler.getIfUnique();
120+
if (expressionHandler == null) {
121+
// no expression handler found
122+
return false;
123+
}
124+
125+
var expression = expressionHandler.getExpressionParser().parseExpression(expr);
126+
127+
var methodInvocation = MethodInvocationUtils.createFromClass(this,
128+
HaloAuthorizeAttrProcessor.class,
129+
"dummyAuthorize",
130+
new Class[] {Authentication.class},
131+
new Object[] {authentication}
132+
);
133+
var evaluationContext =
134+
expressionHandler.createEvaluationContext(authentication, methodInvocation);
135+
136+
var expressionObjects = context.getExpressionObjects();
137+
var wrappedEvolutionContext = SpringVersionSpecificUtils.wrapEvaluationContext(
138+
evaluationContext, expressionObjects
139+
);
140+
141+
return ExpressionUtils.evaluateAsBoolean(expression, wrappedEvolutionContext);
142+
}
143+
144+
/**
145+
* This method is only used to create a method invocation for the expression parser.
146+
*
147+
* @param authentication authentication object
148+
* @return result of authorization expression evaluation
149+
*/
150+
public Boolean dummyAuthorize(Authentication authentication) {
151+
throw new UnsupportedOperationException("Should not be called");
152+
}
153+
154+
}
56155
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package run.halo.app.theme.dialect;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.params.provider.Arguments.arguments;
6+
import static org.springframework.http.MediaType.TEXT_HTML;
7+
8+
import java.util.List;
9+
import java.util.Locale;
10+
import java.util.stream.Stream;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.params.ParameterizedTest;
13+
import org.junit.jupiter.params.provider.Arguments;
14+
import org.junit.jupiter.params.provider.MethodSource;
15+
import org.springframework.beans.factory.ObjectProvider;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.boot.test.context.SpringBootTest;
18+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
19+
import org.springframework.mock.web.server.MockServerWebExchange;
20+
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
21+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
22+
import org.springframework.security.core.authority.AuthorityUtils;
23+
import org.springframework.security.core.context.SecurityContextImpl;
24+
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
25+
import org.thymeleaf.TemplateEngine;
26+
import org.thymeleaf.context.WebContext;
27+
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
28+
import org.thymeleaf.spring6.SpringWebFluxTemplateEngine;
29+
import org.thymeleaf.spring6.web.webflux.SpringWebFluxWebApplication;
30+
import org.thymeleaf.templateresolver.StringTemplateResolver;
31+
32+
// @ExtendWith(MockitoExtension.class)
33+
@SpringBootTest
34+
class HaloSpringSecurityDialectTest {
35+
36+
TemplateEngine templateEngine;
37+
38+
@Autowired
39+
ServerSecurityContextRepository securityContextRepository;
40+
41+
@Autowired
42+
ObjectProvider<MethodSecurityExpressionHandler> expressionHandler;
43+
44+
45+
@BeforeEach
46+
void setUp() {
47+
var haloSpringSecurityDialect =
48+
new HaloSpringSecurityDialect(securityContextRepository, expressionHandler);
49+
templateEngine = new SpringWebFluxTemplateEngine();
50+
templateEngine.addTemplateResolver(new StringTemplateResolver());
51+
templateEngine.addDialect(haloSpringSecurityDialect);
52+
}
53+
54+
static Stream<Arguments> shouldEvaluateSecAuthorizeAttr() {
55+
return Stream.of(
56+
arguments(
57+
"Evaluate sec:authorize to true when role match",
58+
List.of("ROLE_ADMIN"),
59+
"""
60+
<p sec:authorize="hasRole('ROLE_ADMIN')">Admin</p>\
61+
""",
62+
"""
63+
<p>Admin</p>\
64+
"""),
65+
arguments(
66+
"Evaluate sec:authorize to false when role not match",
67+
List.of("ROLE_USER"),
68+
"""
69+
<p sec:authorize="hasRole('ROLE_ADMIN')"></p>\
70+
""",
71+
"")
72+
);
73+
}
74+
75+
@ParameterizedTest(name = "{0}")
76+
@MethodSource
77+
void shouldEvaluateSecAuthorizeAttr(String name, List<String> authorities, String template,
78+
String expected) {
79+
var request = MockServerHttpRequest.get("/halo-sec-authorize").build();
80+
var exchange = new MockServerWebExchange.Builder(request).build();
81+
var webExchange = SpringWebFluxWebApplication.buildApplication(null)
82+
.buildExchange(exchange, Locale.getDefault(), TEXT_HTML, UTF_8);
83+
var context = new WebContext(webExchange);
84+
var authentication = new UsernamePasswordAuthenticationToken("fake-user", "fake-credential",
85+
AuthorityUtils.createAuthorityList(authorities));
86+
var securityContext = new SecurityContextImpl(authentication);
87+
context.setVariable(SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME,
88+
securityContext);
89+
var result = templateEngine.process(template, context);
90+
assertEquals(expected, result);
91+
}
92+
}

0 commit comments

Comments
 (0)