merged master

This commit is contained in:
Florian Scholdei
2019-01-23 13:46:46 +01:00
103 changed files with 2630 additions and 1786 deletions

5
Jenkinsfile vendored
View File

@@ -50,6 +50,11 @@ node('docker') {
def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
if (isMainBranch()) {
stage('Archive') {
archiveArtifacts 'scm-webapp/target/scm-webapp.war'
archiveArtifacts 'scm-server/target/scm-server-app.*'
}
stage('Docker') {
def image = docker.build('cloudogu/scm-manager')
docker.withRegistry('', 'hub.docker.com-cesmarvin') {

View File

@@ -85,7 +85,7 @@ public final class ScmState
public ScmState(String version, User user, Collection<String> groups,
String token, Collection<RepositoryType> repositoryTypes, String defaultUserType,
ScmClientConfig clientConfig, List<String> assignedPermission,
List<PermissionDescriptor> availablePermissions)
Collection<PermissionDescriptor> availablePermissions)
{
this.version = version;
this.user = user;
@@ -119,7 +119,7 @@ public final class ScmState
* @return available global permissions
* @since 1.31
*/
public List<PermissionDescriptor> getAvailablePermissions()
public Collection<PermissionDescriptor> getAvailablePermissions()
{
return availablePermissions;
}
@@ -232,7 +232,7 @@ public final class ScmState
* Avaliable global permission
* @since 1.31
*/
private List<PermissionDescriptor> availablePermissions;
private Collection<PermissionDescriptor> availablePermissions;
/** Field description */
private ScmClientConfig clientConfig;

View File

@@ -134,7 +134,7 @@ public final class ScmStateFactory
User user = collection.oneByType(User.class);
GroupNames groups = collection.oneByType(GroupNames.class);
List<PermissionDescriptor> ap = Collections.EMPTY_LIST;
Collection<PermissionDescriptor> ap = Collections.EMPTY_LIST;
if (subject.hasRole(Role.ADMIN))
{
@@ -150,7 +150,7 @@ public final class ScmStateFactory
private ScmState createState(User user, Collection<String> groups,
String token, List<String> assignedPermissions,
List<PermissionDescriptor> availablePermissions)
Collection<PermissionDescriptor> availablePermissions)
{
User u = user.clone();

View File

@@ -81,7 +81,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
private Long lastModified;
private String namespace;
private String name;
private final Set<Permission> permissions = new HashSet<>();
private final Set<RepositoryPermission> permissions = new HashSet<>();
@XmlElement(name = "public")
private boolean publicReadable = false;
private boolean archived = false;
@@ -122,7 +122,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
* @param permissions permissions for specific users and groups.
*/
public Repository(String id, String type, String namespace, String name, String contact,
String description, Permission... permissions) {
String description, RepositoryPermission... permissions) {
this.id = id;
this.type = type;
this.namespace = namespace;
@@ -201,7 +201,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
return new NamespaceAndName(getNamespace(), getName());
}
public Collection<Permission> getPermissions() {
public Collection<RepositoryPermission> getPermissions() {
return Collections.unmodifiableCollection(permissions);
}
@@ -297,16 +297,16 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
this.name = name;
}
public void setPermissions(Collection<Permission> permissions) {
public void setPermissions(Collection<RepositoryPermission> permissions) {
this.permissions.clear();
this.permissions.addAll(permissions);
}
public void addPermission(Permission newPermission) {
public void addPermission(RepositoryPermission newPermission) {
this.permissions.add(newPermission);
}
public void removePermission(Permission permission) {
public void removePermission(RepositoryPermission permission) {
this.permissions.remove(permission);
}

View File

@@ -53,7 +53,7 @@ import java.io.Serializable;
*/
@XmlRootElement(name = "permissions")
@XmlAccessorType(XmlAccessType.FIELD)
public class Permission implements PermissionObject, Serializable
public class RepositoryPermission implements PermissionObject, Serializable
{
private static final long serialVersionUID = -2915175031430884040L;
@@ -63,41 +63,41 @@ public class Permission implements PermissionObject, Serializable
private PermissionType type = PermissionType.READ;
/**
* Constructs a new {@link Permission}.
* Constructs a new {@link RepositoryPermission}.
* This constructor is used by JAXB.
*
*/
public Permission() {}
public RepositoryPermission() {}
/**
* Constructs a new {@link Permission} with type = {@link PermissionType#READ}
* Constructs a new {@link RepositoryPermission} with type = {@link PermissionType#READ}
* for the specified user.
*
*
* @param name name of the user
*/
public Permission(String name)
public RepositoryPermission(String name)
{
this();
this.name = name;
}
/**
* Constructs a new {@link Permission} with the specified type for
* Constructs a new {@link RepositoryPermission} with the specified type for
* the given user.
*
*
* @param name name of the user
* @param type type of the permission
*/
public Permission(String name, PermissionType type)
public RepositoryPermission(String name, PermissionType type)
{
this(name);
this.type = type;
}
/**
* Constructs a new {@link Permission} with the specified type for
* Constructs a new {@link RepositoryPermission} with the specified type for
* the given user or group.
*
*
@@ -105,7 +105,7 @@ public class Permission implements PermissionObject, Serializable
* @param type type of the permission
* @param groupPermission true if the permission is a permission for a group
*/
public Permission(String name, PermissionType type, boolean groupPermission)
public RepositoryPermission(String name, PermissionType type, boolean groupPermission)
{
this(name, type);
this.groupPermission = groupPermission;
@@ -114,12 +114,12 @@ public class Permission implements PermissionObject, Serializable
//~--- methods --------------------------------------------------------------
/**
* Returns true if the {@link Permission} is the same as the obj argument.
* Returns true if the {@link RepositoryPermission} is the same as the obj argument.
*
*
* @param obj the reference object with which to compare
*
* @return true if the {@link Permission} is the same as the obj argument
* @return true if the {@link RepositoryPermission} is the same as the obj argument
*/
@Override
public boolean equals(Object obj)
@@ -134,7 +134,7 @@ public class Permission implements PermissionObject, Serializable
return false;
}
final Permission other = (Permission) obj;
final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name)
&& Objects.equal(type, other.type)
@@ -142,10 +142,10 @@ public class Permission implements PermissionObject, Serializable
}
/**
* Returns the hash code value for the {@link Permission}.
* Returns the hash code value for the {@link RepositoryPermission}.
*
*
* @return the hash code value for the {@link Permission}
* @return the hash code value for the {@link RepositoryPermission}
*/
@Override
public int hashCode()

View File

@@ -33,6 +33,7 @@ package sonia.scm.security;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* An access token can be used to access scm-manager without providing username and password. An {@link AccessToken} can
@@ -103,6 +104,13 @@ public interface AccessToken {
*/
Scope getScope();
/**
* Returns name of groups, in which the user should be a member.
*
* @return name of groups
*/
Set<String> getGroups();
/**
* Returns an optional value of a custom token field.
*

View File

@@ -99,6 +99,15 @@ 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

@@ -89,8 +89,12 @@ public class AssignedPermission implements PermissionObject, Serializable
*/
public AssignedPermission(String name, String permission)
{
this.name = name;
this.permission = permission;
this(name, new PermissionDescriptor(permission));
}
public AssignedPermission(String name, PermissionDescriptor permission)
{
this(name, false, permission);
}
/**
@@ -103,6 +107,12 @@ public class AssignedPermission implements PermissionObject, Serializable
*/
public AssignedPermission(String name, boolean groupPermission,
String permission)
{
this(name, groupPermission, new PermissionDescriptor(permission));
}
public AssignedPermission(String name, boolean groupPermission,
PermissionDescriptor permission)
{
this.name = name;
this.groupPermission = groupPermission;
@@ -173,12 +183,9 @@ public class AssignedPermission implements PermissionObject, Serializable
}
/**
* Returns the string representation of the permission.
*
*
* @return string representation of the permission
* Returns the description of the permission.
*/
public String getPermission()
public PermissionDescriptor getPermission()
{
return permission;
}
@@ -205,5 +212,5 @@ public class AssignedPermission implements PermissionObject, Serializable
private String name;
/** string representation of the permission */
private String permission;
private PermissionDescriptor permission;
}

View File

@@ -51,7 +51,7 @@ import java.io.Serializable;
* @since 1.31
*/
@Event
public final class StoredAssignedPermissionEvent implements Serializable
public final class AssignedPermissionEvent implements Serializable
{
/** serial version uid */
@@ -60,14 +60,14 @@ public final class StoredAssignedPermissionEvent implements Serializable
//~--- constructors ---------------------------------------------------------
/**
* Constructs a new StoredAssignedPermissionEvent.
* Constructs a new AssignedPermissionEvent.
*
*
* @param type type of the event
* @param permission permission object which has changed
*/
public StoredAssignedPermissionEvent(HandlerEventType type,
StoredAssignedPermission permission)
public AssignedPermissionEvent(HandlerEventType type,
AssignedPermission permission)
{
this.type = type;
this.permission = permission;
@@ -91,8 +91,8 @@ public final class StoredAssignedPermissionEvent implements Serializable
return false;
}
final StoredAssignedPermissionEvent other =
(StoredAssignedPermissionEvent) obj;
final AssignedPermissionEvent other =
(AssignedPermissionEvent) obj;
return Objects.equal(type, other.type)
&& Objects.equal(permission, other.permission);
@@ -140,7 +140,7 @@ public final class StoredAssignedPermissionEvent implements Serializable
*
* @return changed permission
*/
public StoredAssignedPermission getPermission()
public AssignedPermission getPermission()
{
return permission;
}
@@ -148,7 +148,7 @@ public final class StoredAssignedPermissionEvent implements Serializable
//~--- fields ---------------------------------------------------------------
/** changed permission */
private StoredAssignedPermission permission;
private AssignedPermission permission;
/** type of the event */
private HandlerEventType type;

View File

@@ -37,7 +37,6 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException;
@@ -54,6 +53,8 @@ import sonia.scm.group.GroupNames;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
import java.util.Collections;
import static com.google.common.base.Preconditions.checkArgument;
/**
@@ -63,8 +64,7 @@ import static com.google.common.base.Preconditions.checkArgument;
* @author Sebastian Sdorra
* @since 2.0.0
*/
public final class DAORealmHelper
{
public final class DAORealmHelper {
/**
* the logger for DAORealmHelper
@@ -111,35 +111,35 @@ public final class DAORealmHelper
}
/**
* Method description
* Creates {@link AuthenticationInfo} from a {@link UsernamePasswordToken}. The method accepts
* {@link AuthenticationInfo} as argument, so that the caller does not need to cast.
*
* @param token authentication token, it must be {@link UsernamePasswordToken}
*
* @param token
*
* @return
*
* @throws AuthenticationException
* @return authentication info
*/
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) {
checkArgument(token instanceof UsernamePasswordToken, "%s is required", UsernamePasswordToken.class);
UsernamePasswordToken upt = (UsernamePasswordToken) token;
String principal = upt.getUsername();
return getAuthenticationInfo(principal, null, null);
return getAuthenticationInfo(principal, null, null, Collections.emptySet());
}
/**
* Method description
* Returns a builder for {@link AuthenticationInfo}.
*
* @param principal name of principal (username)
*
* @param principal
* @param credentials
* @param scope
*
* @return
* @return authentication info builder
*/
public AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope) {
public AuthenticationInfoBuilder authenticationInfoBuilder(String principal) {
return new AuthenticationInfoBuilder(principal);
}
private AuthenticationInfo getAuthenticationInfo(String principal, String credentials, Scope scope, Iterable<String> groups) {
checkArgument(!Strings.isNullOrEmpty(principal), "username is required");
LOG.debug("try to authenticate {}", principal);
@@ -157,7 +157,7 @@ public final class DAORealmHelper
collection.add(principal, realm);
collection.add(user, realm);
collection.add(collectGroups(principal), realm);
collection.add(collectGroups(principal, groups), realm);
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
String creds = credentials;
@@ -171,11 +171,15 @@ public final class DAORealmHelper
//~--- methods --------------------------------------------------------------
private GroupNames collectGroups(String principal) {
private GroupNames collectGroups(String principal, Iterable<String> groupNames) {
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());
@@ -187,6 +191,69 @@ public final class DAORealmHelper
return groups;
}
/**
* Builder class for {@link AuthenticationInfo}.
*/
public class AuthenticationInfoBuilder {
private final String principal;
private String credentials;
private Scope scope;
private Iterable<String> groups = Collections.emptySet();
private AuthenticationInfoBuilder(String principal) {
this.principal = principal;
}
/**
* With credentials uses the given credentials for the {@link AuthenticationInfo}, this is particularly important
* for caching purposes.
*
* @param credentials credentials such as password
*
* @return {@code this}
*/
public AuthenticationInfoBuilder withCredentials(String credentials) {
this.credentials = credentials;
return this;
}
/**
* With the scope object it is possible to limit the access permissions to scm-manager.
*
* @param scope scope object
*
* @return {@code this}
*/
public AuthenticationInfoBuilder withScope(Scope scope) {
this.scope = scope;
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.
*
* @return authentication info
*/
public AuthenticationInfo build() {
return getAuthenticationInfo(principal, credentials, scope, groups);
}
}
private static class RetryLimitPasswordMatcher implements CredentialsMatcher {
private final LoginAttemptHandler loginAttemptHandler;

View File

@@ -0,0 +1,12 @@
package sonia.scm.security;
import com.github.sdorra.ssp.PermissionObject;
import com.github.sdorra.ssp.StaticPermissions;
@StaticPermissions(
value = "permission",
permissions = {},
globalPermissions = {"list", "read", "assign"}
)
public interface Permission extends PermissionObject {
}

View File

@@ -67,19 +67,8 @@ public class PermissionDescriptor implements Serializable
*/
public PermissionDescriptor() {}
/**
* Constructs ...
*
*
* @param displayName
* @param description
* @param value
*/
public PermissionDescriptor(String displayName, String description,
String value)
public PermissionDescriptor(String value)
{
this.displayName = displayName;
this.description = description;
this.value = value;
}
@@ -103,9 +92,7 @@ public class PermissionDescriptor implements Serializable
final PermissionDescriptor other = (PermissionDescriptor) obj;
return Objects.equal(displayName, other.displayName)
&& Objects.equal(description, other.description)
&& Objects.equal(value, other.value);
return Objects.equal(value, other.value);
}
/**
@@ -114,7 +101,7 @@ public class PermissionDescriptor implements Serializable
@Override
public int hashCode()
{
return Objects.hashCode(displayName, description, value);
return value.hashCode();
}
/**
@@ -126,8 +113,6 @@ public class PermissionDescriptor implements Serializable
//J-
return MoreObjects.toStringHelper(this)
.add("displayName", displayName)
.add("description", description)
.add("value", value)
.toString();
@@ -136,28 +121,6 @@ public class PermissionDescriptor implements Serializable
//~--- get methods ----------------------------------------------------------
/**
* Returns the description of the permission.
*
*
* @return description
*/
public String getDescription()
{
return description;
}
/**
* Returns the display name of the permission.
*
*
* @return display name
*/
public String getDisplayName()
{
return displayName;
}
/**
* Returns the string representation of the permission.
*
@@ -171,13 +134,6 @@ public class PermissionDescriptor implements Serializable
//~--- fields ---------------------------------------------------------------
/** description */
private String description;
/** display name */
@XmlElement(name = "display-name")
private String displayName;
/** value */
private String value;
}

View File

@@ -32,13 +32,8 @@
package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Predicate;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
import java.util.Collection;
import java.util.function.Predicate;
/**
* The SecuritySystem manages global permissions.
@@ -57,7 +52,7 @@ public interface SecuritySystem
*
* @return stored permission
*/
public StoredAssignedPermission addPermission(AssignedPermission permission);
void addPermission(AssignedPermission permission);
/**
* Delete stored permission.
@@ -65,51 +60,17 @@ public interface SecuritySystem
*
* @param permission permission to be deleted
*/
public void deletePermission(StoredAssignedPermission permission);
/**
* Delete stored permission.
*
*
* @param id id of the permission
*/
public void deletePermission(String id);
/**
* Modify stored permission.
*
*
* @param permission stored permisison
*/
public void modifyPermission(StoredAssignedPermission permission);
void deletePermission(AssignedPermission permission);
//~--- get methods ----------------------------------------------------------
/**
* Return all stored permissions.
*
*
* @return stored permission
*/
public List<StoredAssignedPermission> getAllPermissions();
/**
* Return all available permissions.
*
*
* @return available permissions
*/
public List<PermissionDescriptor> getAvailablePermissions();
/**
* Return the stored permission which is stored with the given id.
*
*
* @param id id of the stored permission
*
* @return stored permission
*/
public StoredAssignedPermission getPermission(String id);
Collection<PermissionDescriptor> getAvailablePermissions();
/**
* Returns all stored permissions which are matched by the given
@@ -120,6 +81,5 @@ public interface SecuritySystem
*
* @return filtered permissions
*/
public List<StoredAssignedPermission> getPermissions(
Predicate<AssignedPermission> predicate);
Collection<AssignedPermission> getPermissions(Predicate<AssignedPermission> predicate);
}

View File

@@ -34,6 +34,8 @@ package sonia.scm.security;
//~--- JDK imports ------------------------------------------------------------
import com.google.common.base.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;

View File

@@ -41,6 +41,7 @@ public class VndMediaType {
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
@SuppressWarnings("squid:S2068")
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX;
public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;

View File

@@ -0,0 +1,155 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
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.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;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DAORealmHelperTest {
@Mock
private LoginAttemptHandler loginAttemptHandler;
@Mock
private UserDAO userDAO;
@Mock
private GroupDAO groupDAO;
private DAORealmHelper helper;
@BeforeEach
void setUpObjectUnderTest() {
helper = new DAORealmHelper(loginAttemptHandler, userDAO, groupDAO, "hitchhiker");
}
@Test
void shouldThrowExceptionWithoutUsername() {
assertThrows(IllegalArgumentException.class, () -> helper.authenticationInfoBuilder(null).build());
}
@Test
void shouldThrowExceptionWithEmptyUsername() {
assertThrows(IllegalArgumentException.class, () -> helper.authenticationInfoBuilder("").build());
}
@Test
void shouldThrowExceptionWithUnknownUser() {
assertThrows(UnknownAccountException.class, () -> helper.authenticationInfoBuilder("trillian").build());
}
@Test
void shouldThrowExceptionOnDisabledAccount() {
User user = new User("trillian");
user.setActive(false);
when(userDAO.get("trillian")).thenReturn(user);
assertThrows(DisabledAccountException.class, () -> helper.authenticationInfoBuilder("trillian").build());
}
@Test
void shouldReturnAuthenticationInfo() {
User user = new User("trillian");
when(userDAO.get("trillian")).thenReturn(user);
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
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");
when(userDAO.get("trillian")).thenReturn(user);
Scope scope = Scope.valueOf("user:*", "group:*");
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
.withScope(scope)
.build();
PrincipalCollection principals = authenticationInfo.getPrincipals();
assertThat(principals.oneByType(Scope.class)).isSameAs(scope);
}
@Test
void shouldReturnAuthenticationInfoWithCredentials() {
User user = new User("trillian");
when(userDAO.get("trillian")).thenReturn(user);
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian")
.withCredentials("secret")
.build();
assertThat(authenticationInfo.getCredentials()).isEqualTo("secret");
}
@Test
void shouldReturnAuthenticationInfoWithCredentialsFromUser() {
User user = new User("trillian");
user.setPassword("secret");
when(userDAO.get("trillian")).thenReturn(user);
AuthenticationInfo authenticationInfo = helper.authenticationInfoBuilder("trillian").build();
assertThat(authenticationInfo.getCredentials()).isEqualTo("secret");
}
@Test
void shouldThrowExceptionWithWrongTypeOfToken() {
assertThrows(IllegalArgumentException.class, () -> helper.getAuthenticationInfo(BearerToken.valueOf("__bearer__")));
}
@Test
void shouldGetAuthenticationInfo() {
User user = new User("trillian");
when(userDAO.get("trillian")).thenReturn(user);
AuthenticationInfo authenticationInfo = helper.getAuthenticationInfo(new UsernamePasswordToken("trillian", "secret"));
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

@@ -141,7 +141,7 @@ public class JAXBConfigurationEntryStoreTest
assertNotNull(ap);
assertEquals("tuser4", ap.getName());
assertEquals("repository:create", ap.getPermission());
assertEquals("repository:create", ap.getPermission().getValue());
}
@Test

View File

@@ -1,13 +1,10 @@
package sonia.scm.it;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import static org.assertj.core.api.Assertions.assertThat;
public class MeITCase {
@Before
@@ -23,9 +20,6 @@ public class MeITCase {
.requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.requestMe()
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo("xml"))
.requestChangePassword(TestData.USER_SCM_ADMIN, newPassword)
.assertStatusCode(204);
// assert password is changed -> login with the new Password than undo changes
@@ -33,7 +27,6 @@ public class MeITCase {
.requestIndexResource(TestData.USER_SCM_ADMIN, newPassword)
.requestMe()
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
.assertStatusCode(204);
}
@@ -49,9 +42,6 @@ public class MeITCase {
.requestIndexResource(username, password)
.requestMe()
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.FALSE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo("xml"))
.requestChangePassword(password, newPassword)
.assertStatusCode(204);
// assert password is changed -> login with the new Password than undo changes
@@ -72,9 +62,6 @@ public class MeITCase {
.requestIndexResource(newUser, password)
.requestMe()
.assertStatusCode(200)
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
}

View File

@@ -48,7 +48,7 @@ public class ScmRequests {
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
}
public <SELF extends UserResponse<SELF, T>, T extends ModelResponse> UserResponse<SELF,T> requestUser(String username, String password, String pathParam) {
public UserResponse<UserResponse> requestUser(String username, String password, String pathParam) {
setUsername(username);
setPassword(password);
return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/"+pathParam).toString()), null);
@@ -195,7 +195,7 @@ public class ScmRequests {
return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this);
}
public UserResponse<? extends UserResponse, IndexResponse> requestUser(String username) {
public UserResponse<IndexResponse> requestUser(String username) {
return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this);
}
@@ -307,19 +307,24 @@ public class ScmRequests {
}
public class MeResponse<PREV extends ModelResponse> extends UserResponse<MeResponse<PREV>, PREV> {
public class MeResponse<PREV extends ModelResponse> extends ModelResponse<MeResponse<PREV>, PREV> {
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
public MeResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
public MeResponse<PREV> assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public ChangePasswordResponse<MeResponse> requestChangePassword(String oldPassword, String newPassword) {
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
}
}
public class UserResponse<SELF extends UserResponse<SELF, PREV>, PREV extends ModelResponse> extends ModelResponse<SELF, PREV> {
public class UserResponse<PREV extends ModelResponse> extends ModelResponse<UserResponse<PREV>, PREV> {
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
@@ -327,23 +332,23 @@ public class ScmRequests {
super(response, previousResponse);
}
public SELF assertPassword(Consumer<String> assertPassword) {
public UserResponse<PREV> assertPassword(Consumer<String> assertPassword) {
return super.assertSingleProperty(assertPassword, "password");
}
public SELF assertType(Consumer<String> assertType) {
public UserResponse<PREV> assertType(Consumer<String> assertType) {
return assertSingleProperty(assertType, "type");
}
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
public UserResponse<PREV> assertAdmin(Consumer<Boolean> assertAdmin) {
return assertSingleProperty(assertAdmin, "admin");
}
public SELF assertPasswordLinkDoesNotExists() {
public UserResponse<PREV> assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public SELF assertPasswordLinkExists() {
public UserResponse<PREV> assertPasswordLinkExists() {
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
}

View File

@@ -0,0 +1,43 @@
<?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.dd7s
http://bitbucket.org/sdorra/scm-manager
-->
<permissions>
<permission>
<value>configuration:read:git</value>
</permission>
<permission>
<value>configuration:write:git</value>
</permission>
</permissions>

View File

@@ -33,5 +33,21 @@
},
"success": "Default branch changed!"
}
},
"permissions" : {
"configuration": {
"read": {
"git": {
"displayName": "Read git configuration",
"description": "May read the git configuration"
}
},
"write": {
"git": {
"displayName": "Write git configuration",
"description": "May change the git configuration"
}
}
}
}
}

View File

@@ -4,8 +4,6 @@ import sonia.scm.repository.Modifications;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.javahg.HgLogChangesetCommand;
import java.text.MessageFormat;
public class HgModificationsCommand extends AbstractCommand implements ModificationsCommand {
HgModificationsCommand(HgCommandContext context, Repository repository) {
@@ -17,8 +15,7 @@ public class HgModificationsCommand extends AbstractCommand implements Modificat
public Modifications getModifications(String revision) {
com.aragost.javahg.Repository repository = open();
HgLogChangesetCommand hgLogChangesetCommand = HgLogChangesetCommand.on(repository, getContext().getConfig());
int hgRevision = hgLogChangesetCommand.rev(revision).singleRevision();
Modifications modifications = hgLogChangesetCommand.rev(MessageFormat.format("{0}:{0}", hgRevision)).extractModifications();
Modifications modifications = hgLogChangesetCommand.rev(revision).extractModifications();
modifications.setRevision(revision);
return modifications;
}

View File

@@ -1,10 +1,10 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* <p>
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* <p>
* 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,
@@ -13,7 +13,7 @@
* 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.
*
* <p>
* 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
@@ -24,99 +24,64 @@
* 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.
*
* <p>
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository.spi.javahg;
//~--- non-JDK imports --------------------------------------------------------
import com.aragost.javahg.Repository;
import com.aragost.javahg.internals.HgInputStream;
import com.aragost.javahg.internals.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.HgConfig;
import sonia.scm.repository.Modifications;
import java.io.IOException;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
*/
public class HgLogChangesetCommand extends AbstractChangesetCommand
{
public class HgLogChangesetCommand extends AbstractChangesetCommand {
/**
* Constructs ...
*
*
* @param repository
* @param config
*/
private HgLogChangesetCommand(Repository repository, HgConfig config)
{
private static final Logger LOG = LoggerFactory.getLogger(HgLogChangesetCommand.class);
private HgLogChangesetCommand(Repository repository, HgConfig config) {
super(repository, config);
}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param repository
* @param config
*
* @return
*/
public static HgLogChangesetCommand on(Repository repository, HgConfig config)
{
public static HgLogChangesetCommand on(Repository repository, HgConfig config) {
return new HgLogChangesetCommand(repository, config);
}
/**
* Method description
*
*
* @param branch
*
* @return
*/
public HgLogChangesetCommand branch(String branch)
{
public HgLogChangesetCommand branch(String branch) {
cmdAppend("-b", branch);
return this;
}
/**
* Method description
*
*
* @param files
*
* @return
*/
public List<Changeset> execute(String... files)
{
public List<Changeset> execute(String... files) {
return readListFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH));
}
/**
* Extract Modifications from the Repository files
*
* @param files repo files
* @return modifications
*/
public Modifications extractModifications(String... files) {
return readModificationsFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH));
HgInputStream hgInputStream = getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH);
try {
return readModificationsFromStream(hgInputStream);
} finally {
try {
hgInputStream.close();
} catch (IOException e) {
LOG.error("Could not close HgInputStream", e);
}
}
}
HgInputStream getHgInputStream(String[] files, String changesetStylePath) {
@@ -124,93 +89,39 @@ public class HgLogChangesetCommand extends AbstractChangesetCommand
return launchStream(files);
}
/**
* Method description
*
*
* @param limit
*
* @return
*/
public HgLogChangesetCommand limit(int limit)
{
public HgLogChangesetCommand limit(int limit) {
cmdAppend("-l", limit);
return this;
}
/**
* Method description
*
*
* @param files
*
* @return
*/
public List<Integer> loadRevisions(String... files)
{
public List<Integer> loadRevisions(String... files) {
return loadRevisionsFromStream(getHgInputStream(files, CHANGESET_LAZY_STYLE_PATH));
}
/**
* Method description
*
*
* @param rev
*
* @return
*/
public HgLogChangesetCommand rev(String... rev)
{
public HgLogChangesetCommand rev(String... rev) {
cmdAppend("-r", rev);
return this;
}
/**
* Method description
*
*
* @param files
*
* @return
*/
public Changeset single(String... files)
{
public Changeset single(String... files) {
return Utils.single(execute(files));
}
/**
* Method description
*
*
* @param files
*
* @return
*/
public int singleRevision(String... files)
{
public int singleRevision(String... files) {
Integer rev = Utils.single(loadRevisions(files));
if (rev == null)
{
if (rev == null) {
rev = -1;
}
return rev;
}
//~--- get methods ----------------------------------------------------------
/**
* Method description
*
*
* @return
*/
@Override
public String getCommandName()
{
public String getCommandName() {
return "log";
}
}

View File

@@ -0,0 +1,43 @@
<?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.dd7s
http://bitbucket.org/sdorra/scm-manager
-->
<permissions>
<permission>
<value>configuration:read:hg</value>
</permission>
<permission>
<value>configuration:write:hg</value>
</permission>
</permissions>

View File

@@ -24,5 +24,21 @@
"disabledHelpText": "Enable or disable the Mercurial plugin.",
"required": "This configuration value is required"
}
},
"permissions" : {
"configuration": {
"read": {
"hg": {
"displayName": "Read Mercurial configuration",
"description": "May read the Mercurial configuration"
}
},
"write": {
"hg": {
"displayName": "Write Mercurial configuration",
"description": "May change the Mercurial configuration"
}
}
}
}
}

View File

@@ -0,0 +1,43 @@
<?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.dd7s
http://bitbucket.org/sdorra/scm-manager
-->
<permissions>
<permission>
<value>configuration:read:svn</value>
</permission>
<permission>
<value>configuration:write:svn</value>
</permission>
</permissions>

View File

@@ -22,5 +22,21 @@
"disabledHelpText": "Enable or disable the Git plugin",
"required": "This configuration value is required"
}
},
"permissions": {
"configuration": {
"read": {
"svn": {
"displayName": "Read Subversion configuration",
"description": "May read the Subversion configuration"
}
},
"write": {
"svn": {
"displayName": "Write Subversion configuration",
"description": "May change the Subversion configuration"
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
package sonia.scm.store;
import com.google.common.base.Predicate;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
public class InMemoryConfigurationEntryStore<V> implements ConfigurationEntryStore<V> {
private final Map<String, V> values = new HashMap<>();
@Override
public Collection<V> getMatchingValues(Predicate<V> predicate) {
return values.values().stream().filter(predicate).collect(Collectors.toList());
}
@Override
public String put(V item) {
String key = UUID.randomUUID().toString();
values.put(key, item);
return key;
}
@Override
public void put(String id, V item) {
values.put(id, item);
}
@Override
public Map<String, V> getAll() {
return Collections.unmodifiableMap(values);
}
@Override
public void clear() {
values.clear();
}
@Override
public void remove(String id) {
values.remove(id);
}
@Override
public V get(String id) {
return values.get(id);
}
}

View File

@@ -0,0 +1,28 @@
package sonia.scm.store;
public class InMemoryConfigurationEntryStoreFactory implements ConfigurationEntryStoreFactory {
private ConfigurationEntryStore store;
public static ConfigurationEntryStoreFactory create() {
return new InMemoryConfigurationEntryStoreFactory(new InMemoryConfigurationEntryStore());
}
public InMemoryConfigurationEntryStoreFactory() {
}
public InMemoryConfigurationEntryStoreFactory(ConfigurationEntryStore store) {
this.store = store;
}
@Override
public <T> ConfigurationEntryStore<T> getStore(TypedStoreParameters<T> storeParameters) {
if (store != null) {
return store;
}
return new InMemoryConfigurationEntryStore<>();
}
}

View File

@@ -1,10 +1,7 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import {
RemoveEntryOfTableButton,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
import RemoveEntryOfTableButton from "../buttons/RemoveEntryOfTableButton";
type Props = {
members: string[],
@@ -19,10 +16,6 @@ class MemberNameTable extends React.Component<Props, State> {
const { t } = this.props;
return (
<div>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("group-form.help.memberHelpText")}
/>
<table className="table is-hoverable is-fullwidth">
<tbody>
{this.props.members.map(member => {

View File

@@ -2,6 +2,7 @@
export { default as AddEntryToTableField } from "./AddEntryToTableField.js";
export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEntryToTableField.js";
export { default as MemberNameTable } from "./MemberNameTable.js";
export { default as Checkbox } from "./Checkbox.js";
export { default as InputField } from "./InputField.js";
export { default as Select } from "./Select.js";

View File

@@ -6,5 +6,6 @@ export type Me = {
name: string,
displayName: string,
mail: string,
groups: [],
_links: Links
};

View File

@@ -50,6 +50,9 @@
"username": "Username",
"displayName": "Display Name",
"mail": "E-Mail",
"groups": "Groups",
"information": "Information",
"change-password": "Change password",
"error-title": "Error",
"error-subtitle": "Cannot display profile",
"error": "Error",

View File

@@ -19,7 +19,7 @@
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"editNavLink": "General",
"permissionsNavLink": "Permissions"
"setPermissionsNavLink": "Permissions"
}
},
"add-group": {
@@ -67,5 +67,8 @@
"submit": "Yes",
"cancel": "No"
}
},
"setPasswordNavLink": {
"label": "Set permissions"
}
}

View File

@@ -0,0 +1,6 @@
{
"setPermissions": {
"button": "Set permissions",
"setPermissionsSuccessful": "Permissions set successfully"
}
}

View File

@@ -35,7 +35,8 @@
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"editNavLink": "General",
"setPasswordNavLink": "Password"
"setPasswordNavLink": "Set Password",
"setPermissionsNavLink": "Set Permissions"
}
},
"addUser": {

View File

@@ -3,6 +3,8 @@ import React from "react";
import { translate } from "react-i18next";
import {
AutocompleteAddEntryToTableField,
LabelWithHelpIcon,
MemberNameTable,
InputField,
SubmitButton,
Textarea
@@ -10,7 +12,6 @@ import {
import type { Group, SelectValue } from "@scm-manager/ui-types";
import * as validator from "./groupValidation";
import MemberNameTable from "./MemberNameTable";
type Props = {
t: string => string,
@@ -97,6 +98,10 @@ class GroupForm extends React.Component<Props, State> {
validationError={false}
helpText={t("group-form.help.descriptionHelpText")}
/>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("group-form.help.memberHelpText")}
/>
<MemberNameTable
members={group.members}
memberListChanged={this.memberListChanged}

View File

@@ -0,0 +1,28 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Group } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
type Props = {
t: string => string,
group: Group,
permissionsUrl: String
};
class ChangePermissionNavLink extends React.Component<Props> {
render() {
const { t, permissionsUrl } = this.props;
if (!this.hasPermissionToSetPermission()) {
return null;
}
return <NavLink label={t("singleGroup.menu.setPermissionsNavLink")} to={permissionsUrl} />;
}
hasPermissionToSetPermission = () => {
return this.props.group._links.permissions;
};
}
export default translate("groups")(ChangePermissionNavLink);

View File

@@ -1 +1,2 @@
export { default as GeneralGroupNavLink } from "./GeneralGroupNavLink";
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";

View File

@@ -13,7 +13,8 @@ import {
import { Route } from "react-router";
import { Details } from "./../components/table";
import {
GeneralGroupNavLink
GeneralGroupNavLink,
SetPermissionsNavLink
} from "./../components/navLinks";
import type { Group } from "@scm-manager/ui-types";
import type { History } from "history";
@@ -27,6 +28,8 @@ import {
import { translate } from "react-i18next";
import EditGroup from "./EditGroup";
import { getGroupsLink } from "../../modules/indexResource";
import SetPermissions from "../../permissions/components/SetPermissions";
import {ExtensionPoint} from "@scm-manager/ui-extensions";
type Props = {
name: string,
@@ -79,6 +82,11 @@ class SingleGroup extends React.Component<Props> {
const url = this.matchedUrl();
const extensionProps = {
group,
url
};
return (
<Page title={group.name}>
<div className="columns">
@@ -93,6 +101,18 @@ class SingleGroup extends React.Component<Props> {
exact
component={() => <EditGroup group={group} />}
/>
<Route
path={`${url}/permissions`}
exact
component={() => (
<SetPermissions selectedPermissionsLink={group._links.permissions} />
)}
/>
<ExtensionPoint
name="group.route"
props={extensionProps}
renderAll={true}
/>
</div>
<div className="column">
<Navigation>
@@ -101,6 +121,11 @@ class SingleGroup extends React.Component<Props> {
to={`${url}`}
label={t("singleGroup.menu.informationNavLink")}
/>
<ExtensionPoint
name="group.navigation"
props={extensionProps}
renderAll={true}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleGroup.menu.settingsNavLink")}
@@ -109,6 +134,10 @@ class SingleGroup extends React.Component<Props> {
group={group}
editUrl={`${url}/settings/general`}
/>
<SetPermissionsNavLink
group={group}
permissionsUrl={`${url}/permissions`}
/>
</SubNavigation>
</Section>
</Navigation>

View File

@@ -134,15 +134,6 @@ const callFetchMe = (link: string): Promise<Me> => {
.get(link)
.then(response => {
return response.json();
})
.then(json => {
const { name, displayName, mail, _links } = json;
return {
name,
displayName,
mail,
_links
};
});
};

View File

@@ -0,0 +1,47 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox } from "../../../../scm-ui-components/packages/ui-components/src";
type Props = {
permission: string,
checked: boolean,
onChange: (value: boolean, name: string) => void,
disabled: boolean,
t: string => string
};
class PermissionCheckbox extends React.Component<Props> {
render() {
const { t, permission, checked, onChange, disabled } = this.props;
const key = permission.split(":").join(".");
return (
<Checkbox
name={permission}
label={this.translateOrDefault(
"permissions." + key + ".displayName",
key
)}
checked={checked}
onChange={onChange}
disabled={disabled}
helpText={this.translateOrDefault(
"permissions." + key + ".description",
t("permissions.unknown")
)}
/>
);
}
translateOrDefault = (key: string, defaultText: string) => {
const translation = this.props.t(key);
if (translation === key) {
return defaultText;
} else {
return translation;
}
};
}
export default translate("plugins")(PermissionCheckbox);

View File

@@ -0,0 +1,178 @@
// @flow
import React from "react";
import type { Link } from "@scm-manager/ui-types";
import {
Notification,
ErrorNotification,
SubmitButton
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import {
loadPermissionsForEntity,
setPermissions
} from "./handlePermissions";
import PermissionCheckbox from "./PermissionCheckbox";
import { connect } from "react-redux";
import { getLink } from "../../modules/indexResource";
type Props = {
t: string => string,
availablePermissionLink: string,
selectedPermissionsLink: Link
};
type State = {
permissions: { [string]: boolean },
loading: boolean,
error?: Error,
permissionsChanged: boolean,
permissionsSubmitted: boolean,
overwritePermissionsLink?: Link
};
class SetPermissions extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
permissions: {},
loading: true,
permissionsChanged: false,
permissionsSubmitted: false,
modifiable: false,
overwritePermissionsLink: undefined
};
}
setLoadingState = () => {
this.setState({
loading: true
});
};
setErrorState = (error: Error) => {
this.setState({
error: error,
loading: false
});
};
setSuccessfulState = () => {
this.setState({
loading: false,
error: undefined,
permissionsSubmitted: true,
permissionsChanged: false
});
};
componentDidMount(): void {
loadPermissionsForEntity(
this.props.availablePermissionLink,
this.props.selectedPermissionsLink.href
).then(response => {
const { permissions, overwriteLink } = response;
this.setState({
permissions: permissions,
loading: false,
overwritePermissionsLink: overwriteLink
});
});
}
submit = (event: Event) => {
event.preventDefault();
if (this.state.permissions) {
const { permissions } = this.state;
this.setLoadingState();
const selectedPermissions = Object.entries(permissions)
.filter(e => e[1])
.map(e => e[0]);
if (this.state.overwritePermissionsLink) {
setPermissions(
this.state.overwritePermissionsLink.href,
selectedPermissions
)
.then(result => {
this.setSuccessfulState();
})
.catch(err => {
this.setErrorState(err);
});
}
}
};
render() {
const { t } = this.props;
const { loading, permissionsSubmitted, error } = this.state;
let message = null;
if (permissionsSubmitted) {
message = (
<Notification
type={"success"}
children={t("setPermissions.setPermissionsSuccessful")}
onClose={() => this.onClose()}
/>
);
} else if (error) {
message = <ErrorNotification error={error} />;
}
return (
<form onSubmit={this.submit}>
{message}
{this.renderPermissions()}
<SubmitButton
disabled={!this.state.permissionsChanged}
loading={loading}
label={t("setPermissions.button")}
/>
</form>
);
}
renderPermissions = () => {
const { overwritePermissionsLink, permissions } = this.state;
return Object.keys(permissions).map(p => (
<div key={p}>
<PermissionCheckbox
permission={p}
checked={permissions[p]}
onChange={this.valueChanged}
disabled={!overwritePermissionsLink}
/>
</div>
));
};
valueChanged = (value: boolean, name: string) => {
this.setState(state => {
const newPermissions = state.permissions;
newPermissions[name] = value;
return {
permissions: newPermissions,
permissionsChanged: true
};
});
};
onClose = () => {
this.setState({
permissionsSubmitted: false
});
};
}
const mapStateToProps = state => {
const availablePermissionLink = getLink(state, "permissions");
return {
availablePermissionLink
};
};
export default connect(mapStateToProps)(
translate("permissions")(SetPermissions)
);

View File

@@ -0,0 +1,33 @@
//@flow
import { apiClient } from "@scm-manager/ui-components";
export const CONTENT_TYPE_PERMISSIONS =
"application/vnd.scmm-permissionCollection+json;v=2";
export function setPermissions(url: string, permissions: string[]) {
return apiClient
.put(url, { permissions: permissions }, CONTENT_TYPE_PERMISSIONS)
.then(response => {
return response;
});
}
export function loadPermissionsForEntity(
availableUrl: string,
userUrl: string
) {
return Promise.all([
apiClient.get(availableUrl).then(response => {
return response.json();
}),
apiClient.get(userUrl).then(response => {
return response.json();
})
]).then(values => {
const [availablePermissions, checkedPermissions] = values;
const permissions = {};
availablePermissions.permissions.forEach(p => (permissions[p] = false));
checkedPermissions.permissions.forEach(p => (permissions[p] = true));
return { permissions, overwriteLink: checkedPermissions._links.overwrite };
});
}

View File

@@ -0,0 +1,67 @@
//@flow
import fetchMock from "fetch-mock";
import { loadPermissionsForEntity } from "./handlePermissions";
describe("load permissions for entity", () => {
const AVAILABLE_PERMISSIONS_URL = "/permissions";
const USER_PERMISSIONS_URL = "/user/scmadmin/permissions";
const availablePermissions = `{
"permissions": [
"repository:read,pull:*",
"repository:read,pull,push:*",
"repository:*:*"
]
}`;
const userPermissions = `{
"permissions": [
"repository:read,pull:*"
],
"_links": {
"self": {
"href": "/api/v2/users/rene/permissions"
},
"overwrite": {
"href": "/api/v2/users/rene/permissions"
}
}
}`;
beforeEach(() => {
fetchMock.getOnce(
"/api/v2" + AVAILABLE_PERMISSIONS_URL,
availablePermissions
);
fetchMock.getOnce("/api/v2" + USER_PERMISSIONS_URL, userPermissions);
});
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should return permissions array", done => {
loadPermissionsForEntity(
AVAILABLE_PERMISSIONS_URL,
USER_PERMISSIONS_URL
).then(result => {
const { permissions } = result;
expect(Object.entries(permissions).length).toBe(3);
expect(permissions["repository:read,pull:*"]).toBe(true);
expect(permissions["repository:read,pull,push:*"]).toBe(false);
expect(permissions["repository:*:*"]).toBe(false);
done();
});
});
it("should return overwrite link", done => {
loadPermissionsForEntity(
AVAILABLE_PERMISSIONS_URL,
USER_PERMISSIONS_URL
).then(result => {
const { overwriteLink } = result;
expect(overwriteLink.href).toBe("/api/v2/users/rene/permissions");
done();
});
});
});

View File

@@ -0,0 +1,28 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
type Props = {
t: string => string,
user: User,
permissionsUrl: String
};
class ChangePermissionNavLink extends React.Component<Props> {
render() {
const { t, permissionsUrl } = this.props;
if (!this.hasPermissionToSetPermission()) {
return null;
}
return <NavLink label={t("singleUser.menu.setPermissionsNavLink")} to={permissionsUrl} />;
}
hasPermissionToSetPermission = () => {
return this.props.user._links.permissions;
};
}
export default translate("users")(ChangePermissionNavLink);

View File

@@ -0,0 +1,31 @@
import React from "react";
import { shallow } from "enzyme";
import "../../../tests/enzyme";
import "../../../tests/i18n";
import SetPermissionsNavLink from "./SetPermissionsNavLink";
it("should render nothing, if the permissions link is missing", () => {
const user = {
_links: {}
};
const navLink = shallow(
<SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const user = {
_links: {
permissions: {
href: "/permissions"
}
}
};
const navLink = shallow(
<SetPermissionsNavLink user={user} permissionsUrl="/user/permissions" />
);
expect(navLink.text()).not.toBe("");
});

View File

@@ -1,2 +1,3 @@
export { default as GeneralUserNavLink } from "./GeneralUserNavLink";
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";

View File

@@ -21,10 +21,11 @@ import {
isFetchUserPending,
getFetchUserFailure
} from "../modules/users";
import { GeneralUserNavLink, SetPasswordNavLink } from "./../components/navLinks";
import { GeneralUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks";
import { translate } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource";
import SetUserPassword from "../components/SetUserPassword";
import SetPermissions from "../../permissions/components/SetPermissions";
type Props = {
name: string,
@@ -90,6 +91,14 @@ class SingleUser extends React.Component<Props> {
path={`${url}/settings/password`}
component={() => <SetUserPassword user={user} />}
/>
<Route
path={`${url}/permissions`}
component={() => (
<SetPermissions
selectedPermissionsLink={user._links.permissions}
/>
)}
/>
</div>
<div className="column">
<Navigation>
@@ -110,6 +119,10 @@ class SingleUser extends React.Component<Props> {
user={user}
passwordUrl={`${url}/settings/password`}
/>
<SetPermissionsNavLink
user={user}
permissionsUrl={`${url}/settings/permissions`}
/>
</SubNavigation>
</Section>
</Navigation>

View File

@@ -1,298 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.api.rest.Permission;
import sonia.scm.security.AssignedPermission;
import sonia.scm.security.SecuritySystem;
import sonia.scm.security.StoredAssignedPermission;
//~--- JDK imports ------------------------------------------------------------
import java.net.URI;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
/**
* Abstract base class for global permission resources.
*
* @author Sebastian Sdorra
* @since 1.31
*/
public abstract class AbstractPermissionResource
{
/**
* Constructs a new {@link AbstractPermissionResource}.
*
*
* @param securitySystem security system
* @param name name of the user or group
*/
protected AbstractPermissionResource(SecuritySystem securitySystem,
String name)
{
this.securitySystem = securitySystem;
this.name = name;
}
//~--- methods --------------------------------------------------------------
/**
* Transforms a {@link Permission} to a {@link AssignedPermission}.
*
*
* @param permission permission object to transform
*
* @return transformed {@link AssignedPermission}
*/
protected abstract AssignedPermission transformPermission(
Permission permission);
//~--- get methods ----------------------------------------------------------
/**
* Returns a {@link Predicate} to filter permissions.
*
*
* @return {@link Predicate} to filter permissions
*/
protected abstract Predicate<AssignedPermission> getPredicate();
//~--- methods --------------------------------------------------------------
/**
* Adds a new permission to the user or group managed by the resource.
*
* @param uriInfo uri informations
* @param permission permission to add
*
* @return web response
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "creates", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri to new create permission")
}),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response add(@Context UriInfo uriInfo, Permission permission)
{
AssignedPermission ap = transformPermission(permission);
StoredAssignedPermission sap = securitySystem.addPermission(ap);
URI uri = uriInfo.getAbsolutePathBuilder().path(sap.getId()).build();
return Response.created(uri).build();
}
/**
* Deletes a permission from the user or group managed by the resource.
*
* @param id id of the permission
*
* @return web response
*/
@DELETE
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response delete(@PathParam("id") String id)
{
StoredAssignedPermission sap = getPermission(id);
securitySystem.deletePermission(sap);
return Response.noContent().build();
}
/**
* Updates the specified permission on the user or group managed by the resource.
*
* @param id id of the permission
* @param permission updated permission
*
* @return web response
*/
@PUT
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response update(@PathParam("id") String id, Permission permission)
{
StoredAssignedPermission sap = getPermission(id);
securitySystem.modifyPermission(new StoredAssignedPermission(sap.getId(),
transformPermission(permission)));
return Response.noContent().build();
}
//~--- get methods ----------------------------------------------------------
/**
* Returns the {@link Permission} with the specified id.
*
* @param id id of the {@link Permission}
*
* @return {@link Permission} with the specified id
*/
@GET
@Path("{id}")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, permission id does not belong to the user or group"),
@ResponseCode(code = 404, condition = "not found, no permission with the specified id available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Permission get(@PathParam("id") String id)
{
StoredAssignedPermission sap = getPermission(id);
return new Permission(sap.getId(), sap.getPermission());
}
/**
* Returns all permissions of the user or group managed by the resource.
*
* @return all permissions of the user or group
*/
@GET
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public List<Permission> getAll()
{
return getPermissions(getPredicate());
}
/**
* Returns the {@link StoredAssignedPermission} with the given id.
*
*
* @param id id of the stored permission
*
* @return {@link StoredAssignedPermission} with the given id
*/
private StoredAssignedPermission getPermission(String id)
{
StoredAssignedPermission sap = securitySystem.getPermission(id);
if (sap == null)
{
throw new WebApplicationException(Status.NOT_FOUND);
}
if (!getPredicate().apply(sap))
{
throw new WebApplicationException(Status.BAD_REQUEST);
}
return sap;
}
/**
* Returns all permissions which matches the given {@link Predicate}.
*
*
* @param predicate predicate for filtering
*
* @return all permissions which matches the given {@link Predicate}
*/
private List<Permission> getPermissions(
Predicate<AssignedPermission> predicate)
{
List<StoredAssignedPermission> permissions =
securitySystem.getPermissions(predicate);
return Lists.transform(permissions,
new Function<StoredAssignedPermission, Permission>()
{
@Override
public Permission apply(StoredAssignedPermission mgp)
{
return new Permission(mgp.getId(), mgp.getPermission());
}
});
}
//~--- fields ---------------------------------------------------------------
/** name of the user or the group */
protected String name;
/** security system */
private SecuritySystem securitySystem;
}

View File

@@ -1,127 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Predicate;
import sonia.scm.api.rest.Permission;
import sonia.scm.security.AssignedPermission;
import sonia.scm.security.SecuritySystem;
/**
* Resource to manage global group permission for a specified group.
*
* @author Sebastian Sdorra
* @since 1.31
*/
public class GroupPermissionResource extends AbstractPermissionResource
{
/**
* Constructs a new group permissions resource
*
*
* @param securitySystem security system
* @param name name of the group
*/
public GroupPermissionResource(SecuritySystem securitySystem, String name)
{
super(securitySystem, name);
}
//~--- methods --------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected AssignedPermission transformPermission(Permission permission)
{
return new AssignedPermission(name, true, permission.getValue());
}
//~--- get methods ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected Predicate<AssignedPermission> getPredicate()
{
return new GroupPredicate(name);
}
//~--- inner classes --------------------------------------------------------
/**
* Group predicate to filter permissions.
*/
private static class GroupPredicate implements Predicate<AssignedPermission>
{
/**
* Constructs a new group predicate
*
*
* @param name name of the group
*/
public GroupPredicate(String name)
{
this.name = name;
}
//~--- methods ------------------------------------------------------------
/**
* Returns true if the permission is a group permission and the name is
* equals.
*
* @param input permission
*
* @return true if the permission is a group permission and the name is
* equals
*/
@Override
public boolean apply(AssignedPermission input)
{
return input.isGroupPermission() && input.getName().equals(name);
}
//~--- fields -------------------------------------------------------------
/** name of the group */
private String name;
}
}

View File

@@ -1,106 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import org.apache.shiro.SecurityUtils;
import sonia.scm.security.Role;
import sonia.scm.security.SecuritySystem;
//~--- JDK imports ------------------------------------------------------------
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
* Resource for managing system security permissions.
*
* @author Sebastian Sdorra
*/
@Path("security/permission")
public class SecuritySystemResource
{
/**
* Constructs ...
*
*
* @param system
*/
@Inject
public SecuritySystemResource(SecuritySystem system)
{
this.system = system;
// only administrators can use this resource
SecurityUtils.getSubject().checkRole(Role.ADMIN);
}
//~--- get methods ----------------------------------------------------------
/**
* Returns group permission sub resource.
*
* @param group name of group
*
* @return sub resource
*/
@Path("group/{group}")
public GroupPermissionResource getGroupSubResource(@PathParam("group") String group)
{
return new GroupPermissionResource(system, group);
}
/**
* Returns user permission sub resource.
*
*
* @param user name of user
*
* @return sub resource
*/
@Path("user/{user}")
public UserPermissionResource getUserSubResource(@PathParam("user") String user)
{
return new UserPermissionResource(system, user);
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final SecuritySystem system;
}

View File

@@ -1,127 +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.api.rest.resources;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Predicate;
import sonia.scm.api.rest.Permission;
import sonia.scm.security.AssignedPermission;
import sonia.scm.security.SecuritySystem;
/**
* Resource to manage global user permission for a specified user.
*
* @author Sebastian Sdorra
* @since 1.31
*/
public class UserPermissionResource extends AbstractPermissionResource
{
/**
* Constructs a new user permission resource.
*
*
* @param securitySystem security system
* @param name name of the user
*/
public UserPermissionResource(SecuritySystem securitySystem, String name)
{
super(securitySystem, name);
}
//~--- methods --------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected AssignedPermission transformPermission(Permission permission)
{
return new AssignedPermission(name, permission.getValue());
}
//~--- get methods ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected Predicate<AssignedPermission> getPredicate()
{
return new UserPredicate(name);
}
//~--- inner classes --------------------------------------------------------
/**
* User predicate to filter permissions.
*/
private static class UserPredicate implements Predicate<AssignedPermission>
{
/**
* Constructs a new user predicate.
*
*
* @param name name of the user
*/
public UserPredicate(String name)
{
this.name = name;
}
//~--- methods ------------------------------------------------------------
/**
* Returns true if the permission is a user permission and the name is
* equals.
*
* @param input permission
*
* @return true if the permission is a user permission and the name is
* equals
*/
@Override
public boolean apply(AssignedPermission input)
{
return !input.isGroupPermission() && input.getName().equals(name);
}
//~--- fields -------------------------------------------------------------
/** name of the user */
private String name;
}
}

View File

@@ -0,0 +1,30 @@
package sonia.scm.api.v2.resources;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
@Path("v2/permissions")
public class GlobalPermissionResource {
private PermissionAssigner permissionAssigner;
@Inject
public GlobalPermissionResource(PermissionAssigner permissionAssigner) {
this.permissionAssigner = permissionAssigner;
}
@GET
@Produces(VndMediaType.PERMISSION_COLLECTION)
@Path("")
public Response getAll() {
String[] permissions = permissionAssigner.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new);
return Response.ok(new PermissionListDto(permissions)).build();
}
}

View File

@@ -0,0 +1,79 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
public class GroupPermissionResource {
private final PermissionAssigner permissionAssigner;
private final PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
@Inject
public GroupPermissionResource(PermissionAssigner permissionAssigner, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper) {
this.permissionAssigner = permissionAssigner;
this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper;
}
/**
* Returns permissions for a group.
*
* @param id the id/name of the group
*/
@GET
@Path("")
@Produces(VndMediaType.PERMISSION_COLLECTION)
@TypeHint(PermissionListDto.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the group"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getPermissions(@PathParam("id") String id) {
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForGroup(id);
return Response.ok(permissionCollectionToDtoMapper.mapForGroup(permissions, id)).build();
}
/**
* Sets permissions for a group. Overwrites all existing permissions.
*
* @param id id of the group to be modified
* @param newPermissions New list of permissions for the group
*/
@PUT
@Path("")
@Consumes(VndMediaType.PERMISSION_COLLECTION)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current group does not have the correct privilege"),
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) {
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
.map(PermissionDescriptor::new)
.collect(Collectors.toList());
permissionAssigner.setPermissionsForGroup(id, permissionDescriptors);
return Response.noContent().build();
}
}

View File

@@ -8,7 +8,6 @@ import sonia.scm.group.GroupManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -24,13 +23,15 @@ public class GroupResource {
private final GroupToGroupDtoMapper groupToGroupDtoMapper;
private final GroupDtoToGroupMapper dtoToGroupMapper;
private final IdResourceManagerAdapter<Group, GroupDto> adapter;
private final GroupPermissionResource groupPermissionResource;
@Inject
public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper,
GroupDtoToGroupMapper groupDtoToGroupMapper) {
GroupDtoToGroupMapper groupDtoToGroupMapper, GroupPermissionResource groupPermissionResource) {
this.groupToGroupDtoMapper = groupToGroupDtoMapper;
this.dtoToGroupMapper = groupDtoToGroupMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, Group.class);
this.groupPermissionResource = groupPermissionResource;
}
/**
@@ -100,4 +101,9 @@ public class GroupResource {
public Response update(@PathParam("id") String name, @Valid GroupDto group) {
return adapter.update(name, existing -> dtoToGroupMapper.map(group));
}
@Path("permissions")
public GroupPermissionResource permissions() {
return groupPermissionResource;
}
}

View File

@@ -6,6 +6,7 @@ import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.group.Group;
import sonia.scm.group.GroupPermissions;
import sonia.scm.security.PermissionPermissions;
import javax.inject.Inject;
import java.util.List;
@@ -31,6 +32,9 @@ public abstract class GroupToGroupDtoMapper extends BaseMapper<Group, GroupDto>
if (GroupPermissions.modify(group).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.group().update(target.getName())));
}
if (PermissionPermissions.read().isPermitted()) {
linksBuilder.single(link("permissions", resourceLinks.groupPermissions().permissions(target.getName())));
}
appendLinks(new EdisonLinkAppender(linksBuilder), group);

View File

@@ -7,6 +7,7 @@ import org.apache.shiro.SecurityUtils;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.group.GroupPermissions;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
@@ -52,6 +53,9 @@ public class IndexDtoGenerator extends LinkAppenderMapper {
builder.single(link("config", resourceLinks.config().self()));
}
builder.single(link("repositories", resourceLinks.repositoryCollection().self()));
if (PermissionPermissions.list().isPermitted()) {
builder.single(link("permissions", resourceLinks.permissions().self()));
}
} else {
builder.single(link("login", resourceLinks.authentication().jsonLogin()));
}

View File

@@ -8,7 +8,6 @@ public class MapperModule extends AbstractModule {
@Override
protected void configure() {
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass());
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
bind(UserCollectionToDtoMapper.class);
@@ -27,7 +26,7 @@ public class MapperModule extends AbstractModule {
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass());
bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass());
bind(RepositoryPermissionToRepositoryPermissionDtoMapper.class).to(Mappers.getMapper(RepositoryPermissionToRepositoryPermissionDtoMapper.class).getClass());
bind(ChangesetToChangesetDtoMapper.class).to(Mappers.getMapper(ChangesetToChangesetDtoMapper.class).getClass());
bind(ChangesetToParentDtoMapper.class).to(Mappers.getMapper(ChangesetToParentDtoMapper.class).getClass());
@@ -46,6 +45,7 @@ public class MapperModule extends AbstractModule {
bind(MergeResultToDtoMapper.class).to(Mappers.getMapper(MergeResultToDtoMapper.class).getClass());
// no mapstruct required
bind(MeDtoFactory.class);
bind(UIPluginDtoMapper.class);
bind(UIPluginDtoCollectionMapper.class);

View File

@@ -0,0 +1,26 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class MeDto extends HalRepresentation {
private String name;
private String displayName;
private String mail;
private List<String> groups;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -0,0 +1,81 @@
package sonia.scm.api.v2.resources;
import com.google.common.collect.ImmutableList;
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.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.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
public class MeDtoFactory extends LinkAppenderMapper {
private final ResourceLinks resourceLinks;
private final UserManager userManager;
@Inject
public MeDtoFactory(ResourceLinks resourceLinks, UserManager userManager) {
this.resourceLinks = resourceLinks;
this.userManager = userManager;
}
public MeDto create() {
PrincipalCollection principals = getPrincipalCollection();
MeDto dto = new MeDto();
User user = principals.oneByType(User.class);
mapUserProperties(user, dto);
mapGroups(principals, dto);
appendLinks(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 mapUserProperties(User user, MeDto dto) {
dto.setName(user.getName());
dto.setDisplayName(user.getDisplayName());
dto.setMail(user.getMail());
}
private PrincipalCollection getPrincipalCollection() {
Subject subject = SecurityUtils.getSubject();
return subject.getPrincipals();
}
private void appendLinks(User user, MeDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user);
target.add(linksBuilder.build());
}
}

View File

@@ -3,14 +3,11 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@@ -28,20 +25,18 @@ import javax.ws.rs.core.UriInfo;
*/
@Path(MeResource.ME_PATH_V2)
public class MeResource {
public static final String ME_PATH_V2 = "v2/me/";
private final MeToUserDtoMapper meToUserDtoMapper;
static final String ME_PATH_V2 = "v2/me/";
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
private final MeDtoFactory meDtoFactory;
private final UserManager userManager;
private final PasswordService passwordService;
@Inject
public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) {
this.meToUserDtoMapper = meToUserDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
public MeResource(MeDtoFactory meDtoFactory, UserManager userManager, PasswordService passwordService) {
this.meDtoFactory = meDtoFactory;
this.userManager = userManager;
this.passwordService = passwordService;
this.userManager = manager;
}
/**
@@ -49,17 +44,15 @@ public class MeResource {
*/
@GET
@Path("")
@Produces(VndMediaType.USER)
@TypeHint(UserDto.class)
@Produces(VndMediaType.ME)
@TypeHint(MeDto.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response get(@Context Request request, @Context UriInfo uriInfo) {
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, meToUserDtoMapper::map);
return Response.ok(meDtoFactory.create()).build();
}
/**
@@ -75,7 +68,10 @@ public class MeResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PASSWORD_CHANGE)
public Response changePassword(@Valid PasswordChangeDto passwordChange) {
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChange.getOldPassword()), passwordService.encryptPassword(passwordChange.getNewPassword()));
userManager.changePasswordForLoggedInUser(
passwordService.encryptPassword(passwordChange.getOldPassword()),
passwordService.encryptPassword(passwordChange.getNewPassword())
);
return Response.noContent().build();
}
}

View File

@@ -1,45 +0,0 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class MeToUserDtoMapper extends UserToUserDtoMapper {
@Inject
private UserManager userManager;
@Inject
private ResourceLinks resourceLinks;
@Override
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
appendLinks(new EdisonLinkAppender(linksBuilder), new Me(), user);
target.add(linksBuilder.build());
}
}

View File

@@ -1,51 +1,48 @@
package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import org.mapstruct.Context;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.security.PermissionPermissions;
import java.util.List;
import javax.inject.Inject;
import java.util.Collection;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static java.util.stream.Collectors.toList;
public class PermissionCollectionToDtoMapper {
private final ResourceLinks resourceLinks;
private final PermissionToPermissionDtoMapper permissionToPermissionDtoMapper;
@Inject
public PermissionCollectionToDtoMapper(PermissionToPermissionDtoMapper permissionToPermissionDtoMapper, ResourceLinks resourceLinks) {
public PermissionCollectionToDtoMapper(ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
this.permissionToPermissionDtoMapper = permissionToPermissionDtoMapper;
}
public HalRepresentation map(Repository repository) {
List<PermissionDto> permissionDtoList = repository.getPermissions()
public PermissionListDto mapForUser(Collection<PermissionDescriptor> permissions, String userId) {
return map(permissions, userId, resourceLinks.userPermissions());
}
public PermissionListDto mapForGroup(Collection<PermissionDescriptor> permissions, String groupId) {
return map(permissions, groupId, resourceLinks.groupPermissions());
}
private PermissionListDto map(Collection<PermissionDescriptor> permissions, String id, ResourceLinks.WithPermissionLinks links) {
String[] permissionStrings = permissions
.stream()
.map(permission -> permissionToPermissionDtoMapper.map(permission, repository))
.collect(toList());
return new HalRepresentation(createLinks(repository), embedDtos(permissionDtoList));
.map(PermissionDescriptor::getValue)
.toArray(String[]::new);
PermissionListDto target = new PermissionListDto(permissionStrings);
Links.Builder linksBuilder = linkingTo().self(links.permissions(id));
if (PermissionPermissions.assign().isPermitted()) {
linksBuilder.single(link("overwrite", links.overwritePermissions(id)));
}
private Links createLinks(Repository repository) {
RepositoryPermissions.permissionRead(repository).check();
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(resourceLinks.permission().all(repository.getNamespace(), repository.getName())).build());
if (RepositoryPermissions.permissionWrite(repository).isPermitted()) {
linksBuilder.single(link("create", resourceLinks.permission().create(repository.getNamespace(), repository.getName())));
}
return linksBuilder.build();
}
target.add(linksBuilder.build());
private Embedded embedDtos(List<PermissionDto> permissionDtoList) {
return embeddedBuilder()
.with("permissions", permissionDtoList)
.build();
return target;
}
}

View File

@@ -2,20 +2,20 @@ package sonia.scm.api.v2.resources;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
@Mapper
public abstract class PermissionDtoToPermissionMapper {
public abstract Permission map(PermissionDto permissionDto);
public abstract RepositoryPermission map(RepositoryPermissionDto permissionDto);
/**
* this method is needed to modify an existing permission object
*
* @param target the target permission
* @param permissionDto the source dto
* @param repositoryPermissionDto the source dto
* @return the mapped target permission object
*/
public abstract void modify(@MappingTarget Permission target, PermissionDto permissionDto);
public abstract void modify(@MappingTarget RepositoryPermission target, RepositoryPermissionDto repositoryPermissionDto);
}

View File

@@ -0,0 +1,23 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PermissionListDto extends HalRepresentation {
private String[] permissions;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}
}

View File

@@ -8,14 +8,13 @@ import lombok.extern.slf4j.Slf4j;
import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -33,24 +32,24 @@ import java.util.function.Predicate;
import static sonia.scm.AlreadyExistsException.alreadyExists;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Slf4j
public class PermissionRootResource {
private PermissionDtoToPermissionMapper dtoToModelMapper;
private PermissionToPermissionDtoMapper modelToDtoMapper;
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
private RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
private ResourceLinks resourceLinks;
private final RepositoryManager manager;
@Inject
public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) {
public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper, RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) {
this.dtoToModelMapper = dtoToModelMapper;
this.modelToDtoMapper = modelToDtoMapper;
this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper;
this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper;
this.resourceLinks = resourceLinks;
this.manager = manager;
}
@@ -74,7 +73,7 @@ public class PermissionRootResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
@Path("")
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) {
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid RepositoryPermissionDto permission) {
log.info("try to add new permission: {}", permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
@@ -82,7 +81,7 @@ public class PermissionRootResource {
repository.addPermission(dtoToModelMapper.map(permission));
manager.modify(repository);
String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission);
return Response.created(URI.create(resourceLinks.permission().self(namespace, name, urlPermissionName))).build();
return Response.created(URI.create(resourceLinks.repositoryPermission().self(namespace, name, urlPermissionName))).build();
}
@@ -101,7 +100,7 @@ public class PermissionRootResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.PERMISSION)
@TypeHint(PermissionDto.class)
@TypeHint(RepositoryPermissionDto.class)
@Path("{permission-name}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) {
Repository repository = load(namespace, name);
@@ -112,7 +111,7 @@ public class PermissionRootResource {
.filter(filterPermission(permissionName))
.map(permission -> modelToDtoMapper.map(permission, repository))
.findFirst()
.orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name)))
.orElseThrow(() -> notFound(entity(RepositoryPermission.class, namespace).in(Repository.class, namespace + "/" + name)))
).build();
}
@@ -132,12 +131,12 @@ public class PermissionRootResource {
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.PERMISSION)
@TypeHint(PermissionDto.class)
@TypeHint(RepositoryPermissionDto.class)
@Path("")
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) {
Repository repository = load(namespace, name);
RepositoryPermissions.permissionRead(repository).check();
return Response.ok(permissionCollectionToDtoMapper.map(repository)).build();
return Response.ok(repositoryPermissionCollectionToDtoMapper.map(repository)).build();
}
@@ -161,23 +160,23 @@ public class PermissionRootResource {
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
@Valid PermissionDto permission) {
@Valid RepositoryPermissionDto permission) {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
String extractedPermissionName = getPermissionName(permissionName);
if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) {
throw notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name));
if (!isPermissionExist(new RepositoryPermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) {
throw notFound(entity(RepositoryPermission.class, namespace).in(Repository.class, namespace + "/" + name));
}
permission.setGroupPermission(isGroupPermission(permissionName));
if (!extractedPermissionName.equals(permission.getName())) {
checkPermissionAlreadyExists(permission, repository);
}
Permission existingPermission = repository.getPermissions()
RepositoryPermission existingPermission = repository.getPermissions()
.stream()
.filter(filterPermission(permissionName))
.findFirst()
.orElseThrow(() -> notFound(entity(Permission.class, namespace).in(Repository.class, namespace + "/" + name)));
.orElseThrow(() -> notFound(entity(RepositoryPermission.class, namespace).in(Repository.class, namespace + "/" + name)));
dtoToModelMapper.modify(existingPermission, permission);
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
@@ -216,7 +215,7 @@ public class PermissionRootResource {
return Response.noContent().build();
}
Predicate<Permission> filterPermission(String permissionName) {
Predicate<RepositoryPermission> filterPermission(String permissionName) {
return permission -> getPermissionName(permissionName).equals(permission.getName())
&&
permission.isGroupPermission() == isGroupPermission(permissionName);
@@ -255,13 +254,13 @@ public class PermissionRootResource {
* @param repository the repository to be inspected
* @throws AlreadyExistsException if the permission already exists in the repository
*/
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) {
private void checkPermissionAlreadyExists(RepositoryPermissionDto permission, Repository repository) {
if (isPermissionExist(permission, repository)) {
throw alreadyExists(entity("permission", permission.getName()).in(repository));
}
}
private boolean isPermissionExist(PermissionDto permission, Repository repository) {
private boolean isPermissionExist(RepositoryPermissionDto permission, Repository repository) {
return repository.getPermissions()
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission());

View File

@@ -6,7 +6,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
@@ -100,7 +100,7 @@ public class RepositoryCollectionResource {
private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) {
Repository repository = dtoToRepositoryMapper.map(repositoryDto, null);
repository.setPermissions(singletonList(new Permission(currentUser(), PermissionType.OWNER)));
repository.setPermissions(singletonList(new RepositoryPermission(currentUser(), PermissionType.OWNER)));
return repository;
}

View File

@@ -0,0 +1,51 @@
package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
import java.util.List;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static java.util.stream.Collectors.toList;
public class RepositoryPermissionCollectionToDtoMapper {
private final ResourceLinks resourceLinks;
private final RepositoryPermissionToRepositoryPermissionDtoMapper repositoryPermissionToRepositoryPermissionDtoMapper;
@Inject
public RepositoryPermissionCollectionToDtoMapper(RepositoryPermissionToRepositoryPermissionDtoMapper repositoryPermissionToRepositoryPermissionDtoMapper, ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
this.repositoryPermissionToRepositoryPermissionDtoMapper = repositoryPermissionToRepositoryPermissionDtoMapper;
}
public HalRepresentation map(Repository repository) {
List<RepositoryPermissionDto> repositoryPermissionDtoList = repository.getPermissions()
.stream()
.map(permission -> repositoryPermissionToRepositoryPermissionDtoMapper.map(permission, repository))
.collect(toList());
return new HalRepresentation(createLinks(repository), embedDtos(repositoryPermissionDtoList));
}
private Links createLinks(Repository repository) {
RepositoryPermissions.permissionRead(repository).check();
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(resourceLinks.repositoryPermission().all(repository.getNamespace(), repository.getName())).build());
if (RepositoryPermissions.permissionWrite(repository).isPermitted()) {
linksBuilder.single(link("create", resourceLinks.repositoryPermission().create(repository.getNamespace(), repository.getName())));
}
return linksBuilder.build();
}
private Embedded embedDtos(List<RepositoryPermissionDto> repositoryPermissionDtoList) {
return embeddedBuilder()
.with("permissions", repositoryPermissionDtoList)
.build();
}
}

View File

@@ -13,7 +13,7 @@ import javax.validation.constraints.Pattern;
import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
@Getter @Setter @ToString @NoArgsConstructor
public class PermissionDto extends HalRepresentation {
public class RepositoryPermissionDto extends HalRepresentation {
public static final String GROUP_PREFIX = "@";
@@ -33,7 +33,7 @@ public class PermissionDto extends HalRepresentation {
private boolean groupPermission = false;
public PermissionDto(String permissionName, boolean groupPermission) {
public RepositoryPermissionDto(String permissionName, boolean groupPermission) {
name = permissionName;
this.groupPermission = groupPermission;
}

View File

@@ -7,7 +7,7 @@ import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
@@ -16,16 +16,16 @@ import java.util.Optional;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Mapper
public abstract class PermissionToPermissionDtoMapper {
public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper {
@Inject
private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract PermissionDto map(Permission permission, @Context Repository repository);
public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Repository repository);
@BeforeMapping
@@ -40,20 +40,20 @@ public abstract class PermissionToPermissionDtoMapper {
* @param repository the repository
*/
@AfterMapping
void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) {
void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Repository repository) {
String permissionName = getUrlPermissionName(target);
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName));
.self(resourceLinks.repositoryPermission().self(repository.getNamespace(), repository.getName(), permissionName));
if (RepositoryPermissions.permissionWrite(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), permissionName)));
linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), permissionName)));
linksBuilder.single(link("update", resourceLinks.repositoryPermission().update(repository.getNamespace(), repository.getName(), permissionName)));
linksBuilder.single(link("delete", resourceLinks.repositoryPermission().delete(repository.getNamespace(), repository.getName(), permissionName)));
}
target.add(linksBuilder.build());
}
public String getUrlPermissionName(PermissionDto permissionDto) {
return Optional.of(permissionDto.getName())
.filter(p -> !permissionDto.isGroupPermission())
.orElse(GROUP_PREFIX + permissionDto.getName());
public String getUrlPermissionName(RepositoryPermissionDto repositoryPermissionDto) {
return Optional.of(repositoryPermissionDto.getName())
.filter(p -> !repositoryPermissionDto.isGroupPermission())
.orElse(GROUP_PREFIX + repositoryPermissionDto.getName());
}
}

View File

@@ -41,7 +41,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
}
if (RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName())));
linksBuilder.single(link("permissions", resourceLinks.permission().all(target.getNamespace(), target.getName())));
linksBuilder.single(link("permissions", resourceLinks.repositoryPermission().all(target.getNamespace(), target.getName())));
}
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
if (RepositoryPermissions.pull(repository).isPermitted()) {

View File

@@ -96,6 +96,52 @@ class ResourceLinks {
}
}
interface WithPermissionLinks {
String permissions(String name);
String overwritePermissions(String name);
}
UserPermissionLinks userPermissions() {
return new UserPermissionLinks(scmPathInfoStore.get());
}
static class UserPermissionLinks implements WithPermissionLinks {
private final LinkBuilder userPermissionLinkBuilder;
UserPermissionLinks(ScmPathInfo pathInfo) {
this.userPermissionLinkBuilder = new LinkBuilder(pathInfo, UserRootResource.class, UserResource.class, UserPermissionResource.class);
}
public String permissions(String name) {
return userPermissionLinkBuilder.method("getUserResource").parameters(name).method("permissions").parameters().method("getPermissions").parameters().href();
}
public String overwritePermissions(String name) {
return userPermissionLinkBuilder.method("getUserResource").parameters(name).method("permissions").parameters().method("overwritePermissions").parameters().href();
}
}
GroupPermissionLinks groupPermissions() {
return new GroupPermissionLinks(scmPathInfoStore.get());
}
static class GroupPermissionLinks implements WithPermissionLinks {
private final LinkBuilder groupPermissionLinkBuilder;
GroupPermissionLinks(ScmPathInfo pathInfo) {
this.groupPermissionLinkBuilder = new LinkBuilder(pathInfo, GroupRootResource.class, GroupResource.class, GroupPermissionResource.class);
}
public String permissions(String name) {
return groupPermissionLinkBuilder.method("getGroupResource").parameters(name).method("permissions").parameters().method("getPermissions").parameters().href();
}
public String overwritePermissions(String name) {
return groupPermissionLinkBuilder.method("getGroupResource").parameters(name).method("permissions").parameters().method("overwritePermissions").parameters().href();
}
}
MeLinks me() {
return new MeLinks(scmPathInfoStore.get(), this.user());
}
@@ -459,14 +505,15 @@ class ResourceLinks {
}
public PermissionLinks permission() {
return new PermissionLinks(scmPathInfoStore.get());
public RepositoryPermissionLinks repositoryPermission() {
return new RepositoryPermissionLinks(scmPathInfoStore.get());
}
static class PermissionLinks {
static class RepositoryPermissionLinks {
private final LinkBuilder permissionLinkBuilder;
PermissionLinks(ScmPathInfo pathInfo) {
RepositoryPermissionLinks(ScmPathInfo pathInfo) {
permissionLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
}
@@ -586,4 +633,20 @@ class ResourceLinks {
return mergeLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("merge").parameters().method("dryRun").parameters().href();
}
}
public PermissionsLinks permissions() {
return new PermissionsLinks(scmPathInfoStore.get());
}
static class PermissionsLinks {
private final LinkBuilder permissionsLlinkBuilder;
PermissionsLinks(ScmPathInfo scmPathInfo) {
this.permissionsLlinkBuilder = new LinkBuilder(scmPathInfo, GlobalPermissionResource.class);
}
String self() {
return permissionsLlinkBuilder.method("getAll").parameters().href();
}
}
}

View File

@@ -0,0 +1,79 @@
package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
public class UserPermissionResource {
private final PermissionAssigner permissionAssigner;
private final PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
@Inject
public UserPermissionResource(PermissionAssigner permissionAssigner, PermissionCollectionToDtoMapper permissionCollectionToDtoMapper) {
this.permissionAssigner = permissionAssigner;
this.permissionCollectionToDtoMapper = permissionCollectionToDtoMapper;
}
/**
* Returns permissions for a user.
*
* @param id the id/name of the user
*/
@GET
@Path("")
@Produces(VndMediaType.PERMISSION_COLLECTION)
@TypeHint(PermissionListDto.class)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the user"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response getPermissions(@PathParam("id") String id) {
Collection<PermissionDescriptor> permissions = permissionAssigner.readPermissionsForUser(id);
return Response.ok(permissionCollectionToDtoMapper.mapForUser(permissions, id)).build();
}
/**
* Sets permissions for a user. Overwrites all existing permissions.
*
* @param id id of the user to be modified
* @param newPermissions New list of permissions for the user
*/
@PUT
@Path("")
@Consumes(VndMediaType.PERMISSION_COLLECTION)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the correct privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response overwritePermissions(@PathParam("id") String id, PermissionListDto newPermissions) {
Collection<PermissionDescriptor> permissionDescriptors = Arrays.stream(newPermissions.getPermissions())
.map(PermissionDescriptor::new)
.collect(Collectors.toList());
permissionAssigner.setPermissionsForUser(id, permissionDescriptors);
return Response.noContent().build();
}
}

View File

@@ -9,7 +9,6 @@ import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@@ -28,14 +27,20 @@ public class UserResource {
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final UserManager userManager;
private final PasswordService passwordService;
private final UserPermissionResource userPermissionResource;
@Inject
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) {
public UserResource(
UserDtoToUserMapper dtoToUserMapper,
UserToUserDtoMapper userToDtoMapper,
UserManager manager,
PasswordService passwordService, UserPermissionResource userPermissionResource) {
this.dtoToUserMapper = dtoToUserMapper;
this.userToDtoMapper = userToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.userManager = manager;
this.passwordService = passwordService;
this.userPermissionResource = userPermissionResource;
}
/**
@@ -132,4 +137,9 @@ public class UserResource {
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwrite.getNewPassword()));
return Response.noContent().build();
}
@Path("permissions")
public UserPermissionResource permissions() {
return userPermissionResource;
}
}

View File

@@ -5,6 +5,7 @@ import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
@@ -42,6 +43,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
}
}
if (PermissionPermissions.read().isPermitted()) {
linksBuilder.single(link("permissions", resourceLinks.userPermissions().permissions(target.getName())));
}
appendLinks(new EdisonLinkAppender(linksBuilder), user);

View File

@@ -189,9 +189,9 @@ public class AuthorizationChangedEventProducer {
* @param event permission event
*/
@Subscribe
public void onEvent(StoredAssignedPermissionEvent event) {
public void onEvent(AssignedPermissionEvent event) {
if (event.getEventType().isPost()) {
StoredAssignedPermission permission = event.getPermission();
AssignedPermission permission = event.getPermission();
if (permission.isGroupPermission()) {
handleGroupPermissionChange(permission);
} else {
@@ -200,18 +200,18 @@ public class AuthorizationChangedEventProducer {
}
}
private void handleGroupPermissionChange(StoredAssignedPermission permission) {
private void handleGroupPermissionChange(AssignedPermission permission) {
logger.debug(
"fire authorization changed event, because global group permission {} has changed",
permission.getId()
"fire authorization changed event for group {}, because permission {} has changed",
permission.getName(), permission.getPermission()
);
fireEventForEveryUser();
}
private void handleUserPermissionChange(StoredAssignedPermission permission) {
private void handleUserPermissionChange(AssignedPermission permission) {
logger.debug(
"fire authorization changed event for user {}, because permission {} has changed",
permission.getName(), permission.getId()
permission.getName(), permission.getPermission()
);
fireEventForUser(permission.getName());
}

View File

@@ -101,11 +101,11 @@ public class BearerRealm extends AuthenticatingRealm
BearerToken bt = (BearerToken) token;
AccessToken accessToken = tokenResolver.resolve(bt);
return helper.getAuthenticationInfo(
accessToken.getSubject(),
bt.getCredentials(),
Scopes.fromClaims(accessToken.getClaims())
);
return helper.authenticationInfoBuilder(accessToken.getSubject())
.withCredentials(bt.getCredentials())
.withScope(Scopes.fromClaims(accessToken.getClaims()))
.withGroups(accessToken.getGroups())
.build();
}
}

View File

@@ -51,10 +51,11 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.user.User;
@@ -62,7 +63,6 @@ import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
import java.util.Collection;
import java.util.List;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -76,9 +76,6 @@ import java.util.Set;
public class DefaultAuthorizationCollector implements AuthorizationCollector
{
// TODO move to util class
private static final String SEPARATOR = System.getProperty("line.separator", "\n");
/** Field description */
private static final String ADMIN_PERMISSION = "*";
@@ -98,14 +95,16 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
*
*
*
* @param configuration
* @param cacheManager
* @param repositoryDAO
* @param securitySystem
*/
@Inject
public DefaultAuthorizationCollector(CacheManager cacheManager,
public DefaultAuthorizationCollector(ScmConfiguration configuration, CacheManager cacheManager,
RepositoryDAO repositoryDAO, SecuritySystem securitySystem)
{
this.configuration = configuration;
this.cache = cacheManager.getCache(CACHE_NAME);
this.repositoryDAO = repositoryDAO;
this.securitySystem = securitySystem;
@@ -175,12 +174,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private void collectGlobalPermissions(Builder<String> builder,
final User user, final GroupNames groups)
{
List<StoredAssignedPermission> globalPermissions =
Collection<AssignedPermission> globalPermissions =
securitySystem.getPermissions((AssignedPermission input) -> isUserPermitted(user, groups, input));
for (StoredAssignedPermission gp : globalPermissions)
for (AssignedPermission gp : globalPermissions)
{
String permission = gp.getPermission();
String permission = gp.getPermission().getValue();
logger.trace("add permission {} for user {}", permission, user.getName());
builder.add(permission);
@@ -199,13 +198,13 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
private void collectRepositoryPermissions(Builder<String> builder,
Repository repository, User user, GroupNames groups)
{
Collection<Permission> repositoryPermissions
Collection<RepositoryPermission> repositoryPermissions
= repository.getPermissions();
if (Util.isNotEmpty(repositoryPermissions))
{
boolean hasPermission = false;
for (sonia.scm.repository.Permission permission : repositoryPermissions)
for (RepositoryPermission permission : repositoryPermissions)
{
hasPermission = isUserPermitted(user, groups, permission);
if (hasPermission)
@@ -239,7 +238,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
Set<String> roles;
Set<String> permissions;
if (user.isAdmin())
if (isAdmin(user, groups))
{
if (logger.isDebugEnabled())
{
@@ -270,6 +269,37 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return info;
}
private boolean isAdmin(User user, GroupNames groups) {
boolean admin = user.isAdmin();
if (admin) {
logger.debug("user {} is marked as admin, because of the user flag", user.getName());
return true;
}
if (isUserAdminInConfiguration(user)) {
logger.debug("user {} is marked as admin, because of the admin user configuration", user.getName());
return true;
}
return isUserAdminInGroupConfiguration(user, groups);
}
private boolean isUserAdminInGroupConfiguration(User user, GroupNames groups) {
Set<String> adminGroups = configuration.getAdminGroups();
if (adminGroups != null && groups != null) {
for (String group : groups) {
if (adminGroups.contains(group)) {
logger.debug("user {} is marked as admin, because of the admin group configuration for group {}", user.getName(), group);
return true;
}
}
}
return false;
}
private boolean isUserAdminInConfiguration(User user) {
Set<String> adminUsers = configuration.getAdminUsers();
return adminUsers != null && adminUsers.contains(user.getName());
}
private String getGroupAutocompletePermission() {
return GroupPermissions.autocomplete().asShiroString();
}
@@ -373,6 +403,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
//~--- fields ---------------------------------------------------------------
private final ScmConfiguration configuration;
/** authorization cache */
private final Cache<CacheKey, AuthorizationInfo> cache;

View File

@@ -36,38 +36,22 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.github.legman.Subscribe;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.HandlerEventType;
import sonia.scm.event.ScmEventBus;
import sonia.scm.group.GroupEvent;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.user.UserEvent;
import sonia.scm.util.ClassLoaders;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
@@ -75,6 +59,19 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.Objects.isNull;
//~--- JDK imports ------------------------------------------------------------
/**
* TODO add events
@@ -109,13 +106,13 @@ public class DefaultSecuritySystem implements SecuritySystem
*/
@Inject
@SuppressWarnings("unchecked")
public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory)
public DefaultSecuritySystem(ConfigurationEntryStoreFactory storeFactory, PluginLoader pluginLoader)
{
store = storeFactory
.withType(AssignedPermission.class)
.withName(NAME)
.build();
readAvailablePermissions();
this.availablePermissions = readAvailablePermissions(pluginLoader);
}
//~--- methods --------------------------------------------------------------
@@ -129,9 +126,9 @@ public class DefaultSecuritySystem implements SecuritySystem
* @return
*/
@Override
public StoredAssignedPermission addPermission(AssignedPermission permission)
public void addPermission(AssignedPermission permission)
{
assertIsAdmin();
assertHasPermission();
validatePermission(permission);
String id = store.put(permission);
@@ -140,11 +137,9 @@ public class DefaultSecuritySystem implements SecuritySystem
//J-
ScmEventBus.getInstance().post(
new StoredAssignedPermissionEvent(HandlerEventType.CREATE, sap)
new AssignedPermissionEvent(HandlerEventType.CREATE, permission)
);
//J+
return sap;
}
/**
@@ -154,33 +149,16 @@ public class DefaultSecuritySystem implements SecuritySystem
* @param permission
*/
@Override
public void deletePermission(StoredAssignedPermission permission)
public void deletePermission(AssignedPermission permission)
{
assertIsAdmin();
store.remove(permission.getId());
//J-
assertHasPermission();
boolean deleted = deletePermissions(sap -> Objects.equal(sap.getName(), permission.getName())
&& Objects.equal(sap.isGroupPermission(), permission.isGroupPermission())
&& Objects.equal(sap.getPermission(), permission.getPermission()));
if (deleted) {
ScmEventBus.getInstance().post(
new StoredAssignedPermissionEvent(HandlerEventType.CREATE, permission)
new AssignedPermissionEvent(HandlerEventType.DELETE, permission)
);
//J+
}
/**
* Method description
*
*
* @param id
*/
@Override
public void deletePermission(String id)
{
assertIsAdmin();
AssignedPermission ap = store.get(id);
if (ap != null)
{
deletePermission(new StoredAssignedPermission(id, ap));
}
}
@@ -195,16 +173,8 @@ public class DefaultSecuritySystem implements SecuritySystem
{
if (event.getEventType() == HandlerEventType.DELETE)
{
deletePermissions(new Predicate<AssignedPermission>()
{
@Override
public boolean apply(AssignedPermission p)
{
return !p.isGroupPermission()
&& event.getItem().getName().equals(p.getName());
}
});
deletePermissions(p -> !p.isGroupPermission()
&& event.getItem().getName().equals(p.getName()));
}
}
@@ -219,42 +189,9 @@ public class DefaultSecuritySystem implements SecuritySystem
{
if (event.getEventType() == HandlerEventType.DELETE)
{
deletePermissions(new Predicate<AssignedPermission>()
{
@Override
public boolean apply(AssignedPermission p)
{
return p.isGroupPermission()
&& event.getItem().getName().equals(p.getName());
deletePermissions(p -> p.isGroupPermission()
&& event.getItem().getName().equals(p.getName()));
}
});
}
}
/**
* Method description
*
*
* @param permission
*/
@Override
public void modifyPermission(StoredAssignedPermission permission)
{
assertIsAdmin();
validatePermission(permission);
synchronized (store)
{
store.remove(permission.getId());
store.put(permission.getId(), new AssignedPermission(permission));
}
//J-
ScmEventBus.getInstance().post(
new StoredAssignedPermissionEvent(HandlerEventType.CREATE, permission)
);
//J+
}
//~--- get methods ----------------------------------------------------------
@@ -266,49 +203,13 @@ public class DefaultSecuritySystem implements SecuritySystem
* @return
*/
@Override
public List<StoredAssignedPermission> getAllPermissions()
public Collection<PermissionDescriptor> getAvailablePermissions()
{
return getPermissions(null);
}
/**
* Method description
*
*
* @return
*/
@Override
public List<PermissionDescriptor> getAvailablePermissions()
{
assertIsAdmin();
assertHasPermission();
return availablePermissions;
}
/**
* Method description
*
*
* @param id
*
* @return
*/
@Override
public StoredAssignedPermission getPermission(String id)
{
assertIsAdmin();
StoredAssignedPermission sap = null;
AssignedPermission ap = store.get(id);
if (ap != null)
{
sap = new StoredAssignedPermission(id, ap);
}
return sap;
}
/**
* Method description
*
@@ -318,14 +219,13 @@ public class DefaultSecuritySystem implements SecuritySystem
* @return
*/
@Override
public List<StoredAssignedPermission> getPermissions(
Predicate<AssignedPermission> predicate)
public Collection<AssignedPermission> getPermissions(Predicate<AssignedPermission> predicate)
{
Builder<StoredAssignedPermission> permissions = ImmutableList.builder();
Builder<AssignedPermission> permissions = ImmutableSet.builder();
for (Entry<String, AssignedPermission> e : store.getAll().entrySet())
{
if ((predicate == null) || predicate.apply(e.getValue()))
if ((predicate == null) || predicate.test(e.getValue()))
{
permissions.add(new StoredAssignedPermission(e.getKey(), e.getValue()));
}
@@ -340,9 +240,9 @@ public class DefaultSecuritySystem implements SecuritySystem
* Method description
*
*/
private void assertIsAdmin()
private void assertHasPermission()
{
SecurityUtils.getSubject().checkRole(Role.ADMIN);
PermissionPermissions.assign().check();
}
/**
@@ -351,14 +251,15 @@ public class DefaultSecuritySystem implements SecuritySystem
*
* @param predicate
*/
private void deletePermissions(Predicate<AssignedPermission> predicate)
private boolean deletePermissions(Predicate<AssignedPermission> predicate)
{
List<StoredAssignedPermission> permissions = getPermissions(predicate);
for (StoredAssignedPermission permission : permissions)
{
deletePermission(permission);
}
List<Entry<String, AssignedPermission>> toRemove =
store.getAll()
.entrySet()
.stream()
.filter(e -> (predicate == null) || predicate.test(e.getValue())).collect(Collectors.toList());
toRemove.forEach(e -> store.remove(e.getKey()));
return !toRemove.isEmpty();
}
/**
@@ -371,7 +272,7 @@ public class DefaultSecuritySystem implements SecuritySystem
* @return
*/
@SuppressWarnings("unchecked")
private List<PermissionDescriptor> parsePermissionDescriptor(
private static List<PermissionDescriptor> parsePermissionDescriptor(
JAXBContext context, URL descriptorUrl)
{
List<PermissionDescriptor> descriptors = Collections.EMPTY_LIST;
@@ -399,19 +300,20 @@ public class DefaultSecuritySystem implements SecuritySystem
/**
* Method description
*
* @param pluginLoader
*/
private void readAvailablePermissions()
private static ImmutableSet<PermissionDescriptor> readAvailablePermissions(PluginLoader pluginLoader)
{
Builder<PermissionDescriptor> builder = ImmutableList.builder();
ImmutableSet.Builder<PermissionDescriptor> builder = ImmutableSet.builder();
try
{
JAXBContext context =
JAXBContext.newInstance(PermissionDescriptors.class);
// Querying permissions from uberClassLoader returns also the permissions from plugin
Enumeration<URL> descirptorEnum =
ClassLoaders.getContextClassLoader(
DefaultSecuritySystem.class).getResources(PERMISSION_DESCRIPTOR);
pluginLoader.getUberClassLoader().getResources(PERMISSION_DESCRIPTOR);
while (descirptorEnum.hasMoreElements())
{
@@ -432,7 +334,7 @@ public class DefaultSecuritySystem implements SecuritySystem
"could not create jaxb context to read permission descriptors", ex);
}
availablePermissions = builder.build();
return builder.build();
}
/**
@@ -445,7 +347,7 @@ public class DefaultSecuritySystem implements SecuritySystem
{
Preconditions.checkArgument(!Strings.isNullOrEmpty(perm.getName()),
"name is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(perm.getPermission()),
Preconditions.checkArgument(!isNull(perm.getPermission()),
"permission is required");
}
@@ -459,12 +361,6 @@ public class DefaultSecuritySystem implements SecuritySystem
private static class PermissionDescriptors
{
/**
* Constructs ...
*
*/
public PermissionDescriptors() {}
//~--- get methods --------------------------------------------------------
/**
@@ -498,5 +394,5 @@ public class DefaultSecuritySystem implements SecuritySystem
private final ConfigurationEntryStore<AssignedPermission> store;
/** Field description */
private List<PermissionDescriptor> availablePermissions;
private final ImmutableSet<PermissionDescriptor> availablePermissions;
}

View File

@@ -30,12 +30,15 @@
*/
package sonia.scm.security;
import com.google.common.collect.ImmutableSet;
import io.jsonwebtoken.Claims;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static java.util.Optional.ofNullable;
@@ -49,6 +52,8 @@ public final class JwtAccessToken implements AccessToken {
public static final String REFRESHABLE_UNTIL_CLAIM_KEY = "scm-manager.refreshExpiration";
public static final String PARENT_TOKEN_ID_CLAIM_KEY = "scm-manager.parentTokenId";
public static final String GROUPS_CLAIM_KEY = "scm-manager.groups";
private final Claims claims;
private final String compact;
@@ -103,6 +108,16 @@ public final class JwtAccessToken implements AccessToken {
return Optional.ofNullable(claims.get(key));
}
@Override
@SuppressWarnings("unchecked")
public Set<String> getGroups() {
Iterable<String> groups = claims.get(GROUPS_CLAIM_KEY, Iterable.class);
if (groups != null) {
return ImmutableSet.copyOf(groups);
}
return ImmutableSet.of();
}
@Override
public String compact() {
return compact;

View File

@@ -39,9 +39,12 @@ import io.jsonwebtoken.SignatureAlgorithm;
import java.time.Clock;
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
@@ -74,6 +77,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
private Instant refreshExpiration;
private String parentKeyId;
private Scope scope = Scope.empty();
private Set<String> groups = new HashSet<>();
private final Map<String,Object> custom = Maps.newHashMap();
@@ -134,6 +138,12 @@ 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;
@@ -195,6 +205,10 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
claims.setIssuer(issuer);
}
if (!groups.isEmpty()) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups);
}
// sign token and create compact version
String compact = Jwts.builder()
.setClaims(claims)

View File

@@ -0,0 +1,85 @@
package sonia.scm.security;
import sonia.scm.ContextEntry;
import sonia.scm.NotFoundException;
import javax.inject.Inject;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PermissionAssigner {
private final SecuritySystem securitySystem;
@Inject
public PermissionAssigner(SecuritySystem securitySystem) {
this.securitySystem = securitySystem;
}
public Collection<PermissionDescriptor> getAvailablePermissions() {
PermissionPermissions.read().check();
return securitySystem.getAvailablePermissions();
}
public Collection<PermissionDescriptor> readPermissionsForUser(String id) {
return readPermissions(filterForUser(id));
}
public Collection<PermissionDescriptor> readPermissionsForGroup(String id) {
return readPermissions(filterForGroup(id));
}
private Predicate<AssignedPermission> filterForUser(String id) {
return p -> !p.isGroupPermission() && p.getName().equals(id);
}
private Predicate<AssignedPermission> filterForGroup(String id) {
return p -> p.isGroupPermission() && p.getName().equals(id);
}
private Set<PermissionDescriptor> readPermissions(Predicate<AssignedPermission> predicate) {
PermissionPermissions.read().check();
return securitySystem.getPermissions(predicate)
.stream()
.map(AssignedPermission::getPermission)
.collect(Collectors.toSet());
}
public void setPermissionsForUser(String id, Collection<PermissionDescriptor> permissions) {
Collection<AssignedPermission> existingPermissions = securitySystem.getPermissions(filterForUser(id));
adaptPermissions(id, false, permissions, existingPermissions);
}
public void setPermissionsForGroup(String id, Collection<PermissionDescriptor> permissions) {
Collection<AssignedPermission> existingPermissions = securitySystem.getPermissions(filterForGroup(id));
adaptPermissions(id, true, permissions, existingPermissions);
}
private void adaptPermissions(String id, boolean groupPermission, Collection<PermissionDescriptor> permissions, Collection<AssignedPermission> existingPermissions) {
PermissionPermissions.assign().check();
List<AssignedPermission> toRemove = existingPermissions.stream()
.filter(p -> !permissions.contains(p.getPermission()))
.collect(Collectors.toList());
toRemove.forEach(securitySystem::deletePermission);
Collection<PermissionDescriptor> availablePermissions = this.getAvailablePermissions();
permissions.stream()
.filter(permissionExists(availablePermissions, existingPermissions))
.map(p -> new AssignedPermission(id, groupPermission, p))
.filter(p -> !existingPermissions.contains(p))
.forEach(securitySystem::addPermission);
}
private Predicate<PermissionDescriptor> permissionExists(Collection<PermissionDescriptor> availablePermissions, Collection<AssignedPermission> existingPermissions) {
return p -> {
if (!availablePermissions.contains(p) && existingPermissions.stream().map(AssignedPermission::getPermission).noneMatch(e -> e.equals(p))) {
throw NotFoundException.notFound(ContextEntry.ContextBuilder.entity("permission", p.getValue()));
}
return true;
};
}
}

View File

@@ -34,21 +34,22 @@
<permissions>
<permission>
<display-name>All Repository (read)</display-name>
<description>Read access to all repositories</description>
<value>repository:*:READ</value>
<value>repository:read,pull:*</value>
</permission>
<permission>
<display-name>All Repository (write)</display-name>
<description>Write access to all repositories</description>
<value>repository:*:WRITE</value>
<value>repository:read,pull,push:*</value>
</permission>
<permission>
<display-name>All Repository (owner)</display-name>
<description>Owner access to all repositories</description>
<value>repository:*:OWNER</value>
<value>repository:*:*</value>
</permission>
<permission>
<value>repository:create</value>
</permission>
<permission>
<value>user:*</value>
</permission>
<permission>
<value>group:*</value>
</permission>
</permissions>

View File

@@ -0,0 +1,41 @@
{
"permissions": {
"repository": {
"read,pull": {
"*": {
"displayName": "Read all repositories",
"description": "May see and clone all repositories"
}
},
"read,pull,push": {
"*": {
"displayName": "Write all repositories",
"description": "May see, clone and push to all repositories"
}
},
"*": {
"*": {
"displayName": "Own all repositories",
"description": "May see, clone, push to, configure and delete all repositories"
}
},
"create": {
"displayName": "Create repositories",
"description": "May create repositories"
}
},
"user": {
"*": {
"displayName": "Administer users",
"description": "May administer all users"
}
},
"group": {
"*": {
"displayName": "Administer groups",
"description": "May administer all groups"
}
},
"unknown": "Unknown permission"
}
}

View File

@@ -18,6 +18,8 @@ import sonia.scm.api.rest.JSONContextResolver;
import sonia.scm.api.rest.ObjectMapperProvider;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse;
@@ -25,6 +27,7 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import static java.util.Collections.singletonList;
@@ -54,10 +57,15 @@ public class GroupRootResourceTest {
@Mock
private GroupManager groupManager;
@Mock
private PermissionAssigner permissionAssigner;
@InjectMocks
private GroupDtoToGroupMapperImpl dtoToGroupMapper;
@InjectMocks
private GroupToGroupDtoMapperImpl groupToDtoMapper;
@InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
private ArgumentCaptor<Group> groupCaptor = ArgumentCaptor.forClass(Group.class);
@@ -73,7 +81,8 @@ public class GroupRootResourceTest {
GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks);
GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks);
GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper);
GroupPermissionResource groupPermissionResource = new GroupPermissionResource(permissionAssigner, permissionCollectionToDtoMapper);
GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper, groupPermissionResource);
GroupRootResource groupRootResource = new GroupRootResource(Providers.of(groupCollectionResource), Providers.of(groupResource));
dispatcher = createDispatcher(groupRootResource);
@@ -307,6 +316,48 @@ public class GroupRootResourceTest {
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}"));
}
@Test
public void shouldGetPermissionLink() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"permissions\":{"));
}
@Test
public void shouldGetPermissions() throws URISyntaxException {
when(permissionAssigner.readPermissionsForGroup("admin")).thenReturn(singletonList(new PermissionDescriptor("something:*")));
MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"permissions\":[\"something:*\"]"));
}
@Test
public void shouldSetPermissions() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
.put("/" + GroupRootResource.GROUPS_PATH_V2 + "admin/permissions")
.contentType(VndMediaType.PERMISSION_COLLECTION)
.content("{\"permissions\":[\"other:*\"]}".getBytes());
MockHttpResponse response = new MockHttpResponse();
ArgumentCaptor<Collection<PermissionDescriptor>> captor = ArgumentCaptor.forClass(Collection.class);
doNothing().when(permissionAssigner).setPermissionsForGroup(eq("admin"), captor.capture());
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
assertEquals("other:*", captor.getValue().iterator().next().getValue());
}
private Group createDummyGroup() {
Group group = new Group();
group.setName("admin");

View File

@@ -0,0 +1,186 @@
package sonia.scm.api.v2.resources;
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;
import org.junit.jupiter.api.extension.ExtendWith;
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.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
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;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class MeDtoFactoryTest {
private final URI baseUri = URI.create("https://scm.hitchhiker.com/scm/");
@Mock
private UserManager userManager;
@Mock
private Subject subject;
private MeDtoFactory meDtoFactory;
@BeforeEach
void setUpContext() {
ThreadContext.bind(subject);
ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
meDtoFactory = new MeDtoFactory(resourceLinks, userManager);
}
@AfterEach
void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test
void shouldCreateMeDtoFromUser() {
prepareSubject(UserTestData.createTrillian());
MeDto dto = meDtoFactory.create();
assertThat(dto.getName()).isEqualTo("trillian");
assertThat(dto.getDisplayName()).isEqualTo("Tricia McMillan");
assertThat(dto.getMail()).isEqualTo("tricia.mcmillan@hitchhiker.com");
}
@Test
void shouldCreateMeDtoWithEmptyGroups() {
prepareSubject(UserTestData.createTrillian());
MeDto dto = meDtoFactory.create();
assertThat(dto.getGroups()).isEmpty();
}
@Test
void shouldCreateMeDtoWithGroups() {
prepareSubject(UserTestData.createTrillian(), "HeartOfGold", "Puzzle42");
MeDto dto = meDtoFactory.create();
assertThat(dto.getGroups()).containsOnly("HeartOfGold", "Puzzle42");
}
private void prepareSubject(User user, String... groups) {
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;
}
});
}
@Test
void shouldAppendSelfLink() {
prepareSubject(UserTestData.createTrillian());
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/me/");
}
@Test
void shouldAppendDeleteLink() {
prepareSubject(UserTestData.createTrillian());
when(subject.isPermitted("user:delete:trillian")).thenReturn(true);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("delete").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/users/trillian");
}
@Test
void shouldNotAppendDeleteLink() {
prepareSubject(UserTestData.createTrillian());
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
}
@Test
void shouldAppendUpdateLink() {
prepareSubject(UserTestData.createTrillian());
when(subject.isPermitted("user:modify:trillian")).thenReturn(true);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("update").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/users/trillian");
}
@Test
void shouldNotAppendUpdateLink() {
prepareSubject(UserTestData.createTrillian());
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("update")).isNotPresent();
}
@Test
void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = UserTestData.createTrillian();
prepareSubject(user);
when(subject.isPermitted("user:changePassword:trillian")).thenReturn(true);
when(userManager.isTypeDefault(user)).thenReturn(true);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("password").get().getHref()).isEqualTo("https://scm.hitchhiker.com/scm/v2/me/password");
}
@Test
void shouldNotGetPasswordLinkWithoutPermision() {
User user = UserTestData.createTrillian();
prepareSubject(user);
when(userManager.isTypeDefault(user)).thenReturn(true);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
}
@Test
void shouldNotGetPasswordLinkForNonDefaultUsers() {
User user = UserTestData.createTrillian();
prepareSubject(user);
when(subject.isPermitted("user:changePassword:trillian")).thenReturn(true);
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
}
@Test
void shouldAppendLinks() {
prepareSubject(UserTestData.createTrillian());
LinkEnricherRegistry registry = new LinkEnricherRegistry();
meDtoFactory.setRegistry(registry);
registry.register(Me.class, (ctx, appender) -> {
User user = ctx.oneRequireByType(User.class);
appender.appendOne("profile", "http://hitchhiker.com/users/" + user.getName());
});
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian");
}
}

View File

@@ -2,12 +2,14 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -19,7 +21,6 @@ import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.lang.model.util.Types;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
@@ -27,11 +28,7 @@ import java.net.URISyntaxException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@@ -57,7 +54,7 @@ public class MeResourceTest {
private UserManager userManager;
@InjectMocks
private MeToUserDtoMapperImpl userToDtoMapper;
private MeDtoFactory meDtoFactory;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@@ -66,7 +63,7 @@ public class MeResourceTest {
private User originalUser;
@Before
public void prepareEnvironment() throws Exception {
public void prepareEnvironment() {
initMocks(this);
originalUser = createDummyUser("trillian");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
@@ -74,17 +71,18 @@ public class MeResourceTest {
doNothing().when(userManager).delete(userCaptor.capture());
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getDefaultType()).thenReturn("xml");
MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService);
MeResource meResource = new MeResource(meDtoFactory, userManager, passwordService);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
when(scmPathInfoStore.get()).thenReturn(uriInfo);
dispatcher = createDispatcher(meResource);
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldReturnCurrentlyAuthenticatedUser() throws URISyntaxException {
applyUserToSubject(originalUser);
MockHttpRequest request = MockHttpRequest.get("/" + MeResource.ME_PATH_V2);
request.accept(VndMediaType.USER);
request.accept(VndMediaType.ME);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
@@ -95,8 +93,17 @@ public class MeResourceTest {
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}"));
}
private void applyUserToSubject(User user) {
// use spy here to keep applied permissions from ShiroRule
Subject subject = spy(SecurityUtils.getSubject());
PrincipalCollection collection = mock(PrincipalCollection.class);
when(collection.getPrimaryPrincipal()).thenReturn(user.getName());
when(subject.getPrincipals()).thenReturn(collection);
when(collection.oneByType(User.class)).thenReturn(user);
shiro.setSubject(subject);
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String encryptedNewPassword = "encrypted123";
@@ -124,7 +131,6 @@ public class MeResourceTest {
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnMissingOldPassword() throws Exception {
originalUser.setType("not an xml type");
String newPassword = "pwd123";
@@ -141,7 +147,6 @@ public class MeResourceTest {
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnMissingEmptyPassword() throws Exception {
String newPassword = "pwd123";
String oldPassword = "";
@@ -158,7 +163,6 @@ public class MeResourceTest {
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldMapExceptionFromManager() throws Exception {
String newPassword = "pwd123";
String oldPassword = "secret";

View File

@@ -1,151 +0,0 @@
package sonia.scm.api.v2.resources;
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.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserTestData;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class MeToUserDtoMapperTest {
private final URI baseUri = URI.create("http://example.com/base/");
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks
private MeToUserDtoMapperImpl mapper;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
private URI expectedBaseUri;
private URI expectedUserBaseUri;
@Before
public void init() {
initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(MeResource.ME_PATH_V2 + "/");
expectedUserBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind();
ThreadContext.bind(subject);
}
@After
public void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test
public void shouldMapTheUpdateLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no update link", userDto.getLinks().getLinkBy("update").isPresent());
}
@Test
public void shouldMapTheSelfLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.toString(), userDto.getLinks().getLinkBy("self").get().getHref());
}
@Test
public void shouldMapTheDeleteLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:delete:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref());
when(subject.isPermitted("user:delete:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no delete link", userDto.getLinks().getLinkBy("delete").isPresent());
}
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).as("hide password for the me resource").isBlank();
}
@Test
public void shouldAppendLinks() {
LinkEnricherRegistry registry = new LinkEnricherRegistry();
registry.register(Me.class, (ctx, appender) -> {
User user = ctx.oneRequireByType(User.class);
appender.appendOne("profile", "http://hitchhiker.com/users/" + user.getName());
});
mapper.setRegistry(registry);
User trillian = UserTestData.createTrillian();
UserDto dto = mapper.map(trillian);
assertEquals("http://hitchhiker.com/users/trillian", dto.getLinks().getLinkBy("profile").get().getHref());
}
private User createDefaultUser() {
User user = new User();
user.setName("abc");
user.setCreationDate(1L);
return user;
}
}

View File

@@ -29,7 +29,7 @@ import org.junit.jupiter.api.TestFactory;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
@@ -58,7 +58,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX;
import static sonia.scm.api.v2.resources.RepositoryPermissionDto.GROUP_PREFIX;
@Slf4j
@SubjectAware(
@@ -77,14 +77,14 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/";
private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME;
private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }";
private static final ArrayList<Permission> TEST_PERMISSIONS = Lists
private static final ArrayList<RepositoryPermission> TEST_PERMISSIONS = Lists
.newArrayList(
new Permission("user_write", PermissionType.WRITE, false),
new Permission("user_read", PermissionType.READ, false),
new Permission("user_owner", PermissionType.OWNER, false),
new Permission("group_read", PermissionType.READ, true),
new Permission("group_write", PermissionType.WRITE, true),
new Permission("group_owner", PermissionType.OWNER, true)
new RepositoryPermission("user_write", PermissionType.WRITE, false),
new RepositoryPermission("user_read", PermissionType.READ, false),
new RepositoryPermission("user_owner", PermissionType.OWNER, false),
new RepositoryPermission("group_read", PermissionType.READ, true),
new RepositoryPermission("group_write", PermissionType.WRITE, true),
new RepositoryPermission("group_owner", PermissionType.OWNER, true)
);
private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest()
.description("GET all permissions")
@@ -121,12 +121,12 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@InjectMocks
private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper;
private RepositoryPermissionToRepositoryPermissionDtoMapperImpl permissionToPermissionDtoMapper;
@InjectMocks
private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper;
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
private RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
private PermissionRootResource permissionRootResource;
@@ -137,8 +137,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Before
public void prepareEnvironment() {
initMocks(this);
permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager);
repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks);
permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, resourceLinks, repositoryManager);
super.permissionRootResource = Providers.of(permissionRootResource);
dispatcher = createDispatcher(getRepositoryRootResource());
subjectThreadState.bind();
@@ -207,7 +207,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test
public void shouldGetPermissionByName() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_READ);
Permission expectedPermission = TEST_PERMISSIONS.get(0);
RepositoryPermission expectedPermission = TEST_PERMISSIONS.get(0);
assertExpectedRequest(requestGETPermission
.expectedResponseStatus(200)
.path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName())
@@ -215,8 +215,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
String body = response.getContentAsString();
ObjectMapper mapper = new ObjectMapper();
try {
PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class);
assertThat(actualPermissionDto)
RepositoryPermissionDto actualRepositoryPermissionDto = mapper.readValue(body, RepositoryPermissionDto.class);
assertThat(actualRepositoryPermissionDto)
.as("response payload match permission object model")
.isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission, PERMISSION_READ))
;
@@ -259,10 +259,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test
public void shouldGetCreatedPermissions() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true);
ArrayList<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", PermissionType.WRITE, true);
ArrayList<RepositoryPermission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
permissions.add(newPermission);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(permissions);
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(permissions);
assertExpectedRequest(requestPOSTPermission
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}")
.expectedResponseStatus(201)
@@ -276,7 +276,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test
public void shouldNotAddExistingPermission() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
Permission newPermission = TEST_PERMISSIONS.get(0);
RepositoryPermission newPermission = TEST_PERMISSIONS.get(0);
assertExpectedRequest(requestPOSTPermission
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : false}")
.expectedResponseStatus(409)
@@ -286,10 +286,10 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test
public void shouldGetUpdatedPermissions() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
Permission modifiedPermission = TEST_PERMISSIONS.get(0);
RepositoryPermission modifiedPermission = TEST_PERMISSIONS.get(0);
// modify the type to owner
modifiedPermission.setType(PermissionType.OWNER);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS);
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS);
assertExpectedRequest(requestPUTPermission
.content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}")
.path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName())
@@ -305,8 +305,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test
public void shouldDeletePermissions() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
Permission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size()));
RepositoryPermission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size()));
assertExpectedRequest(requestDELETEPermission
.path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName())
.expectedResponseStatus(204)
@@ -320,8 +320,8 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
@Test
public void deletingNotExistingPermissionShouldProcess() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_OWNER);
Permission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size()));
RepositoryPermission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<RepositoryPermission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size()));
assertExpectedRequest(requestDELETEPermission
.path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName())
.expectedResponseStatus(204)
@@ -340,7 +340,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
assertGettingExpectedPermissions(expectedPermissions, PERMISSION_READ);
}
private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions, String userPermission) throws URISyntaxException {
private void assertGettingExpectedPermissions(ImmutableList<RepositoryPermission> expectedPermissions, String userPermission) throws URISyntaxException {
assertExpectedRequest(requestGETAllPermissions
.expectedResponseStatus(200)
.responseValidator((response) -> {
@@ -349,16 +349,16 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
try {
HalRepresentation halRepresentation = mapper.readValue(body, HalRepresentation.class);
List<HalRepresentation> actualPermissionDtos = halRepresentation.getEmbedded().getItemsBy("permissions", HalRepresentation.class);
List<PermissionDto> permissionDtoStream = actualPermissionDtos.stream()
List<RepositoryPermissionDto> repositoryPermissionDtoStream = actualPermissionDtos.stream()
.map(hal -> {
PermissionDto result = new PermissionDto();
RepositoryPermissionDto result = new RepositoryPermissionDto();
result.setName(hal.getAttribute("name").asText());
result.setType(hal.getAttribute("type").asText());
result.setGroupPermission(hal.getAttribute("groupPermission").asBoolean());
result.add(hal.getLinks());
return result;
}).collect(Collectors.toList());
assertThat(permissionDtoStream)
assertThat(repositoryPermissionDtoStream)
.as("response payload match permission object models")
.hasSize(expectedPermissions.size())
.usingRecursiveFieldByFieldElementComparator()
@@ -371,15 +371,15 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
);
}
private PermissionDto[] getExpectedPermissionDtos(ArrayList<Permission> permissions, String userPermission) {
private RepositoryPermissionDto[] getExpectedPermissionDtos(ArrayList<RepositoryPermission> permissions, String userPermission) {
return permissions
.stream()
.map(p -> getExpectedPermissionDto(p, userPermission))
.toArray(PermissionDto[]::new);
.toArray(RepositoryPermissionDto[]::new);
}
private PermissionDto getExpectedPermissionDto(Permission permission, String userPermission) {
PermissionDto result = new PermissionDto();
private RepositoryPermissionDto getExpectedPermissionDto(RepositoryPermission permission, String userPermission) {
RepositoryPermissionDto result = new RepositoryPermissionDto();
result.setName(permission.getName());
result.setGroupPermission(permission.isGroupPermission());
result.setType(permission.getType().name());
@@ -411,7 +411,7 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
return mockRepository;
}
private void createUserWithRepositoryAndPermissions(ArrayList<Permission> permissions, String userPermission) {
private void createUserWithRepositoryAndPermissions(ArrayList<RepositoryPermission> permissions, String userPermission) {
createUserWithRepository(userPermission).setPermissions(permissions);
}

View File

@@ -7,7 +7,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
@@ -19,7 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@SubjectAware(
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
public class PermissionToPermissionDtoMapperTest {
public class RepositoryPermissionToRepositoryPermissionDtoMapperTest {
@Rule
public ShiroRule shiro = new ShiroRule();
@@ -30,31 +30,31 @@ public class PermissionToPermissionDtoMapperTest {
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@InjectMocks
PermissionToPermissionDtoMapperImpl mapper;
RepositoryPermissionToRepositoryPermissionDtoMapperImpl mapper;
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldMapGroupPermissionCorrectly() {
Repository repository = getDummyRepository();
Permission permission = new Permission("42", PermissionType.OWNER, true);
RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, true);
PermissionDto permissionDto = mapper.map(permission, repository);
RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository);
assertThat(permissionDto.getLinks().getLinkBy("self").isPresent()).isTrue();
assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).contains("@42");
assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").isPresent()).isTrue();
assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").get().getHref()).contains("@42");
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldMapNonGroupPermissionCorrectly() {
Repository repository = getDummyRepository();
Permission permission = new Permission("42", PermissionType.OWNER, false);
RepositoryPermission permission = new RepositoryPermission("42", PermissionType.OWNER, false);
PermissionDto permissionDto = mapper.map(permission, repository);
RepositoryPermissionDto repositoryPermissionDto = mapper.map(permission, repository);
assertThat(permissionDto.getLinks().getLinkBy("self").isPresent()).isTrue();
assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).contains("42");
assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).doesNotContain("@");
assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").isPresent()).isTrue();
assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").get().getHref()).contains("42");
assertThat(repositoryPermissionDto.getLinks().getLinkBy("self").get().getHref()).doesNotContain("@");
}
private Repository getDummyRepository() {

View File

@@ -18,7 +18,7 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.PageResult;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryIsNotArchivedException;
@@ -302,7 +302,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Test
public void shouldNotOverwriteExistingPermissionsOnUpdate() throws Exception {
Repository existingRepository = mockRepository("space", "repo");
existingRepository.setPermissions(singletonList(new Permission("user", PermissionType.READ)));
existingRepository.setPermissions(singletonList(new RepositoryPermission("user", PermissionType.READ)));
URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json");
byte[] repository = Resources.toByteArray(url);

View File

@@ -10,7 +10,7 @@ import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.repository.HealthCheckFailure;
import sonia.scm.repository.Permission;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.Command;
@@ -238,7 +238,7 @@ public class RepositoryToRepositoryDtoMapperTest {
repository.setId("1");
repository.setCreationDate(System.currentTimeMillis());
repository.setHealthCheckFailures(singletonList(new HealthCheckFailure("1", "summary", "url", "failure")));
repository.setPermissions(singletonList(new Permission("permission", PermissionType.READ)));
repository.setPermissions(singletonList(new RepositoryPermission("permission", PermissionType.READ)));
return repository;
}

View File

@@ -16,9 +16,11 @@ public class ResourceLinksMock {
when(resourceLinks.user()).thenReturn(userLinks);
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks));
when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo));
when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(uriInfo));
when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo));
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));
when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(uriInfo));
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo));
when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo));
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo));
@@ -27,7 +29,7 @@ public class ResourceLinksMock {
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(uriInfo));
when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo));
when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo));
when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(uriInfo));
when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo));
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo));
when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo));
@@ -39,6 +41,7 @@ public class ResourceLinksMock {
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
return resourceLinks;
}

View File

@@ -14,9 +14,12 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import sonia.scm.ContextEntry;
import sonia.scm.NotFoundException;
import sonia.scm.PageResult;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.ChangePasswordNotAllowedException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
@@ -26,6 +29,7 @@ import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
@@ -59,10 +63,14 @@ public class UserRootResourceTest {
private PasswordService passwordService;
@Mock
private UserManager userManager;
@Mock
private PermissionAssigner permissionAssigner;
@InjectMocks
private UserDtoToUserMapperImpl dtoToUserMapper;
@InjectMocks
private UserToUserDtoMapperImpl userToDtoMapper;
@InjectMocks
private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
private User originalUser;
@@ -80,7 +88,8 @@ public class UserRootResourceTest {
UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks);
UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper,
userCollectionToDtoMapper, resourceLinks, passwordService);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService);
UserPermissionResource userPermissionResource = new UserPermissionResource(permissionAssigner, permissionCollectionToDtoMapper);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService, userPermissionResource);
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
Providers.of(userResource));
@@ -330,8 +339,6 @@ public class UserRootResourceTest {
dispatcher.invoke(request, response);
System.out.println(response.getContentAsString());
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/?page=0"));
@@ -348,8 +355,6 @@ public class UserRootResourceTest {
dispatcher.invoke(request, response);
System.out.println(response.getContentAsString());
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/?page=1"));
@@ -359,6 +364,48 @@ public class UserRootResourceTest {
assertTrue(response.getContentAsString().contains("\"last\":{\"href\":\"/v2/users/?page=2"));
}
@Test
public void shouldGetPermissionLink() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"permissions\":{"));
}
@Test
public void shouldGetPermissions() throws URISyntaxException {
when(permissionAssigner.readPermissionsForUser("Neo")).thenReturn(singletonList(new PermissionDescriptor("something:*")));
MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"permissions\":[\"something:*\"]"));
}
@Test
public void shouldSetPermissions() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/permissions")
.contentType(VndMediaType.PERMISSION_COLLECTION)
.content("{\"permissions\":[\"other:*\"]}".getBytes());
MockHttpResponse response = new MockHttpResponse();
ArgumentCaptor<Collection<PermissionDescriptor>> captor = ArgumentCaptor.forClass(Collection.class);
doNothing().when(permissionAssigner).setPermissionsForUser(eq("Neo"), captor.capture());
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
assertEquals("other:*", captor.getValue().iterator().next().getValue());
}
private PageResult<User> createSingletonPageResult(int overallCount) {
return new PageResult<>(singletonList(createDummyUser("Neo")), overallCount);
}

View File

@@ -184,7 +184,7 @@ private long calculateAverage(List<Long> times) {
private Repository createTestRepository(int number) {
Repository repository = new Repository(keyGenerator.createKey(), REPOSITORY_TYPE, "namespace", "repo-" + number);
repository.addPermission(new Permission("trillian", PermissionType.READ));
repository.addPermission(new RepositoryPermission("trillian", PermissionType.READ));
return repository;
}

View File

@@ -43,6 +43,7 @@ import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
@@ -173,10 +174,10 @@ public class AuthorizationChangedEventProducerTest {
{
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
repositoryModified.setName("test123");
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test")));
Repository repository = RepositoryTestData.createHeartOfGold();
repository.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
repository.setPermissions(Lists.newArrayList(new RepositoryPermission("test")));
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.BEFORE_CREATE, repositoryModified, repository));
assertEventIsNotFired();
@@ -184,18 +185,18 @@ public class AuthorizationChangedEventProducerTest {
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired();
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test")));
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test")));
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertEventIsNotFired();
repositoryModified.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("test123")));
repositoryModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123")));
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired();
resetStoredEvent();
repositoryModified.setPermissions(
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.READ, true))
Lists.newArrayList(new RepositoryPermission("test", PermissionType.READ, true))
);
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired();
@@ -203,7 +204,7 @@ public class AuthorizationChangedEventProducerTest {
resetStoredEvent();
repositoryModified.setPermissions(
Lists.newArrayList(new sonia.scm.repository.Permission("test", PermissionType.WRITE))
Lists.newArrayList(new RepositoryPermission("test", PermissionType.WRITE))
);
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
assertGlobalEventIsFired();
@@ -214,7 +215,7 @@ public class AuthorizationChangedEventProducerTest {
}
/**
* Tests {@link AuthorizationChangedEventProducer#onEvent(sonia.scm.security.StoredAssignedPermissionEvent)}.
* Tests {@link AuthorizationChangedEventProducer#onEvent(AssignedPermissionEvent)}.
*/
@Test
public void testOnStoredAssignedPermissionEvent()
@@ -222,10 +223,10 @@ public class AuthorizationChangedEventProducerTest {
StoredAssignedPermission groupPermission = new StoredAssignedPermission(
"123", new AssignedPermission("_authenticated", true, "repository:read:*")
);
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission));
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, groupPermission));
assertEventIsNotFired();
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, groupPermission));
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.CREATE, groupPermission));
assertGlobalEventIsFired();
resetStoredEvent();
@@ -233,12 +234,12 @@ public class AuthorizationChangedEventProducerTest {
StoredAssignedPermission userPermission = new StoredAssignedPermission(
"123", new AssignedPermission("trillian", false, "repository:read:*")
);
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission));
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.BEFORE_CREATE, userPermission));
assertEventIsNotFired();
resetStoredEvent();
producer.onEvent(new StoredAssignedPermissionEvent(HandlerEventType.CREATE, userPermission));
producer.onEvent(new AssignedPermissionEvent(HandlerEventType.CREATE, userPermission));
assertUserEventIsFired("trillian");
}

View File

@@ -31,11 +31,15 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableSet;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
@@ -43,6 +47,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import java.util.HashMap;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -65,6 +70,9 @@ class BearerRealmTest {
@Mock
private DAORealmHelper realmHelper;
@Mock
private DAORealmHelper.AuthenticationInfoBuilder builder;
@Mock
private AccessTokenResolver accessTokenResolver;
@@ -84,15 +92,19 @@ class BearerRealmTest {
void shouldDoGetAuthentication() {
BearerToken bearerToken = BearerToken.valueOf("__bearer__");
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.getSubject()).thenReturn("trillian");
when(accessToken.getClaims()).thenReturn(new HashMap<>());
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);
// we have to use answer, because we could not mock the result of Scopes
when(realmHelper.getAuthenticationInfo(
anyString(), anyString(), any(Scope.class)
)).thenAnswer(createAnswer("trillian", "__bearer__", true));
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);
AuthenticationInfo result = realm.doGetAuthenticationInfo(bearerToken);
assertThat(result).isSameAs(authenticationInfo);
@@ -102,25 +114,4 @@ class BearerRealmTest {
void shouldThrowIllegalArgumentExceptionForWrongTypeOfToken() {
assertThrows(IllegalArgumentException.class, () -> realm.doGetAuthenticationInfo(new UsernamePasswordToken()));
}
private Answer<AuthenticationInfo> createAnswer(String expectedSubject, String expectedCredentials, boolean scopeEmpty) {
return (iom) -> {
String subject = iom.getArgument(0);
assertThat(subject).isEqualTo(expectedSubject);
String credentials = iom.getArgument(1);
assertThat(credentials).isEqualTo(expectedCredentials);
Scope scope = iom.getArgument(2);
assertThat(scope.isEmpty()).isEqualTo(scopeEmpty);
return authenticationInfo;
};
}
private class MyAnswer implements Answer<AuthenticationInfo> {
@Override
public AuthenticationInfo answer(InvocationOnMock invocationOnMock) throws Throwable {
return null;
}
}
}

View File

@@ -33,7 +33,7 @@ package sonia.scm.security;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.base.Predicate;
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,10 +49,12 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupNames;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserTestData;
@@ -76,6 +78,8 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class DefaultAuthorizationCollectorTest {
private ScmConfiguration configuration;
@Mock
private Cache cache;
@@ -99,8 +103,38 @@ public class DefaultAuthorizationCollectorTest {
@Before
public void setUp(){
when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache);
configuration = new ScmConfiguration();
collector = new DefaultAuthorizationCollector(configuration, cacheManager, repositoryDAO, securitySystem);
}
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem);
@Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini"
)
public void shouldGetAdminPrivilegedByConfiguration() {
configuration.setAdminUsers(ImmutableSet.of("trillian"));
authenticate(UserTestData.createTrillian(), "main");
AuthorizationInfo authInfo = collector.collect();
assertIsAdmin(authInfo);
}
private void assertIsAdmin(AuthorizationInfo authInfo) {
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER, Role.ADMIN));
assertThat(authInfo.getObjectPermissions(), nullValue());
assertThat(authInfo.getStringPermissions(), Matchers.contains("*"));
}
@Test
@SubjectAware(
configuration = "classpath:sonia/scm/shiro-001.ini"
)
public void shouldGetAdminPrivilegedByGroupConfiguration() {
configuration.setAdminGroups(ImmutableSet.of("heartOfGold"));
authenticate(UserTestData.createTrillian(), "heartOfGold");
AuthorizationInfo authInfo = collector.collect();
assertIsAdmin(authInfo);
}
/**
@@ -142,7 +176,7 @@ public class DefaultAuthorizationCollectorTest {
public void testCollectWithCache() {
authenticate(UserTestData.createTrillian(), "main");
AuthorizationInfo authInfo = collector.collect();
collector.collect();
verify(cache).put(any(), any());
}
@@ -176,9 +210,7 @@ public class DefaultAuthorizationCollectorTest {
authenticate(trillian, "main");
AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER, Role.ADMIN));
assertThat(authInfo.getObjectPermissions(), nullValue());
assertThat(authInfo.getStringPermissions(), Matchers.contains("*"));
assertIsAdmin(authInfo);
}
/**
@@ -193,10 +225,10 @@ public class DefaultAuthorizationCollectorTest {
authenticate(UserTestData.createTrillian(), group);
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
heartOfGold.setId("one");
heartOfGold.setPermissions(Lists.newArrayList(new sonia.scm.repository.Permission("trillian")));
heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian")));
Repository puzzle42 = RepositoryTestData.create42Puzzle();
puzzle42.setId("two");
sonia.scm.repository.Permission permission = new sonia.scm.repository.Permission(group, PermissionType.WRITE, true);
RepositoryPermission permission = new RepositoryPermission(group, PermissionType.WRITE, true);
puzzle42.setPermissions(Lists.newArrayList(permission));
when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42));
@@ -219,7 +251,7 @@ public class DefaultAuthorizationCollectorTest {
StoredAssignedPermission p1 = new StoredAssignedPermission("one", new AssignedPermission("one", "one:one"));
StoredAssignedPermission p2 = new StoredAssignedPermission("two", new AssignedPermission("two", "two:two"));
when(securitySystem.getPermissions(Mockito.any(Predicate.class))).thenReturn(Lists.newArrayList(p1, p2));
when(securitySystem.getPermissions(any())).thenReturn(Lists.newArrayList(p1, p2));
// execute and assert
AuthorizationInfo authInfo = collector.collect();
@@ -238,7 +270,7 @@ public class DefaultAuthorizationCollectorTest {
}
/**
* Tests {@link AuthorizationCollector#invalidateCache(sonia.scm.security.AuthorizationChangedEvent)}.
* Tests {@link DefaultAuthorizationCollector#invalidateCache(sonia.scm.security.AuthorizationChangedEvent)}.
*/
@Test
public void testInvalidateCache() {
@@ -246,7 +278,7 @@ public class DefaultAuthorizationCollectorTest {
verify(cache).clear();
collector.invalidateCache(AuthorizationChangedEvent.createForUser("dent"));
verify(cache).removeAll(Mockito.any(Predicate.class));
verify(cache).removeAll(any());
}
}

View File

@@ -32,28 +32,28 @@
package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Predicate;
import com.google.common.base.Objects;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import sonia.scm.AbstractTestBase;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
import sonia.scm.util.ClassLoaders;
import sonia.scm.util.MockUtil;
import static org.hamcrest.Matchers.*;
import java.util.Collection;
import static org.junit.Assert.*;
//~--- JDK imports ------------------------------------------------------------
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
/**
*
@@ -62,6 +62,12 @@ import java.util.List;
public class DefaultSecuritySystemTest extends AbstractTestBase
{
private JAXBConfigurationEntryStoreFactory jaxbConfigurationEntryStoreFactory;
private PluginLoader pluginLoader;
@InjectMocks
private DefaultSecuritySystem securitySystem;
/**
* Method description
*
@@ -69,12 +75,12 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
@Before
public void createSecuritySystem()
{
JAXBConfigurationEntryStoreFactory factory =
new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() );
jaxbConfigurationEntryStoreFactory =
spy(new JAXBConfigurationEntryStoreFactory(contextProvider , repositoryLocationResolver, new UUIDKeyGenerator() ) {});
pluginLoader = mock(PluginLoader.class);
when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class));
securitySystem = new DefaultSecuritySystem(factory);
// ScmEventBus.getInstance().register(listener);
MockitoAnnotations.initMocks(this);
}
/**
@@ -86,11 +92,10 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
AssignedPermission sap = createPermission("trillian", false, "repository:*:READ");
assertEquals("trillian", sap.getName());
assertEquals("repository:*:READ", sap.getPermission());
assertEquals("repository:*:READ", sap.getPermission().getValue());
assertEquals(false, sap.isGroupPermission());
}
@@ -103,10 +108,10 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
List<PermissionDescriptor> list = securitySystem.getAvailablePermissions();
Collection<PermissionDescriptor> list = securitySystem.getAvailablePermissions();
assertNotNull(list);
assertThat(list.size(), greaterThan(0));
assertThat(list).isNotEmpty();
}
/**
@@ -118,12 +123,12 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
AssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
securitySystem.deletePermission(sap);
assertNull(securitySystem.getPermission(sap.getId()));
assertThat(securitySystem.getPermissions(p -> p.getName().equals("trillian"))).isEmpty();
}
/**
@@ -135,17 +140,17 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
StoredAssignedPermission trillian = createPermission("trillian", false,
AssignedPermission trillian = createPermission("trillian", false,
"repository:*:READ");
StoredAssignedPermission dent = createPermission("dent", false,
AssignedPermission dent = createPermission("dent", false,
"repository:*:READ");
StoredAssignedPermission marvin = createPermission("marvin", false,
AssignedPermission marvin = createPermission("marvin", false,
"repository:*:READ");
List<StoredAssignedPermission> all = securitySystem.getAllPermissions();
Collection<AssignedPermission> all = securitySystem.getPermissions(p -> true);
assertEquals(3, all.size());
assertThat(all, containsInAnyOrder(trillian, dent, marvin));
assertThat(all).contains(trillian, dent, marvin);
}
/**
@@ -157,13 +162,12 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
AssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
StoredAssignedPermission other = securitySystem.getPermission(sap.getId());
Collection<AssignedPermission> other = securitySystem.getPermissions(p -> p.getName().equals("trillian"));
assertEquals(sap.getId(), other.getId());
assertEquals(sap, other);
assertThat(other).containsExactly(sap);
}
/**
@@ -175,49 +179,19 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
StoredAssignedPermission trillian = createPermission("trillian", false,
AssignedPermission trillian = createPermission("trillian", false,
"repository:*:READ");
StoredAssignedPermission dent = createPermission("dent", false,
AssignedPermission dent = createPermission("dent", false,
"repository:*:READ");
createPermission("hitchhiker", true, "repository:*:READ");
List<StoredAssignedPermission> filtered =
securitySystem.getPermissions(new Predicate<AssignedPermission>()
{
Collection<AssignedPermission> filtered =
securitySystem.getPermissions(p -> !p.isGroupPermission());
@Override
public boolean apply(AssignedPermission input)
{
return !input.isGroupPermission();
}
});
assertEquals(2, filtered.size());
assertThat(filtered, containsInAnyOrder(trillian, dent));
}
/**
* Method description
*
*/
@Test
public void testModifyPermission()
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
StoredAssignedPermission modified =
new StoredAssignedPermission(sap.getId(),
new AssignedPermission("trillian", "repository:*:WRITE"));
securitySystem.modifyPermission(modified);
sap = securitySystem.getPermission(modified.getId());
assertEquals(modified.getId(), sap.getId());
assertEquals(modified, sap);
assertThat(filtered)
.hasSize(2)
.contains(trillian, dent);
}
/**
@@ -240,46 +214,13 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
AssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
setUserSubject();
securitySystem.deletePermission(sap);
}
/**
* Method description
*
*/
@Test(expected = UnauthorizedException.class)
public void testUnauthorizedGetPermission()
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
setUserSubject();
securitySystem.getPermission(sap.getId());
}
/**
* Method description
*
*/
@Test(expected = UnauthorizedException.class)
public void testUnauthorizedModifyPermission()
{
setAdminSubject();
StoredAssignedPermission sap = createPermission("trillian", false,
"repository:*:READ");
setUserSubject();
securitySystem.modifyPermission(sap);
}
/**
* Method description
*
@@ -290,17 +231,16 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
*
* @return
*/
private StoredAssignedPermission createPermission(String name,
private AssignedPermission createPermission(String name,
boolean groupPermission, String value)
{
AssignedPermission ap = new AssignedPermission(name, groupPermission,
value);
StoredAssignedPermission sap = securitySystem.addPermission(ap);
securitySystem.addPermission(ap);
assertNotNull(sap);
assertNotNull(sap.getId());
return sap;
return securitySystem.getPermissions(permission -> Objects.equal(name, permission.getName())
&& Objects.equal(groupPermission, permission.isGroupPermission())
&& Objects.equal(value, permission.getPermission().getValue())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found"));
}
//~--- set methods ----------------------------------------------------------
@@ -325,9 +265,4 @@ public class DefaultSecuritySystemTest extends AbstractTestBase
setSubject(MockUtil.createUserSubject(sm));
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private DefaultSecuritySystem securitySystem;
}

Some files were not shown because too many files have changed in this diff Show More