Add RequiredAuthoritiesRepository
Closes gh-18028
This commit is contained in:
+55
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+70
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+40
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+95
@@ -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(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+106
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user