This commit is contained in:
Eduard Heimbuch
2020-09-18 12:54:11 +02:00
32 changed files with 472 additions and 61 deletions

View File

@@ -5,8 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Permissions can be specified for namespaces ([#1335](https://github.com/scm-manager/scm-manager/pull/1335))
### Fixed
- Missing synchronization during repository creation ([#1328](https://github.com/scm-manager/scm-manager/pull/1328))
- Missing BranchCreatedEvent for mercurial ([#1334](https://github.com/scm-manager/scm-manager/pull/1334))
- Branch not found right after creation ([#1334](https://github.com/scm-manager/scm-manager/pull/1334))
## [2.5.0] - 2020-09-10
### Added

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View File

@@ -26,6 +26,8 @@ Icon | Beschreibung
![Repository Sources](assets/repository-overview-sources.png) | Öffnet die Sources-Übersicht für das Repository
![Repository Einstellungen](assets/repository-overview-settings.png) | Öffnet die Einstellungen für das Repository
Zusätzlich können über das Icon rechts neben den Überschriften für die Namespaces weitere Einstellungen auf Namespace-Ebene vorgenommen werden.
### Repository erstellen
Im SCM-Manager können neue Git, Mercurial & Subersion (SVN) Repositories über ein Formular angelegt werden. Dieses kann über den Button "Repository erstellen" aufgerufen werden. Dabei muss ein gültiger Name eingetragen und der Repository-Typ bestimmt werden.

View File

@@ -12,10 +12,12 @@ Innerhalb der Gefahrenzone unten auf der Seite gibt es mit entsprechenden Rechte
![Repository-Settings-General-Git](assets/repository-settings-general-git.png)
### Berechtigungen
Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren Rollen oder auf individuellen Einstellungen, Rechte zugewiesen werden. Berechtigungen können global und auf Repository-Ebene vergeben werden. Globale Berechtigungen werden in der Administrations-Oberfläche des SCM-Managers vergeben. Unter diesem Eintrag handelt es sich um Repository-bezogene Berechtigungen.
Dank des fein granularen Berechtigungskonzepts des SCM-Managers können Nutzern und Gruppen, basierend auf definierbaren Rollen oder auf individuellen Einstellungen, Rechte zugewiesen werden. Berechtigungen können global, auf Namespace-Ebene und auf Repository-Ebene vergeben werden. Globale Berechtigungen werden in der Administrations-Oberfläche des SCM-Managers vergeben. Unter diesem Eintrag handelt es sich um Repository-bezogene Berechtigungen.
Die Berechtigungen können jeweils für Gruppen und für Benutzer vergeben werden. Dabei gibt es die Möglichkeiten die Berechtigungen über Berechtigungsrollen zu definieren oder jede Berechtigung einzeln zu vergeben. Die Berechtigungsrollen können in der Administrations-Oberfläche definiert werden.
Berechtigungen auf Namespace-Ebene können über die Einstellungen für Namespaces bearbeitet werden. Diese sind über das Einstellungs-Symbol neben den Namespace-Überschriften auf der Repository-Übersicht erreichbar.
![Repository-Settings-PermissionOverview](assets/repository-settings-permissionOverview.png)
Für individuelle Berechtigungen kann man über "Erweitert" einen Dialog öffnen, um jede Berechtigung einzeln zu vergeben.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View File

@@ -24,6 +24,8 @@ Icon | Description
![Repository Sources](assets/repository-overview-sources.png) | Opens the sources overview for the repository
![Repository Settings](assets/repository-overview-settings.png) | Opens the settings for the repository
Clicking the icon on the right-hand side of each namespace caption, you can change additional settings for this namespace.
### Create a Repository
In SCM-Manager new Git, Mercurial & Subversion (SVN) repositories can be created via a form that can be accessed via the "Create Repository" button. A valid name and the repository type are mandatory.

View File

@@ -12,10 +12,12 @@ In the danger zone at the bottom you may rename the repository or delete it. If
![Repository-Settings-General-Git](assets/repository-settings-general-git.png)
### Permissions
Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable roles or individual settings. Permissions can be granted globally or repository-specific. Global permissions are managed in the administration area of SCM-Manager. The following image shows repository-specific permissions.
Thanks to the finely granular permission concept of SCM-Manager, users and groups can be authorized based on definable roles or individual settings. Permissions can be granted globally, namespace-wide, or repository-specific. Global permissions are managed in the administration area of SCM-Manager. The following image shows repository-specific permissions.
Permissions can be granted to groups or users. It is possible to manage each permission individually or to create roles that contain several permissions. Roles can be defined in the administration area.
Namespace-wide permissions can be configured in the namespace settings. These can be accessed via the settings icon on the right-hand side of the namespace heading in the repository overview.
![Repository-Settings-PermissionOverview](assets/repository-settings-permissionOverview.png)
To manage permissions individually, an "Advanced" dialog can be opened to manage every single permission.

View File

@@ -903,7 +903,7 @@
<properties>
<!-- test libraries -->
<mockito.version>3.5.6</mockito.version>
<mockito.version>3.5.7</mockito.version>
<hamcrest.version>2.1</hamcrest.version>
<junit.version>5.6.2</junit.version>

View File

@@ -0,0 +1,47 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import sonia.scm.HandlerEventType;
import sonia.scm.event.AbstractHandlerEvent;
import sonia.scm.event.Event;
/**
* The NamespaceEvent is fired if a {@link Namespace} object changes.
*
* @since 2.6.0
*/
@Event
public class NamespaceEvent extends AbstractHandlerEvent<Namespace> {
public NamespaceEvent(HandlerEventType eventType, Namespace namespace) {
super(eventType, namespace);
}
public NamespaceEvent(HandlerEventType eventType, Namespace namespace, Namespace oldNamespace) {
super(eventType, namespace, oldNamespace);
}
}

View File

@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
import sonia.scm.HandlerEventType;
import sonia.scm.ModificationHandlerEvent;
import sonia.scm.event.Event;
/**
* Event which is fired whenever a namespace is modified.
*
* @since 2.6.0
*/
@Event
public final class NamespaceModificationEvent extends NamespaceEvent implements ModificationHandlerEvent<Namespace> {
private final Namespace itemBeforeModification;
public NamespaceModificationEvent(HandlerEventType eventType, Namespace item, Namespace itemBeforeModification) {
super(eventType, item, itemBeforeModification);
this.itemBeforeModification = itemBeforeModification;
}
@Override
public Namespace getItemBeforeModification() {
return itemBeforeModification;
}
}

View File

@@ -21,13 +21,14 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import org.apache.commons.collections.CollectionUtils;
import sonia.scm.security.PermissionObject;
import javax.xml.bind.annotation.XmlAccessType;
@@ -118,8 +119,7 @@ public class RepositoryPermission implements PermissionObject, Serializable
final RepositoryPermission other = (RepositoryPermission) obj;
return Objects.equal(name, other.name)
&& verbs.size() == other.verbs.size()
&& verbs.containsAll(other.verbs)
&& (verbs == null && other.verbs == null || verbs != null && other.verbs != null && CollectionUtils.isEqualCollection(verbs, other.verbs))
&& Objects.equal(role, other.role)
&& Objects.equal(groupPermission, other.groupPermission);
}

View File

@@ -21,21 +21,51 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.BranchCommand;
import java.io.IOException;
/**
* @since 2.0
*/
public final class BranchCommandBuilder {
private static final Logger LOG = LoggerFactory.getLogger(BranchCommandBuilder.class);
private final Repository repository;
private final BranchCommand command;
private final ScmEventBus eventBus;
private final BranchRequest request = new BranchRequest();
public BranchCommandBuilder(Repository repository, BranchCommand command) {
this(repository, command, ScmEventBus.getInstance());
}
/**
* Creates a new {@link BranchCommandBuilder}.
*
* @param command type specific command implementation
*
* @deprecated use {@link #BranchCommandBuilder(Repository, BranchCommand)} instead.
*/
@Deprecated
public BranchCommandBuilder(BranchCommand command) {
this(null, command, ScmEventBus.getInstance());
}
@VisibleForTesting
BranchCommandBuilder(Repository repository, BranchCommand command, ScmEventBus eventBus) {
this.repository = repository;
this.command = command;
this.eventBus = eventBus;
}
/**
@@ -53,17 +83,23 @@ public final class BranchCommandBuilder {
* Execute the command and create a new branch with the given name.
* @param name The name of the new branch.
* @return The created branch.
* @throws IOException
*/
public Branch branch(String name) {
request.setNewBranch(name);
return command.branch(request);
Branch branch = command.branch(request);
fireCreatedEvent(branch);
return branch;
}
private void fireCreatedEvent(Branch branch) {
if (repository != null) {
eventBus.post(new BranchCreatedEvent(repository, branch.getName()));
} else {
LOG.warn("the branch command was created without a repository, so we are not able to fire a BranchCreatedEvent");
}
}
public void delete(String branchName) {
command.deleteOrClose(branchName);
}
private BranchCommand command;
private BranchRequest request = new BranchRequest();
}

View File

@@ -177,7 +177,7 @@ public final class RepositoryService implements Closeable {
LOG.debug("create branch command for repository {}",
repository.getNamespaceAndName());
return new BranchCommandBuilder(provider.getBranchCommand());
return new BranchCommandBuilder(repository, provider.getBranchCommand());
}
/**

View File

@@ -298,10 +298,12 @@ public final class RepositoryServiceFactory {
/**
* Clear caches on repository push.
* We do this synchronously, because there are often workflows which are creating branches and fetch them straight
* after the creation.
*
* @param event hook event
*/
@Subscribe(referenceType = ReferenceType.STRONG)
@Subscribe(async = false, referenceType = ReferenceType.STRONG)
public void onEvent(PostReceiveRepositoryHookEvent event) {
Repository repository = event.getRepository();
@@ -324,13 +326,6 @@ public final class RepositoryServiceFactory {
}
}
@Subscribe(async = false)
@SuppressWarnings({"unchecked", "java:S3740", "rawtypes"})
public void onEvent(BranchCreatedEvent event) {
RepositoryCacheKeyPredicate predicate = new RepositoryCacheKeyPredicate(event.getRepository().getId());
cacheManager.getCache(BranchesCommandBuilder.CACHE_NAME).removeAll(predicate);
}
@Subscribe
public void onEvent(PublicKeyDeletedEvent event) {
cacheManager.getCache(LogCommandBuilder.CACHE_NAME).clear();

View File

@@ -0,0 +1,105 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.spi.BranchCommand;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class BranchCommandBuilderTest {
@Mock
private BranchCommand command;
@Mock
private ScmEventBus eventBus;
private final Repository repository = new Repository("42", "git", "spaceships", "heart-of-gold");
private final String branchName = "feature/infinite_improbability_drive";
private final Branch branch = Branch.normalBranch(branchName, "42");
@Nested
class Creation {
@BeforeEach
void configureMocks() {
when(command.branch(any())).thenReturn(branch);
}
@Test
void shouldDelegateCreationToCommand() {
BranchCommandBuilder builder = new BranchCommandBuilder(repository, command, eventBus);
Branch returnedBranch = builder.branch(branchName);
assertThat(branch).isSameAs(returnedBranch);
}
@Test
void shouldSendBranchCreatedEvent() {
BranchCommandBuilder builder = new BranchCommandBuilder(repository, command, eventBus);
builder.branch(branchName);
ArgumentCaptor<BranchCreatedEvent> captor = ArgumentCaptor.forClass(BranchCreatedEvent.class);
verify(eventBus).post(captor.capture());
BranchCreatedEvent event = captor.getValue();
assertThat(event.getBranchName()).isEqualTo("feature/infinite_improbability_drive");
}
@Test
void shouldNotSendEventWithoutRepository() {
BranchCommandBuilder builder = new BranchCommandBuilder(null, command, eventBus);
builder.branch(branchName);
verify(eventBus, never()).post(any());
}
}
@Nested
class Deletion {
@Test
void shouldDelegateDeletionToCommand() {
BranchCommandBuilder builder = new BranchCommandBuilder(repository, command, eventBus);
builder.delete(branchName);
verify(command).deleteOrClose(branchName);
}
}
}

View File

@@ -30,7 +30,6 @@ import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.BranchCreatedEvent;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
@@ -48,9 +47,7 @@ import java.io.IOException;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static java.util.Collections.*;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class GitBranchCommand extends AbstractGitCommand implements BranchCommand {
@@ -72,8 +69,6 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
eventBus.post(new PreReceiveRepositoryHookEvent(hookEvent));
Ref ref = git.branchCreate().setStartPoint(request.getParentBranch()).setName(request.getNewBranch()).call();
eventBus.post(new PostReceiveRepositoryHookEvent(hookEvent));
// Clear cache synchronously to avoid branch not found in invalid cache
eventBus.post(new BranchCreatedEvent(repository, request.getNewBranch()));
return Branch.normalBranch(request.getNewBranch(), GitUtil.getId(ref.getObjectId()));
} catch (GitAPIException | IOException ex) {
throw new InternalRepositoryException(repository, "could not create branch " + request.getNewBranch(), ex);

View File

@@ -104,7 +104,8 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
@Test
public void shouldThrowExceptionWhenDeletingDefaultBranch() {
String branchToBeDeleted = "master";
assertThrows(CannotDeleteDefaultBranchException.class, () -> createCommand().deleteOrClose(branchToBeDeleted));
GitBranchCommand command = createCommand();
assertThrows(CannotDeleteDefaultBranchException.class, () -> command.deleteOrClose(branchToBeDeleted));
}
private GitBranchCommand createCommand() {
@@ -130,7 +131,6 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
assertThat(events.get(2)).isInstanceOf(BranchCreatedEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).containsExactly("new_branch");

View File

@@ -14,7 +14,7 @@
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"file-loader": "^4.2.0",
"mini-css-extract-plugin": "^0.10.0",
"mini-css-extract-plugin": "^0.11.0",
"mustache": "^3.1.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"react-refresh": "^0.8.0",

View File

@@ -0,0 +1,9 @@
{
"namespaceRoot": {
"menu": {
"navigationLabel": "Namespace",
"settingsNavLink": "Einstellungen",
"permissionsNavLink": "Berechtigungen"
}
}
}

View File

@@ -218,7 +218,8 @@
"renameRepo": {
"button": "Repository umbenennen",
"subtitle": "Benennt dieses Repository um",
"description": "Es werden keine Weiterleitung auf den neuen Namen eingerichtet.",
"description1": "Es werden keine Weiterleitung auf den neuen Namen eingerichtet.",
"description2": "Berechtigungen aus dem Namespace werden bei einem Wechsel nicht übernommen.",
"modal": {
"title": "Repository umbenennen",
"label": {

View File

@@ -218,7 +218,8 @@
"renameRepo": {
"button": "Rename Repository",
"subtitle": "Renames this repository",
"description": "There will be no redirects to the renamed repository.",
"description1": "There will be no redirects to the renamed repository.",
"description2": "Permissions from the namespace will not be adapted when the namespace is changed.",
"modal": {
"title": "Rename repository",
"label": {

View File

@@ -73,21 +73,29 @@ it("should group the repositories by their namespace", () => {
hitchhikerHeartOfGold,
hitchhikerPuzzle42
];
const namespaces = {
_embedded: {
namespaces: [{ namespace: "hitchhiker" }, { namespace: "slarti" }, { namespace: "zaphod" }]
}
};
const expected = [
{
name: "hitchhiker",
namespace: { namespace: "hitchhiker" },
repositories: [hitchhikerHeartOfGold, hitchhikerPuzzle42, hitchhikerRestand]
},
{
name: "slarti",
namespace: { namespace: "slarti" },
repositories: [slartiFjords, slartiBlueprintsFjords]
},
{
name: "zaphod",
namespace: { namespace: "zaphod" },
repositories: [zaphodMarvinFirmware]
}
];
expect(groupByNamespace(repositories)).toEqual(expected);
expect(groupByNamespace(repositories, namespaces)).toEqual(expected);
});

View File

@@ -76,7 +76,7 @@ const DangerZone: FC<Props> = ({ repository, indexLinks }) => {
<>
<hr />
<Subtitle subtitle={t("repositoryForm.dangerZone")} />
<DangerZoneContainer>{dangerZone.map(entry => entry)}</DangerZoneContainer>
<DangerZoneContainer>{dangerZone}</DangerZoneContainer>
</>
);
};

View File

@@ -159,7 +159,9 @@ const RenameRepository: FC<Props> = ({ repository, indexLinks }) => {
<p>
<strong>{t("renameRepo.subtitle")}</strong>
<br />
{t("renameRepo.description")}
{t("renameRepo.description1")}
<br />
{t("renameRepo.description2")}
</p>
}
right={

View File

@@ -123,7 +123,6 @@ class NamespaceRoot extends React.Component<Props> {
</Page>
</StateMenuContextProvider>
);
// return <h1>{`HALLO ${this.props.namespace}`}</h1>;
}
}

View File

@@ -34,7 +34,6 @@ import lombok.extern.slf4j.Slf4j;
import sonia.scm.NotFoundException;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespaceManager;
import sonia.scm.repository.NamespacePermissions;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.web.VndMediaType;
@@ -50,7 +49,6 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Predicate;
@@ -120,7 +118,6 @@ public class NamespacePermissionResource {
public Response create(@PathParam("namespace") String namespaceName, @Valid RepositoryPermissionDto permission) {
log.info("try to add new permission: {}", permission);
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionWrite().check();
checkPermissionAlreadyExists(permission, namespace);
namespace.addPermission(dtoToModelMapper.map(permission));
manager.modify(namespace);
@@ -164,7 +161,6 @@ public class NamespacePermissionResource {
)
public RepositoryPermissionDto get(@PathParam("namespace") String namespaceName, @PathParam("permission-name") String permissionName) {
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionRead().check();
return
namespace.getPermissions()
.stream()
@@ -210,7 +206,6 @@ public class NamespacePermissionResource {
)
public HalRepresentation getAll(@PathParam("namespace") String namespaceMame) {
Namespace namespace = load(namespaceMame);
NamespacePermissions.permissionRead().check();
return repositoryPermissionCollectionToDtoMapper.map(namespace);
}
@@ -241,7 +236,6 @@ public class NamespacePermissionResource {
@Valid RepositoryPermissionDto permission) {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionWrite().check();
String extractedPermissionName = getPermissionName(permissionName);
if (!isPermissionExist(new RepositoryPermissionDto(extractedPermissionName, isGroupPermission(permissionName)), namespace)) {
throw notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName));
@@ -289,7 +283,6 @@ public class NamespacePermissionResource {
@PathParam("permission-name") String permissionName) {
log.info("try to delete the permission with name: {}.", permissionName);
Namespace namespace = load(namespaceName);
NamespacePermissions.permissionWrite().check();
namespace.getPermissions()
.stream()
.filter(filterPermission(permissionName))

View File

@@ -24,6 +24,10 @@
package sonia.scm.repository;
import com.github.legman.Subscribe;
import sonia.scm.HandlerEventType;
import sonia.scm.event.ScmEventBus;
import javax.inject.Inject;
import java.util.Collection;
import java.util.Optional;
@@ -36,11 +40,13 @@ public class DefaultNamespaceManager implements NamespaceManager {
private final RepositoryManager repositoryManager;
private final NamespaceDao dao;
private final ScmEventBus eventBus;
@Inject
public DefaultNamespaceManager(RepositoryManager repositoryManager, NamespaceDao dao) {
public DefaultNamespaceManager(RepositoryManager repositoryManager, NamespaceDao dao, ScmEventBus eventBus) {
this.repositoryManager = repositoryManager;
this.dao = dao;
this.eventBus = eventBus;
}
@Override
@@ -64,15 +70,45 @@ public class DefaultNamespaceManager implements NamespaceManager {
@Override
public void modify(Namespace namespace) {
if (!get(namespace.getNamespace()).isPresent()) {
throw notFound(entity("Namespace", namespace.getNamespace()));
}
NamespacePermissions.permissionWrite().check();
Namespace oldNamespace = get(namespace.getNamespace())
.orElseThrow(() -> notFound(entity(Namespace.class, namespace.getNamespace())));
fireEvent(HandlerEventType.BEFORE_MODIFY, namespace, oldNamespace);
dao.add(namespace);
fireEvent(HandlerEventType.MODIFY, namespace, oldNamespace);
}
@Subscribe
public void cleanupDeletedNamespaces(RepositoryEvent repositoryEvent) {
HandlerEventType eventType = repositoryEvent.getEventType();
if (eventType == HandlerEventType.DELETE || eventType == HandlerEventType.MODIFY && !repositoryEvent.getItem().getNamespace().equals(repositoryEvent.getOldItem().getNamespace())) {
Collection<String> allNamespaces = repositoryManager.getAllNamespaces();
String oldNamespace = getOldNamespace(repositoryEvent);
if (!allNamespaces.contains(oldNamespace)) {
dao.delete(oldNamespace);
}
}
}
public String getOldNamespace(RepositoryEvent repositoryEvent) {
if (repositoryEvent.getEventType() == HandlerEventType.DELETE) {
return repositoryEvent.getItem().getNamespace();
} else {
return repositoryEvent.getOldItem().getNamespace();
}
}
private Namespace createNamespaceForName(String namespace) {
return dao.get(namespace)
.map(Namespace::clone)
.orElse(new Namespace(namespace));
if (NamespacePermissions.permissionRead().isPermitted()) {
return dao.get(namespace)
.map(Namespace::clone)
.orElse(new Namespace(namespace));
} else {
return new Namespace(namespace);
}
}
protected void fireEvent(HandlerEventType event, Namespace namespace, Namespace oldNamespace) {
eventBus.post(new NamespaceModificationEvent(event, namespace, oldNamespace));
}
}

View File

@@ -46,4 +46,8 @@ public class NamespaceDao {
public void add(Namespace namespace) {
store.put(namespace.getNamespace(), namespace);
}
public void delete(String namespace) {
store.remove(namespace);
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import com.github.legman.Subscribe;
@@ -35,14 +35,19 @@ import sonia.scm.event.ScmEventBus;
import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespaceEvent;
import sonia.scm.repository.NamespaceModificationEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.user.User;
import sonia.scm.user.UserEvent;
import sonia.scm.user.UserModificationEvent;
import javax.inject.Singleton;
import java.util.Collection;
/**
* Receives all kinds of events, which affects authorization relevant data and fires an
@@ -146,23 +151,45 @@ public class AuthorizationChangedEventProducer {
}
}
@Subscribe
public void onEvent(NamespaceEvent event) {
if (event.getEventType().isPost() && isModificationEvent(event)) {
handleNamespaceModificationEvent((NamespaceModificationEvent) event);
}
}
private void handleRepositoryModificationEvent(RepositoryModificationEvent event) {
Repository repository = event.getItem();
if (isAuthorizationDataModified(repository, event.getItemBeforeModification())) {
if (isAuthorizationDataModified(repository.getPermissions(), event.getItemBeforeModification().getPermissions())) {
logger.debug(
"fire authorization changed event, because a relevant field of repository {} has changed", repository.getName()
"fire authorization changed event, because a relevant field of repository {}/{} has changed", repository.getNamespace(), repository.getName()
);
fireEventForEveryUser();
} else {
logger.debug(
"authorization changed event is not fired, because non relevant field of repository {} has changed",
repository.getName()
"authorization changed event is not fired, because non relevant field of repository {}/{} has changed",
repository.getNamespace(), repository.getName()
);
}
}
private boolean isAuthorizationDataModified(Repository repository, Repository beforeModification) {
return !(repository.getPermissions().containsAll(beforeModification.getPermissions()) && beforeModification.getPermissions().containsAll(repository.getPermissions()));
private void handleNamespaceModificationEvent(NamespaceModificationEvent event) {
Namespace namespace = event.getItem();
if (isAuthorizationDataModified(namespace.getPermissions(), event.getItemBeforeModification().getPermissions())) {
logger.debug(
"fire authorization changed event, because a relevant field of namespace {} has changed", namespace.getNamespace()
);
fireEventForEveryUser();
} else {
logger.debug(
"authorization changed event is not fired, because non relevant field of namespace {} has changed",
namespace.getNamespace()
);
}
}
private boolean isAuthorizationDataModified(Collection<RepositoryPermission> newPermissions, Collection<RepositoryPermission> permissionsBeforeModification) {
return !(newPermissions.containsAll(permissionsBeforeModification) && permissionsBeforeModification.containsAll(newPermissions));
}
private void fireEventForEveryUser() {

View File

@@ -44,6 +44,12 @@
<permission>
<value>repository:create</value>
</permission>
<permission>
<value>namespace:permissionRead</value>
</permission>
<permission>
<value>namespace:permissionWrite</value>
</permission>
<permission>
<value>user:*</value>
</permission>

View File

@@ -24,11 +24,14 @@
package sonia.scm.repository;
import com.github.legman.EventBus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.HandlerEventType;
import sonia.scm.event.ScmEventBus;
import sonia.scm.store.InMemoryDataStore;
import sonia.scm.store.InMemoryDataStoreFactory;
@@ -37,7 +40,11 @@ import java.util.Optional;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sonia.scm.HandlerEventType.DELETE;
import static sonia.scm.HandlerEventType.MODIFY;
@ExtendWith(MockitoExtension.class)
@@ -45,6 +52,8 @@ class DefaultNamespaceManagerTest {
@Mock
RepositoryManager repositoryManager;
@Mock
ScmEventBus eventBus;
Namespace life;
@@ -56,7 +65,7 @@ class DefaultNamespaceManagerTest {
@BeforeEach
void mockExistingNamespaces() {
dao = new NamespaceDao(new InMemoryDataStoreFactory(new InMemoryDataStore()));
manager = new DefaultNamespaceManager(repositoryManager, dao);
manager = new DefaultNamespaceManager(repositoryManager, dao, eventBus);
when(repositoryManager.getAllNamespaces()).thenReturn(asList("life", "universe", "rest"));
@@ -115,5 +124,29 @@ class DefaultNamespaceManagerTest {
Namespace newLife = manager.get("life").get();
assertThat(newLife).isEqualTo(modifiedNamespace);
verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent)event).getEventType() == HandlerEventType.BEFORE_MODIFY));
verify(eventBus).post(argThat(event -> ((NamespaceModificationEvent)event).getEventType() == HandlerEventType.MODIFY));
}
@Test
void shouldCleanUpPermissionWhenLastRepositoryOfNamespaceWasDeleted() {
when(repositoryManager.getAllNamespaces()).thenReturn(asList("universe", "rest"));
manager.cleanupDeletedNamespaces(new RepositoryEvent(DELETE, new Repository("1", "git", "life", "earth")));
assertThat(dao.get("life")).isEmpty();
}
@Test
void shouldCleanUpPermissionWhenLastRepositoryOfNamespaceWasRenamed() {
when(repositoryManager.getAllNamespaces()).thenReturn(asList("universe", "rest", "highway"));
manager.cleanupDeletedNamespaces(
new RepositoryModificationEvent(
MODIFY,
new Repository("1", "git", "highway", "earth"),
new Repository("1", "git", "life", "earth")));
assertThat(dao.get("life")).isEmpty();
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import com.google.common.collect.Lists;
@@ -31,6 +31,8 @@ import sonia.scm.HandlerEventType;
import sonia.scm.group.Group;
import sonia.scm.group.GroupEvent;
import sonia.scm.group.GroupModificationEvent;
import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespaceModificationEvent;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryEvent;
import sonia.scm.repository.RepositoryModificationEvent;
@@ -251,6 +253,55 @@ public class AuthorizationChangedEventProducerTest {
assertUserEventIsFired("trillian");
}
@Test
public void testOnNamespaceModificationEvent()
{
Namespace namespaceModified = new Namespace("hitchhiker");
namespaceModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false)));
Namespace namespace = new Namespace("hitchhiker");
namespace.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false)));
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.BEFORE_CREATE, namespaceModified, namespace));
assertEventIsNotFired();
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace));
assertEventIsNotFired();
namespaceModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), false)));
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace));
assertEventIsNotFired();
namespaceModified.setPermissions(Lists.newArrayList(new RepositoryPermission("test123", singletonList("read"), false)));
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace));
assertGlobalEventIsFired();
resetStoredEvent();
namespaceModified.setPermissions(
Lists.newArrayList(new RepositoryPermission("test", singletonList("read"), true))
);
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace));
assertGlobalEventIsFired();
resetStoredEvent();
namespaceModified.setPermissions(
Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false))
);
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace));
assertGlobalEventIsFired();
resetStoredEvent();
namespace.setPermissions(Lists.newArrayList(new RepositoryPermission("test", asList("read", "write"), false)));
namespaceModified.setPermissions(
Lists.newArrayList(new RepositoryPermission("test", asList("write", "read"), false))
);
producer.onEvent(new NamespaceModificationEvent(HandlerEventType.CREATE, namespaceModified, namespace));
assertEventIsNotFired();
}
private static class StoringAuthorizationChangedEventProducer extends AuthorizationChangedEventProducer {
private AuthorizationChangedEvent event;