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/SecuritySystem.java b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java index 49f27be3e8..174b64f5e6 100644 --- a/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java +++ b/scm-core/src/main/java/sonia/scm/security/SecuritySystem.java @@ -52,7 +52,7 @@ public interface SecuritySystem * * @return stored permission */ - public void addPermission(AssignedPermission permission); + void addPermission(AssignedPermission permission); /** * Delete stored permission. @@ -60,7 +60,7 @@ public interface SecuritySystem * * @param permission permission to be deleted */ - public void deletePermission(AssignedPermission permission); + void deletePermission(AssignedPermission permission); //~--- get methods ---------------------------------------------------------- @@ -70,7 +70,7 @@ public interface SecuritySystem * * @return available permissions */ - public Collection getAvailablePermissions(); + Collection getAvailablePermissions(); /** * Returns all stored permissions which are matched by the given @@ -81,6 +81,5 @@ public interface SecuritySystem * * @return filtered permissions */ - public Collection getPermissions( - Predicate predicate); + Collection getPermissions(Predicate predicate); } 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-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-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java index 369fd376c7..845aaddd07 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GlobalPermissionPocResource.java @@ -86,15 +86,15 @@ public class GlobalPermissionPocResource { @Path("") public Response getAll() { String[] permissions = securitySystem.getAvailablePermissions().stream().map(PermissionDescriptor::getValue).toArray(String[]::new); - return Response.ok(new PerminssionListDto(permissions)).build(); + return Response.ok(new PermissionListDto(permissions)).build(); } protected void assignExemplaryPermissions() { - AssignedPermission groupPermission = new AssignedPermission("configurers", true,"configuration:*"); + AssignedPermission groupPermission = new AssignedPermission("configurers", true, new PermissionDescriptor("configuration:*")); log.info("try to add new permission: {}", groupPermission); securitySystem.addPermission(groupPermission); - AssignedPermission userPermission = new AssignedPermission("rene", "group:*"); + AssignedPermission userPermission = new AssignedPermission("rene", new PermissionDescriptor("group:*")); log.info("try to add new permission: {}", userPermission); securitySystem.addPermission(userPermission); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java similarity index 87% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java index 3752d089f7..807330dd97 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PerminssionListDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionListDto.java @@ -9,7 +9,7 @@ import lombok.Setter; @Setter @AllArgsConstructor @NoArgsConstructor -public class PerminssionListDto { +public class PermissionListDto { private String[] permissions; } 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 be97af2677..479e4094fb 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 @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.security.AssignedPermission; +import sonia.scm.security.PermissionDescriptor; import sonia.scm.security.SecuritySystem; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -153,7 +154,34 @@ public class UserResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getPermissions(@PathParam("id") String id) { - String[] permissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).toArray(String[]::new); - return Response.ok(new PerminssionListDto(permissions)).build(); + String[] permissions = + securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)) + .stream() + .map(AssignedPermission::getPermission) + .map(PermissionDescriptor::getValue) + .toArray(String[]::new); + return Response.ok(new PermissionListDto(permissions)).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("permissions") + @Consumes(VndMediaType.PASSWORD_OVERWRITE) + @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) { + return Response.noContent().build(); } } 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 903445df3a..5560d77e4a 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -180,7 +180,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector 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); 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 26bf704775..0064a46228 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultSecuritySystem.java @@ -39,11 +39,10 @@ import com.github.legman.Subscribe; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet.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; @@ -68,6 +67,9 @@ 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 ------------------------------------------------------------ @@ -251,14 +253,13 @@ public class DefaultSecuritySystem implements SecuritySystem */ private boolean deletePermissions(Predicate predicate) { - boolean found = false; - for (Entry e : store.getAll().entrySet()) { - if ((predicate == null) || predicate.test(e.getValue())) { - store.remove(e.getKey()); - found = true; - } - } - return found; + 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(); } /** @@ -346,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"); } 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..24895dce8c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/PermissionAssigner.java @@ -0,0 +1,35 @@ +package sonia.scm.security; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class PermissionAssigner { + + private final SecuritySystem securitySystem; + + public PermissionAssigner(SecuritySystem securitySystem) { + this.securitySystem = securitySystem; + } + + public Collection getAvailablePermissions() { + return securitySystem.getAvailablePermissions(); + } + + public Collection readPermissionsForUser(String id) { + return securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)).stream().map(AssignedPermission::getPermission).collect(Collectors.toSet()); + } + + public void setPermissionsForUser(String id, Collection permissions) { + Collection existingPermissions = securitySystem.getPermissions(p -> !p.isGroupPermission() && p.getName().equals(id)); + List toRemove = existingPermissions.stream() + .filter(p -> !permissions.contains(p.getPermission())) + .collect(Collectors.toList()); + toRemove.forEach(securitySystem::deletePermission); + + permissions.stream() + .map(p -> new AssignedPermission(id, false, p)) + .filter(p -> !existingPermissions.contains(p)) + .forEach(securitySystem::addPermission); + } +} 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 2f455469dd..92e3ba71e6 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.Lists; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; @@ -219,7 +218,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(); @@ -246,7 +245,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 74214376ec..e9b4a0ae00 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultSecuritySystemTest.java @@ -95,7 +95,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase 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()); } @@ -256,7 +256,7 @@ public class DefaultSecuritySystemTest extends AbstractTestBase return securitySystem.getPermissions(permission -> Objects.equal(name, permission.getName()) && Objects.equal(groupPermission, permission.isGroupPermission()) - && Objects.equal(value, permission.getPermission())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found")); + && Objects.equal(value, permission.getPermission().getValue())).stream().findAny().orElseThrow(() -> new AssertionError("created permission not found")); } //~--- set methods ---------------------------------------------------------- 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..f698f24bfb --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/PermissionAssignerTest.java @@ -0,0 +1,57 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; +import sonia.scm.util.ClassLoaders; + +import java.util.Collection; + +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(); + + 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); + + 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")); + permissionAssigner = new PermissionAssigner(securitySystem); + } + + @Test + public void shouldFindUserPermissions() { + Collection permissionDescriptors = permissionAssigner.readPermissionsForUser("1"); + + Assertions.assertThat(permissionDescriptors).hasSize(2); + } + + @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); + } +}