diff --git a/scm-core/src/main/java/sonia/scm/ScmState.java b/scm-core/src/main/java/sonia/scm/ScmState.java index cd36aa707c..84fb07a1f7 100644 --- a/scm-core/src/main/java/sonia/scm/ScmState.java +++ b/scm-core/src/main/java/sonia/scm/ScmState.java @@ -85,7 +85,7 @@ public final class ScmState public ScmState(String version, User user, Collection groups, String token, Collection repositoryTypes, String defaultUserType, ScmClientConfig clientConfig, List assignedPermission, - List availablePermissions) + Collection availablePermissions) { this.version = version; this.user = user; @@ -119,7 +119,7 @@ public final class ScmState * @return available global permissions * @since 1.31 */ - public List getAvailablePermissions() + public Collection getAvailablePermissions() { return availablePermissions; } @@ -232,7 +232,7 @@ public final class ScmState * Avaliable global permission * @since 1.31 */ - private List availablePermissions; + private Collection availablePermissions; /** Field description */ private ScmClientConfig clientConfig; diff --git a/scm-core/src/main/java/sonia/scm/ScmStateFactory.java b/scm-core/src/main/java/sonia/scm/ScmStateFactory.java index 41502a6850..e839a0ddcc 100644 --- a/scm-core/src/main/java/sonia/scm/ScmStateFactory.java +++ b/scm-core/src/main/java/sonia/scm/ScmStateFactory.java @@ -134,7 +134,7 @@ public final class ScmStateFactory User user = collection.oneByType(User.class); GroupNames groups = collection.oneByType(GroupNames.class); - List ap = Collections.EMPTY_LIST; + Collection ap = Collections.EMPTY_LIST; if (subject.hasRole(Role.ADMIN)) { @@ -150,7 +150,7 @@ public final class ScmStateFactory private ScmState createState(User user, Collection groups, String token, List assignedPermissions, - List availablePermissions) + Collection availablePermissions) { User u = user.clone(); diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index fd8f07df8d..622eed6ad6 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -81,7 +81,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per private Long lastModified; private String namespace; private String name; - private final Set permissions = new HashSet<>(); + private final Set 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 getPermissions() { + public Collection getPermissions() { return Collections.unmodifiableCollection(permissions); } @@ -297,16 +297,16 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per this.name = name; } - public void setPermissions(Collection permissions) { + public void setPermissions(Collection 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); } diff --git a/scm-core/src/main/java/sonia/scm/repository/Permission.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java similarity index 84% rename from scm-core/src/main/java/sonia/scm/repository/Permission.java rename to scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java index 20cdc83cef..0aff771fce 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Permission.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermission.java @@ -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() diff --git a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java index 56b8d04a41..c98d81f8ba 100644 --- a/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermission.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java similarity index 90% rename from scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java rename to scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java index ad93bf25a9..9ebea488a5 100644 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermissionEvent.java +++ b/scm-core/src/main/java/sonia/scm/security/AssignedPermissionEvent.java @@ -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; diff --git a/scm-core/src/main/java/sonia/scm/security/Permission.java b/scm-core/src/main/java/sonia/scm/security/Permission.java new file mode 100644 index 0000000000..1b7c34f740 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/Permission.java @@ -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 { +} diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java index 20d95958a1..a005583256 100644 --- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java @@ -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; } diff --git a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java index f43afcb7f2..174b64f5e6 100644 --- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java +++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java @@ -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 getAllPermissions(); - /** * Return all available permissions. * * * @return available permissions */ - public List 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 getAvailablePermissions(); /** * Returns all stored permissions which are matched by the given @@ -120,6 +81,5 @@ public interface SecuritySystem * * @return filtered permissions */ - public List getPermissions( - Predicate predicate); + Collection getPermissions(Predicate predicate); } diff --git a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java index 903f86df90..4b2e46b665 100644 --- a/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java +++ b/scm-core/src/main/java/sonia/scm/security/StoredAssignedPermission.java @@ -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; diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 2a409482c8..8596bab754 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -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; diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java index ae84f9d768..3d9fa3f283 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationEntryStoreTest.java @@ -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 diff --git a/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..da11b5164a --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ + + + + + + configuration:read:git + + + configuration:write:git + + + diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index a84f44726f..68194e3a43 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -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" + } + } + } } } diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..205e8cc770 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ + + + + + + configuration:read:hg + + + configuration:write:hg + + + diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json index 504e7d3815..48f3bd57d6 100644 --- a/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-hg-plugin/src/main/resources/locales/en/plugins.json @@ -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" + } + } + } } } diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml new file mode 100644 index 0000000000..3da3526f93 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/resources/META-INF/scm/permissions.xml @@ -0,0 +1,43 @@ + + + + + + configuration:read:svn + + + configuration:write:svn + + + diff --git a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json index a446bb6f26..2a363c77cd 100644 --- a/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-svn-plugin/src/main/resources/locales/en/plugins.json @@ -1,7 +1,7 @@ { "scm-svn-plugin": { "information": { - "checkout" : "Checkout repository" + "checkout": "Checkout repository" }, "config": { "link": "Subversion", @@ -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" + } + } + } } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java new file mode 100644 index 0000000000..40124dd717 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStore.java @@ -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 implements ConfigurationEntryStore { + + private final Map values = new HashMap<>(); + + @Override + public Collection getMatchingValues(Predicate 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 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); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java new file mode 100644 index 0000000000..48e60684b6 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationEntryStoreFactory.java @@ -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 ConfigurationEntryStore getStore(TypedStoreParameters storeParameters) { + if (store != null) { + return store; + } + return new InMemoryConfigurationEntryStore<>(); + } +} diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json index f1ebb95e18..3fbe088029 100644 --- a/scm-ui/public/locales/en/groups.json +++ b/scm-ui/public/locales/en/groups.json @@ -63,5 +63,8 @@ "submit": "Yes", "cancel": "No" } + }, + "set-permissions-button": { + "label": "Set permissions" } } diff --git a/scm-ui/public/locales/en/permissions.json b/scm-ui/public/locales/en/permissions.json new file mode 100644 index 0000000000..52059db60a --- /dev/null +++ b/scm-ui/public/locales/en/permissions.json @@ -0,0 +1,8 @@ +{ + "form": { + "submit-button": { + "label": "Set permissions" + }, + "set-permissions-successful": "Permissions set successfully" + } +} diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index 2a9ee7b79d..afe86deb9b 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -32,6 +32,9 @@ "set-password-button": { "label": "Set password" }, + "set-permissions-button": { + "label": "Set permissions" + }, "user-form": { "submit": "Submit" }, diff --git a/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js new file mode 100644 index 0000000000..41bc57da30 --- /dev/null +++ b/scm-ui/src/groups/components/navLinks/SetPermissionsNavLink.js @@ -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 { + render() { + const { t, permissionsUrl } = this.props; + + if (!this.hasPermissionToSetPermission()) { + return null; + } + return ; + } + + hasPermissionToSetPermission = () => { + return this.props.group._links.permissions; + }; +} + +export default translate("groups")(ChangePermissionNavLink); diff --git a/scm-ui/src/groups/components/navLinks/index.js b/scm-ui/src/groups/components/navLinks/index.js index 30fdd34b6d..e589e5b6c9 100644 --- a/scm-ui/src/groups/components/navLinks/index.js +++ b/scm-ui/src/groups/components/navLinks/index.js @@ -1,2 +1,3 @@ export { default as DeleteGroupNavLink } from "./DeleteGroupNavLink"; -export { default as EditGroupNavLink } from "./EditGroupNavLink"; +export { default as EditGroupNavLink } from "./EditGroupNavLink"; +export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/groups/containers/SingleGroup.js b/scm-ui/src/groups/containers/SingleGroup.js index fb38636a3b..591e6cdff4 100644 --- a/scm-ui/src/groups/containers/SingleGroup.js +++ b/scm-ui/src/groups/containers/SingleGroup.js @@ -11,7 +11,11 @@ import { } from "@scm-manager/ui-components"; import { Route } from "react-router"; import { Details } from "./../components/table"; -import { DeleteGroupNavLink, EditGroupNavLink } from "./../components/navLinks"; +import { + DeleteGroupNavLink, + EditGroupNavLink, + SetPermissionsNavLink +} from "./../components/navLinks"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; import { @@ -27,6 +31,7 @@ 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 = { @@ -113,6 +118,13 @@ class SingleGroup extends React.Component { props={extensionProps} renderAll={true} /> + ( + + )} + />
@@ -121,6 +133,10 @@ class SingleGroup extends React.Component { to={`${url}`} label={t("single-group.information-label")} /> +
void, + disabled: boolean, + t: string => string +}; + +class PermissionCheckbox extends React.Component { + render() { + const { t, permission, checked, onChange, disabled } = this.props; + const key = permission.split(":").join("."); + return ( + + ); + } + + translateOrDefault = (key: string, defaultText: string) => { + const translation = this.props.t(key); + if (translation === key) { + return defaultText; + } else { + return translation; + } + }; +} + +export default translate("plugins")(PermissionCheckbox); diff --git a/scm-ui/src/permissions/components/SetPermissions.js b/scm-ui/src/permissions/components/SetPermissions.js new file mode 100644 index 0000000000..e7e561d739 --- /dev/null +++ b/scm-ui/src/permissions/components/SetPermissions.js @@ -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 { + 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 = ( + this.onClose()} + /> + ); + } else if (error) { + message = ; + } + + return ( +
+ {message} + {this.renderPermissions()} + + + ); + } + + renderPermissions = () => { + const { overwritePermissionsLink, permissions } = this.state; + return Object.keys(permissions).map(p => ( +
+ +
+ )); + }; + + 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) +); diff --git a/scm-ui/src/permissions/components/handlePermissions.js b/scm-ui/src/permissions/components/handlePermissions.js new file mode 100644 index 0000000000..6e48127d6d --- /dev/null +++ b/scm-ui/src/permissions/components/handlePermissions.js @@ -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 }; + }); +} diff --git a/scm-ui/src/permissions/components/handlePermissions.test.js b/scm-ui/src/permissions/components/handlePermissions.test.js new file mode 100644 index 0000000000..9fb697e938 --- /dev/null +++ b/scm-ui/src/permissions/components/handlePermissions.test.js @@ -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(); + }); + }); +}); diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js new file mode 100644 index 0000000000..cfdb1775a3 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.js @@ -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 { + render() { + const { t, permissionsUrl } = this.props; + + if (!this.hasPermissionToSetPermission()) { + return null; + } + return ; + } + + hasPermissionToSetPermission = () => { + return this.props.user._links.permissions; + }; +} + +export default translate("users")(ChangePermissionNavLink); diff --git a/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js new file mode 100644 index 0000000000..0e5fcf4125 --- /dev/null +++ b/scm-ui/src/users/components/navLinks/SetPermissionsNavLink.test.js @@ -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( + + ); + expect(navLink.text()).toBe(""); +}); + +it("should render the navLink", () => { + const user = { + _links: { + permissions: { + href: "/permissions" + } + } + }; + + const navLink = shallow( + + ); + expect(navLink.text()).not.toBe(""); +}); diff --git a/scm-ui/src/users/components/navLinks/index.js b/scm-ui/src/users/components/navLinks/index.js index a6d8370c00..eb39bb6726 100644 --- a/scm-ui/src/users/components/navLinks/index.js +++ b/scm-ui/src/users/components/navLinks/index.js @@ -1,3 +1,4 @@ export { default as DeleteUserNavLink } from "./DeleteUserNavLink"; export { default as EditUserNavLink } from "./EditUserNavLink"; export { default as SetPasswordNavLink } from "./SetPasswordNavLink"; +export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink"; diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index 5f20598962..bd172f3f41 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -27,11 +27,13 @@ import { import { DeleteUserNavLink, EditUserNavLink, - SetPasswordNavLink + 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, @@ -106,6 +108,14 @@ class SingleUser extends React.Component { path={`${url}/password`} component={() => } /> + ( + + )} + />
@@ -119,6 +129,10 @@ class SingleUser extends React.Component { user={user} passwordUrl={`${url}/password`} /> +
diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java deleted file mode 100644 index 040eb6b2dc..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractPermissionResource.java +++ /dev/null @@ -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 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 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 getPermissions( - Predicate predicate) - { - List permissions = - securitySystem.getPermissions(predicate); - - return Lists.transform(permissions, - new Function() - { - - @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; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java deleted file mode 100644 index b591d6a2a3..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/GroupPermissionResource.java +++ /dev/null @@ -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 getPredicate() - { - return new GroupPredicate(name); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Group predicate to filter permissions. - */ - private static class GroupPredicate implements Predicate - { - - /** - * 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; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java deleted file mode 100644 index f9e95232db..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SecuritySystemResource.java +++ /dev/null @@ -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; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java deleted file mode 100644 index 5f82fb98eb..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/UserPermissionResource.java +++ /dev/null @@ -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 getPredicate() - { - return new UserPredicate(name); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * User predicate to filter permissions. - */ - private static class UserPredicate implements Predicate - { - - /** - * 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; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java new file mode 100644 index 0000000000..f9cd015f45 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionResource.java @@ -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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java new file mode 100644 index 0000000000..11934abcb0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupPermissionResource.java @@ -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 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 permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + permissionAssigner.setPermissionsForGroup(id, permissionDescriptors); + return Response.noContent().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 6d0b921d02..cfc1916fc1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -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 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; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java index 9a25e711cd..bf866af350 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapper.java @@ -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 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); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 108d6fae5d..6377f21163 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -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())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 669a10143a..4cf66f7b28 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -26,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()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java index 4789915f3d..87d1aeca9f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionToDtoMapper.java @@ -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 permissionDtoList = repository.getPermissions() + public PermissionListDto mapForUser(Collection permissions, String userId) { + return map(permissions, userId, resourceLinks.userPermissions()); + } + + public PermissionListDto mapForGroup(Collection permissions, String groupId) { + return map(permissions, groupId, resourceLinks.groupPermissions()); + } + + private PermissionListDto map(Collection 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); - 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()))); + Links.Builder linksBuilder = linkingTo().self(links.permissions(id)); + + if (PermissionPermissions.assign().isPermitted()) { + linksBuilder.single(link("overwrite", links.overwritePermissions(id))); } - return linksBuilder.build(); - } - private Embedded embedDtos(List permissionDtoList) { - return embeddedBuilder() - .with("permissions", permissionDtoList) - .build(); + target.add(linksBuilder.build()); + + return target; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java index 1e90c23aa7..8d9761c28c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDtoToPermissionMapper.java @@ -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); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java new file mode 100644 index 0000000000..23d57f4d8e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java @@ -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); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 127a3f450e..cb3821cd67 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -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 filterPermission(String permissionName) { + Predicate 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()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index d8d5280456..420e08fe96 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -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; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java new file mode 100644 index 0000000000..5e678212e8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionCollectionToDtoMapper.java @@ -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 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 repositoryPermissionDtoList) { + return embeddedBuilder() + .with("permissions", repositoryPermissionDtoList) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java similarity index 89% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java index 82405a6ac2..6e6b9fd7fc 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionDto.java @@ -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; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java similarity index 51% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java index d6ab3721cf..9f9971ffce 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapper.java @@ -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()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 743474825b..f15f7c4b00 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -41,7 +41,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper 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 permissionDescriptors = Arrays.stream(newPermissions.getPermissions()) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + permissionAssigner.setPermissionsForUser(id, permissionDescriptors); + return Response.noContent().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 0076d057ca..6e90b4e6ec 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -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 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; + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java index 5874e5767a..3c7e9fd7f1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java @@ -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 { 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); diff --git a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java index cf4c980625..0586db2bb3 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AuthorizationChangedEventProducer.java @@ -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() + "fire authorization changed event for user {}, because permission {} has changed", + permission.getName(), permission.getPermission() ); fireEventForUser(permission.getName()); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index eed03c2cd4..a5d99c2928 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -55,7 +55,7 @@ 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; @@ -63,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 ------------------------------------------------------------ @@ -175,12 +174,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector private void collectGlobalPermissions(Builder builder, final User user, final GroupNames groups) { - List globalPermissions = + Collection 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 builder, Repository repository, User user, GroupNames groups) { - Collection repositoryPermissions + Collection 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) diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java index e93d4de597..0064a46228 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -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- - ScmEventBus.getInstance().post( - new StoredAssignedPermissionEvent(HandlerEventType.CREATE, 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)); + 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 AssignedPermissionEvent(HandlerEventType.DELETE, permission) + ); } } @@ -195,16 +173,8 @@ public class DefaultSecuritySystem implements SecuritySystem { if (event.getEventType() == HandlerEventType.DELETE) { - deletePermissions(new Predicate() - { - - @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,44 +189,11 @@ public class DefaultSecuritySystem implements SecuritySystem { if (event.getEventType() == HandlerEventType.DELETE) { - deletePermissions(new Predicate() - { - - @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 getAllPermissions() + public Collection getAvailablePermissions() { - return getPermissions(null); - } - - /** - * Method description - * - * - * @return - */ - @Override - public List 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 getPermissions( - Predicate predicate) + public Collection getPermissions(Predicate predicate) { - Builder permissions = ImmutableList.builder(); + Builder permissions = ImmutableSet.builder(); for (Entry 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 predicate) + private boolean deletePermissions(Predicate predicate) { - List permissions = getPermissions(predicate); - - for (StoredAssignedPermission permission : permissions) - { - deletePermission(permission); - } + List> 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 parsePermissionDescriptor( + private static List parsePermissionDescriptor( JAXBContext context, URL descriptorUrl) { List descriptors = Collections.EMPTY_LIST; @@ -399,19 +300,20 @@ public class DefaultSecuritySystem implements SecuritySystem /** * Method description * + * @param pluginLoader */ - private void readAvailablePermissions() + private static ImmutableSet readAvailablePermissions(PluginLoader pluginLoader) { - Builder builder = ImmutableList.builder(); + ImmutableSet.Builder builder = ImmutableSet.builder(); try { JAXBContext context = JAXBContext.newInstance(PermissionDescriptors.class); + // Querying permissions from uberClassLoader returns also the permissions from plugin Enumeration 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 store; /** Field description */ - private List availablePermissions; + private final ImmutableSet availablePermissions; } diff --git a/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java new file mode 100644 index 0000000000..faa25d7817 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -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 getAvailablePermissions() { + PermissionPermissions.read().check(); + return securitySystem.getAvailablePermissions(); + } + + public Collection readPermissionsForUser(String id) { + return readPermissions(filterForUser(id)); + } + + public Collection readPermissionsForGroup(String id) { + return readPermissions(filterForGroup(id)); + } + + private Predicate filterForUser(String id) { + return p -> !p.isGroupPermission() && p.getName().equals(id); + } + + private Predicate filterForGroup(String id) { + return p -> p.isGroupPermission() && p.getName().equals(id); + } + + private Set readPermissions(Predicate predicate) { + PermissionPermissions.read().check(); + return securitySystem.getPermissions(predicate) + .stream() + .map(AssignedPermission::getPermission) + .collect(Collectors.toSet()); + } + + public void setPermissionsForUser(String id, Collection permissions) { + Collection existingPermissions = securitySystem.getPermissions(filterForUser(id)); + adaptPermissions(id, false, permissions, existingPermissions); + } + + public void setPermissionsForGroup(String id, Collection permissions) { + Collection existingPermissions = securitySystem.getPermissions(filterForGroup(id)); + adaptPermissions(id, true, permissions, existingPermissions); + } + + private void adaptPermissions(String id, boolean groupPermission, Collection permissions, Collection existingPermissions) { + PermissionPermissions.assign().check(); + List toRemove = existingPermissions.stream() + .filter(p -> !permissions.contains(p.getPermission())) + .collect(Collectors.toList()); + toRemove.forEach(securitySystem::deletePermission); + + Collection 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 permissionExists(Collection availablePermissions, Collection 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; + }; + } +} diff --git a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml index 537ada0bce..b86199d700 100644 --- a/scm-webapp/src/main/resources/META-INF/scm/permissions.xml +++ b/scm-webapp/src/main/resources/META-INF/scm/permissions.xml @@ -34,21 +34,22 @@ - All Repository (read) - Read access to all repositories - repository:*:READ + repository:read,pull:* - - All Repository (write) - Write access to all repositories - repository:*:WRITE + repository:read,pull,push:* - - All Repository (owner) - Owner access to all repositories - repository:*:OWNER + repository:*:* - + + repository:create + + + user:* + + + group:* + + diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json new file mode 100644 index 0000000000..7c75539005 --- /dev/null +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -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" + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index f28cf49d03..646e9d0839 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -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 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> 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"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index c008e0b8db..012656c4cd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -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 TEST_PERMISSIONS = Lists + private static final ArrayList 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 permissions = Lists.newArrayList(TEST_PERMISSIONS); + RepositoryPermission newPermission = new RepositoryPermission("new_group_perm", PermissionType.WRITE, true); + ArrayList permissions = Lists.newArrayList(TEST_PERMISSIONS); permissions.add(newPermission); - ImmutableList expectedPermissions = ImmutableList.copyOf(permissions); + ImmutableList 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 expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS); + ImmutableList 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 expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + RepositoryPermission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList 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 expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size())); + RepositoryPermission deletedPermission = TEST_PERMISSIONS.get(0); + ImmutableList 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 expectedPermissions, String userPermission) throws URISyntaxException { + private void assertGettingExpectedPermissions(ImmutableList 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 actualPermissionDtos = halRepresentation.getEmbedded().getItemsBy("permissions", HalRepresentation.class); - List permissionDtoStream = actualPermissionDtos.stream() + List 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 permissions, String userPermission) { + private RepositoryPermissionDto[] getExpectedPermissionDtos(ArrayList 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 permissions, String userPermission) { + private void createUserWithRepositoryAndPermissions(ArrayList permissions, String userPermission) { createUserWithRepository(userPermission).setPermissions(permissions); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java similarity index 55% rename from scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java rename to scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java index 31c2f0ec31..a6ab00db58 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionToRepositoryPermissionDtoMapperTest.java @@ -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() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index fe403088f2..1677be95b1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -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); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 1ddae1d107..9bf70093fd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -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; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 435d8b4673..655d00fc10 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -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; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 284e7d1d7b..88142e4d50 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -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 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> 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 createSingletonPageResult(int overallCount) { return new PageResult<>(singletonList(createDummyUser("Neo")), overallCount); } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 9d03fa02ca..146810e787 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -184,7 +184,7 @@ private long calculateAverage(List 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; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java index b45e0d72e9..ab8ce5dce8 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/AuthorizationChangedEventProducerTest.java @@ -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"); } @@ -253,4 +254,4 @@ public class AuthorizationChangedEventProducerTest { } -} \ No newline at end of file +} diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index cc39965299..3b3a28861f 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -33,7 +33,6 @@ 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; @@ -55,6 +54,7 @@ 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; @@ -225,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)); @@ -251,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(); @@ -278,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()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java index 2ccdb2b28a..457bb96ae4 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -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 list = securitySystem.getAvailablePermissions(); + Collection 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 all = securitySystem.getAllPermissions(); + Collection 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 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 filtered = - securitySystem.getPermissions(new Predicate() - { + Collection 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; } diff --git a/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java new file mode 100644 index 0000000000..366c16f6b8 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java @@ -0,0 +1,112 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.authz.UnauthorizedException; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import sonia.scm.NotFoundException; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; +import sonia.scm.util.ClassLoaders; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini", username = "dent", password = "secret") +public class PermissionAssignerTest { + + @Rule + public ShiroRule shiroRule = new ShiroRule(); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private DefaultSecuritySystem securitySystem; + private PermissionAssigner permissionAssigner; + + @Before + public void init() { + PluginLoader pluginLoader = mock(PluginLoader.class); + when(pluginLoader.getUberClassLoader()).thenReturn(ClassLoaders.getContextClassLoader(DefaultSecuritySystem.class)); + + securitySystem = new DefaultSecuritySystem(new InMemoryConfigurationEntryStoreFactory(), pluginLoader) { + @Override + public Collection getAvailablePermissions() { + return Arrays.stream(new String[]{"perm:read:1", "perm:read:2", "perm:read:3", "perm:read:4"}) + .map(PermissionDescriptor::new) + .collect(Collectors.toList()); + } + }; + + try { + securitySystem.addPermission(new AssignedPermission("1", "perm:read:1")); + securitySystem.addPermission(new AssignedPermission("1", "perm:read:2")); + securitySystem.addPermission(new AssignedPermission("2", "perm:read:2")); + securitySystem.addPermission(new AssignedPermission("1", true, "perm:read:2")); + } catch (UnauthorizedException e) { + // ignore for tests with limited privileges + } + permissionAssigner = new PermissionAssigner(securitySystem); + } + + @Test + public void shouldFindUserPermissions() { + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } + + @Test + public void shouldFindGroupPermissions() { + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldNotReadUserPermissionsForUnprivilegedUser() { + expectedException.expect(UnauthorizedException.class); + + permissionAssigner.readPermissionsForUser("1"); + } + + @Test + public void shouldOverwriteUserPermissions() { + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:3"), new PermissionDescriptor("perm:read:4"))); + + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("2"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldNotOverwriteUserPermissionsForUnprivilegedUser() { + expectedException.expect(UnauthorizedException.class); + + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:3"), new PermissionDescriptor("perm:read:4"))); + } + + @Test + public void shouldFailForNotExistingPermissions() { + expectedException.expect(NotFoundException.class); + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:4"), new PermissionDescriptor("perm:read:5"))); + } + + @Test + public void shouldAcceptNotExistingPermissionsWhenTheyWereAssignedBefore() { + securitySystem.addPermission(new AssignedPermission("2", "perm:read:5")); + + permissionAssigner.setPermissionsForUser("2", asList(new PermissionDescriptor("perm:read:5"))); + } +}