mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-12 13:23:29 +00:00
Use PathPatternMessageMatcher By Default
Issue gh-17501
This commit is contained in:
parent
ff7359b54a
commit
684775b46a
@ -16,29 +16,24 @@
|
|||||||
|
|
||||||
package org.springframework.security.config.annotation.web.socket;
|
package org.springframework.security.config.annotation.web.socket;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Fallback;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
|
import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
|
||||||
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
|
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcherFactory;
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
|
||||||
|
|
||||||
final class MessageMatcherAuthorizationManagerConfiguration {
|
final class MessageMatcherAuthorizationManagerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Fallback
|
||||||
|
PathPatternMessageMatcherBuilderFactoryBean messageMatcherBuilderFactoryBean() {
|
||||||
|
return new PathPatternMessageMatcherBuilderFactoryBean();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Scope("prototype")
|
@Scope("prototype")
|
||||||
MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder(
|
MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder() {
|
||||||
ApplicationContext context) {
|
return MessageMatcherDelegatingAuthorizationManager.builder();
|
||||||
MessageMatcherFactory.setApplicationContext(context);
|
|
||||||
if (MessageMatcherFactory.usesPathPatterns()) {
|
|
||||||
return MessageMatcherDelegatingAuthorizationManager.builder();
|
|
||||||
}
|
|
||||||
return MessageMatcherDelegatingAuthorizationManager.builder()
|
|
||||||
.simpDestPathMatcher(
|
|
||||||
() -> (context.getBeanNamesForType(SimpAnnotationMethodMessageHandler.class).length > 0)
|
|
||||||
? context.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher()
|
|
||||||
: new AntPathMatcher());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,6 @@ import org.springframework.context.ApplicationContextAware;
|
|||||||
import org.springframework.messaging.simp.SimpMessageType;
|
import org.springframework.messaging.simp.SimpMessageType;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||||
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
||||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
|
|
||||||
import org.springframework.util.AntPathMatcher;
|
|
||||||
import org.springframework.util.PathMatcher;
|
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatcher<?>>, ApplicationContextAware {
|
public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatcher<?>>, ApplicationContextAware {
|
||||||
@ -36,8 +33,6 @@ public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatch
|
|||||||
|
|
||||||
private final String path;
|
private final String path;
|
||||||
|
|
||||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
|
||||||
|
|
||||||
public MessageMatcherFactoryBean(String path) {
|
public MessageMatcherFactoryBean(String path) {
|
||||||
this(path, null);
|
this(path, null);
|
||||||
}
|
}
|
||||||
@ -49,16 +44,7 @@ public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatch
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MessageMatcher<?> getObject() throws Exception {
|
public MessageMatcher<?> getObject() throws Exception {
|
||||||
if (this.builder != null) {
|
return this.builder.matcher(this.method, this.path);
|
||||||
return this.builder.matcher(this.method, this.path);
|
|
||||||
}
|
|
||||||
if (this.method == SimpMessageType.SUBSCRIBE) {
|
|
||||||
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.path, this.pathMatcher);
|
|
||||||
}
|
|
||||||
if (this.method == SimpMessageType.MESSAGE) {
|
|
||||||
return SimpDestinationMessageMatcher.createMessageMatcher(this.path, this.pathMatcher);
|
|
||||||
}
|
|
||||||
return new SimpDestinationMessageMatcher(this.path, this.pathMatcher);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -66,13 +52,9 @@ public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatch
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPathMatcher(PathMatcher pathMatcher) {
|
|
||||||
this.pathMatcher = pathMatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||||
this.builder = context.getBeanProvider(PathPatternMessageMatcher.Builder.class).getIfUnique();
|
this.builder = context.getBean(PathPatternMessageMatcher.Builder.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,9 @@ import org.springframework.beans.BeansException;
|
|||||||
import org.springframework.beans.PropertyValue;
|
import org.springframework.beans.PropertyValue;
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
import org.springframework.beans.factory.config.BeanDefinition;
|
import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.beans.factory.config.BeanReference;
|
|
||||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||||
|
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||||
@ -56,6 +56,7 @@ import org.springframework.security.authorization.AuthorizationManager;
|
|||||||
import org.springframework.security.authorization.AuthorizationResult;
|
import org.springframework.security.authorization.AuthorizationResult;
|
||||||
import org.springframework.security.config.Elements;
|
import org.springframework.security.config.Elements;
|
||||||
import org.springframework.security.config.http.MessageMatcherFactoryBean;
|
import org.springframework.security.config.http.MessageMatcherFactoryBean;
|
||||||
|
import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||||
@ -134,7 +135,7 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||||||
|
|
||||||
private static final String TYPE_ATTR = "type";
|
private static final String TYPE_ATTR = "type";
|
||||||
|
|
||||||
private static final String PATH_MATCHER_BEAN_NAME = "springSecurityMessagePathMatcher";
|
private static final String MESSAGE_MATCHER_BUILDER_BEAN_NAME = "HttpConfigurationBuilder-pathPatternMessageMatcherBuilder";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param element
|
* @param element
|
||||||
@ -144,13 +145,17 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||||||
@Override
|
@Override
|
||||||
public BeanDefinition parse(Element element, ParserContext parserContext) {
|
public BeanDefinition parse(Element element, ParserContext parserContext) {
|
||||||
String id = element.getAttribute(ID_ATTR);
|
String id = element.getAttribute(ID_ATTR);
|
||||||
|
if (!parserContext.getRegistry().containsBeanDefinition(MESSAGE_MATCHER_BUILDER_BEAN_NAME)) {
|
||||||
|
BeanDefinitionBuilder pathPatternMessageMatcherBuilder = BeanDefinitionBuilder
|
||||||
|
.rootBeanDefinition(PathPatternMessageMatcherBuilderFactoryBean.class);
|
||||||
|
pathPatternMessageMatcherBuilder.setFallback(true);
|
||||||
|
BeanDefinition bean = pathPatternMessageMatcherBuilder.getBeanDefinition();
|
||||||
|
parserContext.registerBeanComponent(new BeanComponentDefinition(bean, MESSAGE_MATCHER_BUILDER_BEAN_NAME));
|
||||||
|
}
|
||||||
String inSecurityInterceptorName = parseAuthorization(element, parserContext);
|
String inSecurityInterceptorName = parseAuthorization(element, parserContext);
|
||||||
BeanDefinitionRegistry registry = parserContext.getRegistry();
|
BeanDefinitionRegistry registry = parserContext.getRegistry();
|
||||||
if (StringUtils.hasText(id)) {
|
if (StringUtils.hasText(id)) {
|
||||||
registry.registerAlias(inSecurityInterceptorName, id);
|
registry.registerAlias(inSecurityInterceptorName, id);
|
||||||
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
|
|
||||||
registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
|
boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
|
||||||
@ -286,7 +291,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||||||
+ " with a pattern because the type does not have a destination.", interceptMessage);
|
+ " with a pattern because the type does not have a destination.", interceptMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matcher.addPropertyValue("pathMatcher", new RuntimeBeanReference("springSecurityMessagePathMatcher"));
|
|
||||||
return matcher.getBeanDefinition();
|
return matcher.getBeanDefinition();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,13 +346,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||||||
}
|
}
|
||||||
argResolvers.add(beanDefinition);
|
argResolvers.add(beanDefinition);
|
||||||
bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers);
|
bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers);
|
||||||
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
|
|
||||||
PropertyValue pathMatcherProp = bd.getPropertyValues().getPropertyValue("pathMatcher");
|
|
||||||
Object pathMatcher = (pathMatcherProp != null) ? pathMatcherProp.getValue() : null;
|
|
||||||
if (pathMatcher instanceof BeanReference) {
|
|
||||||
registry.registerAlias(((BeanReference) pathMatcher).getBeanName(), PATH_MATCHER_BEAN_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (CSRF_HANDSHAKE_HANDLER_CLASSES.contains(beanClassName)) {
|
else if (CSRF_HANDSHAKE_HANDLER_CLASSES.contains(beanClassName)) {
|
||||||
addCsrfTokenHandshakeInterceptor(bd);
|
addCsrfTokenHandshakeInterceptor(bd);
|
||||||
@ -376,9 +373,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
|
|||||||
interceptors.addAll(currentInterceptors);
|
interceptors.addAll(currentInterceptors);
|
||||||
}
|
}
|
||||||
inboundChannel.getPropertyValues().add(INTERCEPTORS_PROP, interceptors);
|
inboundChannel.getPropertyValues().add(INTERCEPTORS_PROP, interceptors);
|
||||||
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
|
|
||||||
registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addCsrfTokenHandshakeInterceptor(BeanDefinition bd) {
|
private void addCsrfTokenHandshakeInterceptor(BeanDefinition bd) {
|
||||||
|
@ -45,6 +45,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.http.server.PathContainer;
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
@ -70,6 +71,7 @@ import org.springframework.security.authorization.AuthorizationManager;
|
|||||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.observation.SecurityObservationSettings;
|
import org.springframework.security.config.observation.SecurityObservationSettings;
|
||||||
|
import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
|
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
@ -99,6 +101,7 @@ import org.springframework.web.socket.server.HandshakeHandler;
|
|||||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||||
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
|
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
|
||||||
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
|
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
|
||||||
|
import org.springframework.web.util.pattern.PathPatternParser;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
@ -507,6 +510,13 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
|||||||
@Import(SyncExecutorConfig.class)
|
@Import(SyncExecutorConfig.class)
|
||||||
static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
|
static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
PathPatternMessageMatcherBuilderFactoryBean messageMatcherBuilder() {
|
||||||
|
PathPatternParser parser = new PathPatternParser();
|
||||||
|
parser.setPathOptions(PathContainer.Options.MESSAGE_ROUTE);
|
||||||
|
return new PathPatternMessageMatcherBuilderFactoryBean(parser);
|
||||||
|
}
|
||||||
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
@Override
|
@Override
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
@ -518,7 +528,6 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||||
registry.setPathMatcher(new AntPathMatcher("."));
|
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||||
registry.setApplicationDestinationPrefixes("/app");
|
registry.setApplicationDestinationPrefixes("/app");
|
||||||
}
|
}
|
||||||
@ -567,7 +576,6 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
|||||||
@Bean
|
@Bean
|
||||||
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
|
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
|
||||||
messages
|
messages
|
||||||
.simpDestPathMatcher(new AntPathMatcher())
|
|
||||||
.simpDestMatchers("/app/a/*").permitAll()
|
.simpDestMatchers("/app/a/*").permitAll()
|
||||||
.anyMessage().denyAll();
|
.anyMessage().denyAll();
|
||||||
return messages.build();
|
return messages.build();
|
||||||
|
@ -23,7 +23,6 @@ import org.springframework.expression.Expression;
|
|||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.security.access.ConfigAttribute;
|
import org.springframework.security.access.ConfigAttribute;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,7 +41,7 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
|
|||||||
|
|
||||||
private final Expression authorizeExpression;
|
private final Expression authorizeExpression;
|
||||||
|
|
||||||
private final MessageMatcher<?> matcher;
|
private final MessageMatcher<Object> matcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance
|
* Creates a new instance
|
||||||
@ -53,7 +52,7 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
|
|||||||
Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
|
Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
|
||||||
Assert.notNull(matcher, "matcher cannot be null");
|
Assert.notNull(matcher, "matcher cannot be null");
|
||||||
this.authorizeExpression = authorizeExpression;
|
this.authorizeExpression = authorizeExpression;
|
||||||
this.matcher = matcher;
|
this.matcher = (MessageMatcher<Object>) matcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression getAuthorizeExpression() {
|
Expression getAuthorizeExpression() {
|
||||||
@ -72,12 +71,9 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EvaluationContext postProcess(EvaluationContext ctx, Message<?> message) {
|
public EvaluationContext postProcess(EvaluationContext ctx, Message<?> message) {
|
||||||
if (this.matcher instanceof SimpDestinationMessageMatcher) {
|
Map<String, String> variables = this.matcher.matcher(message).getVariables();
|
||||||
Map<String, String> variables = ((SimpDestinationMessageMatcher) this.matcher)
|
for (Map.Entry<String, String> entry : variables.entrySet()) {
|
||||||
.extractPathVariables(message);
|
ctx.setVariable(entry.getKey(), entry.getValue());
|
||||||
for (Map.Entry<String, String> entry : variables.entrySet()) {
|
|
||||||
ctx.setVariable(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,9 @@ import java.util.function.Supplier;
|
|||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
import org.springframework.core.log.LogMessage;
|
import org.springframework.core.log.LogMessage;
|
||||||
import org.springframework.messaging.Message;
|
import org.springframework.messaging.Message;
|
||||||
import org.springframework.messaging.simp.SimpMessageType;
|
import org.springframework.messaging.simp.SimpMessageType;
|
||||||
@ -33,15 +36,10 @@ import org.springframework.security.authorization.AuthorizationResult;
|
|||||||
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
import org.springframework.security.authorization.SingleResultAuthorizationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcherFactory;
|
|
||||||
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
||||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
|
|
||||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
|
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
|
||||||
import org.springframework.util.AntPathMatcher;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.PathMatcher;
|
|
||||||
import org.springframework.util.function.SingletonSupplier;
|
|
||||||
|
|
||||||
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
|
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
|
||||||
|
|
||||||
@ -99,12 +97,11 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
/**
|
/**
|
||||||
* A builder for {@link MessageMatcherDelegatingAuthorizationManager}.
|
* A builder for {@link MessageMatcherDelegatingAuthorizationManager}.
|
||||||
*/
|
*/
|
||||||
public static final class Builder {
|
public static final class Builder implements ApplicationContextAware {
|
||||||
|
|
||||||
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>();
|
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>();
|
||||||
|
|
||||||
@Deprecated
|
private PathPatternMessageMatcher.Builder messageMatcherBuilder = PathPatternMessageMatcher.withDefaults();
|
||||||
private Supplier<PathMatcher> pathMatcher = AntPathMatcher::new;
|
|
||||||
|
|
||||||
public Builder() {
|
public Builder() {
|
||||||
}
|
}
|
||||||
@ -142,11 +139,9 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or
|
* Maps a {@link List} of {@link PathPatternMessageMatcher}s instances without
|
||||||
* {@link PathPatternMessageMatcher} if the application has configured a
|
* regard to the {@link SimpMessageType}. If no destination is found on the
|
||||||
* {@link PathPatternMessageMatcher.Builder} bean) instances without regard to the
|
* Message, then the Matcher returns false.
|
||||||
* {@link SimpMessageType}. If no destination is found on the Message, then the
|
|
||||||
* Matcher returns false.
|
|
||||||
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
||||||
*/
|
*/
|
||||||
public Builder.Constraint simpDestMatchers(String... patterns) {
|
public Builder.Constraint simpDestMatchers(String... patterns) {
|
||||||
@ -154,10 +149,8 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or
|
* Maps a {@link List} of {@link PathPatternMessageMatcher}s instances that match
|
||||||
* {@link PathPatternMessageMatcher} if the application has configured a
|
* on {@code SimpMessageType.MESSAGE}. If no destination is found on the Message,
|
||||||
* {@link PathPatternMessageMatcher.Builder} bean) instances that match on
|
|
||||||
* {@code SimpMessageType.MESSAGE}. If no destination is found on the Message,
|
|
||||||
* then the Matcher returns false.
|
* then the Matcher returns false.
|
||||||
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
||||||
*/
|
*/
|
||||||
@ -166,11 +159,9 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or
|
* Maps a {@link List} of {@link PathPatternMessageMatcher}s instances that match
|
||||||
* {@link PathPatternMessageMatcher} if the application has configured a
|
* on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the
|
||||||
* {@link PathPatternMessageMatcher.Builder} bean) instances that match on
|
* Message, then the Matcher returns false.
|
||||||
* {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the Message,
|
|
||||||
* then the Matcher returns false.
|
|
||||||
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
||||||
*/
|
*/
|
||||||
public Builder.Constraint simpSubscribeDestMatchers(String... patterns) {
|
public Builder.Constraint simpSubscribeDestMatchers(String... patterns) {
|
||||||
@ -178,10 +169,8 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or
|
* Maps a {@link List} of {@link PathPatternMessageMatcher} instances. If no
|
||||||
* {@link PathPatternMessageMatcher} if the application has configured a
|
* destination is found on the Message, then the Matcher returns false.
|
||||||
* {@link PathPatternMessageMatcher.Builder} bean) instances. If no destination is
|
|
||||||
* found on the Message, then the Matcher returns false.
|
|
||||||
* @param type the {@link SimpMessageType} to match on. If null, the
|
* @param type the {@link SimpMessageType} to match on. If null, the
|
||||||
* {@link SimpMessageType} is not considered for matching.
|
* {@link SimpMessageType} is not considered for matching.
|
||||||
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
* @param patterns the patterns to create {@code MessageMatcher}s from.
|
||||||
@ -191,44 +180,12 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
|
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
|
||||||
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
|
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
|
||||||
for (String pattern : patterns) {
|
for (String pattern : patterns) {
|
||||||
MessageMatcher<Object> matcher = MessageMatcherFactory.usesPathPatterns()
|
MessageMatcher<Object> matcher = this.messageMatcherBuilder.matcher(type, pattern);
|
||||||
? MessageMatcherFactory.matcher(type, pattern)
|
|
||||||
: new LazySimpDestinationMessageMatcher(pattern, type);
|
|
||||||
matchers.add(matcher);
|
matchers.add(matcher);
|
||||||
}
|
}
|
||||||
return new Builder.Constraint(matchers);
|
return new Builder.Constraint(matchers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link PathMatcher} to be used with the
|
|
||||||
* {@link Builder#simpDestMatchers(String...)}. The default is to use the default
|
|
||||||
* constructor of {@link AntPathMatcher}.
|
|
||||||
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
|
|
||||||
* @return the {@link Builder} for further customization.
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public Builder simpDestPathMatcher(PathMatcher pathMatcher) {
|
|
||||||
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
|
|
||||||
this.pathMatcher = () -> pathMatcher;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link PathMatcher} to be used with the
|
|
||||||
* {@link Builder#simpDestMatchers(String...)}. Use this method to delay the
|
|
||||||
* computation or lookup of the {@link PathMatcher}.
|
|
||||||
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
|
|
||||||
* @return the {@link Builder} for further customization.
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public Builder simpDestPathMatcher(Supplier<PathMatcher> pathMatcher) {
|
|
||||||
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
|
|
||||||
this.pathMatcher = pathMatcher;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a {@link List} of {@link MessageMatcher} instances to a security
|
* Maps a {@link List} of {@link MessageMatcher} instances to a security
|
||||||
* expression.
|
* expression.
|
||||||
@ -248,6 +205,12 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
return new MessageMatcherDelegatingAuthorizationManager(this.mappings);
|
return new MessageMatcherDelegatingAuthorizationManager(this.mappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||||
|
this.messageMatcherBuilder = context.getBeanProvider(PathPatternMessageMatcher.Builder.class)
|
||||||
|
.getIfUnique(PathPatternMessageMatcher::withDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the security constraint to be applied to the {@link MessageMatcher}
|
* Represents the security constraint to be applied to the {@link MessageMatcher}
|
||||||
* instances.
|
* instances.
|
||||||
@ -379,39 +342,6 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
private final class LazySimpDestinationMessageMatcher implements MessageMatcher<Object> {
|
|
||||||
|
|
||||||
private final Supplier<SimpDestinationMessageMatcher> delegate;
|
|
||||||
|
|
||||||
private LazySimpDestinationMessageMatcher(String pattern, SimpMessageType type) {
|
|
||||||
this.delegate = SingletonSupplier.of(() -> {
|
|
||||||
PathMatcher pathMatcher = Builder.this.pathMatcher.get();
|
|
||||||
if (type == null) {
|
|
||||||
return new SimpDestinationMessageMatcher(pattern, pathMatcher);
|
|
||||||
}
|
|
||||||
if (SimpMessageType.MESSAGE == type) {
|
|
||||||
return SimpDestinationMessageMatcher.createMessageMatcher(pattern, pathMatcher);
|
|
||||||
}
|
|
||||||
if (SimpMessageType.SUBSCRIBE == type) {
|
|
||||||
return SimpDestinationMessageMatcher.createSubscribeMatcher(pattern, pathMatcher);
|
|
||||||
}
|
|
||||||
throw new IllegalStateException(type + " is not supported since it does not have a destination");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean matches(Message<?> message) {
|
|
||||||
return this.delegate.get().matches(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MatchResult matcher(Message<?> message) {
|
|
||||||
return this.delegate.get().matcher(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class Entry<T> {
|
private static final class Entry<T> {
|
||||||
|
@ -28,7 +28,7 @@ import org.springframework.messaging.Message;
|
|||||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||||
import org.springframework.messaging.support.MessageBuilder;
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
|
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
@ -81,7 +81,7 @@ public class MessageExpressionConfigAttributeTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void postProcessContext() {
|
public void postProcessContext() {
|
||||||
SimpDestinationMessageMatcher matcher = new SimpDestinationMessageMatcher("/topics/{topic}/**");
|
PathPatternMessageMatcher matcher = PathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**");
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
Message<?> message = MessageBuilder.withPayload("M")
|
Message<?> message = MessageBuilder.withPayload("M")
|
||||||
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1")
|
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1")
|
||||||
|
@ -78,6 +78,7 @@ public class MessageExpressionVoterTests {
|
|||||||
@Test
|
@Test
|
||||||
public void voteGranted() {
|
public void voteGranted() {
|
||||||
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(true);
|
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(true);
|
||||||
|
given(this.matcher.matcher(any())).willCallRealMethod();
|
||||||
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
|
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
|
||||||
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
|
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
|
||||||
}
|
}
|
||||||
@ -85,6 +86,7 @@ public class MessageExpressionVoterTests {
|
|||||||
@Test
|
@Test
|
||||||
public void voteDenied() {
|
public void voteDenied() {
|
||||||
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(false);
|
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(false);
|
||||||
|
given(this.matcher.matcher(any())).willCallRealMethod();
|
||||||
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
|
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
|
||||||
.isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
|
.isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
|
||||||
}
|
}
|
||||||
@ -127,6 +129,7 @@ public class MessageExpressionVoterTests {
|
|||||||
given(this.expressionHandler.createEvaluationContext(this.authentication, this.message))
|
given(this.expressionHandler.createEvaluationContext(this.authentication, this.message))
|
||||||
.willReturn(this.evaluationContext);
|
.willReturn(this.evaluationContext);
|
||||||
given(this.expression.getValue(this.evaluationContext, Boolean.class)).willReturn(true);
|
given(this.expression.getValue(this.evaluationContext, Boolean.class)).willReturn(true);
|
||||||
|
given(this.matcher.matcher(any())).willCallRealMethod();
|
||||||
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
|
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
|
||||||
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
|
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
|
||||||
verify(this.expressionHandler).createEvaluationContext(this.authentication, this.message);
|
verify(this.expressionHandler).createEvaluationContext(this.authentication, this.message);
|
||||||
|
@ -37,10 +37,11 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
|
|||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
import org.springframework.security.authorization.AuthorizationManager;
|
import org.springframework.security.authorization.AuthorizationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcherFactory;
|
|
||||||
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
|
||||||
|
import org.springframework.web.util.pattern.PathPatternParser;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,7 +59,7 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
Mockito.when(this.context.getBeanProvider(PathPatternMessageMatcher.Builder.class)).thenReturn(this.provider);
|
Mockito.when(this.context.getBeanProvider(PathPatternMessageMatcher.Builder.class)).thenReturn(this.provider);
|
||||||
MessageMatcherFactory.setApplicationContext(this.context);
|
Mockito.when(this.provider.getIfUnique(any())).thenReturn(PathPatternMessageMatcher.withDefaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -135,8 +136,7 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void checkWhenMessageTypeAndPathPatternMatches() {
|
void checkWhenMessageTypeAndPathPatternMatches() {
|
||||||
Mockito.when(this.provider.getIfUnique()).thenReturn(PathPatternMessageMatcher.withDefaults());
|
Mockito.when(this.provider.getIfUnique(any())).thenReturn(PathPatternMessageMatcher.withDefaults());
|
||||||
MessageMatcherFactory.setApplicationContext(this.context);
|
|
||||||
AuthorizationManager<Message<?>> authorizationManager = builder().simpMessageDestMatchers("/destination")
|
AuthorizationManager<Message<?>> authorizationManager = builder().simpMessageDestMatchers("/destination")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.simpSubscribeDestMatchers("/destination")
|
.simpSubscribeDestMatchers("/destination")
|
||||||
@ -154,10 +154,32 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
|
|||||||
assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse();
|
assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkWhenMessageTypeAndPathPatternMatchesCaseInsensitive() {
|
||||||
|
PathPatternParser pathPatternParser = new PathPatternParser();
|
||||||
|
pathPatternParser.setCaseSensitive(false);
|
||||||
|
PathPatternMessageMatcher.Builder messageMatcherBuilder = PathPatternMessageMatcher
|
||||||
|
.withPathPatternParser(pathPatternParser);
|
||||||
|
Mockito.when(this.provider.getIfUnique(any())).thenReturn(messageMatcherBuilder);
|
||||||
|
AuthorizationManager<Message<?>> authorizationManager = builder().simpMessageDestMatchers("/desTinaTion")
|
||||||
|
.permitAll()
|
||||||
|
.simpSubscribeDestMatchers("/desTinaTion")
|
||||||
|
.denyAll()
|
||||||
|
.anyMessage()
|
||||||
|
.denyAll()
|
||||||
|
.build();
|
||||||
|
MessageHeaders headers = new MessageHeaders(Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER,
|
||||||
|
SimpMessageType.MESSAGE, SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination"));
|
||||||
|
Message<?> message = new GenericMessage<>(new Object(), headers);
|
||||||
|
assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue();
|
||||||
|
MessageHeaders headers2 = new MessageHeaders(Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER,
|
||||||
|
SimpMessageType.SUBSCRIBE, SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination"));
|
||||||
|
Message<?> message2 = new GenericMessage<>(new Object(), headers2);
|
||||||
|
assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void checkPatternMismatch() {
|
void checkPatternMismatch() {
|
||||||
Mockito.when(this.provider.getIfUnique()).thenReturn(PathPatternMessageMatcher.withDefaults());
|
|
||||||
MessageMatcherFactory.setApplicationContext(this.context);
|
|
||||||
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("/destination/*")
|
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("/destination/*")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyMessage()
|
.anyMessage()
|
||||||
@ -170,7 +192,10 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MessageMatcherDelegatingAuthorizationManager.Builder builder() {
|
private MessageMatcherDelegatingAuthorizationManager.Builder builder() {
|
||||||
return MessageMatcherDelegatingAuthorizationManager.builder();
|
MessageMatcherDelegatingAuthorizationManager.Builder builder = MessageMatcherDelegatingAuthorizationManager
|
||||||
|
.builder();
|
||||||
|
builder.setApplicationContext(this.context);
|
||||||
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Builder variable(String name) {
|
private Builder variable(String name) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user