Add AuthorizationManager to Messaging
Closes gh-11076
This commit is contained in:
+4
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,10 @@ import org.springframework.expression.EvaluationContext;
|
||||
*
|
||||
* @author Daniel Bustamante Ospina
|
||||
* @since 5.2
|
||||
* @deprecated Since {@link MessageExpressionVoter} is deprecated, there is no more need
|
||||
* for this class
|
||||
*/
|
||||
@Deprecated
|
||||
interface EvaluationContextPostProcessor<I> {
|
||||
|
||||
/**
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,7 +35,11 @@ import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 4.0
|
||||
* @deprecated Use
|
||||
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
|
||||
* instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
|
||||
|
||||
private ExpressionBasedMessageSecurityMetadataSourceFactory() {
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.messaging.access.expression;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.security.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
|
||||
|
||||
/**
|
||||
* An expression handler for {@link MessageAuthorizationContext}.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.8
|
||||
*/
|
||||
public final class MessageAuthorizationContextSecurityExpressionHandler
|
||||
implements SecurityExpressionHandler<MessageAuthorizationContext<?>> {
|
||||
|
||||
private final SecurityExpressionHandler<Message<?>> delegate;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public MessageAuthorizationContextSecurityExpressionHandler() {
|
||||
this(new DefaultMessageSecurityExpressionHandler());
|
||||
}
|
||||
|
||||
public MessageAuthorizationContextSecurityExpressionHandler(
|
||||
SecurityExpressionHandler<Message<?>> expressionHandler) {
|
||||
this.delegate = expressionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionParser getExpressionParser() {
|
||||
return this.delegate.getExpressionParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationContext createEvaluationContext(Authentication authentication,
|
||||
MessageAuthorizationContext<?> message) {
|
||||
return createEvaluationContext(() -> authentication, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
|
||||
MessageAuthorizationContext<?> message) {
|
||||
EvaluationContext context = this.delegate.createEvaluationContext(authentication, message.getMessage());
|
||||
Map<String, String> variables = message.getVariables();
|
||||
if (variables != null) {
|
||||
for (Map.Entry<String, String> entry : variables.entrySet()) {
|
||||
context.setVariable(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
}
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,7 +32,11 @@ import org.springframework.util.Assert;
|
||||
* @author Rob Winch
|
||||
* @author Daniel Bustamante Ospina
|
||||
* @since 4.0
|
||||
* @deprecated Use
|
||||
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
|
||||
* instead
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("serial")
|
||||
class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<Message<?>> {
|
||||
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -37,7 +37,11 @@ import org.springframework.util.Assert;
|
||||
* @author Rob Winch
|
||||
* @author Daniel Bustamante Ospina
|
||||
* @since 4.0
|
||||
* @deprecated Use
|
||||
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
|
||||
* instead
|
||||
*/
|
||||
@Deprecated
|
||||
public class MessageExpressionVoter<T> implements AccessDecisionVoter<Message<T>> {
|
||||
|
||||
private SecurityExpressionHandler<Message<T>> expressionHandler = new DefaultMessageSecurityExpressionHandler<>();
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.messaging.access.intercept;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Authorizes {@link Message} resources using the provided {@link AuthorizationManager}
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.8
|
||||
*/
|
||||
public final class AuthorizationChannelInterceptor implements ChannelInterceptor {
|
||||
|
||||
static final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null) {
|
||||
throw new AuthenticationCredentialsNotFoundException(
|
||||
"An Authentication object was not found in the SecurityContext");
|
||||
}
|
||||
return authentication;
|
||||
};
|
||||
|
||||
private final Log logger = LogFactory.getLog(this.getClass());
|
||||
|
||||
private final AuthorizationManager<Message<?>> preSendAuthorizationManager;
|
||||
|
||||
private AuthorizationEventPublisher eventPublisher = new NoopAuthorizationEventPublisher();
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
* @param preSendAuthorizationManager the {@link AuthorizationManager} to use. Cannot
|
||||
* be null.
|
||||
*
|
||||
*/
|
||||
public AuthorizationChannelInterceptor(AuthorizationManager<Message<?>> preSendAuthorizationManager) {
|
||||
Assert.notNull(preSendAuthorizationManager, "preSendAuthorizationManager cannot be null");
|
||||
this.preSendAuthorizationManager = preSendAuthorizationManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
this.logger.debug(LogMessage.of(() -> "Authorizing message send"));
|
||||
AuthorizationDecision decision = this.preSendAuthorizationManager.check(AUTHENTICATION_SUPPLIER, message);
|
||||
this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, message, decision);
|
||||
if (decision == null || !decision.isGranted()) { // default deny
|
||||
this.logger.debug(LogMessage.of(() -> "Failed to authorize message with authorization manager "
|
||||
+ this.preSendAuthorizationManager + " and decision " + decision));
|
||||
throw new AccessDeniedException("Access Denied");
|
||||
}
|
||||
this.logger.debug(LogMessage.of(() -> "Authorized message send"));
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link AuthorizationEventPublisher} to publish the
|
||||
* {@link AuthorizationManager} result.
|
||||
* @param eventPublisher
|
||||
*/
|
||||
public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) {
|
||||
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
private static class NoopAuthorizationEventPublisher implements AuthorizationEventPublisher {
|
||||
|
||||
@Override
|
||||
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
|
||||
AuthorizationDecision decision) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,7 +36,9 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 4.0
|
||||
* @deprecated Use {@link AuthorizationChannelInterceptor} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class ChannelSecurityInterceptor extends AbstractSecurityInterceptor implements ChannelInterceptor {
|
||||
|
||||
private static final ThreadLocal<InterceptorStatusToken> tokenHolder = new ThreadLocal<>();
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,7 +40,9 @@ import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||
* @since 4.0
|
||||
* @see ChannelSecurityInterceptor
|
||||
* @see ExpressionBasedMessageSecurityMetadataSourceFactory
|
||||
* @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public final class DefaultMessageSecurityMetadataSource implements MessageSecurityMetadataSource {
|
||||
|
||||
private final Map<MessageMatcher<?>, Collection<ConfigAttribute>> messageMap;
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.messaging.access.intercept;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* An {@link Message} authorization context.
|
||||
*
|
||||
* @author Josh Cummings
|
||||
* @since 5.8
|
||||
*/
|
||||
public final class MessageAuthorizationContext<T> {
|
||||
|
||||
private final Message<T> message;
|
||||
|
||||
private final Map<String, String> variables;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
* @param message the {@link HttpServletRequest} to use
|
||||
*/
|
||||
public MessageAuthorizationContext(Message<T> message) {
|
||||
this(message, Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
* @param message the {@link HttpServletRequest} to use
|
||||
* @param variables a map containing key-value pairs representing extracted variable
|
||||
* names and variable values
|
||||
*/
|
||||
public MessageAuthorizationContext(Message<T> message, Map<String, String> variables) {
|
||||
this.message = message;
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link HttpServletRequest}.
|
||||
* @return the {@link HttpServletRequest} to use
|
||||
*/
|
||||
public Message<T> getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extracted variable values where the key is the variable name and the
|
||||
* value is the variable value.
|
||||
* @return a map containing key-value pairs representing extracted variable names and
|
||||
* variable values
|
||||
*/
|
||||
public Map<String, String> getVariables() {
|
||||
return this.variables;
|
||||
}
|
||||
|
||||
}
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.messaging.access.intercept;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorityAuthorizationManager;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.messaging.util.matcher.MessageMatcher;
|
||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
|
||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings;
|
||||
|
||||
private MessageMatcherDelegatingAuthorizationManager(
|
||||
List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings) {
|
||||
Assert.notEmpty(mappings, "mappings cannot be empty");
|
||||
this.mappings = mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegates to a specific {@link AuthorizationManager} based on a
|
||||
* {@link MessageMatcher} evaluation.
|
||||
* @param authentication the {@link Supplier} of the {@link Authentication} to check
|
||||
* @param message the {@link Message} to check
|
||||
* @return an {@link AuthorizationDecision}. If there is no {@link MessageMatcher}
|
||||
* matching the message, or the {@link AuthorizationManager} could not decide, then
|
||||
* null is returned
|
||||
*/
|
||||
@Override
|
||||
public AuthorizationDecision check(Supplier<Authentication> authentication, Message<?> message) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Authorizing message"));
|
||||
}
|
||||
for (Entry<AuthorizationManager<MessageAuthorizationContext<?>>> mapping : this.mappings) {
|
||||
MessageMatcher<?> matcher = mapping.getMessageMatcher();
|
||||
MessageAuthorizationContext<?> authorizationContext = authorizationContext(matcher, message);
|
||||
if (authorizationContext != null) {
|
||||
AuthorizationManager<MessageAuthorizationContext<?>> manager = mapping.getEntry();
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace(LogMessage.format("Checking authorization on message using %s", manager));
|
||||
}
|
||||
return manager.check(authentication, authorizationContext);
|
||||
}
|
||||
}
|
||||
this.logger.trace("Abstaining since did not find matching MessageMatcher");
|
||||
return null;
|
||||
}
|
||||
|
||||
private MessageAuthorizationContext<?> authorizationContext(MessageMatcher<?> matcher, Message<?> message) {
|
||||
if (!matcher.matches((Message) message)) {
|
||||
return null;
|
||||
}
|
||||
if (matcher instanceof SimpDestinationMessageMatcher) {
|
||||
SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher;
|
||||
return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message));
|
||||
}
|
||||
return new MessageAuthorizationContext<>(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for {@link MessageMatcherDelegatingAuthorizationManager}.
|
||||
* @return the new {@link Builder} instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link MessageMatcherDelegatingAuthorizationManager}.
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>();
|
||||
|
||||
private Supplier<PathMatcher> pathMatcher = () -> new AntPathMatcher();
|
||||
|
||||
public Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps any {@link Message} to a security expression.
|
||||
* @return the Expression to associate
|
||||
*/
|
||||
public Builder.Constraint anyMessage() {
|
||||
return matchers(MessageMatcher.ANY_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps any {@link Message} that has a null SimpMessageHeaderAccessor destination
|
||||
* header (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, UNSUBSCRIBE, DISCONNECT,
|
||||
* DISCONNECT_ACK, OTHER)
|
||||
* @return the Expression to associate
|
||||
*/
|
||||
public Builder.Constraint nullDestMatcher() {
|
||||
return matchers(SimpDestinationMessageMatcher.NULL_DESTINATION_MATCHER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances.
|
||||
* @param typesToMatch the {@link SimpMessageType} instance to match on
|
||||
* @return the {@link Builder.Constraint} associated to the matchers.
|
||||
*/
|
||||
public Builder.Constraint simpTypeMatchers(SimpMessageType... typesToMatch) {
|
||||
MessageMatcher<?>[] typeMatchers = new MessageMatcher<?>[typesToMatch.length];
|
||||
for (int i = 0; i < typesToMatch.length; i++) {
|
||||
SimpMessageType typeToMatch = typesToMatch[i];
|
||||
typeMatchers[i] = new SimpMessageTypeMatcher(typeToMatch);
|
||||
}
|
||||
return matchers(typeMatchers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances without
|
||||
* regard to the {@link SimpMessageType}. If no destination is found on the
|
||||
* Message, then the Matcher returns false.
|
||||
* @param patterns the patterns to create
|
||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
|
||||
* from.
|
||||
*/
|
||||
public Builder.Constraint simpDestMatchers(String... patterns) {
|
||||
return simpDestMatchers(null, patterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that
|
||||
* match on {@code SimpMessageType.MESSAGE}. If no destination is found on the
|
||||
* Message, then the Matcher returns false.
|
||||
* @param patterns the patterns to create
|
||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
|
||||
* from.
|
||||
*/
|
||||
public Builder.Constraint simpMessageDestMatchers(String... patterns) {
|
||||
return simpDestMatchers(SimpMessageType.MESSAGE, patterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that
|
||||
* match on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the
|
||||
* Message, then the Matcher returns false.
|
||||
* @param patterns the patterns to create
|
||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
|
||||
* from.
|
||||
*/
|
||||
public Builder.Constraint simpSubscribeDestMatchers(String... patterns) {
|
||||
return simpDestMatchers(SimpMessageType.SUBSCRIBE, patterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} 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
|
||||
* {@link SimpMessageType} is not considered for matching.
|
||||
* @param patterns the patterns to create
|
||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
|
||||
* from.
|
||||
* @return the {@link Builder.Constraint} that is associated to the
|
||||
* {@link MessageMatcher}
|
||||
*/
|
||||
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
|
||||
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
|
||||
for (String pattern : patterns) {
|
||||
Supplier<MessageMatcher<Object>> supplier = new Builder.PathMatcherMessageMatcherBuilder(pattern, type);
|
||||
MessageMatcher<?> matcher = new Builder.SupplierMessageMatcher(supplier);
|
||||
matchers.add(matcher);
|
||||
}
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
* expression.
|
||||
* @param matchers the {@link MessageMatcher} instances to map.
|
||||
* @return The {@link Builder.Constraint} that is associated to the
|
||||
* {@link MessageMatcher} instances
|
||||
*/
|
||||
public Builder.Constraint matchers(MessageMatcher<?>... matchers) {
|
||||
List<MessageMatcher<?>> builders = new ArrayList<>(matchers.length);
|
||||
for (MessageMatcher<?> matcher : matchers) {
|
||||
builders.add(matcher);
|
||||
}
|
||||
return new Builder.Constraint(builders);
|
||||
}
|
||||
|
||||
public AuthorizationManager<Message<?>> build() {
|
||||
return new MessageMatcherDelegatingAuthorizationManager(this.mappings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the security constraint to be applied to the {@link MessageMatcher}
|
||||
* instances.
|
||||
*/
|
||||
public final class Constraint {
|
||||
|
||||
private final List<? extends MessageMatcher<?>> messageMatchers;
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
* @param messageMatchers the {@link MessageMatcher} instances to map to this
|
||||
* constraint
|
||||
*/
|
||||
private Constraint(List<? extends MessageMatcher<?>> messageMatchers) {
|
||||
Assert.notEmpty(messageMatchers, "messageMatchers cannot be null or empty");
|
||||
this.messageMatchers = messageMatchers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for specifying {@link Message} instances require a particular
|
||||
* role. If you do not want to have "ROLE_" automatically inserted see
|
||||
* {@link #hasAuthority(String)}.
|
||||
* @param role the role to require (i.e. USER, ADMIN, etc). Note, it should
|
||||
* not start with "ROLE_" as this is automatically inserted.
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder hasRole(String role) {
|
||||
return access(AuthorityAuthorizationManager.hasRole(role));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for specifying {@link Message} instances require any of a number
|
||||
* of roles. If you do not want to have "ROLE_" automatically inserted see
|
||||
* {@link #hasAnyAuthority(String...)}
|
||||
* @param roles the roles to require (i.e. USER, ADMIN, etc). Note, it should
|
||||
* not start with "ROLE_" as this is automatically inserted.
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder hasAnyRole(String... roles) {
|
||||
return access(AuthorityAuthorizationManager.hasAnyRole(roles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that {@link Message} instances require a particular authority.
|
||||
* @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN,
|
||||
* etc).
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder hasAuthority(String authority) {
|
||||
return access(AuthorityAuthorizationManager.hasAuthority(authority));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that {@link Message} instances requires any of a number
|
||||
* authorities.
|
||||
* @param authorities the requests require at least one of the authorities
|
||||
* (i.e. "ROLE_USER","ROLE_ADMIN" would mean either "ROLE_USER" or
|
||||
* "ROLE_ADMIN" is required).
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder hasAnyAuthority(String... authorities) {
|
||||
return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that Messages are allowed by anyone.
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder permitAll() {
|
||||
return access((authentication, context) -> new AuthorizationDecision(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that Messages are not allowed by anyone.
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder denyAll() {
|
||||
return access((authorization, context) -> new AuthorizationDecision(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify that Messages are allowed by any authenticated user.
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder authenticated() {
|
||||
return access(AuthenticatedAuthorizationManager.authenticated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows specifying that Messages are secured by an arbitrary expression
|
||||
* @param authorizationManager the {@link AuthorizationManager} to secure the
|
||||
* destinations
|
||||
* @return the {@link Builder} for further customization
|
||||
*/
|
||||
public Builder access(AuthorizationManager<MessageAuthorizationContext<?>> authorizationManager) {
|
||||
for (MessageMatcher<?> messageMatcher : this.messageMatchers) {
|
||||
Builder.this.mappings.add(new Entry<>(messageMatcher, authorizationManager));
|
||||
}
|
||||
return Builder.this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SupplierMessageMatcher implements MessageMatcher<Object> {
|
||||
|
||||
private final Supplier<MessageMatcher<Object>> supplier;
|
||||
|
||||
private volatile MessageMatcher<Object> delegate;
|
||||
|
||||
SupplierMessageMatcher(Supplier<MessageMatcher<Object>> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Message<?> message) {
|
||||
if (this.delegate == null) {
|
||||
synchronized (this.supplier) {
|
||||
if (this.delegate == null) {
|
||||
this.delegate = this.supplier.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.delegate.matches(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class PathMatcherMessageMatcherBuilder implements Supplier<MessageMatcher<Object>> {
|
||||
|
||||
private final String pattern;
|
||||
|
||||
private final SimpMessageType type;
|
||||
|
||||
private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) {
|
||||
this.pattern = pattern;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private PathMatcher resolvePathMatcher() {
|
||||
return Builder.this.pathMatcher.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageMatcher<Object> get() {
|
||||
PathMatcher pathMatcher = resolvePathMatcher();
|
||||
if (this.type == null) {
|
||||
return new SimpDestinationMessageMatcher(this.pattern, pathMatcher);
|
||||
}
|
||||
if (SimpMessageType.MESSAGE == this.type) {
|
||||
return SimpDestinationMessageMatcher.createMessageMatcher(this.pattern, pathMatcher);
|
||||
}
|
||||
if (SimpMessageType.SUBSCRIBE == this.type) {
|
||||
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.pattern, pathMatcher);
|
||||
}
|
||||
throw new IllegalStateException(this.type + " is not supported since it does not have a destination");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class Entry<T> {
|
||||
|
||||
private final MessageMatcher<?> messageMatcher;
|
||||
|
||||
private final T entry;
|
||||
|
||||
Entry(MessageMatcher requestMatcher, T entry) {
|
||||
this.messageMatcher = requestMatcher;
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
MessageMatcher<?> getMessageMatcher() {
|
||||
return this.messageMatcher;
|
||||
}
|
||||
|
||||
T getEntry() {
|
||||
return this.entry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,7 +26,9 @@ import org.springframework.security.access.SecurityMetadataSource;
|
||||
* @since 4.0
|
||||
* @see ChannelSecurityInterceptor
|
||||
* @see DefaultMessageSecurityMetadataSource
|
||||
* @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public interface MessageSecurityMetadataSource extends SecurityMetadataSource {
|
||||
|
||||
}
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.messaging.access.intercept;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationEventPublisher;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorizationChannelInterceptor}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class AuthorizationChannelInterceptorTests {
|
||||
|
||||
@Mock
|
||||
Message<Object> message;
|
||||
|
||||
@Mock
|
||||
MessageChannel channel;
|
||||
|
||||
@Mock
|
||||
AuthorizationManager<Message<?>> authorizationManager;
|
||||
|
||||
@Mock
|
||||
AuthorizationEventPublisher eventPublisher;
|
||||
|
||||
Authentication originalAuth;
|
||||
|
||||
AuthorizationChannelInterceptor interceptor;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.interceptor = new AuthorizationChannelInterceptor(this.authorizationManager);
|
||||
this.originalAuth = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
|
||||
SecurityContextHolder.getContext().setAuthentication(this.originalAuth);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationManagerNullThenIllegalArgument() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new AuthorizationChannelInterceptor(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenAllowThenSameMessage() {
|
||||
given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true));
|
||||
assertThat(this.interceptor.preSend(this.message, this.channel)).isSameAs(this.message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenDenyThenException() {
|
||||
given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false));
|
||||
assertThatExceptionOfType(AccessDeniedException.class)
|
||||
.isThrownBy(() -> this.interceptor.preSend(this.message, this.channel));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setEventPublisherWhenNullThenException() {
|
||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
||||
.isThrownBy(() -> this.interceptor.setAuthorizationEventPublisher(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preSendWhenAuthorizationEventPublisherThenPublishes() {
|
||||
this.interceptor.setAuthorizationEventPublisher(this.eventPublisher);
|
||||
given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true));
|
||||
this.interceptor.preSend(this.message, this.channel);
|
||||
verify(this.eventPublisher).publishAuthorizationEvent(any(), any(), any());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user