This commit is contained in:
Eduard Heimbuch
2019-08-02 15:29:08 +02:00
34 changed files with 370 additions and 1056 deletions

View File

@@ -1,22 +0,0 @@
package sonia.scm.group;
import java.util.Collection;
/**
* This class represents all associated groups which are provided by external systems for a certain user.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public class ExternalGroupNames extends GroupNames {
public ExternalGroupNames() {
}
public ExternalGroupNames(String groupName, String... groupNames) {
super(groupName, groupNames);
}
public ExternalGroupNames(Collection<String> collection) {
super(collection);
}
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.group;
import java.util.Set;
public interface GroupCollector {
String AUTHENTICATED = "_authenticated";
Set<String> collect(String principal);
}

View File

@@ -1,187 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.group;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
//~--- JDK imports ------------------------------------------------------------
/**
* This class represents all associated groups for a user.
*
* @author Sebastian Sdorra
* @since 1.21
*/
public class GroupNames implements Serializable, Iterable<String>
{
/**
* Group for all authenticated users
* @since 1.31
*/
public static final String AUTHENTICATED = "_authenticated";
/** Field description */
private static final long serialVersionUID = 8615685985213897947L;
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*/
public GroupNames()
{
this(Collections.emptyList());
}
/**
* Constructs ...
*
*
* @param groupName
* @param groupNames
*/
public GroupNames(String groupName, String... groupNames)
{
this(Lists.asList(groupName, groupNames));
}
/**
* Constructs ...
*
*
* @param collection
*/
public GroupNames(Collection<String> collection)
{
this.collection = Collections.unmodifiableCollection(collection);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param groupName
*
* @return
*/
public boolean contains(String groupName)
{
return collection.contains(groupName);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final GroupNames other = (GroupNames) obj;
return Objects.equal(collection, other.collection);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode()
{
return Objects.hashCode(collection);
}
/**
* Method description
*
*
* @return
*/
@Override
public Iterator<String> iterator()
{
return collection.iterator();
}
/**
* Method description
*
*
* @return
*/
@Override
public String toString()
{
return Joiner.on(", ").join(collection);
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
public Collection<String> getCollection()
{
return collection;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Collection<String> collection;
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.group;
import sonia.scm.plugin.ExtensionPoint;
import java.util.Set;
@ExtensionPoint
public interface GroupResolver {
Set<String> resolve(String principal);
}

View File

@@ -82,7 +82,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private String namespace;
private String name;
@XmlElement(name = "permission")
private final Set<RepositoryPermission> permissions = new HashSet<>();
private Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
@@ -331,6 +331,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
try {
repository = (Repository) super.clone();
// fix permission reference on clone
repository.permissions = new HashSet<>(permissions);
} catch (CloneNotSupportedException ex) {
throw new RuntimeException(ex);
}

View File

@@ -99,15 +99,6 @@ public interface AccessTokenBuilder {
*/
AccessTokenBuilder scope(Scope scope);
/**
* Define the logged in user as member of the given groups.
*
* @param groups group names
*
* @return {@code this}
*/
AccessTokenBuilder groups(String... groups);
/**
* Creates a new {@link AccessToken} with the provided settings.
*

View File

@@ -45,7 +45,6 @@ import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.GroupDAO;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
@@ -71,8 +70,6 @@ public final class DAORealmHelper {
private final UserDAO userDAO;
private final GroupCollector groupCollector;
private final String realm;
//~--- constructors ---------------------------------------------------------
@@ -83,14 +80,12 @@ public final class DAORealmHelper {
*
* @param loginAttemptHandler login attempt handler for wrapping credentials matcher
* @param userDAO user dao
* @param groupCollector collect groups for a principal
* @param realm name of realm
*/
public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupCollector groupCollector, String realm) {
public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, String realm) {
this.loginAttemptHandler = loginAttemptHandler;
this.realm = realm;
this.userDAO = userDAO;
this.groupCollector = groupCollector;
}
//~--- get methods ----------------------------------------------------------
@@ -120,7 +115,7 @@ public final class DAORealmHelper {
UsernamePasswordToken upt = (UsernamePasswordToken) token;
String principal = upt.getUsername();
return getAuthenticationInfo(principal, null, null, Collections.emptySet());
return getAuthenticationInfo(principal, null, null);
}
/**
@@ -135,7 +130,7 @@ public final class DAORealmHelper {
}
private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable<String> groups) {
private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) {
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
LOG.debug("try to authenticate {}", principal);
@@ -153,7 +148,6 @@ public final class DAORealmHelper {
collection.add(principal, realm);
collection.add(user, realm);
collection.add(groupCollector.collect(principal, groups), realm);
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
String creds = credentials;
@@ -207,17 +201,17 @@ public final class DAORealmHelper {
return this;
}
/**
* With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info.
*
* @param groups extra groups
*
* @return {@code this}
*/
public AuthenticationInfoBuilder withGroups(Iterable<String> groups) {
this.groups = groups;
return this;
}
// /**
// * With groups adds extra groups, besides those which come from the {@link GroupDAO}, to the authentication info.
// *
// * @param groups extra groups
// *
// * @return {@code this}
// */
// public AuthenticationInfoBuilder withGroups(Iterable<String> groups) {
// this.groups = groups;
// return this;
// }
/**
* Build creates the authentication info from the given information.
@@ -225,7 +219,7 @@ public final class DAORealmHelper {
* @return authentication info
*/
public AuthenticationInfo build() {
return getAuthenticationInfo(principal, credentials, scope, groups);
return getAuthenticationInfo(principal, credentials, scope);
}
}

View File

@@ -30,7 +30,7 @@
*/
package sonia.scm.security;
import sonia.scm.group.GroupDAO;
import sonia.scm.cache.CacheManager;
import sonia.scm.user.UserDAO;
import javax.inject.Inject;
@@ -45,20 +45,19 @@ public final class DAORealmHelperFactory {
private final LoginAttemptHandler loginAttemptHandler;
private final UserDAO userDAO;
private final GroupCollector groupCollector;
private final CacheManager cacheManager;
/**
* Constructs a new instance.
*
* @param loginAttemptHandler login attempt handler
* @param userDAO user dao
* @param groupDAO group dao
* @param cacheManager
*/
@Inject
public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) {
public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, CacheManager cacheManager) {
this.loginAttemptHandler = loginAttemptHandler;
this.userDAO = userDAO;
this.groupCollector = new GroupCollector(groupDAO);
this.cacheManager = cacheManager;
}
/**
@@ -69,7 +68,7 @@ public final class DAORealmHelperFactory {
* @return new {@link DAORealmHelper} instance.
*/
public DAORealmHelper create(String realm) {
return new DAORealmHelper(loginAttemptHandler, userDAO, groupCollector, realm);
return new DAORealmHelper(loginAttemptHandler, userDAO, realm);
}
}

View File

@@ -1,43 +0,0 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
/**
* Collect groups for a certain principal.
* <strong>Warning</strong>: The class is only for internal use and should never used directly.
*/
class GroupCollector {
private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class);
private final GroupDAO groupDAO;
GroupCollector(GroupDAO groupDAO) {
this.groupDAO = groupDAO;
}
GroupNames collect(String principal, Iterable<String> groupNames) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
builder.add(GroupNames.AUTHENTICATED);
for (String group : groupNames) {
builder.add(group);
}
for (Group group : groupDAO.getAll()) {
if (group.isMember(principal)) {
builder.add(group.getName());
}
}
GroupNames groups = new GroupNames(builder.build());
LOG.debug("collected following groups for principal {}: {}", principal, groups);
return groups;
}
}

View File

@@ -32,24 +32,15 @@ import com.google.inject.Inject;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.group.ExternalGroupNames;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupManager;
import sonia.scm.plugin.Extension;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext;
import java.util.Collection;
import java.util.Collections;
import static java.util.Arrays.asList;
/**
* Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated
* users with the local database.
@@ -60,12 +51,9 @@ import static java.util.Arrays.asList;
@Extension
public final class SyncingRealmHelper {
private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class);
private final AdministrationContext ctx;
private final UserManager userManager;
private final GroupManager groupManager;
private final GroupCollector groupCollector;
/**
* Constructs a new SyncingRealmHelper.
@@ -73,134 +61,28 @@ public final class SyncingRealmHelper {
* @param ctx administration context
* @param userManager user manager
* @param groupManager group manager
* @param groupDAO group dao
*/
@Inject
public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO) {
public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) {
this.ctx = ctx;
this.userManager = userManager;
this.groupManager = groupManager;
this.groupCollector = new GroupCollector(groupDAO);
}
/**
* Create {@link AuthenticationInfo} from user and groups.
*/
public AuthenticationInfoBuilder.ForRealm authenticationInfo() {
return new AuthenticationInfoBuilder().new ForRealm();
}
public class AuthenticationInfoBuilder {
private String realm;
private User user;
private Collection<String> groups = Collections.emptySet();
private Collection<String> externalGroups = Collections.emptySet();
private AuthenticationInfo build() {
return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, externalGroups);
}
public class ForRealm {
private ForRealm() {
}
/**
* Sets the realm.
* @param realm name of the realm
*/
public ForUser forRealm(String realm) {
AuthenticationInfoBuilder.this.realm = realm;
return AuthenticationInfoBuilder.this.new ForUser();
}
}
public class ForUser {
private ForUser() {
}
/**
* Sets the user.
* @param user authenticated user
*/
public AuthenticationInfoBuilder.WithGroups andUser(User user) {
AuthenticationInfoBuilder.this.user = user;
return AuthenticationInfoBuilder.this.new WithGroups();
}
}
public class WithGroups {
private WithGroups() {
}
/**
* Set the internal groups for the user.
* @param groups groups of the authenticated user
* @return builder step for groups
*/
public WithGroups withGroups(String... groups) {
return withGroups(asList(groups));
}
/**
* Set the internal groups for the user.
* @param groups groups of the authenticated user
* @return builder step for groups
*/
public WithGroups withGroups(Collection<String> groups) {
AuthenticationInfoBuilder.this.groups = groups;
return this;
}
/**
* Set the external groups for the user.
* @param externalGroups external groups of the authenticated user
* @return builder step for groups
*/
public WithGroups withExternalGroups(String... externalGroups) {
return withExternalGroups(asList(externalGroups));
}
/**
* Set the external groups for the user.
* @param externalGroups external groups of the authenticated user
* @return builder step for groups
*/
public WithGroups withExternalGroups(Collection<String> externalGroups) {
AuthenticationInfoBuilder.this.externalGroups = externalGroups;
return this;
}
/**
* Builds the {@link AuthenticationInfo} from the given options.
*
* @return complete autentication info
*/
public AuthenticationInfo build() {
return AuthenticationInfoBuilder.this.build();
}
}
}
//~--- methods --------------------------------------------------------------
/**
* Create {@link AuthenticationInfo} from user and groups.
*
*
* @param realm name of the realm
* @param user authenticated user
* @param groups groups of the authenticated user
*
* @return authentication info
*/
private AuthenticationInfo createAuthenticationInfo(String realm, User user,
Collection<String> groups, Collection<String> externalGroups) {
public AuthenticationInfo createAuthenticationInfo(String realm, User user) {
SimplePrincipalCollection collection = new SimplePrincipalCollection();
collection.add(user.getId(), realm);
collection.add(user, realm);
collection.add(groupCollector.collect(user.getId(), groups), realm);
collection.add(new ExternalGroupNames(externalGroups), realm);
return new SimpleAuthenticationInfo(collection, user.getPassword());
}

View File

@@ -0,0 +1,22 @@
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class RepositoryTest {
@Test
void shouldCreateNewPermissionOnClone() {
Repository repository = new Repository();
repository.setPermissions(Arrays.asList(new RepositoryPermission("one", "role", false)));
Repository cloned = repository.clone();
cloned.setPermissions(Arrays.asList(new RepositoryPermission("two", "role", false)));
assertThat(repository.getPermissions()).extracting(r -> r.getName()).containsOnly("one");
}
}

View File

@@ -1,20 +1,16 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableList;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.junit.Ignore;
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 sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
@@ -38,7 +34,7 @@ class DAORealmHelperTest {
@BeforeEach
void setUpObjectUnderTest() {
helper = new DAORealmHelper(loginAttemptHandler, userDAO, new GroupCollector(groupDAO), "hitchhiker");
helper = new DAORealmHelper(loginAttemptHandler, userDAO, "hitchhiker");
}
@Test
@@ -73,29 +69,9 @@ class DAORealmHelperTest {
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build();
PrincipalCollection principals = authenticationInfo.getPrincipals();
assertThat(principals.oneByType(User.class)).isSameAs(user);
assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated");
assertThat(principals.oneByType(Scope.class)).isEmpty();
}
@Test
@Ignore
void shouldReturnAuthenticationInfoWithGroups() {
User user = new User("trillian");
when(userDAO.get("trillian")).thenReturn(user);
Group one = new Group("xml", "one", "trillian");
Group two = new Group("xml", "two", "trillian");
Group six = new Group("xml", "six", "dent");
when(groupDAO.getAll()).thenReturn(ImmutableList.of(one, two, six));
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
.withGroups(ImmutableList.of("three"))
.build();
PrincipalCollection principals = authenticationInfo.getPrincipals();
assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated", "one", "two", "three");
}
@Test
void shouldReturnAuthenticationInfoWithScope() {
User user = new User("trillian");
@@ -148,7 +124,6 @@ class DAORealmHelperTest {
PrincipalCollection principals = authenticationInfo.getPrincipals();
assertThat(principals.oneByType(User.class)).isSameAs(user);
assertThat(principals.oneByType(GroupNames.class)).containsOnly("_authenticated");
assertThat(principals.oneByType(Scope.class)).isEmpty();
assertThat(authenticationInfo.getCredentials()).isNull();

View File

@@ -1,64 +0,0 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GroupCollectorTest {
@Mock
private GroupDAO groupDAO;
@InjectMocks
private GroupCollector collector;
@Test
void shouldAlwaysReturnAuthenticatedGroup() {
GroupNames groupNames = collector.collect("trillian", Collections.emptySet());
assertThat(groupNames).containsOnly("_authenticated");
}
@Nested
class WithGroupsFromDao {
@BeforeEach
void setUpGroupsDao() {
List<Group> groups = Lists.newArrayList(
new Group("xml", "heartOfGold", "trillian"),
new Group("xml", "g42", "dent", "prefect"),
new Group("xml", "fjordsOfAfrican", "dent", "trillian")
);
when(groupDAO.getAll()).thenReturn(groups);
}
@Test
void shouldReturnGroupsFromDao() {
GroupNames groupNames = collector.collect("trillian", Collections.emptySet());
assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican");
}
@Test
void shouldCombineGivenWithDao() {
GroupNames groupNames = collector.collect("trillian", ImmutableList.of("awesome", "incredible"));
assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible");
}
}
}

View File

@@ -36,31 +36,30 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import org.apache.shiro.authc.AuthenticationInfo;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.AlreadyExistsException;
import sonia.scm.group.ExternalGroupNames;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupNames;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext;
import sonia.scm.web.security.PrivilegedAction;
import java.io.IOException;
import java.util.List;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
@@ -78,9 +77,6 @@ public class SyncingRealmHelperTest {
@Mock
private UserManager userManager;
@Mock
private GroupDAO groupDAO;
private SyncingRealmHelper helper;
/**
@@ -106,7 +102,7 @@ public class SyncingRealmHelperTest {
}
};
helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO);
helper = new SyncingRealmHelper(ctx, userManager, groupManager);
}
/**
@@ -183,67 +179,15 @@ public class SyncingRealmHelperTest {
verify(userManager, times(1)).modify(user);
}
@Test
public void builderShouldSetInternalGroups() {
AuthenticationInfo authenticationInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(new User("ziltoid"))
.withGroups("internal")
.build();
GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
Assertions.assertThat(groupNames.getCollection()).contains("_authenticated", "internal");
}
@Test
public void builderShouldSetExternalGroups() {
AuthenticationInfo authenticationInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(new User("ziltoid"))
.withExternalGroups("external")
.build();
ExternalGroupNames groupNames = authenticationInfo.getPrincipals().oneByType(ExternalGroupNames.class);
Assertions.assertThat(groupNames.getCollection()).containsOnly("external");
}
@Test
public void builderShouldSetValues() {
User user = new User("ziltoid");
AuthenticationInfo authInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(user)
.build();
AuthenticationInfo authInfo = helper.createAuthenticationInfo("unit-test", user);
assertNotNull(authInfo);
assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal());
assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
}
@Test
public void shouldReturnCombinedGroupNames() {
User user = new User("tricia");
List<Group> groups = Lists.newArrayList(new Group("xml", "heartOfGold", "tricia"));
when(groupDAO.getAll()).thenReturn(groups);
AuthenticationInfo authInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(user)
.withGroups("fjordsOfAfrican")
.withExternalGroups("g42")
.build();
GroupNames groupNames = authInfo.getPrincipals().oneByType(GroupNames.class);
Assertions.assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican");
ExternalGroupNames externalGroupNames = authInfo.getPrincipals().oneByType(ExternalGroupNames.class);
Assertions.assertThat(externalGroupNames).contains("g42");
}
}

View File

@@ -123,23 +123,6 @@ public class TestData {
;
}
public static void createUserPermission(String username, String roleName, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
LOG.info("create permission with name {} and role {} using the endpoint: {}", username, roleName, defaultPermissionUrl);
given(VndMediaType.REPOSITORY_PERMISSION)
.when()
.content("{\n" +
"\t\"role\": " + roleName + ",\n" +
"\t\"name\": \"" + username + "\",\n" +
"\t\"groupPermission\": false\n" +
"\t\n" +
"}")
.post(defaultPermissionUrl)
.then()
.statusCode(HttpStatus.SC_CREATED)
;
}
public static List<Map> getUserPermissions(String username, String password, String repositoryType) {
return callUserPermissions(username, password, repositoryType, HttpStatus.SC_OK)
.extract()

View File

@@ -7,7 +7,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
import java.util.Set;
@Getter
@Setter
@@ -17,7 +17,7 @@ public class MeDto extends HalRepresentation {
private String name;
private String displayName;
private String mail;
private List<String> groups;
private Set<String> groups;
MeDto(Links links, Embedded embedded) {
super(links, embedded);

View File

@@ -1,18 +1,16 @@
package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableList;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.Links;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupCollector;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import java.util.Collections;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
@@ -22,11 +20,13 @@ public class MeDtoFactory extends HalAppenderMapper {
private final ResourceLinks resourceLinks;
private final UserManager userManager;
private final GroupCollector groupCollector;
@Inject
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) {
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager, GroupCollector groupCollector) {
this.resourceLinks = resourceLinks;
this.userManager = userManager;
this.groupCollector = groupCollector;
}
public MeDto create() {
@@ -35,16 +35,12 @@ public class MeDtoFactory extends HalAppenderMapper {
MeDto dto = createDto(user);
mapUserProperties(user, dto);
mapGroups(principals, dto);
mapGroups(user, dto);
return dto;
}
private void mapGroups(PrincipalCollection principals, MeDto dto) {
Iterable<String> groups = principals.oneByType(GroupNames.class);
if (groups == null) {
groups = Collections.emptySet();
}
dto.setGroups(ImmutableList.copyOf(groups));
private void mapGroups(User user, MeDto dto) {
dto.setGroups(groupCollector.collect(user.getName()));
}
private void mapUserProperties(User user, MeDto dto) {

View File

@@ -0,0 +1,72 @@
package sonia.scm.group;
import com.cronutils.utils.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Set;
/**
* Collect groups for a certain principal.
* <strong>Warning</strong>: The class is only for internal use and should never used directly.
*/
@Singleton
public class DefaultGroupCollector implements GroupCollector {
private static final Logger LOG = LoggerFactory.getLogger(DefaultGroupCollector.class);
@VisibleForTesting
static final String CACHE_NAME = "sonia.cache.externalGroups";
private final GroupDAO groupDAO;
private final Cache<String, Set<String>> cache;
private final Set<GroupResolver> groupResolvers;
@Inject
public DefaultGroupCollector(GroupDAO groupDAO, CacheManager cacheManager, Set<GroupResolver> groupResolvers) {
this.groupDAO = groupDAO;
this.cache = cacheManager.getCache(CACHE_NAME);
this.groupResolvers = groupResolvers;
}
@Override
public Set<String> collect(String principal) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
builder.add(AUTHENTICATED);
builder.addAll(resolveExternalGroups(principal));
appendInternalGroups(principal, builder);
Set<String> groups = builder.build();
LOG.debug("collected following groups for principal {}: {}", principal, groups);
return groups;
}
private void appendInternalGroups(String principal, ImmutableSet.Builder<String> builder) {
for (Group group : groupDAO.getAll()) {
if (group.isMember(principal)) {
builder.add(group.getName());
}
}
}
private Set<String> resolveExternalGroups(String principal) {
Set<String> externalGroups = cache.get(principal);
if (externalGroups == null) {
ImmutableSet.Builder<String> newExternalGroups = ImmutableSet.builder();
for (GroupResolver groupResolver : groupResolvers) {
newExternalGroups.addAll(groupResolver.resolve(principal));
}
externalGroups = newExternalGroups.build();
cache.put(principal, externalGroups);
}
return externalGroups;
}
}

View File

@@ -50,8 +50,10 @@ import sonia.scm.cache.CacheManager;
import sonia.scm.cache.GuavaCacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.group.DefaultGroupCollector;
import sonia.scm.group.DefaultGroupDisplayManager;
import sonia.scm.group.DefaultGroupManager;
import sonia.scm.group.GroupCollector;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupDisplayManager;
import sonia.scm.group.GroupManager;
@@ -195,7 +197,7 @@ class ScmServletModule extends ServletModule {
bindDecorated(GroupManager.class, DefaultGroupManager.class,
GroupManagerProvider.class);
bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class);
bind(GroupCollector.class, DefaultGroupCollector.class);
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);
// bind sslcontext provider

View File

@@ -48,6 +48,8 @@ import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
import javax.inject.Singleton;
/**
* Receives all kinds of events, which affects authorization relevant data and fires an
* {@link AuthorizationChangedEvent} if authorization data has changed.
@@ -55,6 +57,7 @@ import sonia.scm.user.UserModificationEvent;
* @author Sebastian Sdorra
* @since 1.52
*/
@Singleton
@EagerSingleton
public class AuthorizationChangedEventProducer {

View File

@@ -104,7 +104,6 @@ public class BearerRealm extends AuthenticatingRealm
return helper.authenticationInfoBuilder(accessToken.getSubject())
.withCredentials(bt.getCredentials())
.withScope(Scopes.fromClaims(accessToken.getClaims()))
.withGroups(accessToken.getGroups())
.build();
}

View File

@@ -52,17 +52,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupCollector;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.user.User;
import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
import java.util.Collection;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -88,19 +89,21 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
/**
* Constructs ...
* @param cacheManager
* @param cacheManager
* @param repositoryDAO
* @param securitySystem
* @param repositoryPermissionProvider
* @param groupCollector
*/
@Inject
public DefaultAuthorizationCollector(CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider)
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector)
{
this.cache = cacheManager.getCache(CACHE_NAME);
this.repositoryDAO = repositoryDAO;
this.securitySystem = securitySystem;
this.repositoryPermissionProvider = repositoryPermissionProvider;
this.groupCollector = groupCollector;
}
//~--- methods --------------------------------------------------------------
@@ -145,16 +148,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
Preconditions.checkNotNull(user, "no user found in principal collection");
GroupNames groupNames = principals.oneByType(GroupNames.class);
Set<String> groups = groupCollector.collect(user.getName());
CacheKey cacheKey = new CacheKey(user.getId(), groupNames);
CacheKey cacheKey = new CacheKey(user.getId(), groups);
AuthorizationInfo info = cache.get(cacheKey);
if (info == null)
{
logger.trace("collect AuthorizationInfo for user {}", user.getName());
info = createAuthorizationInfo(user, groupNames);
info = createAuthorizationInfo(user, groups);
cache.put(cacheKey, info);
}
else if (logger.isTraceEnabled())
@@ -166,7 +169,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
private void collectGlobalPermissions(Builder<String> builder,
final User user, final GroupNames groups)
final User user, final Set<String> groups)
{
Collection<AssignedPermission> globalPermissions =
securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input));
@@ -181,7 +184,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
private void collectRepositoryPermissions(Builder<String> builder, User user,
GroupNames groups)
Set<String> groups)
{
for (Repository repository : repositoryDAO.getAll())
{
@@ -190,7 +193,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
}
private void collectRepositoryPermissions(Builder<String> builder,
Repository repository, User user, GroupNames groups)
Repository repository, User user, Set<String> groups)
{
Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions();
@@ -245,7 +248,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
.getVerbs();
}
private AuthorizationInfo createAuthorizationInfo(User user, GroupNames groups) {
private AuthorizationInfo createAuthorizationInfo(User user, Set<String> groups) {
Builder<String> builder = ImmutableSet.builder();
collectGlobalPermissions(builder, user, groups);
@@ -279,7 +282,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- get methods ----------------------------------------------------------
private boolean isUserPermitted(User user, GroupNames groups,
private boolean isUserPermitted(User user, Set<String> groups,
PermissionObject perm)
{
//J-
@@ -314,7 +317,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
*/
private static class CacheKey
{
private CacheKey(String username, GroupNames groupnames)
private CacheKey(String username, Set<String> groupnames)
{
this.username = username;
this.groupnames = groupnames;
@@ -356,7 +359,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- fields -------------------------------------------------------------
/** group names */
private final GroupNames groupnames;
private final Set<String> groupnames;
/** username */
private final String username;
@@ -374,4 +377,5 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private final SecuritySystem securitySystem;
private final RepositoryPermissionProvider repositoryPermissionProvider;
private final GroupCollector groupCollector;
}

View File

@@ -34,7 +34,6 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
@@ -45,21 +44,16 @@ import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import sonia.scm.group.GroupNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.Extension;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/**
* Default authorizing realm.
*
@@ -103,6 +97,9 @@ public class DefaultRealm extends AuthorizingRealm
matcher.setPasswordService(service);
setCredentialsMatcher(helper.wrapCredentialsMatcher(matcher));
setAuthenticationTokenClass(UsernamePasswordToken.class);
// we cache in the AuthorizationCollector
setCachingEnabled(false);
}
//~--- methods --------------------------------------------------------------
@@ -149,7 +146,7 @@ public class DefaultRealm extends AuthorizingRealm
LOG.trace("principal does not contain scope information, returning all permissions");
log(principals, info, null);
}
return info;
}
@@ -180,8 +177,6 @@ public class DefaultRealm extends AuthorizingRealm
StringBuilder buffer = new StringBuilder("authorization summary: ");
buffer.append(SEPARATOR).append("username : ").append(collection.getPrimaryPrincipal());
buffer.append(SEPARATOR).append("groups : ");
append(buffer, collection.oneByType(GroupNames.class));
buffer.append(SEPARATOR).append("roles : ");
append(buffer, original.getRoles());
buffer.append(SEPARATOR).append("scope : ");

View File

@@ -40,11 +40,9 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.ExternalGroupNames;
import java.time.Clock;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -139,12 +137,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
return this;
}
@Override
public JwtAccessTokenBuilder groups(String... groups) {
Collections.addAll(this.groups, groups);
return this;
}
JwtAccessTokenBuilder refreshExpiration(Instant refreshExpiration) {
this.refreshExpiration = refreshExpiration;
this.refreshableFor = 0;
@@ -206,16 +198,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
claims.setIssuer(issuer);
}
if (!groups.isEmpty()) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups);
} else {
Subject currentSubject = SecurityUtils.getSubject();
ExternalGroupNames externalGroupNames = currentSubject.getPrincipals().oneByType(ExternalGroupNames.class);
if (externalGroupNames != null) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, externalGroupNames.getCollection().toArray(new String[]{}));
}
}
// sign token and create compact version
String compact = Jwts.builder()
.setClaims(claims)

View File

@@ -38,7 +38,6 @@ package sonia.scm.web.security;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
@@ -46,21 +45,17 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.group.GroupNames;
import sonia.scm.security.Role;
import sonia.scm.user.User;
import sonia.scm.util.AssertUtil;
//~--- JDK imports ------------------------------------------------------------
import javax.xml.bind.JAXB;
import java.net.URL;
import javax.xml.bind.JAXB;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -161,7 +156,6 @@ public class DefaultAdministrationContext implements AdministrationContext
collection.add(adminUser.getId(), REALM);
collection.add(adminUser, REALM);
collection.add(new GroupNames(), REALM);
collection.add(AdministrationContextMarker.MARKER, REALM);
return collection;

View File

@@ -1,271 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2010, Sebastian Sdorra
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of SCM-Manager; nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
http://bitbucket.org/sdorra/scm-manager
-->
<!--
Document : ehcache.xml
Created on : October 14, 2010, 6:54 AM
Author : sdorra
Description:
Purpose of the document follows.
-->
<ehcache xmlns="http://ehcache.org/ehcache.xsd"
updateCheck="false"
maxBytesLocalDisk="512M">
<!--
Sets the path to the directory where cache .data files are created.
If the path is a Java System Property it is replaced by
its value in the running VM.
The following properties are translated:
user.home - User's home directory
user.dir - User's current working directory
java.io.tmpdir - Default temp file path
-->
<diskStore path="java.io.tmpdir"/>
<!--
Default Cache configuration. These will applied to caches programmatically created through
the CacheManager.
The following attributes are required:
maxElementsInMemory - Sets the maximum number of objects that will be created in memory
eternal - Sets whether elements are eternal. If eternal, timeouts are ignored and the
element is never expired.
overflowToDisk - Sets whether elements can overflow to disk when the in-memory cache
has reached the maxInMemory limit.
The following attributes are optional:
timeToIdleSeconds - Sets the time to idle for an element before it expires.
i.e. The maximum amount of time between accesses before an element expires
Is only used if the element is not eternal.
Optional attribute. A value of 0 means that an Element can idle for infinity.
The default value is 0.
timeToLiveSeconds - Sets the time to live for an element before it expires.
i.e. The maximum time between creation time and when an element expires.
Is only used if the element is not eternal.
Optional attribute. A value of 0 means that and Element can live for infinity.
The default value is 0.
diskPersistent - Whether the disk store persists between restarts of the Virtual Machine.
The default value is false.
diskExpiryThreadIntervalSeconds- The number of seconds between runs of the disk expiry thread. The default value
is 120 seconds.
-->
<defaultCache
maxEntriesLocalHeap="100"
maxEntriesLocalDisk="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="1200"
timeToLiveSeconds="2400"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!--
Authentication cache
average: 1K
-->
<cache
name="sonia.cache.auth"
maxEntriesLocalHeap="1000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="30"
timeToLiveSeconds="60"
diskPersistent="false"
/>
<!--
Authorization cache
average: 3K
-->
<cache
name="sonia.cache.authorizing"
maxEntriesLocalHeap="1000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="2400"
diskPersistent="false"
copyOnRead="true"
/>
<!--
PluginCenter cache
average: 30K
-->
<cache
name="sonia.cache.plugins"
maxEntriesLocalHeap="5"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="3600"
diskPersistent="false"
/>
<!--
Search cache for users
average: 0.5K
-->
<cache
name="sonia.cache.search.users"
maxEntriesLocalHeap="10000"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="5400"
diskPersistent="false"
/>
<!--
Search cache for groups
average: 0.5K
-->
<cache
name="sonia.cache.search.groups"
maxEntriesLocalHeap="1000"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="5400"
diskPersistent="false"
/>
<!-- new repository api -->
<!--
Changeset cache
average: 25K
-->
<cache
name="sonia.cache.cmd.log"
maxEntriesLocalHeap="200"
maxEntriesLocalDisk="10000"
eternal="true"
overflowToDisk="true"
diskPersistent="false"
copyOnRead="true"
copyOnWrite="true"
/>
<!--
FileObject cache
average: 1.5K
-->
<cache
name="sonia.cache.cmd.browse"
maxEntriesLocalHeap="3000"
maxEntriesLocalDisk="20000"
eternal="true"
overflowToDisk="true"
diskPersistent="false"
copyOnRead="true"
copyOnWrite="true"
/>
<!--
BlameResult cache
average: 15K
-->
<cache
name="sonia.cache.cmd.blame"
maxEntriesLocalHeap="1000"
maxEntriesLocalDisk="10000"
eternal="true"
overflowToDisk="true"
diskPersistent="false"
copyOnRead="true"
copyOnWrite="true"
/>
<!--
Tag cache
average: 5K
-->
<cache
name="sonia.cache.cmd.tags"
maxEntriesLocalHeap="500"
eternal="true"
overflowToDisk="false"
diskPersistent="false"
/>
<!--
Branch cache
average: 2.5K
-->
<cache
name="sonia.cache.cmd.branches"
maxEntriesLocalHeap="1000"
eternal="true"
overflowToDisk="false"
diskPersistent="false"
/>
<!-- deprecated old repository api -->
<cache
name="sonia.cache.repository.changesets"
maxEntriesLocalHeap="200"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="86400"
diskPersistent="false"
/>
<cache
name="sonia.cache.repository.browser"
maxEntriesLocalHeap="200"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="86400"
diskPersistent="false"
/>
<cache
name="sonia.cache.repository.blame"
maxEntriesLocalHeap="100"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="86400"
diskPersistent="false"
/>
</ehcache>

View File

@@ -40,15 +40,15 @@
/>
<!--
Authentication cache
External group cache
average: 1K
-->
<cache
name="sonia.cache.auth"
name="sonia.cache.externalGroups"
maximumSize="1000"
expireAfterAccess="30"
expireAfterWrite="60"
expireAfterAccess="60"
expireAfterWrite="120"
/>
<!--

View File

@@ -1,9 +1,9 @@
package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -12,7 +12,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupCollector;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserTestData;
@@ -20,7 +20,6 @@ import sonia.scm.user.UserTestData;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -33,6 +32,9 @@ class MeDtoFactoryTest {
@Mock
private UserManager userManager;
@Mock
private GroupCollector groupCollector;
@Mock
private Subject subject;
@@ -42,7 +44,7 @@ class MeDtoFactoryTest {
void setUpContext() {
ThreadContext.bind(subject);
ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
meDtoFactory = new MeDtoFactory(resourceLinks, userManager);
meDtoFactory = new MeDtoFactory(resourceLinks, userManager, groupCollector);
}
@AfterEach
@@ -69,24 +71,16 @@ class MeDtoFactoryTest {
@Test
void shouldCreateMeDtoWithGroups() {
prepareSubject(UserTestData.createTrillian(), "HeartOfGold", "Puzzle42");
when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("HeartOfGold", "Puzzle42"));
prepareSubject(UserTestData.createTrillian());
MeDto dto = meDtoFactory.create();
assertThat(dto.getGroups()).containsOnly("HeartOfGold", "Puzzle42");
}
private void prepareSubject(User user, String... groups) {
private void prepareSubject(User user) {
PrincipalCollection collection = mock(PrincipalCollection.class);
when(subject.getPrincipals()).thenReturn(collection);
when(collection.oneByType(any(Class.class))).then(ic -> {
Class<?> type = ic.getArgument(0);
if (type.isAssignableFrom(User.class)) {
return user;
} else if (type.isAssignableFrom(GroupNames.class)) {
return new GroupNames(Lists.newArrayList(groups));
} else {
return null;
}
});
when(collection.oneByType(User.class)).thenReturn(user);
}
@Test

View File

@@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.PrincipalCollection;
@@ -16,6 +17,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.ContextEntry;
import sonia.scm.group.GroupCollector;
import sonia.scm.user.InvalidPasswordException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
@@ -28,7 +30,12 @@ import java.net.URISyntaxException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@@ -50,6 +57,9 @@ public class MeResourceTest {
@Mock
private ScmPathInfoStore scmPathInfoStore;
@Mock
private GroupCollector groupCollector;
@Mock
private UserManager userManager;
@@ -69,6 +79,7 @@ public class MeResourceTest {
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture());
when(groupCollector.collect("trillian")).thenReturn(ImmutableSet.of("group1", "group2"));
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getDefaultType()).thenReturn("xml");
MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService);

View File

@@ -0,0 +1,100 @@
package sonia.scm.group;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.cache.MapCache;
import sonia.scm.cache.MapCacheManager;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DefaultGroupCollectorTest {
@Mock
private GroupDAO groupDAO;
@Mock
private GroupResolver groupResolver;
private MapCacheManager mapCacheManager;
private Set<GroupResolver> groupResolvers;
private DefaultGroupCollector collector;
@BeforeEach
void initCollector() {
groupResolvers = new HashSet<>();
mapCacheManager = new MapCacheManager();
collector = new DefaultGroupCollector(groupDAO, mapCacheManager, groupResolvers);
}
@Test
void shouldAlwaysReturnAuthenticatedGroup() {
Iterable<String> groupNames = collector.collect("trillian");
assertThat(groupNames).containsOnly("_authenticated");
}
@Test
void shouldReturnGroupsFromCache() {
MapCache<String, Set<String>> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME);
cache.put("trillian", ImmutableSet.of("awesome", "incredible"));
Set<String> groups = collector.collect("trillian");
assertThat(groups).containsOnly("_authenticated", "awesome", "incredible");
}
@Test
void shouldNotCallResolverIfExternalGroupsAreCached() {
groupResolvers.add(groupResolver);
MapCache<String, Set<String>> cache = mapCacheManager.getCache(DefaultGroupCollector.CACHE_NAME);
cache.put("trillian", ImmutableSet.of("awesome", "incredible"));
Set<String> groups = collector.collect("trillian");
assertThat(groups).containsOnly("_authenticated", "awesome", "incredible");
verify(groupResolver, never()).resolve("trillian");
}
@Nested
class WithGroupsFromDao {
@BeforeEach
void setUpGroupsDao() {
List<Group> groups = Lists.newArrayList(
new Group("xml", "heartOfGold", "trillian"),
new Group("xml", "g42", "dent", "prefect"),
new Group("xml", "fjordsOfAfrican", "dent", "trillian")
);
when(groupDAO.getAll()).thenReturn(groups);
}
@Test
void shouldReturnGroupsFromDao() {
Iterable<String> groupNames = collector.collect("trillian");
assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican");
}
@Test
void shouldCombineWithResolvers() {
when(groupResolver.resolve("trillian")).thenReturn(ImmutableSet.of("awesome", "incredible"));
groupResolvers.add(groupResolver);
Iterable<String> groupNames = collector.collect("trillian");
assertThat(groupNames).containsOnly("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible");
}
}
}

View File

@@ -90,12 +90,10 @@ class BearerRealmTest {
Set<String> groups = ImmutableSet.of("HeartOfGold", "Puzzle42");
when(accessToken.getSubject()).thenReturn("trillian");
when(accessToken.getGroups()).thenReturn(groups);
when(accessToken.getClaims()).thenReturn(new HashMap<>());
when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken);
when(realmHelper.authenticationInfoBuilder("trillian")).thenReturn(builder);
when(builder.withGroups(groups)).thenReturn(builder);
when(builder.withCredentials("__bearer__")).thenReturn(builder);
when(builder.withScope(any(Scope.class))).thenReturn(builder);
when(builder.build()).thenReturn(authenticationInfo);

View File

@@ -33,6 +33,7 @@ package sonia.scm.security;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
@@ -49,7 +50,7 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupCollector;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermission;
@@ -58,8 +59,6 @@ import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserTestData;
import java.util.Collections;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -96,6 +95,9 @@ public class DefaultAuthorizationCollectorTest {
@Mock
private RepositoryPermissionProvider repositoryPermissionProvider;
@Mock
private GroupCollector groupCollector;
private DefaultAuthorizationCollector collector;
@Rule
@@ -107,7 +109,7 @@ public class DefaultAuthorizationCollectorTest {
@Before
public void setUp(){
when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache);
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider);
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider, groupCollector);
}
/**
@@ -290,9 +292,13 @@ public class DefaultAuthorizationCollectorTest {
SimplePrincipalCollection spc = new SimplePrincipalCollection();
spc.add(user.getName(), "unit");
spc.add(user, "unit");
spc.add(new GroupNames(group, groups), "unit");
Subject subject = new Subject.Builder().authenticated(true).principals(spc).buildSubject();
shiro.setSubject(subject);
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
builder.add(group);
builder.add(groups);
when(groupCollector.collect(user.getName())).thenReturn(builder.build());
}
/**

View File

@@ -36,8 +36,6 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
@@ -45,43 +43,44 @@ import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.DefaultPasswordService;
import org.apache.shiro.crypto.hash.DefaultHashService;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
import sonia.scm.user.UserTestData;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
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.WildcardPermissionResolver;
import org.apache.shiro.crypto.hash.DefaultHashService;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.group.GroupDAO;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
import sonia.scm.user.UserTestData;
import java.util.HashSet;
import java.util.Set;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------
/**
*
@@ -206,32 +205,6 @@ public class DefaultRealmTest
);
}
/**
* Method description
*
*/
@Test
public void testGroupCollection()
{
User user = UserTestData.createTrillian();
//J-
List<Group> groups = Lists.newArrayList(
new Group(DefaultRealm.REALM, "scm", user.getName()),
new Group(DefaultRealm.REALM, "developers", "perfect")
);
//J+
when(groupDAO.getAll()).thenReturn(groups);
UsernamePasswordToken token = daoUser(user, "secret");
AuthenticationInfo info = realm.getAuthenticationInfo(token);
GroupNames groupNames = info.getPrincipals().oneByType(GroupNames.class);
assertNotNull(groupNames);
assertThat(groupNames.getCollection(), hasSize(2));
assertThat(groupNames, hasItems("scm", GroupNames.AUTHENTICATED));
}
/**
* Method description
*
@@ -251,12 +224,6 @@ public class DefaultRealmTest
assertThat(collection.getRealmNames(), hasSize(1));
assertThat(collection.getRealmNames(), hasItem(DefaultRealm.REALM));
assertEquals(user, collection.oneByType(User.class));
GroupNames groups = collection.oneByType(GroupNames.class);
assertNotNull(groups);
assertThat(groups.getCollection(), hasSize(1));
assertThat(groups.getCollection(), hasItem(GroupNames.AUTHENTICATED));
}
/**

View File

@@ -36,27 +36,25 @@ import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.group.ExternalGroupNames;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.when;
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
/**
@@ -137,7 +135,6 @@ public class JwtAccessTokenBuilderTest {
.issuer("https://www.scm-manager.org")
.expiresIn(5, TimeUnit.SECONDS)
.custom("a", "b")
.groups("one", "two", "three")
.scope(Scope.valueOf("repo:*"))
.build();
@@ -154,36 +151,6 @@ public class JwtAccessTokenBuilderTest {
assertClaims(new JwtAccessToken(claims, compact));
}
@Test
public void testWithExternalGroups() {
applyExternalGroupsToSubject(true, "external");
JwtAccessToken token = factory.create().subject("dent").build();
assertArrayEquals(new String[]{"external"}, token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).map(x -> (String[]) x).get());
}
@Test
public void testWithInternalGroups() {
applyExternalGroupsToSubject(false, "external");
JwtAccessToken token = factory.create().subject("dent").build();
assertFalse(token.getCustom(JwtAccessToken.GROUPS_CLAIM_KEY).isPresent());
}
private void applyExternalGroupsToSubject(boolean external, String... groups) {
Subject subject = spy(SecurityUtils.getSubject());
when(subject.getPrincipals()).thenAnswer(invocation -> enrichWithGroups(invocation, groups, external));
shiro.setSubject(subject);
}
private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable {
PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod());
List<String> groupCollection = Arrays.asList(groups);
if (external) {
when(principals.oneByType(ExternalGroupNames.class)).thenReturn(new ExternalGroupNames(groupCollection));
}
return principals;
}
private void assertClaims(JwtAccessToken token){
assertThat(token.getId(), not(isEmptyOrNullString()));
assertNotNull( token.getIssuedAt() );
@@ -194,6 +161,5 @@ public class JwtAccessTokenBuilderTest {
assertEquals(token.getIssuer().get(), "https://www.scm-manager.org");
assertEquals("b", token.getCustom("a").get());
assertEquals("[\"repo:*\"]", token.getScope().toString());
assertThat(token.getGroups(), containsInAnyOrder("one", "two", "three"));
}
}