Parcourir la source

Add RequiredAuthoritiesRepository

Closes gh-18028
Rob Winch il y a 4 jours
Parent
commit
473baad6bd

+ 55 - 0
core/src/main/java/org/springframework/security/authorization/MapRequiredAuthoritiesRepository.java

@@ -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
core/src/main/java/org/springframework/security/authorization/RequiredAuthoritiesAuthorizationManager.java

@@ -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
core/src/main/java/org/springframework/security/authorization/RequiredAuthoritiesRepository.java

@@ -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
core/src/test/java/org/springframework/security/authorization/MapRequiredAuthoritiesRepositoryTests.java

@@ -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
core/src/test/java/org/springframework/security/authorization/RequiredAuthoritiesAuthorizationManagerTests.java

@@ -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();
+	}
+
+}