mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-04 20:45:52 +01:00
Add scope from role for api token realm
This commit is contained in:
@@ -33,6 +33,7 @@ import org.apache.shiro.authc.credential.DefaultPasswordService;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.apache.shiro.authc.pam.AuthenticationStrategy;
|
||||
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
|
||||
import org.apache.shiro.authz.permission.PermissionResolver;
|
||||
import org.apache.shiro.crypto.hash.DefaultHashService;
|
||||
import org.apache.shiro.guice.web.ShiroWebModule;
|
||||
import org.apache.shiro.realm.Realm;
|
||||
@@ -48,6 +49,7 @@ import javax.servlet.ServletContext;
|
||||
import org.apache.shiro.mgt.RememberMeManager;
|
||||
import sonia.scm.security.DisabledRememberMeManager;
|
||||
import sonia.scm.security.ScmAtLeastOneSuccessfulStrategy;
|
||||
import sonia.scm.security.ScmPermissionResolver;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -102,6 +104,7 @@ public class ScmSecurityModule extends ShiroWebModule
|
||||
bind(ModularRealmAuthenticator.class);
|
||||
bind(Authenticator.class).to(ModularRealmAuthenticator.class);
|
||||
bind(AuthenticationStrategy.class).to(ScmAtLeastOneSuccessfulStrategy.class);
|
||||
bind(PermissionResolver.class).to(ScmPermissionResolver.class);
|
||||
|
||||
// bind realm
|
||||
for (Class<? extends Realm> realm : extensionProcessor.byExtensionPoint(Realm.class))
|
||||
|
||||
@@ -24,18 +24,19 @@
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.UsernamePasswordToken;
|
||||
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.realm.AuthenticatingRealm;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.RepositoryRole;
|
||||
import sonia.scm.repository.RepositoryRoleManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@Singleton
|
||||
@@ -44,24 +45,36 @@ public class ApiKeyRealm extends AuthenticatingRealm {
|
||||
|
||||
private final ApiKeyService apiKeyService;
|
||||
private final DAORealmHelper helper;
|
||||
private final RepositoryRoleManager repositoryRoleManager;
|
||||
|
||||
@Inject
|
||||
public ApiKeyRealm(ApiKeyService apiKeyService, DAORealmHelperFactory helperFactory) {
|
||||
public ApiKeyRealm(ApiKeyService apiKeyService, DAORealmHelperFactory helperFactory, RepositoryRoleManager repositoryRoleManager) {
|
||||
this.apiKeyService = apiKeyService;
|
||||
this.helper = helperFactory.create("ApiTokenRealm");
|
||||
this.repositoryRoleManager = repositoryRoleManager;
|
||||
setAuthenticationTokenClass(BearerToken.class);
|
||||
setCredentialsMatcher(new AllowAllCredentialsMatcher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof UsernamePasswordToken || token instanceof BearerToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
|
||||
checkArgument(token instanceof BearerToken, "%s is required", BearerToken.class);
|
||||
BearerToken bt = (BearerToken) token;
|
||||
ApiKeyService.CheckResult check = apiKeyService.check(bt.getCredentials());
|
||||
RepositoryRole repositoryRole = repositoryRoleManager.get(check.getRole());
|
||||
if (repositoryRole == null) {
|
||||
throw new AuthorizationException("api key has unknown role: " + check.getRole());
|
||||
}
|
||||
String scope = "repository:" + String.join(",", repositoryRole.getVerbs()) + ":*";
|
||||
return helper
|
||||
.authenticationInfoBuilder(check.getUser())
|
||||
.withSessionId(bt.getPrincipal())
|
||||
// .withScope()
|
||||
.withScope(Scope.valueOf(scope))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ public class DefaultRealm extends AuthorizingRealm
|
||||
/** Field description */
|
||||
@VisibleForTesting
|
||||
static final String REALM = "DefaultRealm";
|
||||
private final ScmPermissionResolver permissionResolver;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -90,11 +91,18 @@ public class DefaultRealm extends AuthorizingRealm
|
||||
matcher.setPasswordService(service);
|
||||
setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher));
|
||||
setAuthenticationTokenClass(UsernamePasswordToken.class);
|
||||
permissionResolver = new ScmPermissionResolver();
|
||||
setPermissionResolver(permissionResolver);
|
||||
|
||||
// we cache in the AuthorizationCollector
|
||||
setCachingEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScmPermissionResolver getPermissionResolver() {
|
||||
return permissionResolver;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.apache.shiro.authz.permission.PermissionResolver;
|
||||
|
||||
public class ScmPermissionResolver implements PermissionResolver {
|
||||
@Override
|
||||
public ScmWildcardPermission resolvePermission(String permissionString) {
|
||||
return new ScmWildcardPermission(permissionString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.shiro.authz.permission.WildcardPermission;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Collections.singleton;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
public class ScmWildcardPermission extends WildcardPermission {
|
||||
public ScmWildcardPermission(String permissionString) {
|
||||
super(permissionString);
|
||||
}
|
||||
|
||||
Collection<ScmWildcardPermission> limit(Scope scope) {
|
||||
Collection<ScmWildcardPermission> result = new ArrayList<>();
|
||||
for (String s : scope) {
|
||||
limit(s).ifPresent(result::add);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Optional<ScmWildcardPermission> limit(String scope) {
|
||||
return limit(new ScmWildcardPermission(scope));
|
||||
}
|
||||
|
||||
Optional<ScmWildcardPermission> limit(ScmWildcardPermission scope) {
|
||||
if (this.implies(scope)) {
|
||||
return of(scope);
|
||||
}
|
||||
if (scope.implies(this)) {
|
||||
return of(this);
|
||||
}
|
||||
|
||||
final List<Set<String>> theseParts = getParts();
|
||||
final List<Set<String>> scopeParts = scope.getParts();
|
||||
|
||||
if (!getEntries(theseParts, 0).equals(getEntries(scopeParts, 0))) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
String type = getEntries(scopeParts, 0).iterator().next();
|
||||
Collection<String> verbs = intersect(theseParts, scopeParts, 1);
|
||||
Collection<String> ids = intersect(theseParts, scopeParts, 2);
|
||||
|
||||
if (verbs.isEmpty() || ids.isEmpty()) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
return of(new ScmWildcardPermission(type + ":" + String.join(",", verbs) + ":" + String.join(",", ids)));
|
||||
}
|
||||
|
||||
private Collection<String> intersect(List<Set<String>> theseParts, List<Set<String>> scopeParts, int position) {
|
||||
final Set<String> theseEntries = getEntries(theseParts, position);
|
||||
final Set<String> scopeEntries = getEntries(scopeParts, position);
|
||||
if (isWildcard(theseEntries)) {
|
||||
return scopeEntries;
|
||||
}
|
||||
if (isWildcard(scopeEntries)) {
|
||||
return theseEntries;
|
||||
}
|
||||
return CollectionUtils.intersection(theseEntries, scopeEntries);
|
||||
}
|
||||
|
||||
private Set<String> getEntries(List<Set<String>> theseParts, int position) {
|
||||
if (position >= theseParts.size()) {
|
||||
return singleton(WILDCARD_TOKEN);
|
||||
}
|
||||
return theseParts.get(position);
|
||||
}
|
||||
|
||||
private boolean isWildcard(Set<String> entries) {
|
||||
return entries.size() == 1 && entries.contains(WILDCARD_TOKEN);
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,18 @@
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.Permission;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.Permission;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.authz.permission.PermissionResolver;
|
||||
|
||||
/**
|
||||
* Util methods for {@link Scope}.
|
||||
@@ -85,21 +83,24 @@ public final class Scopes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter permissions from {@link AuthorizationInfo} by scope values. Only permission definitions from the scope will
|
||||
* be returned and only if a permission from the {@link AuthorizationInfo} implies the requested scope permission.
|
||||
* Limit permissions from {@link AuthorizationInfo} by scope values. Permission definitions from the
|
||||
* {@link AuthorizationInfo} will be returned, if a permission from the scope implies the original permission.
|
||||
* If a permission from the {@link AuthorizationInfo} exceeds the permissions defined by the scope, it will
|
||||
* be reduced. If the latter computation results in an empty permission, it will be omitted.
|
||||
*
|
||||
* @param resolver permission resolver
|
||||
* @param authz authorization info
|
||||
* @param scope scope
|
||||
*
|
||||
* @return filtered {@link AuthorizationInfo}
|
||||
* @return limited {@link AuthorizationInfo}
|
||||
*/
|
||||
public static AuthorizationInfo filter(PermissionResolver resolver, AuthorizationInfo authz, Scope scope) {
|
||||
public static AuthorizationInfo filter(ScmPermissionResolver resolver, AuthorizationInfo authz, Scope scope) {
|
||||
List<Permission> authzPermissions = authzPermissions(resolver, authz);
|
||||
Predicate<Permission> predicate = implies(authzPermissions);
|
||||
Set<Permission> filteredPermissions = resolve(resolver, ImmutableList.copyOf(scope))
|
||||
Set<Permission> filteredPermissions = authzPermissions
|
||||
.stream()
|
||||
.filter(predicate)
|
||||
.map(p -> asScmWildcardPermission(p))
|
||||
.map(p -> p.limit(scope))
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> roles = ImmutableSet.copyOf(nullToEmpty(authz.getRoles()));
|
||||
@@ -108,26 +109,19 @@ public final class Scopes {
|
||||
return authzFiltered;
|
||||
}
|
||||
|
||||
public static ScmWildcardPermission asScmWildcardPermission(Permission p) {
|
||||
return p instanceof ScmWildcardPermission ? (ScmWildcardPermission) p : new ScmWildcardPermission(p.toString());
|
||||
}
|
||||
|
||||
private static <T> Collection<T> nullToEmpty(Collection<T> collection) {
|
||||
return collection != null ? collection : Collections.emptySet();
|
||||
}
|
||||
|
||||
private static Collection<Permission> resolve(PermissionResolver resolver, Collection<String> permissions) {
|
||||
private static Collection<ScmWildcardPermission> resolve(ScmPermissionResolver resolver, Collection<String> permissions) {
|
||||
return Collections2.transform(nullToEmpty(permissions), resolver::resolvePermission);
|
||||
}
|
||||
|
||||
private static Predicate<Permission> implies(Iterable<Permission> authzPermissions){
|
||||
return (scopePermission) -> {
|
||||
for ( Permission authzPermission : authzPermissions ) {
|
||||
if (authzPermission.implies(scopePermission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private static List<Permission> authzPermissions(PermissionResolver resolver, AuthorizationInfo authz){
|
||||
private static List<Permission> authzPermissions(ScmPermissionResolver resolver, AuthorizationInfo authz){
|
||||
List<Permission> authzPermissions = Lists.newArrayList();
|
||||
authzPermissions.addAll(nullToEmpty(authz.getObjectPermissions()));
|
||||
authzPermissions.addAll(resolve(resolver, authz.getStringPermissions()));
|
||||
|
||||
105
scm-webapp/src/test/java/sonia/scm/security/ApiKeyRealmTest.java
Normal file
105
scm-webapp/src/test/java/sonia/scm/security/ApiKeyRealmTest.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.RepositoryRole;
|
||||
import sonia.scm.repository.RepositoryRoleManager;
|
||||
|
||||
import static java.util.Collections.singleton;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.security.BearerToken.valueOf;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ApiKeyRealmTest {
|
||||
|
||||
@Mock
|
||||
ApiKeyService apiKeyService;
|
||||
@Mock
|
||||
DAORealmHelperFactory helperFactory;
|
||||
@Mock
|
||||
DAORealmHelper helper;
|
||||
@Mock(answer = Answers.RETURNS_SELF)
|
||||
DAORealmHelper.AuthenticationInfoBuilder authenticationInfoBuilder;
|
||||
@Mock
|
||||
RepositoryRoleManager repositoryRoleManager;
|
||||
|
||||
ApiKeyRealm realm;
|
||||
|
||||
@BeforeEach
|
||||
void initRealmHelper() {
|
||||
lenient().when(helperFactory.create("ApiTokenRealm")).thenReturn(helper);
|
||||
lenient().when(helper.authenticationInfoBuilder(any())).thenReturn(authenticationInfoBuilder);
|
||||
realm = new ApiKeyRealm(apiKeyService, helperFactory, repositoryRoleManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAuthenticationWithScope() {
|
||||
when(apiKeyService.check("towel")).thenReturn(new ApiKeyService.CheckResult("ford", "READ"));
|
||||
when(repositoryRoleManager.get("READ")).thenReturn(new RepositoryRole("guide", singleton("read"), "system"));
|
||||
|
||||
realm.doGetAuthenticationInfo(valueOf("towel"));
|
||||
|
||||
verify(helper).authenticationInfoBuilder("ford");
|
||||
verifyScopeSet("repository:read:*");
|
||||
verify(authenticationInfoBuilder).withSessionId(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithoutBearerToken() {
|
||||
AuthenticationToken otherToken = mock(AuthenticationToken.class);
|
||||
assertThrows(IllegalArgumentException.class, () -> realm.doGetAuthenticationInfo(otherToken));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithUnknownRole() {
|
||||
when(apiKeyService.check("towel")).thenReturn(new ApiKeyService.CheckResult("ford", "READ"));
|
||||
when(repositoryRoleManager.get("READ")).thenReturn(null);
|
||||
|
||||
BearerToken token = valueOf("towel");
|
||||
assertThrows(AuthorizationException.class, () -> realm.doGetAuthenticationInfo(token));
|
||||
}
|
||||
|
||||
void verifyScopeSet(String... permissions) {
|
||||
verify(authenticationInfoBuilder).withScope(argThat(scope -> {
|
||||
assertThat(scope).containsExactly(permissions);
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ScmWildcardPermissionTest {
|
||||
|
||||
@Test
|
||||
void shouldEliminatePermissionsWithDifferentSubject() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("user:write:*");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermissions = permission.limit("repository:write:*");
|
||||
|
||||
assertThat(limitedPermissions).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnScopeIfPermissionImpliesScope() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("*");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:read:42");
|
||||
|
||||
assertThat(limitedPermission).get().hasToString("repository:read:42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPermissionIfScopeImpliesPermission() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("repository:read:42");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:*:42");
|
||||
|
||||
assertThat(limitedPermission).get().hasToString("repository:read:42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLimitExplicitParts() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("repository:read,write:42,43,44");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:read,write,pull:42");
|
||||
|
||||
assertThat(limitedPermission).get().hasToString("repository:read,write:42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectWildcard() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("repository:read,write:*");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:*:42");
|
||||
|
||||
assertThat(limitedPermission).get().hasToString("repository:read,write:42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMissingEntriesAsWildcard() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("repository:read,write");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:*:42");
|
||||
|
||||
assertThat(limitedPermission).get().hasToString("repository:read,write:42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEliminateEmptyVerbs() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("repository:read:42");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:pull:42");
|
||||
|
||||
assertThat(limitedPermission).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEliminateEmptyId() {
|
||||
ScmWildcardPermission permission = new ScmWildcardPermission("repository:read:42");
|
||||
|
||||
Optional<ScmWildcardPermission> limitedPermission = permission.limit("repository:read:23");
|
||||
|
||||
assertThat(limitedPermission).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -26,15 +26,18 @@ package sonia.scm.security;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.Set;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.Permission;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.authz.permission.WildcardPermission;
|
||||
import org.apache.shiro.authz.permission.WildcardPermissionResolver;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.emptyCollectionOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Scopes}.
|
||||
@@ -43,7 +46,7 @@ import static org.hamcrest.Matchers.*;
|
||||
*/
|
||||
public class ScopesTest {
|
||||
|
||||
private final WildcardPermissionResolver resolver = new WildcardPermissionResolver();
|
||||
private final ScmPermissionResolver resolver = new ScmPermissionResolver();
|
||||
|
||||
/**
|
||||
* Tests that filter keep roles.
|
||||
@@ -67,6 +70,14 @@ public class ScopesTest {
|
||||
assertPermissions(Scopes.filter(resolver, authz, scope), "repository:read:123");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterX() {
|
||||
Scope scope = Scope.valueOf("repository:read,write:*");
|
||||
AuthorizationInfo authz = authz("repository:*:123");
|
||||
|
||||
assertPermissions(Scopes.filter(resolver, authz, scope), "repository:read,write:123");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests filter with a simple deny.
|
||||
*/
|
||||
@@ -88,7 +99,7 @@ public class ScopesTest {
|
||||
Scope scope = Scope.valueOf("repo:read,modify:1", "repo:read:2", "repo:*:3", "repo:modify:4");
|
||||
AuthorizationInfo authz = authz("repo:read:*");
|
||||
|
||||
assertPermissions(Scopes.filter(resolver, authz, scope), "repo:read:2");
|
||||
assertPermissions(Scopes.filter(resolver, authz, scope), "repo:read:1", "repo:read:2", "repo:read:3");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,10 +121,8 @@ public class ScopesTest {
|
||||
Scope scope = Scope.valueOf("*");
|
||||
AuthorizationInfo authz = authz("repository:*");
|
||||
|
||||
assertThat(
|
||||
Scopes.filter(resolver, authz, scope).getObjectPermissions(),
|
||||
is(emptyCollectionOf(Permission.class))
|
||||
);
|
||||
assertPermissions(Scopes.filter(resolver, authz, scope),
|
||||
"repository:*");
|
||||
}
|
||||
|
||||
private void assertPermissions(AuthorizationInfo authz, Object... permissions) {
|
||||
@@ -128,7 +137,7 @@ public class ScopesTest {
|
||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(Sets.newHashSet("unit", "test"));
|
||||
Set<Permission> permissions = Sets.newLinkedHashSet();
|
||||
for ( String value : values ) {
|
||||
permissions.add(new WildcardPermission(value));
|
||||
permissions.add(new ScmWildcardPermission(value));
|
||||
}
|
||||
info.setObjectPermissions(permissions);
|
||||
return info;
|
||||
|
||||
Reference in New Issue
Block a user