1
0
mirror of synced 2026-09-12 12:15:13 +00:00

Move Messaging Access API

Issue gh-17847
This commit is contained in:
Josh Cummings
2025-09-02 15:50:57 -06:00
parent eedcec9d5c
commit 3182883e2e
14 changed files with 3 additions and 2 deletions
@@ -0,0 +1,102 @@
/*
* Copyright 2004-present 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.Collection;
import java.util.LinkedHashMap;
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.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
public class ExpressionBasedMessageSecurityMetadataSourceFactoryTests {
@Mock
MessageMatcher<Object> matcher1;
@Mock
MessageMatcher<Object> matcher2;
@Mock
Message<Object> message;
@Mock
Authentication authentication;
String expression1;
String expression2;
LinkedHashMap<MessageMatcher<?>, String> matcherToExpression;
MessageSecurityMetadataSource source;
MessageSecurityExpressionRoot rootObject;
@BeforeEach
public void setup() {
this.expression1 = "permitAll";
this.expression2 = "denyAll";
this.matcherToExpression = new LinkedHashMap<>();
this.matcherToExpression.put(this.matcher1, this.expression1);
this.matcherToExpression.put(this.matcher2, this.expression2);
this.source = ExpressionBasedMessageSecurityMetadataSourceFactory
.createExpressionMessageMetadataSource(this.matcherToExpression);
this.rootObject = new MessageSecurityExpressionRoot(this.authentication, this.message);
}
@Test
public void createExpressionMessageMetadataSourceNoMatch() {
Collection<ConfigAttribute> attrs = this.source.getAttributes(this.message);
assertThat(attrs).isEmpty();
}
@Test
public void createExpressionMessageMetadataSourceMatchFirst() {
given(this.matcher1.matches(this.message)).willReturn(true);
Collection<ConfigAttribute> attrs = this.source.getAttributes(this.message);
assertThat(attrs).hasSize(1);
ConfigAttribute attr = attrs.iterator().next();
assertThat(attr).isInstanceOf(MessageExpressionConfigAttribute.class);
assertThat(((MessageExpressionConfigAttribute) attr).getAuthorizeExpression().getValue(this.rootObject))
.isEqualTo(true);
}
@Test
public void createExpressionMessageMetadataSourceMatchSecond() {
given(this.matcher2.matches(this.message)).willReturn(true);
Collection<ConfigAttribute> attrs = this.source.getAttributes(this.message);
assertThat(attrs).hasSize(1);
ConfigAttribute attr = attrs.iterator().next();
assertThat(attr).isInstanceOf(MessageExpressionConfigAttribute.class);
assertThat(((MessageExpressionConfigAttribute) attr).getAuthorizeExpression().getValue(this.rootObject))
.isEqualTo(false);
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2004-present 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 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.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class MessageExpressionConfigAttributeTests {
@Mock
Expression expression;
@Mock
MessageMatcher<?> matcher;
MessageExpressionConfigAttribute attribute;
@BeforeEach
public void setup() {
this.attribute = new MessageExpressionConfigAttribute(this.expression, this.matcher);
}
@Test
public void constructorNullExpression() {
assertThatIllegalArgumentException().isThrownBy(() -> new MessageExpressionConfigAttribute(null, this.matcher));
}
@Test
public void constructorNullMatcher() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MessageExpressionConfigAttribute(this.expression, null));
}
@Test
public void getAuthorizeExpression() {
assertThat(this.attribute.getAuthorizeExpression()).isSameAs(this.expression);
}
@Test
public void getAttribute() {
assertThat(this.attribute.getAttribute()).isNull();
}
@Test
public void toStringUsesExpressionString() {
given(this.expression.getExpressionString()).willReturn("toString");
assertThat(this.attribute.toString()).isEqualTo(this.expression.getExpressionString());
}
@Test
public void postProcessContext() {
PathPatternMessageMatcher matcher = PathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**");
// @formatter:off
Message<?> message = MessageBuilder.withPayload("M")
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1")
.build();
// @formatter:on
EvaluationContext context = mock(EvaluationContext.class);
this.attribute = new MessageExpressionConfigAttribute(this.expression, matcher);
this.attribute.postProcess(context, message);
verify(context).setVariable("topic", "someTopic");
}
}
@@ -0,0 +1,153 @@
/*
* Copyright 2004-present 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.Arrays;
import java.util.Collection;
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.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class MessageExpressionVoterTests {
@Mock
Authentication authentication;
@Mock
Message<Object> message;
Collection<ConfigAttribute> attributes;
@Mock
Expression expression;
@Mock
MessageMatcher<?> matcher;
@Mock
SecurityExpressionHandler<Message> expressionHandler;
@Mock
EvaluationContext evaluationContext;
MessageExpressionVoter voter;
@BeforeEach
public void setup() {
this.attributes = Arrays
.<ConfigAttribute>asList(new MessageExpressionConfigAttribute(this.expression, this.matcher));
this.voter = new MessageExpressionVoter();
}
@Test
public void voteGranted() {
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))
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
}
@Test
public void voteDenied() {
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))
.isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
}
@Test
public void voteAbstain() {
this.attributes = Arrays.<ConfigAttribute>asList(new SecurityConfig("ROLE_USER"));
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
.isEqualTo(AccessDecisionVoter.ACCESS_ABSTAIN);
}
@Test
public void supportsObjectClassFalse() {
assertThat(this.voter.supports(Object.class)).isFalse();
}
@Test
public void supportsMessageClassTrue() {
assertThat(this.voter.supports(Message.class)).isTrue();
}
@Test
public void supportsSecurityConfigFalse() {
assertThat(this.voter.supports(new SecurityConfig("ROLE_USER"))).isFalse();
}
@Test
public void supportsMessageExpressionConfigAttributeTrue() {
assertThat(this.voter.supports(new MessageExpressionConfigAttribute(this.expression, this.matcher))).isTrue();
}
@Test
public void setExpressionHandlerNull() {
assertThatIllegalArgumentException().isThrownBy(() -> this.voter.setExpressionHandler(null));
}
@Test
public void customExpressionHandler() {
this.voter.setExpressionHandler(this.expressionHandler);
given(this.expressionHandler.createEvaluationContext(this.authentication, this.message))
.willReturn(this.evaluationContext);
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))
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
verify(this.expressionHandler).createEvaluationContext(this.authentication, this.message);
}
@Test
public void postProcessEvaluationContext() {
final MessageExpressionConfigAttribute configAttribute = mock(MessageExpressionConfigAttribute.class);
this.voter.setExpressionHandler(this.expressionHandler);
given(this.expressionHandler.createEvaluationContext(this.authentication, this.message))
.willReturn(this.evaluationContext);
given(configAttribute.getAuthorizeExpression()).willReturn(this.expression);
this.attributes = Arrays.<ConfigAttribute>asList(configAttribute);
given(configAttribute.postProcess(this.evaluationContext, this.message)).willReturn(this.evaluationContext);
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(true);
assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
verify(configAttribute).postProcess(this.evaluationContext, this.message);
}
}
@@ -0,0 +1,171 @@
/*
* Copyright 2004-present 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.Arrays;
import java.util.Collection;
import java.util.List;
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.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.intercept.RunAsManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
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.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
@ExtendWith(MockitoExtension.class)
public class ChannelSecurityInterceptorTests {
@Mock
Message<Object> message;
@Mock
MessageChannel channel;
@Mock
MessageSecurityMetadataSource source;
@Mock
AccessDecisionManager accessDecisionManager;
@Mock
RunAsManager runAsManager;
@Mock
Authentication runAs;
Authentication originalAuth;
List<ConfigAttribute> attrs;
ChannelSecurityInterceptor interceptor;
@BeforeEach
public void setup() {
this.attrs = Arrays.<ConfigAttribute>asList(new SecurityConfig("ROLE_USER"));
this.interceptor = new ChannelSecurityInterceptor(this.source);
this.interceptor.setAccessDecisionManager(this.accessDecisionManager);
this.interceptor.setRunAsManager(this.runAsManager);
this.originalAuth = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
SecurityContextHolder.getContext().setAuthentication(this.originalAuth);
}
@AfterEach
public void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorMessageSecurityMetadataSourceNull() {
assertThatIllegalArgumentException().isThrownBy(() -> new ChannelSecurityInterceptor(null));
}
@Test
public void getSecureObjectClass() {
assertThat(this.interceptor.getSecureObjectClass()).isEqualTo(Message.class);
}
@Test
public void obtainSecurityMetadataSource() {
assertThat(this.interceptor.obtainSecurityMetadataSource()).isEqualTo(this.source);
}
@Test
public void preSendNullAttributes() {
assertThat(this.interceptor.preSend(this.message, this.channel)).isSameAs(this.message);
}
@Test
public void preSendGrant() {
given(this.source.getAttributes(this.message)).willReturn(this.attrs);
Message<?> result = this.interceptor.preSend(this.message, this.channel);
assertThat(result).isSameAs(this.message);
}
@Test
public void preSendDeny() {
given(this.source.getAttributes(this.message)).willReturn(this.attrs);
willThrow(new AccessDeniedException("")).given(this.accessDecisionManager)
.decide(any(Authentication.class), eq(this.message), eq(this.attrs));
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.interceptor.preSend(this.message, this.channel));
}
@SuppressWarnings("unchecked")
@Test
public void preSendPostSendRunAs() {
given(this.source.getAttributes(this.message)).willReturn(this.attrs);
given(this.runAsManager.buildRunAs(any(Authentication.class), any(), any(Collection.class)))
.willReturn(this.runAs);
Message<?> preSend = this.interceptor.preSend(this.message, this.channel);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.runAs);
this.interceptor.postSend(preSend, this.channel, true);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.originalAuth);
}
@Test
public void afterSendCompletionNotTokenMessageNoExceptionThrown() {
this.interceptor.afterSendCompletion(this.message, this.channel, true, null);
}
@SuppressWarnings("unchecked")
@Test
public void preSendFinallySendRunAs() {
given(this.source.getAttributes(this.message)).willReturn(this.attrs);
given(this.runAsManager.buildRunAs(any(Authentication.class), any(), any(Collection.class)))
.willReturn(this.runAs);
Message<?> preSend = this.interceptor.preSend(this.message, this.channel);
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.runAs);
this.interceptor.afterSendCompletion(preSend, this.channel, true, new RuntimeException());
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.originalAuth);
}
@Test
public void preReceive() {
assertThat(this.interceptor.preReceive(this.channel)).isTrue();
}
@Test
public void postReceive() {
assertThat(this.interceptor.postReceive(this.message, this.channel)).isSameAs(this.message);
}
@Test
public void afterReceiveCompletionNullExceptionNoExceptionThrown() {
this.interceptor.afterReceiveCompletion(this.message, this.channel, null);
}
}
@@ -0,0 +1,101 @@
/*
* Copyright 2004-present 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.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
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.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
public class DefaultMessageSecurityMetadataSourceTests {
@Mock
MessageMatcher<Object> matcher1;
@Mock
MessageMatcher<Object> matcher2;
@Mock
Message<?> message;
@Mock
Authentication authentication;
SecurityConfig config1;
SecurityConfig config2;
LinkedHashMap<MessageMatcher<?>, Collection<ConfigAttribute>> messageMap;
MessageSecurityMetadataSource source;
@BeforeEach
public void setup() {
this.messageMap = new LinkedHashMap<>();
this.messageMap.put(this.matcher1, Arrays.<ConfigAttribute>asList(this.config1));
this.messageMap.put(this.matcher2, Arrays.<ConfigAttribute>asList(this.config2));
this.source = new DefaultMessageSecurityMetadataSource(this.messageMap);
}
@Test
public void getAttributesEmpty() {
assertThat(this.source.getAttributes(this.message)).isEmpty();
}
@Test
public void getAttributesFirst() {
given(this.matcher1.matches(this.message)).willReturn(true);
assertThat(this.source.getAttributes(this.message)).containsOnly(this.config1);
}
@Test
public void getAttributesSecond() {
given(this.matcher1.matches(this.message)).willReturn(true);
assertThat(this.source.getAttributes(this.message)).containsOnly(this.config2);
}
@Test
public void getAllConfigAttributes() {
assertThat(this.source.getAllConfigAttributes()).containsOnly(this.config1, this.config2);
}
@Test
public void supportsFalse() {
assertThat(this.source.supports(Object.class)).isFalse();
}
@Test
public void supportsTrue() {
assertThat(this.source.supports(Message.class)).isTrue();
}
}