mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 00:15:44 +01:00
Merge remote-tracking branch 'origin/develop' into bugfix/branchselector_overflow
This commit is contained in:
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## Unreleased
|
||||
### Added
|
||||
- Tags overview for repository [#1331](https://github.com/scm-manager/scm-manager/pull/1331)
|
||||
- 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))
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 187 KiB |
@@ -27,6 +27,8 @@ Icon | Beschreibung
|
||||
 | Öffnet die Sources-Übersicht für das Repository
|
||||
 | Ö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.
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ Innerhalb der Gefahrenzone unten auf der Seite gibt es mit entsprechenden Rechte
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
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 |
@@ -25,6 +25,8 @@ Icon | Description
|
||||
 | Opens the sources overview for the repository
|
||||
 | 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.
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ In the danger zone at the bottom you may rename the repository or delete it. If
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
To manage permissions individually, an "Advanced" dialog can be opened to manage every single permission.
|
||||
|
||||
130
scm-core/src/main/java/sonia/scm/repository/Namespace.java
Normal file
130
scm-core/src/main/java/sonia/scm/repository/Namespace.java
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 com.github.sdorra.ssp.PermissionObject;
|
||||
import com.github.sdorra.ssp.StaticPermissions;
|
||||
import org.apache.commons.lang.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang.builder.HashCodeBuilder;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Collections.unmodifiableCollection;
|
||||
|
||||
@StaticPermissions(
|
||||
value = "namespace",
|
||||
globalPermissions = {"permissionRead", "permissionWrite"},
|
||||
permissions = {},
|
||||
custom = true, customGlobal = true
|
||||
)
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "namespaces")
|
||||
public class Namespace implements PermissionObject, Cloneable {
|
||||
|
||||
private String namespace;
|
||||
private Set<RepositoryPermission> permissions = new HashSet<>();
|
||||
|
||||
public Namespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for JaxB, only.
|
||||
*/
|
||||
Namespace() {
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
|
||||
public Collection<RepositoryPermission> getPermissions() {
|
||||
return unmodifiableCollection(permissions);
|
||||
}
|
||||
|
||||
public void setPermissions(Collection<RepositoryPermission> permissions) {
|
||||
this.permissions.clear();
|
||||
this.permissions.addAll(permissions);
|
||||
}
|
||||
|
||||
public void addPermission(RepositoryPermission newPermission) {
|
||||
this.permissions.add(newPermission);
|
||||
}
|
||||
|
||||
public boolean removePermission(RepositoryPermission permission) {
|
||||
return this.permissions.remove(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return getNamespace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
if (!(o instanceof Namespace)) return false;
|
||||
|
||||
Namespace namespace1 = (Namespace) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.append(namespace, namespace1.namespace)
|
||||
.append(permissions, namespace1.permissions)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.append(namespace)
|
||||
.append(permissions)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Namespace{" +
|
||||
"namespace='" + namespace + '\'' +
|
||||
", permissions=" + permissions +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public Namespace clone() {
|
||||
try {
|
||||
Namespace clone = (Namespace) super.clone();
|
||||
clone.permissions = new HashSet<>(permissions);
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Manages namespaces. Mind that namespaces do not have a lifecycle on their own, but only do exist through
|
||||
* repositories. Therefore you cannot create or delete namespaces, but just change related settings like permissions
|
||||
* associated with them.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public interface NamespaceManager {
|
||||
|
||||
/**
|
||||
* Returns the Namespace with the given name.
|
||||
*
|
||||
* @param namespace The name of the requested namespace.
|
||||
* @return Optional with the namespace for the given name, or an empty Optional if there is no such namespace
|
||||
* (that is, there is no repository with this namespace).
|
||||
*/
|
||||
Optional<Namespace> get(String namespace);
|
||||
|
||||
/**
|
||||
* Returns a {@link java.util.Collection} of all namespaces.
|
||||
*
|
||||
* @return all namespaces
|
||||
*/
|
||||
Collection<Namespace> getAll();
|
||||
|
||||
/**
|
||||
* Modifies the given namespace.
|
||||
*
|
||||
* @param namespace The namespace to be modified.
|
||||
*/
|
||||
void modify(Namespace namespace);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,12 +119,16 @@ 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)
|
||||
&& equalVerbs(other)
|
||||
&& Objects.equal(role, other.role)
|
||||
&& Objects.equal(groupPermission, other.groupPermission);
|
||||
}
|
||||
|
||||
public boolean equalVerbs(RepositoryPermission other) {
|
||||
return verbs == null && other.verbs == null
|
||||
|| verbs != null && other.verbs != null && CollectionUtils.isEqualCollection(verbs, other.verbs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hash code value for the {@link RepositoryPermission}.
|
||||
*
|
||||
|
||||
@@ -27,7 +27,7 @@ import classNames from "classnames";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
name: ReactNode;
|
||||
url?: string;
|
||||
elements: ReactNode[];
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
import queryString from "query-string";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
|
||||
//@ts-ignore
|
||||
export const contextPath = window.ctxPath || "";
|
||||
@@ -80,3 +81,19 @@ function parsePageNumber(pageAsString: string) {
|
||||
export function getQueryStringFromLocation(location: any) {
|
||||
return location.search ? queryString.parse(location.search).q : undefined;
|
||||
}
|
||||
|
||||
export function stripEndingSlash(url: string) {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function matchedUrlFromMatch(match: any) {
|
||||
return stripEndingSlash(match.url);
|
||||
}
|
||||
|
||||
export function matchedUrl(props: RouteComponentProps) {
|
||||
const match = props.match;
|
||||
return matchedUrlFromMatch(match);
|
||||
}
|
||||
|
||||
@@ -58,5 +58,6 @@ export type NamespaceCollection = {
|
||||
|
||||
export type RepositoryGroup = {
|
||||
name: string;
|
||||
namespace?: Namespace;
|
||||
repositories: Repository[];
|
||||
};
|
||||
|
||||
14
scm-ui/ui-webapp/public/locales/de/namespaces.json
Normal file
14
scm-ui/ui-webapp/public/locales/de/namespaces.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"namespaceRoot": {
|
||||
"menu": {
|
||||
"navigationLabel": "Namespace",
|
||||
"settingsNavLink": "Einstellungen",
|
||||
"permissionsNavLink": "Berechtigungen"
|
||||
}
|
||||
},
|
||||
"repositoryOverview": {
|
||||
"settings": {
|
||||
"tooltip": "Einstellungen für den Namespace"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +234,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": {
|
||||
|
||||
14
scm-ui/ui-webapp/public/locales/en/namespaces.json
Normal file
14
scm-ui/ui-webapp/public/locales/en/namespaces.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"namespaceRoot": {
|
||||
"menu": {
|
||||
"navigationLabel": "Namespace",
|
||||
"settingsNavLink": "Settings",
|
||||
"permissionsNavLink": "Permissions"
|
||||
}
|
||||
},
|
||||
"repositoryOverview": {
|
||||
"settings": {
|
||||
"tooltip": "Namespace related settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,7 +234,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": {
|
||||
|
||||
@@ -45,6 +45,7 @@ import GlobalConfig from "./GlobalConfig";
|
||||
import RepositoryRoles from "../roles/containers/RepositoryRoles";
|
||||
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
|
||||
import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -54,30 +55,17 @@ type Props = RouteComponentProps &
|
||||
};
|
||||
|
||||
class Admin extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
if (url.includes("role")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
matchesRoles = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}/role/`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { links, availablePluginsLink, installedPluginsLink, t } = this.props;
|
||||
const { links, availablePluginsLink, installedPluginsLink, match, t } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const extensionProps = {
|
||||
links,
|
||||
url
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { History } from "history";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
@@ -34,39 +34,29 @@ import { fetchRoleByName, getFetchRoleFailure, getRoleByName, isFetchRolePending
|
||||
import PermissionRoleDetail from "../components/PermissionRoleDetails";
|
||||
import EditRepositoryRole from "./EditRepositoryRole";
|
||||
import { compose } from "redux";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
roleName: string;
|
||||
role: RepositoryRole;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
repositoryRolesLink: string;
|
||||
disabled: boolean;
|
||||
type Props = WithTranslation &
|
||||
RouteComponentProps & {
|
||||
roleName: string;
|
||||
role: RepositoryRole;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
repositoryRolesLink: string;
|
||||
disabled: boolean;
|
||||
|
||||
// dispatcher function
|
||||
fetchRoleByName: (p1: string, p2: string) => void;
|
||||
// dispatcher function
|
||||
fetchRoleByName: (p1: string, p2: string) => void;
|
||||
|
||||
// context objects
|
||||
match: any;
|
||||
history: History;
|
||||
};
|
||||
// context objects
|
||||
history: History;
|
||||
};
|
||||
|
||||
class SingleRepositoryRole extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchRoleByName(this.props.repositoryRolesLink, this.props.roleName);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, role } = this.props;
|
||||
|
||||
@@ -80,7 +70,7 @@ class SingleRepositoryRole extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
role,
|
||||
|
||||
@@ -46,6 +46,7 @@ import CreateGroup from "../groups/containers/CreateGroup";
|
||||
import Admin from "../admin/containers/Admin";
|
||||
|
||||
import Profile from "./Profile";
|
||||
import NamespaceRoot from "../repos/namespaces/containers/NamespaceRoot";
|
||||
|
||||
type Props = {
|
||||
me: Me;
|
||||
@@ -80,6 +81,7 @@ class Main extends React.Component<Props> {
|
||||
<ProtectedRoute exact path="/repos/:namespace" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute exact path="/repos/:namespace/:page" component={Overview} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/repo/:namespace/:name" component={RepositoryRoot} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/namespace/:namespaceName" component={NamespaceRoot} authenticated={authenticated} />
|
||||
<Redirect exact strict from="/users" to="/users/" />
|
||||
<ProtectedRoute exact path="/users/" component={Users} authenticated={authenticated} />
|
||||
<ProtectedRoute path="/users/create" component={CreateUser} authenticated={authenticated} />
|
||||
|
||||
@@ -44,6 +44,7 @@ import ProfileInfo from "./ProfileInfo";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys";
|
||||
import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -54,17 +55,6 @@ type Props = RouteComponentProps &
|
||||
};
|
||||
|
||||
class Profile extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
mayChangePassword = () => {
|
||||
const { me } = this.props;
|
||||
return !!me?._links?.password;
|
||||
@@ -76,7 +66,7 @@ class Profile extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const { me, t } = this.props;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import { Details } from "./../components/table";
|
||||
import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks";
|
||||
import EditGroup from "./EditGroup";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -63,17 +64,6 @@ class SingleGroup extends React.Component<Props> {
|
||||
this.props.fetchGroupByName(this.props.groupLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, group } = this.props;
|
||||
|
||||
@@ -85,7 +75,7 @@ class SingleGroup extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
group,
|
||||
|
||||
@@ -31,6 +31,7 @@ import { fetchBranch, getBranch, getFetchBranchFailure, isFetchBranchPending } f
|
||||
import { ErrorNotification, Loading, NotFoundError } from "@scm-manager/ui-components";
|
||||
import { History } from "history";
|
||||
import queryString from "query-string";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -54,21 +55,10 @@ class BranchRoot extends React.Component<Props> {
|
||||
fetchBranch(repository, branchName);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, branch, loading, error, match, location } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof NotFoundError && queryString.parse(location.search).create === "true") {
|
||||
|
||||
@@ -22,21 +22,37 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { CardColumnGroup, RepositoryEntry } from "@scm-manager/ui-components";
|
||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||
import { Icon } from "@scm-manager/ui-components";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
type Props = WithTranslation & {
|
||||
group: RepositoryGroup;
|
||||
};
|
||||
|
||||
class RepositoryGroupEntry extends React.Component<Props> {
|
||||
render() {
|
||||
const { group } = this.props;
|
||||
const { group, t } = this.props;
|
||||
const settingsLink = group.namespace?._links?.permissions && (
|
||||
<Link to={`/namespace/${group.name}/settings`}>
|
||||
<Icon color={"is-link"} name={"cog"} title={t("repositoryOverview.settings.tooltip")} />
|
||||
</Link>
|
||||
);
|
||||
const namespaceHeader = (
|
||||
<>
|
||||
<Link to={`/repos/${group.name}/`} className={"has-text-dark"}>
|
||||
{group.name}
|
||||
</Link>{" "}
|
||||
{settingsLink}
|
||||
</>
|
||||
);
|
||||
const entries = group.repositories.map((repository, index) => {
|
||||
return <RepositoryEntry repository={repository} key={index} />;
|
||||
});
|
||||
return <CardColumnGroup name={group.name} url={`/repos/${group.name}/`} elements={entries} />;
|
||||
return <CardColumnGroup name={namespaceHeader} elements={entries} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default RepositoryGroupEntry;
|
||||
export default withTranslation("namespaces")(RepositoryGroupEntry);
|
||||
|
||||
@@ -23,20 +23,21 @@
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { NamespaceCollection, Repository } from "@scm-manager/ui-types";
|
||||
|
||||
import groupByNamespace from "./groupByNamespace";
|
||||
import RepositoryGroupEntry from "./RepositoryGroupEntry";
|
||||
|
||||
type Props = {
|
||||
repositories: Repository[];
|
||||
namespaces: NamespaceCollection;
|
||||
};
|
||||
|
||||
class RepositoryList extends React.Component<Props> {
|
||||
render() {
|
||||
const { repositories } = this.props;
|
||||
const { repositories, namespaces } = this.props;
|
||||
|
||||
const groups = groupByNamespace(repositories);
|
||||
const groups = groupByNamespace(repositories, namespaces);
|
||||
return (
|
||||
<div className="content">
|
||||
{groups.map(group => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -22,17 +22,22 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Repository, RepositoryGroup } from "@scm-manager/ui-types";
|
||||
import { NamespaceCollection, Repository, RepositoryGroup } from "@scm-manager/ui-types";
|
||||
|
||||
export default function groupByNamespace(repositories: Repository[]): RepositoryGroup[] {
|
||||
export default function groupByNamespace(
|
||||
repositories: Repository[],
|
||||
namespaces: NamespaceCollection
|
||||
): RepositoryGroup[] {
|
||||
const groups = {};
|
||||
for (const repository of repositories) {
|
||||
const groupName = repository.namespace;
|
||||
|
||||
let group = groups[groupName];
|
||||
if (!group) {
|
||||
const namespace = findNamespace(namespaces, groupName);
|
||||
group = {
|
||||
name: groupName,
|
||||
namespace: namespace,
|
||||
repositories: []
|
||||
};
|
||||
groups[groupName] = group;
|
||||
@@ -58,3 +63,7 @@ function sortByName(a, b) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findNamespace(namespaces: NamespaceCollection, namespaceToFind: string) {
|
||||
return namespaces._embedded.namespaces.find(namespace => namespace.namespace === namespaceToFind);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import Changesets from "./Changesets";
|
||||
import { compose } from "redux";
|
||||
import CodeActionBar from "../codeSection/components/CodeActionBar";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation &
|
||||
RouteComponentProps & {
|
||||
@@ -38,13 +39,6 @@ type Props = WithTranslation &
|
||||
};
|
||||
|
||||
class ChangesetsRoot extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
isBranchAvailable = () => {
|
||||
const { branches, selectedBranch } = this.props;
|
||||
return branches?.filter(b => b.name === selectedBranch).length === 0;
|
||||
@@ -75,7 +69,7 @@ class ChangesetsRoot extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = this.stripEndingSlash(match.url);
|
||||
const url = urls.stripEndingSlash(match.url);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import RepositoryForm from "../components/form";
|
||||
import { Repository, Links } from "@scm-manager/ui-types";
|
||||
import { getModifyRepoFailure, isModifyRepoPending, modifyRepo, modifyRepoReset } from "../modules/repos";
|
||||
@@ -33,8 +33,9 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import { compose } from "redux";
|
||||
import DangerZone from "./DangerZone";
|
||||
import { getLinks } from "../../modules/indexResource";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
type Props = RouteComponentProps & {
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
indexLinks: Links;
|
||||
@@ -45,7 +46,6 @@ type Props = {
|
||||
// context props
|
||||
repository: Repository;
|
||||
history: History;
|
||||
match: any;
|
||||
};
|
||||
|
||||
class EditRepo extends React.Component<Props> {
|
||||
@@ -59,21 +59,10 @@ class EditRepo extends React.Component<Props> {
|
||||
history.push(`/repo/${repository.namespace}/${repository.name}`);
|
||||
};
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, indexLinks } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
repository,
|
||||
|
||||
@@ -133,12 +133,12 @@ class Overview extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderRepositoryList() {
|
||||
const { collection, page, location, t } = this.props;
|
||||
const { collection, page, location, namespaces, t } = this.props;
|
||||
|
||||
if (collection._embedded && collection._embedded.repositories.length > 0) {
|
||||
return (
|
||||
<>
|
||||
<RepositoryList repositories={collection._embedded.repositories} />
|
||||
<RepositoryList repositories={collection._embedded.repositories} namespaces={namespaces} />
|
||||
<LinkPaginator collection={collection} page={page} filter={urls.getQueryStringFromLocation(location)} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -56,6 +56,7 @@ import SourceExtensions from "../sources/containers/SourceExtensions";
|
||||
import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components";
|
||||
import TagsOverview from "../tags/container/TagsOverview";
|
||||
import TagRoot from "../tags/container/TagRoot";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -84,31 +85,20 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
matchesBranches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}/branch/.+/info`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
matchesTags = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}/tag/.+/info`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
matchesCode = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
const regex = new RegExp(`${url}(/code)/.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
@@ -126,7 +116,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
|
||||
evaluateDestinationForCodeLink = () => {
|
||||
const { repository } = this.props;
|
||||
const url = `${this.matchedUrl()}/code`;
|
||||
const url = `${urls.matchedUrl(this.props)}/code`;
|
||||
if (repository?._links?.sources) {
|
||||
return `${url}/sources/`;
|
||||
}
|
||||
@@ -146,7 +136,7 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
repository,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import * as types from "../../modules/types";
|
||||
import {
|
||||
Action,
|
||||
Action, Namespace,
|
||||
NamespaceCollection,
|
||||
Repository,
|
||||
RepositoryCollection,
|
||||
@@ -66,6 +66,11 @@ export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
||||
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
|
||||
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
export const FETCH_NAMESPACE = "scm/repos/FETCH_NAMESPACE";
|
||||
export const FETCH_NAMESPACE_PENDING = `${FETCH_NAMESPACE}_${types.PENDING_SUFFIX}`;
|
||||
export const FETCH_NAMESPACE_SUCCESS = `${FETCH_NAMESPACE}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_NAMESPACE_FAILURE = `${FETCH_NAMESPACE}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
export const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
||||
|
||||
export const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy";
|
||||
@@ -388,6 +393,50 @@ export function deleteRepoFailure(repository: Repository, error: Error): Action
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespace(link: string, namespaceName: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchNamespacePending(namespaceName));
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(namespace => {
|
||||
dispatch(fetchNamespaceSuccess(namespace));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchNamespaceFailure(namespaceName, err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespacePending(namespaceName: string): Action {
|
||||
return {
|
||||
type: FETCH_NAMESPACE_PENDING,
|
||||
payload: {
|
||||
namespaceName
|
||||
},
|
||||
itemId: namespaceName
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespaceSuccess(namespace: Namespace): Action {
|
||||
return {
|
||||
type: FETCH_NAMESPACE_SUCCESS,
|
||||
payload: namespace,
|
||||
itemId: namespace.namespace
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchNamespaceFailure(namespaceName: string, error: Error): Action {
|
||||
return {
|
||||
type: FETCH_NAMESPACE_FAILURE,
|
||||
payload: {
|
||||
namespaceName,
|
||||
error
|
||||
},
|
||||
itemId: namespaceName
|
||||
};
|
||||
}
|
||||
|
||||
// reducer
|
||||
|
||||
function createIdentifier(repository: Repository) {
|
||||
@@ -425,6 +474,17 @@ const reducerByNames = (state: object, repository: Repository) => {
|
||||
};
|
||||
};
|
||||
|
||||
const reducerForNamespace = (state: object, namespace: Namespace) => {
|
||||
const identifier = namespace.namespace;
|
||||
return {
|
||||
...state,
|
||||
namespacesByNames: {
|
||||
...state.namespacesByNames,
|
||||
[identifier]: namespace
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const reducerForNamespaces = (state: object, namespaces: NamespaceCollection) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -449,6 +509,8 @@ export default function reducer(
|
||||
return reducerForNamespaces(state, action.payload);
|
||||
case FETCH_REPO_SUCCESS:
|
||||
return reducerByNames(state, action.payload);
|
||||
case FETCH_NAMESPACE_SUCCESS:
|
||||
return reducerForNamespace(state, action.payload);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -497,10 +559,17 @@ export function getFetchNamespacesFailure(state: object) {
|
||||
return getFailure(state, FETCH_NAMESPACES);
|
||||
}
|
||||
|
||||
export function getNamespace(state: object, namespace: string) {
|
||||
if (state.namespaces) {
|
||||
return state.namespaces[namespace];
|
||||
}
|
||||
export function isFetchNamespacePending(state: object) {
|
||||
return isPending(state, FETCH_NAMESPACE);
|
||||
}
|
||||
|
||||
export function getFetchNamespaceFailure(state: object) {
|
||||
return getFailure(state, FETCH_NAMESPACE);
|
||||
}
|
||||
|
||||
export function fetchNamespaceByName(link: string, namespaceName: string) {
|
||||
const namespaceUrl = link.endsWith("/") ? link : link + "/";
|
||||
return fetchNamespace(`${namespaceUrl}${namespaceName}`, namespaceName);
|
||||
}
|
||||
|
||||
export function isFetchRepoPending(state: object, namespace: string, name: string) {
|
||||
@@ -511,6 +580,12 @@ export function getFetchRepoFailure(state: object, namespace: string, name: stri
|
||||
return getFailure(state, FETCH_REPO, namespace + "/" + name);
|
||||
}
|
||||
|
||||
export function getNamespace(state: object, namespaceName: string) {
|
||||
if (state.repos && state.repos.namespacesByNames) {
|
||||
return state.repos.namespacesByNames[namespaceName];
|
||||
}
|
||||
}
|
||||
|
||||
export function isAbleToCreateRepos(state: object) {
|
||||
return !!(state.repos && state.repos.list && state.repos.list._links && state.repos.list._links.create);
|
||||
}
|
||||
@@ -539,7 +614,12 @@ export function getDeleteRepoFailure(state: object, namespace: string, name: str
|
||||
return getFailure(state, DELETE_REPO, namespace + "/" + name);
|
||||
}
|
||||
|
||||
export function getPermissionsLink(state: object, namespace: string, name: string) {
|
||||
const repo = getRepository(state, namespace, name);
|
||||
return repo && repo._links ? repo._links.permissions.href : undefined;
|
||||
export function getPermissionsLink(state: object, namespaceName: string, repoName?: string) {
|
||||
if (repoName) {
|
||||
const repo = getRepository(state, namespaceName, repoName);
|
||||
return repo?._links ? repo._links.permissions.href : undefined;
|
||||
} else {
|
||||
const namespace = getNamespace(state, namespaceName);
|
||||
return namespace?._links ? namespace?._links?.permissions?.href : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom";
|
||||
import { fetchNamespaceByName, getNamespace, isFetchNamespacePending } from "../../modules/repos";
|
||||
import { getNamespacesLink } from "../../../modules/indexResource";
|
||||
import { Namespace } from "@scm-manager/ui-types";
|
||||
import {
|
||||
CustomQueryFlexWrappedColumns,
|
||||
ErrorPage,
|
||||
Loading,
|
||||
Page,
|
||||
PrimaryContentColumn,
|
||||
SecondaryNavigation,
|
||||
SecondaryNavigationColumn,
|
||||
StateMenuContextProvider,
|
||||
SubNavigation
|
||||
} from "@scm-manager/ui-components";
|
||||
import Permissions from "../../permissions/containers/Permissions";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
loading: boolean;
|
||||
namespaceName: string;
|
||||
namespacesLink: string;
|
||||
namespace: Namespace;
|
||||
error: Error;
|
||||
|
||||
// dispatch functions
|
||||
fetchNamespace: (link: string, namespace: string) => void;
|
||||
};
|
||||
|
||||
class NamespaceRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { namespacesLink, namespaceName, fetchNamespace } = this.props;
|
||||
fetchNamespace(namespacesLink, namespaceName);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, error, namespaceName, namespace, t } = this.props;
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
namespace,
|
||||
url
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage title={t("namespaceRoot.errorTitle")} subtitle={t("namespaceRoot.errorSubtitle")} error={error} />
|
||||
);
|
||||
}
|
||||
|
||||
if (!namespace || loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StateMenuContextProvider>
|
||||
<Page title={namespaceName}>
|
||||
<CustomQueryFlexWrappedColumns>
|
||||
<PrimaryContentColumn>
|
||||
<Switch>
|
||||
<Redirect exact from={`${url}/settings`} to={`${url}/settings/permissions`} />
|
||||
<Route
|
||||
path={`${url}/settings/permissions`}
|
||||
render={() => {
|
||||
return <Permissions namespace={namespaceName} />;
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</PrimaryContentColumn>
|
||||
<SecondaryNavigationColumn>
|
||||
<SecondaryNavigation label={t("namespaceRoot.menu.navigationLabel")}>
|
||||
<ExtensionPoint name="namespace.navigation.topLevel" props={extensionProps} renderAll={true} />
|
||||
<ExtensionPoint name="namespace.route" props={extensionProps} renderAll={true} />
|
||||
<SubNavigation
|
||||
to={`${url}/settings`}
|
||||
label={t("namespaceRoot.menu.settingsNavLink")}
|
||||
title={t("namespaceRoot.menu.settingsNavLink")}
|
||||
>
|
||||
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} namespace={namespace} />
|
||||
<ExtensionPoint name="namespace.setting" props={extensionProps} renderAll={true} />
|
||||
</SubNavigation>
|
||||
</SecondaryNavigation>
|
||||
</SecondaryNavigationColumn>
|
||||
</CustomQueryFlexWrappedColumns>
|
||||
</Page>
|
||||
</StateMenuContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { namespaceName } = ownProps.match.params;
|
||||
const namespacesLink = getNamespacesLink(state);
|
||||
const namespace = getNamespace(state, namespaceName);
|
||||
const loading = isFetchNamespacePending(state);
|
||||
return { namespaceName, namespacesLink, loading, namespace };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchNamespace: (link: string, namespaceName: string) => {
|
||||
dispatch(fetchNamespaceByName(link, namespaceName));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("namespaces")(NamespaceRoot));
|
||||
@@ -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.
|
||||
*/
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Namespace } from "@scm-manager/ui-types";
|
||||
import { NavLink } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
permissionUrl: string;
|
||||
namespace: Namespace;
|
||||
};
|
||||
|
||||
class PermissionsNavLink extends React.Component<Props> {
|
||||
hasPermissionsLink = () => {
|
||||
return this.props.namespace?._links?.permissions;
|
||||
};
|
||||
render() {
|
||||
if (!this.hasPermissionsLink()) {
|
||||
return null;
|
||||
}
|
||||
const { permissionUrl, t } = this.props;
|
||||
return <NavLink to={permissionUrl} label={t("namespaceRoot.menu.permissionsNavLink")} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("namespaces")(PermissionsNavLink);
|
||||
@@ -57,13 +57,12 @@ import {
|
||||
getRepositoryVerbsLink,
|
||||
getUserAutoCompleteLink
|
||||
} from "../../../modules/indexResource";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
availablePermissions: boolean;
|
||||
availableRepositoryRoles: RepositoryRole[];
|
||||
availableVerbs: string[];
|
||||
namespace: string;
|
||||
repoName: string;
|
||||
repoName?: string;
|
||||
loading: boolean;
|
||||
error: Error;
|
||||
permissions: PermissionCollection;
|
||||
@@ -77,17 +76,17 @@ type Props = WithTranslation & {
|
||||
|
||||
// dispatch functions
|
||||
fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => void;
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => void;
|
||||
fetchPermissions: (link: string, namespace: string, repoName?: string) => void;
|
||||
createPermission: (
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
repoName?: string,
|
||||
callback?: () => void
|
||||
) => void;
|
||||
createPermissionReset: (p1: string, p2: string) => void;
|
||||
modifyPermissionReset: (p1: string, p2: string) => void;
|
||||
deletePermissionReset: (p1: string, p2: string) => void;
|
||||
createPermissionReset: (namespace: string, repoName?: string) => void;
|
||||
modifyPermissionReset: (namespace: string, repoName?: string) => void;
|
||||
deletePermissionReset: (namespace: string, repoName?: string) => void;
|
||||
|
||||
// context props
|
||||
match: any;
|
||||
@@ -241,7 +240,7 @@ const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => {
|
||||
fetchPermissions: (link: string, namespace: string, repoName?: string) => {
|
||||
dispatch(fetchPermissions(link, namespace, repoName));
|
||||
},
|
||||
fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => {
|
||||
@@ -256,13 +255,13 @@ const mapDispatchToProps = (dispatch: any) => {
|
||||
) => {
|
||||
dispatch(createPermission(link, permission, namespace, repoName, callback));
|
||||
},
|
||||
createPermissionReset: (namespace: string, repoName: string) => {
|
||||
createPermissionReset: (namespace: string, repoName?: string) => {
|
||||
dispatch(createPermissionReset(namespace, repoName));
|
||||
},
|
||||
modifyPermissionReset: (namespace: string, repoName: string) => {
|
||||
modifyPermissionReset: (namespace: string, repoName?: string) => {
|
||||
dispatch(modifyPermissionReset(namespace, repoName));
|
||||
},
|
||||
deletePermissionReset: (namespace: string, repoName: string) => {
|
||||
deletePermissionReset: (namespace: string, repoName?: string) => {
|
||||
dispatch(deletePermissionReset(namespace, repoName));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,14 +43,14 @@ type Props = WithTranslation & {
|
||||
availableRepositoryRoles: RepositoryRole[];
|
||||
availableRepositoryVerbs: string[];
|
||||
submitForm: (p: Permission) => void;
|
||||
modifyPermission: (permission: Permission, namespace: string, name: string) => void;
|
||||
modifyPermission: (permission: Permission, namespace: string, name?: string) => void;
|
||||
permission: Permission;
|
||||
namespace: string;
|
||||
repoName: string;
|
||||
repoName?: string;
|
||||
match: any;
|
||||
history: History;
|
||||
loading: boolean;
|
||||
deletePermission: (permission: Permission, namespace: string, name: string) => void;
|
||||
deletePermission: (permission: Permission, namespace: string, name?: string) => void;
|
||||
deleteLoading: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ export function fetchAvailableFailure(error: Error): Action {
|
||||
|
||||
// fetch permissions
|
||||
|
||||
export function fetchPermissions(link: string, namespace: string, repoName: string) {
|
||||
export function fetchPermissions(link: string, namespace: string, repoName?: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchPermissionsPending(namespace, repoName));
|
||||
return apiClient
|
||||
@@ -146,26 +146,26 @@ export function fetchPermissions(link: string, namespace: string, repoName: stri
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPermissionsPending(namespace: string, repoName: string): Action {
|
||||
export function fetchPermissionsPending(namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: FETCH_PERMISSIONS_PENDING,
|
||||
payload: {
|
||||
namespace,
|
||||
repoName
|
||||
},
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPermissionsSuccess(permissions: any, namespace: string, repoName: string): Action {
|
||||
export function fetchPermissionsSuccess(permissions: any, namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: FETCH_PERMISSIONS_SUCCESS,
|
||||
payload: permissions,
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchPermissionsFailure(namespace: string, repoName: string, error: Error): Action {
|
||||
export function fetchPermissionsFailure(namespace: string, repoName?: string, error: Error): Action {
|
||||
return {
|
||||
type: FETCH_PERMISSIONS_FAILURE,
|
||||
payload: {
|
||||
@@ -173,13 +173,13 @@ export function fetchPermissionsFailure(namespace: string, repoName: string, err
|
||||
repoName,
|
||||
error
|
||||
},
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
// modify permission
|
||||
|
||||
export function modifyPermission(permission: Permission, namespace: string, repoName: string, callback?: () => void) {
|
||||
export function modifyPermission(permission: Permission, namespace: string, repoName?: string, callback?: () => void) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(modifyPermissionPending(permission, namespace, repoName));
|
||||
return apiClient
|
||||
@@ -196,7 +196,7 @@ export function modifyPermission(permission: Permission, namespace: string, repo
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyPermissionPending(permission: Permission, namespace: string, repoName: string): Action {
|
||||
export function modifyPermissionPending(permission: Permission, namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: MODIFY_PERMISSION_PENDING,
|
||||
payload: permission,
|
||||
@@ -204,12 +204,12 @@ export function modifyPermissionPending(permission: Permission, namespace: strin
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyPermissionSuccess(permission: Permission, namespace: string, repoName: string): Action {
|
||||
export function modifyPermissionSuccess(permission: Permission, namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: MODIFY_PERMISSION_SUCCESS,
|
||||
payload: {
|
||||
permission,
|
||||
position: namespace + "/" + repoName
|
||||
position: createPermissionStateKey(namespace, repoName)
|
||||
},
|
||||
itemId: createItemId(permission, namespace, repoName)
|
||||
};
|
||||
@@ -219,7 +219,7 @@ export function modifyPermissionFailure(
|
||||
permission: Permission,
|
||||
error: Error,
|
||||
namespace: string,
|
||||
repoName: string
|
||||
repoName?: string
|
||||
): Action {
|
||||
return {
|
||||
type: MODIFY_PERMISSION_FAILURE,
|
||||
@@ -240,14 +240,14 @@ function newPermissions(oldPermissions: PermissionCollection, newPermission: Per
|
||||
}
|
||||
}
|
||||
|
||||
export function modifyPermissionReset(namespace: string, repoName: string) {
|
||||
export function modifyPermissionReset(namespace: string, repoName?: string) {
|
||||
return {
|
||||
type: MODIFY_PERMISSION_RESET,
|
||||
payload: {
|
||||
namespace,
|
||||
repoName
|
||||
},
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ export function createPermission(
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
repoName?: string,
|
||||
callback?: () => void
|
||||
) {
|
||||
return function(dispatch: Dispatch) {
|
||||
@@ -281,48 +281,48 @@ export function createPermission(
|
||||
export function createPermissionPending(
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string
|
||||
repoName?: string
|
||||
): Action {
|
||||
return {
|
||||
type: CREATE_PERMISSION_PENDING,
|
||||
payload: permission,
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
export function createPermissionSuccess(
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string
|
||||
repoName?: string
|
||||
): Action {
|
||||
return {
|
||||
type: CREATE_PERMISSION_SUCCESS,
|
||||
payload: {
|
||||
permission,
|
||||
position: namespace + "/" + repoName
|
||||
position: createPermissionStateKey(namespace, repoName)
|
||||
},
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
export function createPermissionFailure(error: Error, namespace: string, repoName: string): Action {
|
||||
export function createPermissionFailure(error: Error, namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: CREATE_PERMISSION_FAILURE,
|
||||
payload: error,
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
export function createPermissionReset(namespace: string, repoName: string) {
|
||||
export function createPermissionReset(namespace: string, repoName?: string) {
|
||||
return {
|
||||
type: CREATE_PERMISSION_RESET,
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
// delete permission
|
||||
|
||||
export function deletePermission(permission: Permission, namespace: string, repoName: string, callback?: () => void) {
|
||||
export function deletePermission(permission: Permission, namespace: string, repoName?: string, callback?: () => void) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(deletePermissionPending(permission, namespace, repoName));
|
||||
return apiClient
|
||||
@@ -339,7 +339,7 @@ export function deletePermission(permission: Permission, namespace: string, repo
|
||||
};
|
||||
}
|
||||
|
||||
export function deletePermissionPending(permission: Permission, namespace: string, repoName: string): Action {
|
||||
export function deletePermissionPending(permission: Permission, namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: DELETE_PERMISSION_PENDING,
|
||||
payload: permission,
|
||||
@@ -347,12 +347,12 @@ export function deletePermissionPending(permission: Permission, namespace: strin
|
||||
};
|
||||
}
|
||||
|
||||
export function deletePermissionSuccess(permission: Permission, namespace: string, repoName: string): Action {
|
||||
export function deletePermissionSuccess(permission: Permission, namespace: string, repoName?: string): Action {
|
||||
return {
|
||||
type: DELETE_PERMISSION_SUCCESS,
|
||||
payload: {
|
||||
permission,
|
||||
position: namespace + "/" + repoName
|
||||
position: createPermissionStateKey(namespace, repoName)
|
||||
},
|
||||
itemId: createItemId(permission, namespace, repoName)
|
||||
};
|
||||
@@ -361,7 +361,7 @@ export function deletePermissionSuccess(permission: Permission, namespace: strin
|
||||
export function deletePermissionFailure(
|
||||
permission: Permission,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
repoName?: string,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
@@ -374,14 +374,14 @@ export function deletePermissionFailure(
|
||||
};
|
||||
}
|
||||
|
||||
export function deletePermissionReset(namespace: string, repoName: string) {
|
||||
export function deletePermissionReset(namespace: string, repoName?: string) {
|
||||
return {
|
||||
type: DELETE_PERMISSION_RESET,
|
||||
payload: {
|
||||
namespace,
|
||||
repoName
|
||||
},
|
||||
itemId: namespace + "/" + repoName
|
||||
itemId: createPermissionStateKey(namespace, repoName)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -398,9 +398,9 @@ function deletePermissionFromState(oldPermissions: PermissionCollection, permiss
|
||||
return newPermission;
|
||||
}
|
||||
|
||||
function createItemId(permission: Permission, namespace: string, repoName: string) {
|
||||
function createItemId(permission: Permission, namespace: string, repoName?: string) {
|
||||
const groupPermission = permission.groupPermission ? "@" : "";
|
||||
return namespace + "/" + repoName + "/" + groupPermission + permission.name;
|
||||
return createPermissionStateKey(namespace, repoName) + "/" + groupPermission + permission.name;
|
||||
}
|
||||
|
||||
// reducer
|
||||
@@ -427,7 +427,7 @@ export default function reducer(
|
||||
createPermission: !!action.payload._links.create
|
||||
}
|
||||
};
|
||||
case MODIFY_PERMISSION_SUCCESS:
|
||||
case MODIFY_PERMISSION_SUCCESS: {
|
||||
const positionOfPermission = action.payload.position;
|
||||
const newPermission = newPermissions(state[action.payload.position].entries, action.payload.permission);
|
||||
return {
|
||||
@@ -437,7 +437,8 @@ export default function reducer(
|
||||
entries: newPermission
|
||||
}
|
||||
};
|
||||
case CREATE_PERMISSION_SUCCESS:
|
||||
}
|
||||
case CREATE_PERMISSION_SUCCESS: {
|
||||
// return state;
|
||||
const position = action.payload.position;
|
||||
const permissions = state[action.payload.position].entries;
|
||||
@@ -449,9 +450,10 @@ export default function reducer(
|
||||
entries: permissions
|
||||
}
|
||||
};
|
||||
case DELETE_PERMISSION_SUCCESS:
|
||||
}
|
||||
case DELETE_PERMISSION_SUCCESS: {
|
||||
const permissionPosition = action.payload.position;
|
||||
const new_Permissions = deletePermissionFromState(
|
||||
const newPermissions = deletePermissionFromState(
|
||||
state[action.payload.position].entries,
|
||||
action.payload.permission
|
||||
);
|
||||
@@ -459,9 +461,10 @@ export default function reducer(
|
||||
...state,
|
||||
[permissionPosition]: {
|
||||
...state[permissionPosition],
|
||||
entries: new_Permissions
|
||||
entries: newPermissions
|
||||
}
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -490,9 +493,9 @@ function available(state: object) {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getPermissionsOfRepo(state: object, namespace: string, repoName: string) {
|
||||
if (state.permissions && state.permissions[namespace + "/" + repoName]) {
|
||||
return state.permissions[namespace + "/" + repoName].entries;
|
||||
export function getPermissionsOfRepo(state: object, namespace: string, repoName?: string) {
|
||||
if (state.permissions && state.permissions[createPermissionStateKey(namespace, repoName)]) {
|
||||
return state.permissions[createPermissionStateKey(namespace, repoName)].entries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,52 +503,62 @@ export function isFetchAvailablePermissionsPending(state: object) {
|
||||
return isPending(state, FETCH_AVAILABLE, "available");
|
||||
}
|
||||
|
||||
export function isFetchPermissionsPending(state: object, namespace: string, repoName: string) {
|
||||
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
|
||||
export function isFetchPermissionsPending(state: object, namespace: string, repoName?: string) {
|
||||
return isPending(state, FETCH_PERMISSIONS, createPermissionStateKey(namespace, repoName));
|
||||
}
|
||||
|
||||
export function getFetchAvailablePermissionsFailure(state: object) {
|
||||
return getFailure(state, FETCH_AVAILABLE, "available");
|
||||
}
|
||||
|
||||
export function getFetchPermissionsFailure(state: object, namespace: string, repoName: string) {
|
||||
return getFailure(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
|
||||
export function getFetchPermissionsFailure(state: object, namespace: string, repoName?: string) {
|
||||
return getFailure(state, FETCH_PERMISSIONS, createPermissionStateKey(namespace, repoName));
|
||||
}
|
||||
|
||||
export function isModifyPermissionPending(state: object, namespace: string, repoName: string, permission: Permission) {
|
||||
return isPending(state, MODIFY_PERMISSION, createItemId(permission, namespace, repoName));
|
||||
export function isModifyPermissionPending(state: object, namespace: string, repoName?: string, permission: Permission) {
|
||||
return isPending(state, MODIFY_PERMISSION, createItemId(permission, createPermissionStateKey(namespace, repoName)));
|
||||
}
|
||||
|
||||
export function getModifyPermissionFailure(state: object, namespace: string, repoName: string, permission: Permission) {
|
||||
return getFailure(state, MODIFY_PERMISSION, createItemId(permission, namespace, repoName));
|
||||
export function getModifyPermissionFailure(
|
||||
state: object,
|
||||
namespace: string,
|
||||
repoName?: string,
|
||||
permission: Permission
|
||||
) {
|
||||
return getFailure(state, MODIFY_PERMISSION, createItemId(permission, createPermissionStateKey(namespace, repoName)));
|
||||
}
|
||||
|
||||
export function hasCreatePermission(state: object, namespace: string, repoName: string) {
|
||||
if (state.permissions && state.permissions[namespace + "/" + repoName])
|
||||
return state.permissions[namespace + "/" + repoName].createPermission;
|
||||
export function hasCreatePermission(state: object, namespace: string, repoName?: string) {
|
||||
if (state.permissions && state.permissions[createPermissionStateKey(namespace, repoName)])
|
||||
return state.permissions[createPermissionStateKey(namespace, repoName)].createPermission;
|
||||
else return null;
|
||||
}
|
||||
|
||||
export function isCreatePermissionPending(state: object, namespace: string, repoName: string) {
|
||||
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
|
||||
export function isCreatePermissionPending(state: object, namespace: string, repoName?: string) {
|
||||
return isPending(state, CREATE_PERMISSION, createPermissionStateKey(namespace, repoName));
|
||||
}
|
||||
|
||||
export function getCreatePermissionFailure(state: object, namespace: string, repoName: string) {
|
||||
return getFailure(state, CREATE_PERMISSION, namespace + "/" + repoName);
|
||||
export function getCreatePermissionFailure(state: object, namespace: string, repoName?: string) {
|
||||
return getFailure(state, CREATE_PERMISSION, createPermissionStateKey(namespace, repoName));
|
||||
}
|
||||
|
||||
export function isDeletePermissionPending(state: object, namespace: string, repoName: string, permission: Permission) {
|
||||
export function isDeletePermissionPending(state: object, namespace: string, repoName?: string, permission: Permission) {
|
||||
return isPending(state, DELETE_PERMISSION, createItemId(permission, namespace, repoName));
|
||||
}
|
||||
|
||||
export function getDeletePermissionFailure(state: object, namespace: string, repoName: string, permission: Permission) {
|
||||
export function getDeletePermissionFailure(
|
||||
state: object,
|
||||
namespace: string,
|
||||
repoName?: string,
|
||||
permission: Permission
|
||||
) {
|
||||
return getFailure(state, DELETE_PERMISSION, createItemId(permission, namespace, repoName));
|
||||
}
|
||||
|
||||
export function getDeletePermissionsFailure(state: object, namespace: string, repoName: string) {
|
||||
export function getDeletePermissionsFailure(state: object, namespace: string, repoName?: string) {
|
||||
const permissions =
|
||||
state.permissions && state.permissions[namespace + "/" + repoName]
|
||||
? state.permissions[namespace + "/" + repoName].entries
|
||||
state.permissions && state.permissions[createPermissionStateKey(namespace, repoName)]
|
||||
? state.permissions[createPermissionStateKey(namespace, repoName)].entries
|
||||
: null;
|
||||
if (permissions == null) return undefined;
|
||||
for (let i = 0; i < permissions.length; i++) {
|
||||
@@ -556,10 +569,10 @@ export function getDeletePermissionsFailure(state: object, namespace: string, re
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getModifyPermissionsFailure(state: object, namespace: string, repoName: string) {
|
||||
export function getModifyPermissionsFailure(state: object, namespace: string, repoName?: string) {
|
||||
const permissions =
|
||||
state.permissions && state.permissions[namespace + "/" + repoName]
|
||||
? state.permissions[namespace + "/" + repoName].entries
|
||||
state.permissions && state.permissions[createPermissionStateKey(namespace, repoName)]
|
||||
? state.permissions[createPermissionStateKey(namespace, repoName)].entries
|
||||
: null;
|
||||
if (permissions == null) return undefined;
|
||||
for (let i = 0; i < permissions.length; i++) {
|
||||
@@ -570,6 +583,10 @@ export function getModifyPermissionsFailure(state: object, namespace: string, re
|
||||
return null;
|
||||
}
|
||||
|
||||
function createPermissionStateKey(namespace: string, repoName?: string) {
|
||||
return namespace + (repoName ? "/" + repoName : "");
|
||||
}
|
||||
|
||||
export function findVerbsForRole(availableRepositoryRoles: RepositoryRole[], roleName: string) {
|
||||
const matchingRole = availableRepositoryRoles.find(role => roleName === role.name);
|
||||
if (matchingRole) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Redirect, Switch, useLocation, useRouteMatch, Route } from "react-route
|
||||
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TagView from "../components/TagView";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -65,17 +66,6 @@ const TagRoot: FC<Props> = ({ repository, baseUrl }) => {
|
||||
}
|
||||
}, [tags]);
|
||||
|
||||
const stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const matchedUrl = () => {
|
||||
return stripEndingSlash(match.url);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
@@ -84,7 +74,7 @@ const TagRoot: FC<Props> = ({ repository, baseUrl }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = matchedUrl();
|
||||
const url = urls.matchedUrlFromMatch(match);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
|
||||
@@ -47,6 +47,7 @@ import { mustGetUsersLink } from "../../modules/indexResource";
|
||||
import SetUserPassword from "../components/SetUserPassword";
|
||||
import SetPermissions from "../../permissions/components/SetPermissions";
|
||||
import SetPublicKeys from "../components/publicKeys/SetPublicKeys";
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = RouteComponentProps &
|
||||
WithTranslation & {
|
||||
@@ -65,17 +66,6 @@ class SingleUser extends React.Component<Props> {
|
||||
this.props.fetchUserByName(this.props.usersLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, loading, error, user } = this.props;
|
||||
|
||||
@@ -87,7 +77,7 @@ class SingleUser extends React.Component<Props> {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const url = urls.matchedUrl(this.props);
|
||||
|
||||
const extensionProps = {
|
||||
user,
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* 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.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.headers.Header;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.validation.Valid;
|
||||
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.core.Response;
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
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.RepositoryPermissionDto.GROUP_PREFIX;
|
||||
|
||||
@Slf4j
|
||||
public class NamespacePermissionResource {
|
||||
|
||||
private final RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper;
|
||||
private final RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper;
|
||||
private final RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper;
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final NamespaceManager manager;
|
||||
|
||||
@Inject
|
||||
public NamespacePermissionResource(
|
||||
RepositoryPermissionDtoToRepositoryPermissionMapper dtoToModelMapper,
|
||||
RepositoryPermissionToRepositoryPermissionDtoMapper modelToDtoMapper,
|
||||
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper,
|
||||
ResourceLinks resourceLinks,
|
||||
NamespaceManager manager) {
|
||||
this.dtoToModelMapper = dtoToModelMapper;
|
||||
this.modelToDtoMapper = modelToDtoMapper;
|
||||
this.repositoryPermissionCollectionToDtoMapper = repositoryPermissionCollectionToDtoMapper;
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new namespace permission for the user or group
|
||||
*
|
||||
* @param permission permission to add
|
||||
* @return a web response with the status code 201 and the url to GET the added permission
|
||||
*/
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "Create namespace-specific permission", description = "Adds a new permission to the namespace for the user or group.", tags = {"Namespace", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "creates",
|
||||
headers = @Header(
|
||||
name = "Location",
|
||||
description = "uri of the created permission",
|
||||
schema = @Schema(type = "string")
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(responseCode = "409", description = "conflict")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public Response create(@PathParam("namespace") String namespaceName, @Valid RepositoryPermissionDto permission) {
|
||||
log.info("try to add new permission: {}", permission);
|
||||
Namespace namespace = load(namespaceName);
|
||||
checkPermissionAlreadyExists(permission, namespace);
|
||||
namespace.addPermission(dtoToModelMapper.map(permission));
|
||||
manager.modify(namespace);
|
||||
String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission);
|
||||
return Response.created(URI.create(resourceLinks.namespacePermission().self(namespaceName, urlPermissionName))).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the searched permission with permission name related to a namespace
|
||||
*
|
||||
* @param namespaceName the name of the namespace
|
||||
* @return the http response with a list of permissionDto objects
|
||||
* @throws NotFoundException if the namespace or the permission does not exists
|
||||
*/
|
||||
@GET
|
||||
@Path("{permission-name}")
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "Get single repository-specific permission", description = "Get the searched permission with permission name related to a repository.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_PERMISSION,
|
||||
schema = @Schema(implementation = RepositoryPermissionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public RepositoryPermissionDto get(@PathParam("namespace") String namespaceName, @PathParam("permission-name") String permissionName) {
|
||||
Namespace namespace = load(namespaceName);
|
||||
return
|
||||
namespace.getPermissions()
|
||||
.stream()
|
||||
.filter(filterPermission(permissionName))
|
||||
.map(permission -> modelToDtoMapper.map(permission, namespace))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions related to a namespace
|
||||
*
|
||||
* @param namespaceMame the name of the namespace
|
||||
* @return the http response with a list of permissionDto objects
|
||||
* @throws NotFoundException if the namespace does not exists
|
||||
*/
|
||||
@GET
|
||||
@Path("")
|
||||
@Produces(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "List of namespace-specific permissions", description = "Get all permissions related to a namespace.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "success",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.REPOSITORY_PERMISSION,
|
||||
schema = @Schema(implementation = RepositoryPermissionDto.class)
|
||||
)
|
||||
)
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "not found",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
))
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public HalRepresentation getAll(@PathParam("namespace") String namespaceMame) {
|
||||
Namespace namespace = load(namespaceMame);
|
||||
return repositoryPermissionCollectionToDtoMapper.map(namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a permission to the user or group managed by the repository
|
||||
* ignore the user input for groupPermission and take it from the path parameter (if the group prefix (@) exists it is a group permission)
|
||||
*
|
||||
* @param permission permission to modify
|
||||
* @param permissionName permission to modify
|
||||
* @return a web response with the status code 204
|
||||
*/
|
||||
@PUT
|
||||
@Path("{permission-name}")
|
||||
@Consumes(VndMediaType.REPOSITORY_PERMISSION)
|
||||
@Operation(summary = "Update repository-specific permission", description = "Update a permission to the user or group managed by the repository.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(responseCode = "204", description = "update success")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public void update(@PathParam("namespace") String namespaceName,
|
||||
@PathParam("permission-name") String permissionName,
|
||||
@Valid RepositoryPermissionDto permission) {
|
||||
Namespace namespace = load(namespaceName);
|
||||
String extractedPermissionName = getPermissionName(permissionName);
|
||||
if (!isPermissionExist(new RepositoryPermissionDto(extractedPermissionName, isGroupPermission(permissionName)), namespace)) {
|
||||
throw notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName));
|
||||
}
|
||||
permission.setGroupPermission(isGroupPermission(permissionName));
|
||||
if (!extractedPermissionName.equals(permission.getName())) {
|
||||
checkPermissionAlreadyExists(permission, namespace);
|
||||
}
|
||||
|
||||
RepositoryPermission existingPermission = namespace.getPermissions()
|
||||
.stream()
|
||||
.filter(filterPermission(permissionName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> notFound(entity(RepositoryPermission.class, permissionName).in(Namespace.class, namespaceName)));
|
||||
RepositoryPermission newPermission = dtoToModelMapper.map(permission);
|
||||
if (!namespace.removePermission(existingPermission)) {
|
||||
throw new IllegalStateException(String.format("could not delete modified permission %s from namespace %s", existingPermission, namespaceName));
|
||||
}
|
||||
namespace.addPermission(newPermission);
|
||||
manager.modify(namespace);
|
||||
log.info("the permission with name: {} is updated to {}.", permissionName, permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a permission to the user or group managed by the repository
|
||||
*
|
||||
* @param permissionName permission to delete
|
||||
* @return a web response with the status code 204
|
||||
*/
|
||||
@DELETE
|
||||
@Path("{permission-name}")
|
||||
@Operation(summary = "Delete repository-specific permission", description = "Delete a permission with the given name.", tags = {"Repository", "Permissions"})
|
||||
@ApiResponse(responseCode = "204", description = "delete success or nothing to delete")
|
||||
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
|
||||
@ApiResponse(responseCode = "403", description = "not authorized")
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "internal server error",
|
||||
content = @Content(
|
||||
mediaType = VndMediaType.ERROR_TYPE,
|
||||
schema = @Schema(implementation = ErrorDto.class)
|
||||
)
|
||||
)
|
||||
public void delete(@PathParam("namespace") String namespaceName,
|
||||
@PathParam("permission-name") String permissionName) {
|
||||
log.info("try to delete the permission with name: {}.", permissionName);
|
||||
Namespace namespace = load(namespaceName);
|
||||
namespace.getPermissions()
|
||||
.stream()
|
||||
.filter(filterPermission(permissionName))
|
||||
.findFirst()
|
||||
.ifPresent(permission -> {
|
||||
namespace.removePermission(permission);
|
||||
manager.modify(namespace);
|
||||
});
|
||||
log.info("the permission with name: {} is deleted.", permissionName);
|
||||
}
|
||||
|
||||
private Predicate<RepositoryPermission> filterPermission(String name) {
|
||||
return permission ->
|
||||
getPermissionName(name).equals(permission.getName())
|
||||
&& permission.isGroupPermission() == isGroupPermission(name);
|
||||
}
|
||||
|
||||
private String getPermissionName(String permissionName) {
|
||||
return Optional.of(permissionName)
|
||||
.filter(p -> !isGroupPermission(permissionName))
|
||||
.orElse(permissionName.substring(1));
|
||||
}
|
||||
|
||||
private boolean isGroupPermission(String permissionName) {
|
||||
return permissionName.startsWith(GROUP_PREFIX);
|
||||
}
|
||||
|
||||
private Namespace load(String namespaceMame) {
|
||||
return manager.get(namespaceMame)
|
||||
.orElseThrow(() -> notFound(entity("Namespace", namespaceMame)));
|
||||
}
|
||||
|
||||
private void checkPermissionAlreadyExists(RepositoryPermissionDto permission, Namespace namespace) {
|
||||
if (isPermissionExist(permission, namespace)) {
|
||||
throw alreadyExists(entity("Permission", permission.getName()).in(Namespace.class, namespace.getNamespace()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPermissionExist(RepositoryPermissionDto permission, Namespace namespace) {
|
||||
return namespace.getPermissions()
|
||||
.stream()
|
||||
.anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@@ -44,11 +45,13 @@ public class NamespaceResource {
|
||||
|
||||
private final RepositoryManager manager;
|
||||
private final NamespaceToNamespaceDtoMapper namespaceMapper;
|
||||
private final Provider<NamespacePermissionResource> namespacePermissionResource;
|
||||
|
||||
@Inject
|
||||
public NamespaceResource(RepositoryManager manager, NamespaceToNamespaceDtoMapper namespaceMapper) {
|
||||
public NamespaceResource(RepositoryManager manager, NamespaceToNamespaceDtoMapper namespaceMapper, Provider<NamespacePermissionResource> namespacePermissionResource) {
|
||||
this.manager = manager;
|
||||
this.namespaceMapper = namespaceMapper;
|
||||
this.namespacePermissionResource = namespacePermissionResource;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,4 +100,8 @@ public class NamespaceResource {
|
||||
.orElseThrow(() -> notFound(entity("Namespace", namespace)));
|
||||
}
|
||||
|
||||
@Path("permissions")
|
||||
public NamespacePermissionResource permissions() {
|
||||
return namespacePermissionResource.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.repository.NamespacePermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
@@ -39,12 +42,15 @@ class NamespaceToNamespaceDtoMapper {
|
||||
}
|
||||
|
||||
NamespaceDto map(String namespace) {
|
||||
return new NamespaceDto(
|
||||
namespace,
|
||||
linkingTo()
|
||||
.self(links.namespace().self(namespace))
|
||||
.single(link("repositories", links.repositoryCollection().forNamespace(namespace)))
|
||||
.build()
|
||||
);
|
||||
Links.Builder linkingTo = linkingTo();
|
||||
linkingTo
|
||||
.self(links.namespace().self(namespace))
|
||||
.single(link("repositories", links.repositoryCollection().forNamespace(namespace)));
|
||||
|
||||
if (NamespacePermissions.permissionRead().isPermitted() || NamespacePermissions.permissionWrite().isPermitted()) {
|
||||
linkingTo
|
||||
.single(link("permissions", links.namespacePermission().all(namespace)));
|
||||
}
|
||||
return new NamespaceDto(namespace, linkingTo.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,15 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
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.Namespace;
|
||||
import sonia.scm.repository.NamespacePermissions;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
|
||||
@@ -57,6 +59,14 @@ public class RepositoryPermissionCollectionToDtoMapper {
|
||||
return new HalRepresentation(createLinks(repository), embedDtos(repositoryPermissionDtoList));
|
||||
}
|
||||
|
||||
public HalRepresentation map(Namespace namespace) {
|
||||
List<RepositoryPermissionDto> repositoryPermissionDtoList = namespace.getPermissions()
|
||||
.stream()
|
||||
.map(permission -> repositoryPermissionToRepositoryPermissionDtoMapper.map(permission, namespace))
|
||||
.collect(toList());
|
||||
return new HalRepresentation(createLinks(namespace), embedDtos(repositoryPermissionDtoList));
|
||||
}
|
||||
|
||||
private Links createLinks(Repository repository) {
|
||||
RepositoryPermissions.permissionRead(repository).check();
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
@@ -67,6 +77,18 @@ public class RepositoryPermissionCollectionToDtoMapper {
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
private Links createLinks(Namespace namespace) {
|
||||
if (!NamespacePermissions.permissionWrite().isPermitted()) {
|
||||
NamespacePermissions.permissionRead().check();
|
||||
}
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.with(Links.linkingTo().self(resourceLinks.namespacePermission().all(namespace.getNamespace())).build());
|
||||
if (NamespacePermissions.permissionWrite().isPermitted()) {
|
||||
linksBuilder.single(link("create", resourceLinks.namespacePermission().create(namespace.getNamespace())));
|
||||
}
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
private Embedded embedDtos(List<RepositoryPermissionDto> repositoryPermissionDtoList) {
|
||||
return embeddedBuilder()
|
||||
.with("permissions", repositoryPermissionDtoList)
|
||||
|
||||
@@ -21,18 +21,19 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.BeforeMapping;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.NamespacePermissions;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -51,18 +52,9 @@ public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper {
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Repository repository);
|
||||
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
public abstract RepositoryPermissionDto map(RepositoryPermission permission, @Context Namespace namespace);
|
||||
|
||||
@BeforeMapping
|
||||
void validatePermissions(@Context Repository repository) {
|
||||
RepositoryPermissions.permissionRead(repository).check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the self, update and delete links.
|
||||
*
|
||||
* @param target the mapped dto
|
||||
* @param repository the repository
|
||||
*/
|
||||
@AfterMapping
|
||||
void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Repository repository) {
|
||||
String permissionName = getUrlPermissionName(target);
|
||||
@@ -75,6 +67,18 @@ public abstract class RepositoryPermissionToRepositoryPermissionDtoMapper {
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
@AfterMapping
|
||||
void appendLinks(@MappingTarget RepositoryPermissionDto target, @Context Namespace namespace) {
|
||||
String permissionName = getUrlPermissionName(target);
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.self(resourceLinks.namespacePermission().self(namespace.getNamespace(), permissionName));
|
||||
if (NamespacePermissions.permissionWrite().isPermitted()) {
|
||||
linksBuilder.single(link("update", resourceLinks.namespacePermission().update(namespace.getNamespace(), permissionName)));
|
||||
linksBuilder.single(link("delete", resourceLinks.namespacePermission().delete(namespace.getNamespace(), permissionName)));
|
||||
}
|
||||
target.add(linksBuilder.build());
|
||||
}
|
||||
|
||||
public String getUrlPermissionName(RepositoryPermissionDto repositoryPermissionDto) {
|
||||
return Optional.of(repositoryPermissionDto.getName())
|
||||
.filter(p -> !repositoryPermissionDto.isGroupPermission())
|
||||
|
||||
@@ -916,4 +916,40 @@ class ResourceLinks {
|
||||
return namespaceLinkBuilder.method("getNamespaceResource").parameters().method("get").parameters(namespace).href();
|
||||
}
|
||||
}
|
||||
|
||||
public NamespacePermissionLinks namespacePermission() {
|
||||
return new NamespacePermissionLinks(scmPathInfoStore.get());
|
||||
}
|
||||
|
||||
static class NamespacePermissionLinks {
|
||||
private final LinkBuilder permissionLinkBuilder;
|
||||
|
||||
NamespacePermissionLinks(ScmPathInfo pathInfo) {
|
||||
permissionLinkBuilder = new LinkBuilder(pathInfo, NamespaceRootResource.class, NamespaceResource.class, NamespacePermissionResource.class);
|
||||
}
|
||||
|
||||
String all(String namespace) {
|
||||
return permissionLinkBuilder.method("getNamespaceResource").parameters(namespace).method("permissions").parameters().method("getAll").parameters().href();
|
||||
}
|
||||
|
||||
String create(String namespace) {
|
||||
return permissionLinkBuilder.method("getNamespaceResource").parameters(namespace).method("permissions").parameters().method("create").parameters().href();
|
||||
}
|
||||
|
||||
String self(String namespace, String permissionName) {
|
||||
return getLink(namespace, permissionName, "get");
|
||||
}
|
||||
|
||||
String update(String namespace, String permissionName) {
|
||||
return getLink(namespace, permissionName, "update");
|
||||
}
|
||||
|
||||
String delete(String namespace, String permissionName) {
|
||||
return getLink(namespace, permissionName, "delete");
|
||||
}
|
||||
|
||||
private String getLink(String namespace, String permissionName, String methodName) {
|
||||
return permissionLinkBuilder.method("getNamespaceResource").parameters(namespace).method("permissions").parameters().method(methodName).parameters(permissionName).href();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +64,12 @@ import sonia.scm.net.ahc.XmlContentTransformer;
|
||||
import sonia.scm.plugin.DefaultPluginManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
import sonia.scm.repository.DefaultNamespaceManager;
|
||||
import sonia.scm.repository.DefaultRepositoryManager;
|
||||
import sonia.scm.repository.DefaultRepositoryProvider;
|
||||
import sonia.scm.repository.DefaultRepositoryRoleManager;
|
||||
import sonia.scm.repository.HealthCheckContextListener;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceStrategyProvider;
|
||||
import sonia.scm.repository.Repository;
|
||||
@@ -191,6 +193,7 @@ class ScmServletModule extends ServletModule {
|
||||
bindDecorated(GroupManager.class, DefaultGroupManager.class,
|
||||
GroupManagerProvider.class);
|
||||
bind(GroupDisplayManager.class, DefaultGroupDisplayManager.class);
|
||||
bind(NamespaceManager.class, DefaultNamespaceManager.class);
|
||||
bind(GroupCollector.class, DefaultGroupCollector.class);
|
||||
bind(CGIExecutorFactory.class, DefaultCGIExecutorFactory.class);
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 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;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
public class DefaultNamespaceManager implements NamespaceManager {
|
||||
|
||||
private final RepositoryManager repositoryManager;
|
||||
private final NamespaceDao dao;
|
||||
private final ScmEventBus eventBus;
|
||||
|
||||
@Inject
|
||||
public DefaultNamespaceManager(RepositoryManager repositoryManager, NamespaceDao dao, ScmEventBus eventBus) {
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.dao = dao;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Namespace> get(String namespace) {
|
||||
return repositoryManager
|
||||
.getAllNamespaces()
|
||||
.stream()
|
||||
.filter(n -> n.equals(namespace))
|
||||
.map(this::createNamespaceForName)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Namespace> getAll() {
|
||||
return repositoryManager
|
||||
.getAllNamespaces()
|
||||
.stream()
|
||||
.map(this::createNamespaceForName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(Namespace namespace) {
|
||||
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) {
|
||||
if (namespaceRelevantChange(repositoryEvent)) {
|
||||
Collection<String> allNamespaces = repositoryManager.getAllNamespaces();
|
||||
String oldNamespace = getOldNamespace(repositoryEvent);
|
||||
if (!allNamespaces.contains(oldNamespace)) {
|
||||
dao.delete(oldNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean namespaceRelevantChange(RepositoryEvent repositoryEvent) {
|
||||
HandlerEventType eventType = repositoryEvent.getEventType();
|
||||
return eventType == HandlerEventType.DELETE
|
||||
|| eventType == HandlerEventType.MODIFY && !repositoryEvent.getItem().getNamespace().equals(repositoryEvent.getOldItem().getNamespace());
|
||||
}
|
||||
|
||||
public String getOldNamespace(RepositoryEvent repositoryEvent) {
|
||||
if (repositoryEvent.getEventType() == HandlerEventType.DELETE) {
|
||||
return repositoryEvent.getItem().getNamespace();
|
||||
} else {
|
||||
return repositoryEvent.getOldItem().getNamespace();
|
||||
}
|
||||
}
|
||||
|
||||
private Namespace createNamespaceForName(String namespace) {
|
||||
if (NamespacePermissions.permissionRead().isPermitted() || NamespacePermissions.permissionWrite().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));
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
return getAll(null, start, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link NamespaceManager#getAll()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public Collection<String> getAllNamespaces() {
|
||||
return getAll().stream()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.store.DataStore;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
public class NamespaceDao {
|
||||
|
||||
private final DataStore<Namespace> store;
|
||||
|
||||
@Inject
|
||||
NamespaceDao(DataStoreFactory storeFactory) {
|
||||
this.store = storeFactory.withType(Namespace.class).withName("namespaces").build();
|
||||
}
|
||||
|
||||
public Optional<Namespace> get(String namespace) {
|
||||
return store.getOptional(namespace);
|
||||
}
|
||||
|
||||
public void add(Namespace namespace) {
|
||||
store.put(namespace.getNamespace(), namespace);
|
||||
}
|
||||
|
||||
public void delete(String namespace) {
|
||||
store.remove(namespace);
|
||||
}
|
||||
}
|
||||
@@ -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,50 @@ 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 the permissions of repository {}/{} have changed", repository.getNamespace(), repository.getName()
|
||||
);
|
||||
fireEventForEveryUser();
|
||||
} else if (!event.getItem().getNamespace().equals(event.getItemBeforeModification().getNamespace())) {
|
||||
logger.debug(
|
||||
"fire authorization changed event, because the namespace 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() {
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.github.legman.Subscribe;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -46,17 +44,19 @@ import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.group.GroupCollector;
|
||||
import sonia.scm.group.GroupPermissions;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.NamespaceDao;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserPermissions;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -85,16 +85,18 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
* @param securitySystem
|
||||
* @param repositoryPermissionProvider
|
||||
* @param groupCollector
|
||||
* @param namespaceDao
|
||||
*/
|
||||
@Inject
|
||||
public DefaultAuthorizationCollector(CacheManager cacheManager,
|
||||
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector)
|
||||
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector, NamespaceDao namespaceDao)
|
||||
{
|
||||
this.cache = cacheManager.getCache(CACHE_NAME);
|
||||
this.repositoryDAO = repositoryDAO;
|
||||
this.securitySystem = securitySystem;
|
||||
this.repositoryPermissionProvider = repositoryPermissionProvider;
|
||||
this.groupCollector = groupCollector;
|
||||
this.namespaceDao = namespaceDao;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
@@ -186,28 +188,27 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
private void collectRepositoryPermissions(Builder<String> builder,
|
||||
Repository repository, User user, Set<String> groups)
|
||||
{
|
||||
Collection<RepositoryPermission> repositoryPermissions = repository.getPermissions();
|
||||
Optional<Namespace> namespace = namespaceDao.get(repository.getNamespace());
|
||||
|
||||
if (Util.isNotEmpty(repositoryPermissions))
|
||||
boolean hasPermission = false;
|
||||
for (RepositoryPermission permission : repository.getPermissions())
|
||||
{
|
||||
boolean hasPermission = false;
|
||||
for (RepositoryPermission permission : repositoryPermissions)
|
||||
{
|
||||
hasPermission = isUserPermitted(user, groups, permission);
|
||||
if (hasPermission) {
|
||||
addRepositoryPermission(builder, repository, user, permission);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPermission && logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("no permission for user {} defined at repository {}", user.getName(), repository.getName());
|
||||
hasPermission = isUserPermitted(user, groups, permission);
|
||||
if (hasPermission) {
|
||||
addRepositoryPermission(builder, repository, user, permission);
|
||||
}
|
||||
}
|
||||
else if (logger.isTraceEnabled())
|
||||
for (RepositoryPermission permission : namespace.map(Namespace::getPermissions).orElse(emptySet()))
|
||||
{
|
||||
logger.trace("repository {} has no permission entries",
|
||||
repository.getName());
|
||||
hasPermission = isUserPermitted(user, groups, permission);
|
||||
if (hasPermission) {
|
||||
addRepositoryPermission(builder, repository, user, permission);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPermission && logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("no permission for user {} defined at repository {}", user.getName(), repository.getNamespaceAndName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,4 +372,5 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|
||||
private final RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private final GroupCollector groupCollector;
|
||||
private final NamespaceDao namespaceDao;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -93,6 +93,16 @@
|
||||
"description": "Darf alle installierten und verfügbaren Plugins lesen und verwalten"
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"permissionRead": {
|
||||
"displayName": "Berechtigungen auf Namespaces lesen",
|
||||
"description": "Darf die Berechtigungen auf Namespace-Ebene sehen"
|
||||
},
|
||||
"permissionWrite": {
|
||||
"displayName": "Berechtigungen auf Namespaces modifizieren",
|
||||
"description": "Darf die Berechtigungen auf Namespace-Ebene lesen und bearbeiten"
|
||||
}
|
||||
},
|
||||
"unknown": "Unbekannte Berechtigung"
|
||||
},
|
||||
"verbs": {
|
||||
|
||||
@@ -93,6 +93,16 @@
|
||||
"description": "May see and manage all installed and available plugins"
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"permissionRead": {
|
||||
"displayName": "Read permissions on namespaces",
|
||||
"description": "May see the permissions set for namespaces"
|
||||
},
|
||||
"permissionWrite": {
|
||||
"displayName": "Modify permissions on namespaces",
|
||||
"description": "May read and modify the permissions set for namespaces"
|
||||
}
|
||||
},
|
||||
"unknown": "Unknown permission"
|
||||
},
|
||||
"verbs": {
|
||||
|
||||
@@ -24,83 +24,334 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.mock.MockHttpRequest;
|
||||
import org.jboss.resteasy.mock.MockHttpResponse;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.web.RestDispatcher;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.inject.util.Providers.of;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singleton;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class NamespaceRootResourceTest {
|
||||
|
||||
@Mock
|
||||
RepositoryManager repositoryManager;
|
||||
@Mock
|
||||
NamespaceManager namespaceManager;
|
||||
@Mock
|
||||
Subject subject;
|
||||
|
||||
RestDispatcher dispatcher = new RestDispatcher();
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
ResourceLinks links = ResourceLinksMock.createMock(URI.create("/"));
|
||||
|
||||
@InjectMocks
|
||||
RepositoryPermissionToRepositoryPermissionDtoMapperImpl repositoryPermissionToRepositoryPermissionDtoMapper;
|
||||
|
||||
@BeforeEach
|
||||
void mockSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpResources() {
|
||||
NamespaceToNamespaceDtoMapper namespaceMapper = new NamespaceToNamespaceDtoMapper(links);
|
||||
NamespaceCollectionToDtoMapper namespaceCollectionToDtoMapper = new NamespaceCollectionToDtoMapper(namespaceMapper, links);
|
||||
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(repositoryPermissionToRepositoryPermissionDtoMapper, links);
|
||||
RepositoryPermissionDtoToRepositoryPermissionMapperImpl dtoToModelMapper = new RepositoryPermissionDtoToRepositoryPermissionMapperImpl();
|
||||
|
||||
NamespaceCollectionResource namespaceCollectionResource = new NamespaceCollectionResource(repositoryManager, namespaceCollectionToDtoMapper);
|
||||
NamespaceResource namespaceResource = new NamespaceResource(repositoryManager, namespaceMapper);
|
||||
NamespacePermissionResource namespacePermissionResource = new NamespacePermissionResource(dtoToModelMapper, repositoryPermissionToRepositoryPermissionDtoMapper, repositoryPermissionCollectionToDtoMapper, links, namespaceManager);
|
||||
NamespaceResource namespaceResource = new NamespaceResource(repositoryManager, namespaceMapper, of(namespacePermissionResource));
|
||||
dispatcher.addSingletonResource(new NamespaceRootResource(of(namespaceCollectionResource), of(namespaceResource)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAllNamespaces() throws URISyntaxException, UnsupportedEncodingException {
|
||||
when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2);
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"self\":{\"href\":\"/v2/namespaces/\"}")
|
||||
.contains("\"_embedded\"")
|
||||
.contains("\"namespace\":\"hitchhiker\"")
|
||||
.contains("\"namespace\":\"space\"");
|
||||
@BeforeEach
|
||||
void mockExistingNamespaces() {
|
||||
lenient().when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
|
||||
Namespace hitchhikerNamespace = new Namespace("hitchhiker");
|
||||
hitchhikerNamespace.setPermissions(singleton(new RepositoryPermission("humans", "READ", true)));
|
||||
Namespace spaceNamespace = new Namespace("space");
|
||||
lenient().when(namespaceManager.getAll()).thenReturn(asList(hitchhikerNamespace, spaceNamespace));
|
||||
lenient().when(namespaceManager.get("hitchhiker")).thenReturn(Optional.of(hitchhikerNamespace));
|
||||
lenient().when(namespaceManager.get("space")).thenReturn(Optional.of(spaceNamespace));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSingleNamespace() throws URISyntaxException, UnsupportedEncodingException {
|
||||
when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
|
||||
@Nested
|
||||
class WithoutSpecialPermission {
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
|
||||
@BeforeEach
|
||||
void mockNoPermissions() {
|
||||
lenient().when(subject.isPermitted(anyString())).thenReturn(false);
|
||||
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionRead");
|
||||
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionWrite");
|
||||
}
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
@Test
|
||||
void shouldReturnAllNamespaces() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"namespace\":\"space\"")
|
||||
.contains("\"self\":{\"href\":\"/v2/namespaces/space\"}");
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"self\":{\"href\":\"/v2/namespaces/\"}")
|
||||
.contains("\"_embedded\"")
|
||||
.contains("\"namespace\":\"hitchhiker\"")
|
||||
.contains("\"namespace\":\"space\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSingleNamespace() throws URISyntaxException, UnsupportedEncodingException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"namespace\":\"space\"")
|
||||
.contains("\"self\":{\"href\":\"/v2/namespaces/space\"}")
|
||||
.doesNotContain("permissions");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUnknownNamespace() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "unknown");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotReturnPermissions() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(403);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUnknownNamespace() throws URISyntaxException, UnsupportedEncodingException {
|
||||
when(repositoryManager.getAllNamespaces()).thenReturn(asList("hitchhiker", "space"));
|
||||
@Nested
|
||||
class WithReadPermission {
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "unknown");
|
||||
@BeforeEach
|
||||
void grantReadPermission() {
|
||||
lenient().when(subject.isPermitted("namespace:permissionRead")).thenReturn(true);
|
||||
lenient().when(subject.isPermitted("namespace:permissionWrite")).thenReturn(false);
|
||||
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionWrite");
|
||||
}
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
@Test
|
||||
void shouldContainPermissionLinkWhenPermitted() throws UnsupportedEncodingException, URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"permissions\":{\"href\":\"/v2/namespaces/space/permissions\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPermissions() throws UnsupportedEncodingException, URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"self\":{\"href\":\"/v2/namespaces/hitchhiker/permissions\"}")
|
||||
.contains("{\"name\":\"humans\",\"verbs\":[],\"role\":\"READ\",\"groupPermission\":true,\"")
|
||||
.doesNotContain("create");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSinglePermission() throws UnsupportedEncodingException, URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"self\":{\"href\":\"/v2/namespaces/hitchhiker/permissions/@humans\"}")
|
||||
.contains("{\"name\":\"humans\",\"verbs\":[],\"role\":\"READ\",\"groupPermission\":true,\"")
|
||||
.doesNotContain("update")
|
||||
.doesNotContain("delete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMissingNamespace() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "no_such_namespace/permissions");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithWritePermission {
|
||||
|
||||
@BeforeEach
|
||||
void grantWritePermission() {
|
||||
lenient().when(subject.isPermitted("namespace:permissionWrite")).thenReturn(true);
|
||||
lenient().doNothing().when(subject).checkPermission("namespace:permissionWrite");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContainCreateLink() throws UnsupportedEncodingException, URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"create\":{\"href\":\"/v2/namespaces/space/permissions\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContainModificationLinks() throws UnsupportedEncodingException, URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentAsString())
|
||||
.contains("\"update\":{\"href\":\"/v2/namespaces/hitchhiker/permissions/@humans\"")
|
||||
.contains("\"delete\":{\"href\":\"/v2/namespaces/hitchhiker/permissions/@humans\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewPermission() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space/permissions")
|
||||
.content("{\"name\":\"dent\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":false}".getBytes())
|
||||
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(201);
|
||||
verify(namespaceManager)
|
||||
.modify(argThat(
|
||||
namespace -> {
|
||||
assertThat(namespace.getPermissions()).hasSize(1);
|
||||
RepositoryPermission permission = namespace.getPermissions().iterator().next();
|
||||
assertThat(permission.getName()).isEqualTo("dent");
|
||||
assertThat(permission.getRole()).isEqualTo("WRITE");
|
||||
assertThat(permission.getVerbs()).isEmpty();
|
||||
assertThat(permission.isGroupPermission()).isFalse();
|
||||
return true;
|
||||
})
|
||||
);
|
||||
assertThat(response.getOutputHeaders().get("Location"))
|
||||
.containsExactly(URI.create("/v2/namespaces/space/permissions/dent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdatePermission() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.put("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans")
|
||||
.content("{\"name\":\"humans\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":true}".getBytes())
|
||||
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
verify(namespaceManager)
|
||||
.modify(argThat(
|
||||
namespace -> {
|
||||
assertThat(namespace.getPermissions()).hasSize(1);
|
||||
RepositoryPermission permission = namespace.getPermissions().iterator().next();
|
||||
assertThat(permission.getName()).isEqualTo("humans");
|
||||
assertThat(permission.getRole()).isEqualTo("WRITE");
|
||||
assertThat(permission.getVerbs()).isEmpty();
|
||||
assertThat(permission.isGroupPermission()).isTrue();
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleNotExistingPermissionOnUpdate() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.put("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/humans")
|
||||
.content("{\"name\":\"humans\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":true}".getBytes())
|
||||
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleExistingPermissionOnCreate() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions")
|
||||
.content("{\"name\":\"humans\",\"verbs\":[],\"role\":\"WRITE\",\"groupPermission\":true}".getBytes())
|
||||
.header("Content-Type", "application/vnd.scmm-repositoryPermission+json;v=2");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(409);
|
||||
verify(namespaceManager, never()).modify(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteExistingPermission() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.delete("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/@humans");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
verify(namespaceManager)
|
||||
.modify(argThat(
|
||||
namespace -> {
|
||||
assertThat(namespace.getPermissions()).isEmpty();
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleRedundantDeleteIdempotent() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.delete("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "hitchhiker/permissions/humans");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
verify(namespaceManager, never()).modify(any());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ public class ResourceLinksMock {
|
||||
lenient().when(resourceLinks.annotate()).thenReturn(new ResourceLinks.AnnotateLinks(pathInfo));
|
||||
lenient().when(resourceLinks.namespace()).thenReturn(new ResourceLinks.NamespaceLinks(pathInfo));
|
||||
lenient().when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(pathInfo));
|
||||
lenient().when(resourceLinks.namespacePermission()).thenReturn(new ResourceLinks.NamespacePermissionLinks(pathInfo));
|
||||
|
||||
return resourceLinks;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.store.InMemoryDataStore;
|
||||
import sonia.scm.store.InMemoryDataStoreFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
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.lenient;
|
||||
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)
|
||||
class DefaultNamespaceManagerTest {
|
||||
|
||||
@Mock
|
||||
RepositoryManager repositoryManager;
|
||||
@Mock
|
||||
ScmEventBus eventBus;
|
||||
@Mock
|
||||
Subject subject;
|
||||
|
||||
Namespace life;
|
||||
|
||||
NamespaceDao dao;
|
||||
DefaultNamespaceManager manager;
|
||||
private Namespace universe;
|
||||
private Namespace rest;
|
||||
|
||||
@BeforeEach
|
||||
void mockExistingNamespaces() {
|
||||
dao = new NamespaceDao(new InMemoryDataStoreFactory(new InMemoryDataStore<Namespace>()));
|
||||
|
||||
when(repositoryManager.getAllNamespaces()).thenReturn(asList("life", "universe", "rest"));
|
||||
|
||||
life = new Namespace("life");
|
||||
RepositoryPermission lifePermission = new RepositoryPermission("humans", "OWNER", true);
|
||||
life.addPermission(lifePermission);
|
||||
dao.add(life);
|
||||
|
||||
universe = new Namespace("universe");
|
||||
rest = new Namespace("rest");
|
||||
|
||||
manager = new DefaultNamespaceManager(repositoryManager, dao, eventBus);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void mockSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateEmptyOptionalIfNamespaceDoesNotExist() {
|
||||
Optional<Namespace> namespace = manager.get("dolphins");
|
||||
|
||||
assertThat(namespace).isEmpty();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithPermissionToReadPermissions {
|
||||
|
||||
@BeforeEach
|
||||
void grantReadPermission() {
|
||||
when(subject.isPermitted("namespace:permissionRead")).thenReturn(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewNamespaceObjectIfNotInStore() {
|
||||
Namespace namespace = manager.get("universe").orElse(null);
|
||||
|
||||
assertThat(namespace).isEqualTo(universe);
|
||||
assertThat(namespace.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichExistingNamespaceWithPermissions() {
|
||||
Namespace namespace = manager.get("life").orElse(null);
|
||||
|
||||
assertThat(namespace.getPermissions()).containsExactly(life.getPermissions().toArray(new RepositoryPermission[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEnrichExistingNamespaceWithPermissionsInGetAll() {
|
||||
Collection<Namespace> namespaces = manager.getAll();
|
||||
|
||||
assertThat(namespaces).containsExactly(
|
||||
life,
|
||||
universe,
|
||||
rest
|
||||
);
|
||||
Namespace foundLifeNamespace = namespaces.stream().filter(namespace -> namespace.getNamespace().equals("life")).findFirst().get();
|
||||
assertThat(
|
||||
foundLifeNamespace.getPermissions()).containsExactly(life.getPermissions().toArray(new RepositoryPermission[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldModifyExistingNamespaceWithPermissions() {
|
||||
Namespace modifiedNamespace = manager.get("life").get();
|
||||
|
||||
modifiedNamespace.setPermissions(asList(new RepositoryPermission("Arthur Dent", "READ", false)));
|
||||
manager.modify(modifiedNamespace);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithoutPermissionToReadOrWritePermissions {
|
||||
|
||||
@BeforeEach
|
||||
void grantReadPermission() {
|
||||
when(subject.isPermitted("namespace:permissionRead")).thenReturn(false);
|
||||
lenient().doThrow(AuthorizationException.class).when(subject).checkPermission("namespace:permissionWrite");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotEnrichExistingNamespaceWithPermissions() {
|
||||
Namespace namespace = manager.get("life").orElse(null);
|
||||
|
||||
assertThat(namespace.getPermissions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotEnrichExistingNamespaceWithPermissionsInGetAll() {
|
||||
Collection<Namespace> namespaces = manager.getAll();
|
||||
|
||||
assertThat(namespaces).containsExactly(
|
||||
new Namespace("life"),
|
||||
universe,
|
||||
rest
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotModifyExistingNamespaceWithPermissions() {
|
||||
Namespace modifiedNamespace = manager.get("life").get();
|
||||
|
||||
modifiedNamespace.setPermissions(asList(new RepositoryPermission("Arthur Dent", "READ", false)));
|
||||
|
||||
Assertions.assertThrows(AuthorizationException.class, () -> manager.modify(modifiedNamespace));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -218,6 +220,18 @@ public class AuthorizationChangedEventProducerTest {
|
||||
assertEventIsNotFired();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnRepositoryNamespaceChanged()
|
||||
{
|
||||
Repository repositoryModified = RepositoryTestData.createHeartOfGold();
|
||||
repositoryModified.setName("test123");
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
repositoryModified.setNamespace("new_namespace");
|
||||
producer.onEvent(new RepositoryModificationEvent(HandlerEventType.CREATE, repositoryModified, repository));
|
||||
assertGlobalEventIsFired();
|
||||
}
|
||||
|
||||
private void resetStoredEvent(){
|
||||
producer.event = null;
|
||||
}
|
||||
@@ -251,6 +265,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;
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
@@ -45,6 +44,8 @@ import sonia.scm.SCMContext;
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.group.GroupCollector;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.NamespaceDao;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
@@ -53,8 +54,10 @@ import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Optional.of;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
@@ -92,6 +95,9 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@Mock
|
||||
private GroupCollector groupCollector;
|
||||
|
||||
@Mock
|
||||
private NamespaceDao namespaceDao;
|
||||
|
||||
private DefaultAuthorizationCollector collector;
|
||||
|
||||
@Rule
|
||||
@@ -103,7 +109,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@Before
|
||||
public void setUp(){
|
||||
when(cacheManager.getCache(Mockito.any(String.class))).thenReturn(cache);
|
||||
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider, groupCollector);
|
||||
collector = new DefaultAuthorizationCollector(cacheManager, repositoryDAO, securitySystem, repositoryPermissionProvider, groupCollector, namespaceDao);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,12 +201,44 @@ public class DefaultAuthorizationCollectorTest {
|
||||
authenticate(UserTestData.createTrillian(), group);
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
heartOfGold.setId("one");
|
||||
heartOfGold.setPermissions(Lists.newArrayList(new RepositoryPermission("trillian", asList("read", "pull"), false)));
|
||||
heartOfGold.setPermissions(newArrayList(new RepositoryPermission("trillian", asList("read", "pull"), false)));
|
||||
Repository puzzle42 = RepositoryTestData.create42Puzzle();
|
||||
puzzle42.setId("two");
|
||||
RepositoryPermission permission = new RepositoryPermission(group, asList("read", "pull", "push"), true);
|
||||
puzzle42.setPermissions(Lists.newArrayList(permission));
|
||||
when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42));
|
||||
puzzle42.setPermissions(newArrayList(permission));
|
||||
when(repositoryDAO.getAll()).thenReturn(newArrayList(heartOfGold, puzzle42));
|
||||
|
||||
// execute and assert
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with repository permissions.
|
||||
*/
|
||||
@Test
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||
)
|
||||
public void testCollectWithNamespacePermissions() {
|
||||
String group = "heart-of-gold-crew";
|
||||
authenticate(UserTestData.createTrillian(), group);
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
heartOfGold.setId("one");
|
||||
Namespace heartOfGoldNamespace = new Namespace(heartOfGold.getNamespace());
|
||||
heartOfGoldNamespace.setPermissions(newArrayList(new RepositoryPermission("trillian", asList("read", "pull"), false)));
|
||||
|
||||
Repository puzzle42 = RepositoryTestData.create42Puzzle();
|
||||
puzzle42.setNamespace("guide");
|
||||
puzzle42.setId("two");
|
||||
Namespace puzzleNamespace = new Namespace(puzzle42.getNamespace());
|
||||
puzzleNamespace.setPermissions(newArrayList(new RepositoryPermission(group, asList("read", "pull", "push"), true)));
|
||||
|
||||
when(repositoryDAO.getAll()).thenReturn(newArrayList(heartOfGold, puzzle42));
|
||||
when(namespaceDao.get(heartOfGold.getNamespace())).thenReturn(of(heartOfGoldNamespace));
|
||||
when(namespaceDao.get(puzzle42.getNamespace())).thenReturn(of(puzzleNamespace));
|
||||
|
||||
// execute and assert
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
@@ -228,15 +266,15 @@ public class DefaultAuthorizationCollectorTest {
|
||||
authenticate(UserTestData.createTrillian(), group);
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
heartOfGold.setId("one");
|
||||
heartOfGold.setPermissions(Lists.newArrayList(
|
||||
heartOfGold.setPermissions(newArrayList(
|
||||
new RepositoryPermission("trillian", "user role", false),
|
||||
new RepositoryPermission("trillian", "system role", false)
|
||||
));
|
||||
Repository puzzle42 = RepositoryTestData.create42Puzzle();
|
||||
puzzle42.setId("two");
|
||||
RepositoryPermission permission = new RepositoryPermission(group, "group role", true);
|
||||
puzzle42.setPermissions(Lists.newArrayList(permission));
|
||||
when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold, puzzle42));
|
||||
puzzle42.setPermissions(newArrayList(permission));
|
||||
when(repositoryDAO.getAll()).thenReturn(newArrayList(heartOfGold, puzzle42));
|
||||
|
||||
// execute and assert
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
@@ -272,7 +310,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
heartOfGold.setPermissions(singletonList(
|
||||
new RepositoryPermission("trillian", "unknown", false)
|
||||
));
|
||||
when(repositoryDAO.getAll()).thenReturn(Lists.newArrayList(heartOfGold));
|
||||
when(repositoryDAO.getAll()).thenReturn(newArrayList(heartOfGold));
|
||||
|
||||
// execute and assert
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
@@ -290,7 +328,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(any())).thenReturn(Lists.newArrayList(p1, p2));
|
||||
when(securitySystem.getPermissions(any())).thenReturn(newArrayList(p1, p2));
|
||||
|
||||
// execute and assert
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
|
||||
Reference in New Issue
Block a user