- add global rename repositories permission

- add api call on rename action
This commit is contained in:
Eduard Heimbuch
2020-06-25 09:16:19 +02:00
parent e32130cd0b
commit cd8a9873a9
8 changed files with 113 additions and 21 deletions

View File

@@ -0,0 +1,37 @@
/*
* 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;
@Event
public class RepositoryRenamedEvent extends AbstractHandlerEvent<Repository> {
public RepositoryRenamedEvent(HandlerEventType eventType, Repository item, Repository oldItem) {
super(eventType, item, oldItem);
}
}

View File

@@ -23,7 +23,8 @@
*/ */
import React, { FC, useState } from "react"; import React, { FC, useState } from "react";
import { Repository } from "@scm-manager/ui-types"; import { Repository, Link } from "@scm-manager/ui-types";
import { CONTENT_TYPE } from "../modules/repos";
import { import {
ErrorNotification, ErrorNotification,
Level, Level,
@@ -35,6 +36,8 @@ import {
ButtonGroup ButtonGroup
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { apiClient } from "@scm-manager/ui-components/src";
import { useHistory } from "react-router-dom";
type Props = { type Props = {
repository: Repository; repository: Repository;
@@ -42,12 +45,13 @@ type Props = {
}; };
const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => { const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
let history = useHistory();
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
const [error, setError] = useState<Error | undefined>(undefined); const [error, setError] = useState<Error | undefined>(undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [repositoryName, setRepositoryName] = useState(repository.name); const [name, setName] = useState(repository.name);
const [repositoryNamespace, setRepositoryNamespace] = useState(repository.namespace); const [namespace, setNamespace] = useState(repository.namespace);
if (error) { if (error) {
return <ErrorNotification error={error} />; return <ErrorNotification error={error} />;
@@ -58,24 +62,37 @@ const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
} }
const isValid = const isValid =
validation.isNameValid(repositoryName) && validation.isNameValid(name) &&
validation.isNameValid(repositoryNamespace) && validation.isNameValid(namespace) &&
(repository.name !== repositoryName || repository.namespace !== repositoryNamespace); (repository.name !== name || repository.namespace !== namespace);
const rename = () => {
setLoading(true);
const url = renameNamespace
? (repository?._links?.renameWithNamespace as Link).href
: (repository?._links?.rename as Link).href;
apiClient
.post(url, { name, namespace }, CONTENT_TYPE)
.then(() => setLoading(false))
.then(() => history.push(`/repo/${namespace}/${name}`))
.catch(setError);
};
const modalBody = ( const modalBody = (
<div> <div>
<InputField <InputField
label={t("renameRepo.modal.label.repoName")} label={t("renameRepo.modal.label.repoName")}
name={t("renameRepo.modal.label.repoName")} name={t("renameRepo.modal.label.repoName")}
value={repositoryName} value={name}
onChange={setRepositoryName} onChange={setName}
/> />
{renameNamespace && ( {renameNamespace && (
<InputField <InputField
label={t("renameRepo.modal.label.repoNamespace")} label={t("renameRepo.modal.label.repoNamespace")}
name={t("renameRepo.modal.label.repoNamespace")} name={t("renameRepo.modal.label.repoNamespace")}
value={repositoryNamespace} value={namespace}
onChange={setRepositoryNamespace} onChange={setNamespace}
/> />
)} )}
</div> </div>
@@ -90,8 +107,13 @@ const RenameRepository: FC<Props> = ({ repository, renameNamespace }) => {
label={t("renameRepo.modal.button.rename")} label={t("renameRepo.modal.button.rename")}
disabled={!isValid} disabled={!isValid}
title={t("renameRepo.modal.button.rename")} title={t("renameRepo.modal.button.rename")}
action={rename}
/>
<Button
label={t("renameRepo.modal.button.cancel")}
title={t("renameRepo.modal.button.cancel")}
action={() => setShowModal(false)}
/> />
<Button label={t("renameRepo.modal.button.cancel")} action={() => setShowModal(false)} />
</ButtonGroup> </ButtonGroup>
</> </>
); );

View File

@@ -74,6 +74,13 @@ class RepositoryRoot extends React.Component<Props> {
fetchRepoByName(repoLink, namespace, name); fetchRepoByName(repoLink, namespace, name);
} }
componentDidUpdate(prevProps: Props) {
const { fetchRepoByName, namespace, name, repoLink } = this.props;
if (namespace !== prevProps.namespace || name !== prevProps.name) {
fetchRepoByName(repoLink, namespace, name);
}
}
stripEndingSlash = (url: string) => { stripEndingSlash = (url: string) => {
if (url.endsWith("/")) { if (url.endsWith("/")) {
return url.substring(0, url.length - 1); return url.substring(0, url.length - 1);

View File

@@ -56,7 +56,7 @@ export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`; export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`; export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2"; export const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
// fetch repos // fetch repos

View File

@@ -29,11 +29,14 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.HandlerEventType;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.RepositoryRenamedEvent;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.inject.Inject; import javax.inject.Inject;
@@ -205,26 +208,31 @@ public class RepositoryResource {
)) ))
@ApiResponse(responseCode = "500", description = "internal server error") @ApiResponse(responseCode = "500", description = "internal server error")
public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) { public Response rename(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryRenameDto renameDto) {
Repository repo = loadBy(namespace, name).get(); Supplier<Repository> repoSupplier = loadBy(namespace, name);
Repository unchangedRepo = repoSupplier.get();
Repository changedRepo = unchangedRepo.clone();
if (isRenameForbidden(repo)) { if (isRenameForbidden(unchangedRepo, renameDto)) {
return Response.status(403).build(); return Response.status(403).build();
} }
if (hasNamespaceOrNameNotChanged(repo, renameDto)) { if (hasNamespaceOrNameNotChanged(unchangedRepo, renameDto)) {
return Response.status(400).build(); return Response.status(400).build();
} }
if (!Strings.isNullOrEmpty(renameDto.getName())) { if (!Strings.isNullOrEmpty(renameDto.getName())) {
repo.setName(renameDto.getName()); changedRepo.setName(renameDto.getName());
} }
if (!Strings.isNullOrEmpty(renameDto.getNamespace())) { if (!Strings.isNullOrEmpty(renameDto.getNamespace())) {
repo.setNamespace(renameDto.getNamespace()); changedRepo.setNamespace(renameDto.getNamespace());
} }
return adapter.update( return adapter.update(
loadBy(namespace, name), repoSupplier,
existing -> repo, existing -> {
ScmEventBus.getInstance().post(new RepositoryRenamedEvent(HandlerEventType.MODIFY, changedRepo, unchangedRepo));
return changedRepo;
},
changed -> true, changed -> true,
r -> r.getNamespaceAndName().logString() r -> r.getNamespaceAndName().logString()
); );
@@ -235,8 +243,9 @@ public class RepositoryResource {
&& repo.getNamespace().equals(renameDto.getNamespace()); && repo.getNamespace().equals(renameDto.getNamespace());
} }
private boolean isRenameForbidden(Repository repo) { private boolean isRenameForbidden(Repository repo, RepositoryRenameDto renameDto) {
return !scmConfiguration.getNamespaceStrategy().equals("CustomNamespaceStrategy") return !scmConfiguration.getNamespaceStrategy().equals("CustomNamespaceStrategy")
&& !repo.getNamespace().equals(renameDto.getNamespace())
|| !RepositoryPermissions.rename(repo).isPermitted(); || !RepositoryPermissions.rename(repo).isPermitted();
} }

View File

@@ -35,6 +35,9 @@
<permission> <permission>
<value>repository:read,pull,push:*</value> <value>repository:read,pull,push:*</value>
</permission> </permission>
<permission>
<value>repository:read,rename:*</value>
</permission>
<permission> <permission>
<value>repository:*</value> <value>repository:*</value>
</permission> </permission>

View File

@@ -103,6 +103,10 @@
"displayName": "Repository Löschen", "displayName": "Repository Löschen",
"description": "Darf das Repository löschen." "description": "Darf das Repository löschen."
}, },
"rename": {
"displayName": "Repository umbenennen",
"description": "Darf das Repository umbenennen."
},
"pull": { "pull": {
"displayName": "Pull/Checkout", "displayName": "Pull/Checkout",
"description": "Darf pull/checkout auf das Repository ausführen." "description": "Darf pull/checkout auf das Repository ausführen."

View File

@@ -28,6 +28,12 @@
"description": "May see, clone and push to all repositories" "description": "May see, clone and push to all repositories"
} }
}, },
"read,rename": {
"*": {
"displayName": "Rename all repositories",
"description": "May see and rename all repositories"
}
},
"*": { "*": {
"displayName": "Own all repositories", "displayName": "Own all repositories",
"description": "May see, clone, push to, configure and delete all repositories" "description": "May see, clone, push to, configure and delete all repositories"
@@ -103,6 +109,10 @@
"displayName": "delete repository", "displayName": "delete repository",
"description": "May delete the repository" "description": "May delete the repository"
}, },
"rename": {
"displayName": "rename repository",
"description": "May rename the repository."
},
"pull": { "pull": {
"displayName": "pull/checkout repository", "displayName": "pull/checkout repository",
"description": "May pull/checkout the repository" "description": "May pull/checkout the repository"