1
0
mirror of synced 2026-05-22 13:23:17 +00:00

Add RequiredAuthoritiesRepository

Closes gh-18028
This commit is contained in:
Rob Winch
2025-10-07 15:42:08 -05:00
parent 586081c125
commit 473baad6bd
5 changed files with 366 additions and 0 deletions
@@ -0,0 +1,55 @@
/*
* 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.authorization;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.util.Assert;
/**
* A {@link Map} based implementation of {@link RequiredAuthoritiesRepository}.
*
* @author Rob Winch
* @since 7.0
*/
public final class MapRequiredAuthoritiesRepository implements RequiredAuthoritiesRepository {
private final Map<String, List<String>> usernameToAuthorities = new ConcurrentHashMap<>();
@Override
public List<String> findRequiredAuthorities(String username) {
Assert.hasText(username, "username cannot be empty");
return this.usernameToAuthorities.getOrDefault(username, Collections.emptyList());
}
public void saveRequiredAuthorities(String username, List<String> authorities) {
Assert.hasText(username, "username cannot be empty");
Assert.notNull(authorities, "authorities cannot be null");
List<String> userAuthorities = new ArrayList<>(authorities);
this.usernameToAuthorities.put(username, userAuthorities);
}
public void deleteRequiredAuthorities(String username) {
Assert.hasText(username, "username cannot be empty");
this.usernameToAuthorities.remove(username);
}
}
@@ -0,0 +1,70 @@
/*
* 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.authorization;
import java.util.List;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An {@link AuthorizationManager} that requires all the authorities returned by a
* {@link RequiredAuthoritiesRepository} implementation.
*
* @param <T> the type
* @author Rob Winch
* @since 7.0
* @see AllAuthoritiesAuthorizationManager
*/
public class RequiredAuthoritiesAuthorizationManager<T> implements AuthorizationManager<T> {
private final RequiredAuthoritiesRepository authorities;
/**
* Creates a new instance.
* @param authorities the {@link RequiredAuthoritiesRepository} to use. Cannot be
* null.
*/
public RequiredAuthoritiesAuthorizationManager(RequiredAuthoritiesRepository authorities) {
Assert.notNull(authorities, "authorities cannot be null");
this.authorities = authorities;
}
@Override
public @Nullable AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication,
T object) {
List<String> authorities = findAuthorities(authentication.get());
if (authorities.isEmpty()) {
return new AuthorizationDecision(true);
}
AllAuthoritiesAuthorizationManager<T> delegate = AllAuthoritiesAuthorizationManager
.hasAllAuthorities(authorities);
return delegate.authorize(authentication, object);
}
private List<String> findAuthorities(@Nullable Authentication authentication) {
if (authentication == null) {
return List.of();
}
String username = authentication.getName();
return this.authorities.findRequiredAuthorities(username);
}
}
@@ -0,0 +1,40 @@
/*
* 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.authorization;
import java.util.List;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
/**
* Finds additional required authorities for the provided {@link Authentication#getName()}
*
* @author Rob Winch
* @since 7.0
*/
public interface RequiredAuthoritiesRepository {
/**
* Finds additional required {@link GrantedAuthority#getAuthority()}s for the provided
* username.
* @param username the username. Cannot be null or empty.
* @return the additional authorities required.
*/
List<String> findRequiredAuthorities(String username);
}
@@ -0,0 +1,95 @@
/*
* 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.authorization;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.GrantedAuthorities;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Test {@link MapRequiredAuthoritiesRepository}.
*
* @author Rob Winch
* @since 7.0
*/
class MapRequiredAuthoritiesRepositoryTests {
private MapRequiredAuthoritiesRepository repository = new MapRequiredAuthoritiesRepository();
private String username = "user";
private List<String> authorities = List.of(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
GrantedAuthorities.FACTOR_OTT_AUTHORITY);
@Test
void workflow() {
this.repository.saveRequiredAuthorities(this.username, this.authorities);
assertThat(this.repository.findRequiredAuthorities(this.username))
.containsExactlyInAnyOrderElementsOf(this.authorities);
List<String> otherAuthorities = List.of(GrantedAuthorities.FACTOR_PASSWORD_AUTHORITY,
GrantedAuthorities.FACTOR_WEBAUTHN_AUTHORITY);
this.repository.saveRequiredAuthorities(this.username, otherAuthorities);
assertThat(this.repository.findRequiredAuthorities(this.username))
.containsExactlyInAnyOrderElementsOf(otherAuthorities);
this.repository.deleteRequiredAuthorities(this.username);
assertThat(this.repository.findRequiredAuthorities(this.username)).isEmpty();
}
@Test
void findRequiredAuthoritiesWhenNullUsernameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.findRequiredAuthorities(null));
}
@Test
void findRequiredAuthoritiesWhenEmptyUsernameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.findRequiredAuthorities(""));
}
@Test
void saveRequiredAuthoritiesWhenNullUsernameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.repository.saveRequiredAuthorities(null, this.authorities));
}
@Test
void saveRequiredAuthoritiesWhenEmptyUsernameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.repository.saveRequiredAuthorities("", this.authorities));
}
@Test
void saveRequiredAuthoritiesWhenNullAuthoritiesThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.repository.saveRequiredAuthorities(this.username, null));
}
@Test
void deleteRequiredAuthoritiesWhenNullUsernameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.deleteRequiredAuthorities(null));
}
@Test
void deleteRequiredAuthoritiesWhenEmptyUsernameThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.repository.deleteRequiredAuthorities(""));
}
}
@@ -0,0 +1,106 @@
/*
* 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.authorization;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
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.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
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.BDDMockito.given;
/**
* Tests for {@link RequiredAuthoritiesAuthorizationManager}.
*
* @author Rob Winch
* @since 7.0
*/
@ExtendWith(MockitoExtension.class)
class RequiredAuthoritiesAuthorizationManagerTests {
@Mock
private RequiredAuthoritiesRepository repository;
private static final Object DOES_NOT_MATTER = "";
private RequiredAuthoritiesAuthorizationManager<Object> manager;
private Supplier<Authentication> authentication = () -> new TestingAuthenticationToken("user", "password",
"ROLE_USER", "ROLE_ADMIN");
@BeforeEach
void setup() {
this.manager = new RequiredAuthoritiesAuthorizationManager<>(this.repository);
}
@Test
void constructorWhenNullRepositoryThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new RequiredAuthoritiesAuthorizationManager(null));
}
@Test
void authorizeWhenNoResults() {
returnAuthorities(Collections.emptyList());
assertGranted();
}
@Test
void authorizeWhenAdditionalAuthoriteisAndGranted() {
returnAuthorities(
this.authentication.get().getAuthorities().stream().map(GrantedAuthority::getAuthority).toList());
assertGranted();
}
@Test
void authorizeWhenAdditionalAuthoriteisAndDenied() {
returnAuthorities(List.of("NOT_FOUND"));
assertDenied();
}
@Test
void authorizeWhenOneFoundAndDenied() {
returnAuthorities(List.of("ROLE_USER", "NOT_FOUND"));
assertDenied();
}
private void returnAuthorities(List<String> authorities) {
given(this.repository.findRequiredAuthorities(any())).willReturn(authorities);
}
private void assertGranted() {
AuthorizationResult authz = this.manager.authorize(this.authentication, DOES_NOT_MATTER);
assertThat(authz.isGranted()).isTrue();
}
private void assertDenied() {
AuthorizationResult authz = this.manager.authorize(this.authentication, DOES_NOT_MATTER);
assertThat(authz.isGranted()).isFalse();
}
}