mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-08-05 02:40:46 +02:00
Implement namespace configurations & permissions
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com> Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com> Reviewed-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
@@ -67,6 +67,13 @@ 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.
|
||||
|
||||
Auf Namespace-Ebene vergebene Berechtigungen gelten für den Namespace selber als auch für alle Repositories in diesem
|
||||
Namespace. Die Berechtigung "Alle Namespace Rechte" zum Beispiel, die entweder über die Rolle "OWNER" oder manuell
|
||||
gesetzt werden kann, erlaubt konfigurierten Nutzer:innen bzw. Gruppen Zugriff auf alle Einstellungen betreffend des
|
||||
Namespaces selber (inclusive der Berechtigungen) und darüber hinaus vollen Zugriff auf alle in diesem Namespace
|
||||
befindlichen Repositories, wiederum mit allen Berechtigungen für Konfigurationen und dem vollen Zugriff auf die
|
||||
Repository-Daten.
|
||||
|
||||

|
||||
|
||||
Für individuelle Berechtigungen kann man über "Erweitert" einen Dialog öffnen, um jede Berechtigung einzeln zu vergeben.
|
||||
|
||||
@@ -63,6 +63,12 @@ that contain several permissions. Roles can be defined in the administration are
|
||||
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.
|
||||
|
||||
Permissions configured for namespaces will be applied to the namespace itself and to all repositories in this namespace.
|
||||
For example, the permission "own namespace" that can be set either with the role "OWNER" or manually grants the
|
||||
configured user or group all permissions to configure everything regarding the namespace itself (including the
|
||||
permission settings) and gives full access to all repositories for this namespace, again including all configurations
|
||||
and the repository data itself.
|
||||
|
||||

|
||||
|
||||
To manage permissions individually, an "Advanced" dialog can be opened to manage every single permission.
|
||||
|
||||
@@ -33,6 +33,12 @@ package sonia.scm.store;
|
||||
public interface StoreParameters {
|
||||
|
||||
String getName();
|
||||
|
||||
String getRepositoryId();
|
||||
|
||||
/**
|
||||
* Returns optional namespace to which the store is related
|
||||
* @return namespace
|
||||
* @since 2.44.0
|
||||
*/
|
||||
String getNamespace();
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ public final class StoreParametersBuilder<S> {
|
||||
private static class StoreParametersImpl implements StoreParameters {
|
||||
private final String name;
|
||||
private String repositoryId;
|
||||
private String namespace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class TypedStoreParametersBuilder<T,S> {
|
||||
private final Class<T> type;
|
||||
private String name;
|
||||
private String repositoryId;
|
||||
private String namespace;
|
||||
private ClassLoader classLoader;
|
||||
private Set<XmlAdapter<?, ?>> adapters;
|
||||
|
||||
@@ -90,6 +91,9 @@ public final class TypedStoreParametersBuilder<T,S> {
|
||||
* @return Floating API to finish the call.
|
||||
*/
|
||||
public OptionalRepositoryBuilder forRepository(Repository repository) {
|
||||
if (parameters.getNamespace() != null) {
|
||||
throw new IllegalStateException("Namespace for store already set. Cannot apply repository id for store.");
|
||||
}
|
||||
parameters.setRepositoryId(repository.getId());
|
||||
return this;
|
||||
}
|
||||
@@ -101,10 +105,29 @@ public final class TypedStoreParametersBuilder<T,S> {
|
||||
* @return Floating API to finish the call.
|
||||
*/
|
||||
public OptionalRepositoryBuilder forRepository(String repositoryId) {
|
||||
if (parameters.getNamespace() != null) {
|
||||
throw new IllegalStateException("Namespace for store already set. Cannot apply repository id for store.");
|
||||
}
|
||||
parameters.setRepositoryId(repositoryId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to create or get a store for a specific namespace. This step is optional. If you
|
||||
* want to have a global store, omit this.
|
||||
* @param namespace The name of the optional namespace for the store.
|
||||
* @return Floating API to finish the call.
|
||||
*
|
||||
* @since 2.44.0
|
||||
*/
|
||||
public OptionalRepositoryBuilder forNamespace(String namespace) {
|
||||
if (parameters.getRepositoryId() != null) {
|
||||
throw new IllegalStateException("Repository id for store already set. Cannot apply namespace for store.");
|
||||
}
|
||||
parameters.setNamespace(namespace);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link ClassLoader} which is used as context class loader during marshaling and unmarshalling.
|
||||
* This is especially useful for storing class objects which come from an unknown source, in this case the
|
||||
|
||||
@@ -53,6 +53,7 @@ public interface StoreUpdateStepUtilFactory {
|
||||
private final StoreType type;
|
||||
private final String name;
|
||||
private String repositoryId;
|
||||
private String namespace;
|
||||
|
||||
public UtilForNameBuilder(StoreUpdateStepUtilFactory factory, StoreType type, String name) {
|
||||
this.factory = factory;
|
||||
@@ -65,6 +66,11 @@ public interface StoreUpdateStepUtilFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
public UtilForNameBuilder forNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StoreUpdateStepUtil build() {
|
||||
return factory.build(
|
||||
type,
|
||||
@@ -78,6 +84,11 @@ public interface StoreUpdateStepUtilFactory {
|
||||
public String getRepositoryId() {
|
||||
return repositoryId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,8 @@ import java.nio.file.Path;
|
||||
*/
|
||||
public abstract class FileBasedStoreFactory {
|
||||
|
||||
/**
|
||||
* the logger for FileBasedStoreFactory
|
||||
*/
|
||||
private static final String NAMESPACES_DIR = "namespaces";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileBasedStoreFactory.class);
|
||||
private final SCMContextProvider contextProvider;
|
||||
private final RepositoryLocationResolver repositoryLocationResolver;
|
||||
@@ -62,16 +61,19 @@ public abstract class FileBasedStoreFactory {
|
||||
}
|
||||
|
||||
protected File getStoreLocation(StoreParameters storeParameters) {
|
||||
return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepositoryId());
|
||||
return getStoreLocation(storeParameters.getName(), null, storeParameters.getRepositoryId(), storeParameters.getNamespace());
|
||||
}
|
||||
|
||||
protected File getStoreLocation(TypedStoreParameters storeParameters) {
|
||||
return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepositoryId());
|
||||
protected File getStoreLocation(TypedStoreParameters<?> storeParameters) {
|
||||
return getStoreLocation(storeParameters.getName(), storeParameters.getType(), storeParameters.getRepositoryId(), storeParameters.getNamespace());
|
||||
}
|
||||
|
||||
protected File getStoreLocation(String name, Class type, String repositoryId) {
|
||||
protected File getStoreLocation(String name, Class<?> type, String repositoryId, String namespace) {
|
||||
File storeDirectory;
|
||||
if (repositoryId != null) {
|
||||
if (namespace != null) {
|
||||
LOG.debug("create store with type: {}, name: {} and namespace: {}", type, name, namespace);
|
||||
storeDirectory = this.getNamespaceStoreDirectory(store, namespace);
|
||||
} else if (repositoryId != null) {
|
||||
LOG.debug("create store with type: {}, name: {} and repository id: {}", type, name, repositoryId);
|
||||
storeDirectory = this.getStoreDirectory(store, repositoryId);
|
||||
} else {
|
||||
@@ -97,6 +99,17 @@ public abstract class FileBasedStoreFactory {
|
||||
return new File(repositoryLocationResolver.forClass(Path.class).getLocation(repositoryId).toFile(), store.getRepositoryStoreDirectory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store directory of a specific namespace
|
||||
*
|
||||
* @param store the type of the store
|
||||
* @param namespace the name of the namespace
|
||||
* @return the store directory of a specific namespace
|
||||
*/
|
||||
private File getNamespaceStoreDirectory(Store store, String namespace) {
|
||||
return new File(contextProvider.getBaseDirectory().toPath().resolve(NAMESPACES_DIR).resolve(namespace).toString(), store.getNamespaceStoreDirectory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global store directory
|
||||
*
|
||||
|
||||
@@ -36,14 +36,13 @@ import sonia.scm.security.KeyGenerator;
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
|
||||
implements ConfigurationEntryStoreFactory {
|
||||
|
||||
private KeyGenerator keyGenerator;
|
||||
private final KeyGenerator keyGenerator;
|
||||
|
||||
@Inject
|
||||
public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) {
|
||||
@@ -54,7 +53,10 @@ public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory
|
||||
@Override
|
||||
public <T> ConfigurationEntryStore<T> getStore(TypedStoreParameters<T> storeParameters) {
|
||||
return new JAXBConfigurationEntryStore<>(
|
||||
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION), storeParameters.getType(), storeParameters.getRepositoryId()),
|
||||
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
|
||||
storeParameters.getType(),
|
||||
storeParameters.getRepositoryId(),
|
||||
storeParameters.getNamespace()),
|
||||
keyGenerator,
|
||||
storeParameters.getType(),
|
||||
TypedStoreContext.of(storeParameters)
|
||||
|
||||
@@ -62,7 +62,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme
|
||||
storeParameters.getType(),
|
||||
getStoreLocation(storeParameters.getName().concat(StoreConstants.FILE_EXTENSION),
|
||||
storeParameters.getType(),
|
||||
storeParameters.getRepositoryId()),
|
||||
storeParameters.getRepositoryId(),
|
||||
storeParameters.getNamespace()),
|
||||
() -> mustBeReadOnly(storeParameters)
|
||||
);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ enum Store {
|
||||
}
|
||||
}
|
||||
|
||||
private String directory;
|
||||
private final String directory;
|
||||
|
||||
Store(String directory) {
|
||||
|
||||
@@ -69,6 +69,20 @@ enum Store {
|
||||
return STORE_DIRECTORY + File.separator + directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative store directory path to be stored in the namespace root
|
||||
* <p>
|
||||
* The namespace store directories are:
|
||||
* namespace_dir/store/config/
|
||||
* namespace_dir/store/blob/
|
||||
* namespace_dir/store/data/
|
||||
*
|
||||
* @return the relative store directory path to be stored in the namespace root
|
||||
*/
|
||||
public String getNamespaceStoreDirectory() {
|
||||
return STORE_DIRECTORY + File.separator + directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative store directory path to be stored in the global root
|
||||
* <p>
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ComponentProps } from "react";
|
||||
import React, { ComponentProps, ComponentType, FC } from "react";
|
||||
import { binder, extensionPoints } from "@scm-manager/ui-extensions";
|
||||
import { NavLink } from "../navigation";
|
||||
import { Route } from "react-router-dom";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Link, Links, Repository } from "@scm-manager/ui-types";
|
||||
import { useTranslation, WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Link, Links, Namespace, Repository } from "@scm-manager/ui-types";
|
||||
import { urls } from "@scm-manager/ui-api";
|
||||
|
||||
type GlobalRouteProps = {
|
||||
@@ -34,6 +34,11 @@ type GlobalRouteProps = {
|
||||
links: Links;
|
||||
};
|
||||
|
||||
type NamespaceRouteProps = {
|
||||
url: string;
|
||||
namespace: Namespace;
|
||||
};
|
||||
|
||||
type RepositoryRouteProps = {
|
||||
url: string;
|
||||
repository: Repository;
|
||||
@@ -41,6 +46,8 @@ type RepositoryRouteProps = {
|
||||
|
||||
type RepositoryNavProps = WithTranslation & { url: string };
|
||||
|
||||
type NamespaceNavProps = WithTranslation & { url: string };
|
||||
|
||||
class ConfigurationBinder {
|
||||
i18nNamespace = "plugins";
|
||||
|
||||
@@ -54,11 +61,7 @@ class ConfigurationBinder {
|
||||
}
|
||||
|
||||
route(path: string, Component: any) {
|
||||
return (
|
||||
<Route path={urls.escapeUrlForRoute(path)}>
|
||||
{Component}
|
||||
</Route>
|
||||
);
|
||||
return <Route path={urls.escapeUrlForRoute(path)}>{Component}</Route>;
|
||||
}
|
||||
|
||||
bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) {
|
||||
@@ -171,6 +174,36 @@ class ConfigurationBinder {
|
||||
// bind config route to extension point
|
||||
binder.bind("repository.route", RepoRoute, repoPredicate);
|
||||
}
|
||||
|
||||
bindNamespaceSetting(
|
||||
to: string,
|
||||
labelI18nKey: string,
|
||||
linkName: string,
|
||||
ConfigurationComponent: ComponentType<{ link: string; namespace: Namespace }>
|
||||
) {
|
||||
const namespacePredicate = (props: (extensionPoints.NamespaceSetting | extensionPoints.NamespaceRoute)["props"]) =>
|
||||
props.namespace && props.namespace._links && props.namespace._links[linkName];
|
||||
|
||||
const NamespaceNavLink: FC<extensionPoints.NamespaceRoute["props"]> = ({ url }) => {
|
||||
const [t] = useTranslation(this.i18nNamespace);
|
||||
return this.navLink(url + "/settings" + to, labelI18nKey, t, { activeOnlyWhenExact: false });
|
||||
};
|
||||
|
||||
binder.bind<extensionPoints.NamespaceSetting>("namespace.setting", NamespaceNavLink, namespacePredicate);
|
||||
|
||||
const NamespaceRoute: FC<extensionPoints.NamespaceRoute["props"]> = ({ url, namespace, ...additionalProps }) => {
|
||||
const link = namespace._links[linkName];
|
||||
if (link) {
|
||||
return this.route(
|
||||
urls.unescapeUrlForRoute(url) + "/settings" + to,
|
||||
<ConfigurationComponent namespace={namespace} link={(link as Link).href} {...additionalProps} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
binder.bind<extensionPoints.NamespaceRoute>("namespace.route", NamespaceRoute, namespacePredicate);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConfigurationBinder();
|
||||
|
||||
@@ -389,6 +389,7 @@
|
||||
},
|
||||
"permission": {
|
||||
"title": "Berechtigungen",
|
||||
"namespace-help": "Die hier gesetzten Berechtigungen gelten nicht nur für diesen Namespace, sondern auch für alle Repositories dieses Namespace.",
|
||||
"noPermissions": "Keine Berechtigungen gefunden.",
|
||||
"user": "Benutzer",
|
||||
"group": "Gruppe",
|
||||
|
||||
@@ -389,6 +389,7 @@
|
||||
},
|
||||
"permission": {
|
||||
"title": "Permissions",
|
||||
"namespace-help": "Permissions set here do not only apply for this namespace, but also for all repositories in this namespace.",
|
||||
"noPermissions": "No permissions found.",
|
||||
"user": "User",
|
||||
"group": "Group",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Checkbox } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
entityType: "namespace" | "repository";
|
||||
checked: boolean;
|
||||
onChange?: (value: boolean, name?: string) => void;
|
||||
disabled: boolean;
|
||||
@@ -37,7 +38,7 @@ type InnerProps = Props & {
|
||||
innerRef: React.Ref<HTMLInputElement>;
|
||||
};
|
||||
|
||||
const PermissionCheckbox: FC<InnerProps> = ({ name, checked, onChange, disabled, role, innerRef }) => {
|
||||
const PermissionCheckbox: FC<InnerProps> = ({ name, entityType, checked, onChange, disabled, role, innerRef }) => {
|
||||
const [t] = useTranslation("plugins");
|
||||
const key = name.split(":").join(".");
|
||||
|
||||
@@ -50,11 +51,19 @@ const PermissionCheckbox: FC<InnerProps> = ({ name, checked, onChange, disabled,
|
||||
}
|
||||
};
|
||||
|
||||
const namespaceOrRepositoryLabel = (name: string, type: "displayName" | "description") => {
|
||||
if (entityType === "repository") {
|
||||
return t("verbs.repository." + name + "." + type);
|
||||
} else {
|
||||
return translateOrDefault("verbs.namespace." + name + "." + type, t("verbs.repository." + name + "." + type));
|
||||
}
|
||||
};
|
||||
|
||||
const label = role
|
||||
? t("verbs.repository." + name + ".displayName")
|
||||
? namespaceOrRepositoryLabel(name, "displayName")
|
||||
: translateOrDefault("permissions." + key + ".displayName", key);
|
||||
const helpText = role
|
||||
? t("verbs.repository." + name + ".description")
|
||||
? namespaceOrRepositoryLabel(name, "description")
|
||||
: translateOrDefault("permissions." + key + ".description", t("permissions.unknown"));
|
||||
|
||||
const commonCheckboxProps = {
|
||||
|
||||
@@ -87,6 +87,11 @@ const NamespaceRoot: FC = () => {
|
||||
<Route path={`${url}/settings/permissions`}>
|
||||
<Permissions namespaceOrRepository={namespace} />
|
||||
</Route>
|
||||
<ExtensionPoint<extensionPoints.NamespaceRoute>
|
||||
name="namespace.route"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</Switch>
|
||||
</PrimaryContentColumn>
|
||||
<SecondaryNavigationColumn>
|
||||
@@ -96,11 +101,6 @@ const NamespaceRoot: FC = () => {
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
<ExtensionPoint<extensionPoints.NamespaceRoute>
|
||||
name="namespace.route"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
<SubNavigation
|
||||
to={`${url}/settings`}
|
||||
label={t("namespaceRoot.menu.settingsNavLink")}
|
||||
|
||||
@@ -34,6 +34,7 @@ type Props = {
|
||||
readOnly: boolean;
|
||||
onSubmit: (verbs: string[]) => void;
|
||||
onClose: () => void;
|
||||
entityType: "namespace" | "repository";
|
||||
};
|
||||
|
||||
const createPreSelection = (availableVerbs: string[], selectedVerbs?: string[]): SelectedVerbs => {
|
||||
@@ -42,7 +43,7 @@ const createPreSelection = (availableVerbs: string[], selectedVerbs?: string[]):
|
||||
return verbs;
|
||||
};
|
||||
|
||||
const AdvancedPermissionsDialog: FC<Props> = ({ availableVerbs, selectedVerbs, readOnly, onSubmit, onClose }) => {
|
||||
const AdvancedPermissionsDialog: FC<Props> = ({ availableVerbs, selectedVerbs, entityType, readOnly, onSubmit, onClose }) => {
|
||||
const [verbs, setVerbs] = useState<SelectedVerbs>({});
|
||||
const [t] = useTranslation("repos");
|
||||
const initialFocusRef = useRef<HTMLInputElement>(null);
|
||||
@@ -68,6 +69,7 @@ const AdvancedPermissionsDialog: FC<Props> = ({ availableVerbs, selectedVerbs, r
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
role={true}
|
||||
entityType={entityType}
|
||||
ref={index === 0 ? initialFocusRef : undefined}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -33,6 +33,10 @@ type Props = {
|
||||
namespaceOrRepository: Namespace | Repository;
|
||||
};
|
||||
|
||||
const isRepository = (namespaceOrRepository: Namespace | Repository): namespaceOrRepository is Repository => {
|
||||
return (namespaceOrRepository as Repository).name !== undefined;
|
||||
};
|
||||
|
||||
const usePermissionData = (namespaceOrRepository: Namespace | Repository) => {
|
||||
const permissions = usePermissions(namespaceOrRepository);
|
||||
const availablePermissions = useAvailablePermissions();
|
||||
@@ -56,9 +60,12 @@ const Permissions: FC<Props> = ({ namespaceOrRepository }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const helpText = isRepository(namespaceOrRepository) ? null : <div>{t("permission.namespace-help")}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("permission.title")} />
|
||||
{helpText}
|
||||
<PermissionsTable
|
||||
availableRoles={availablePermissions.repositoryRoles}
|
||||
availableVerbs={availablePermissions.repositoryVerbs}
|
||||
|
||||
@@ -57,6 +57,10 @@ const PermissionIcon: FC<{ permission: Permission }> = ({ permission }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isRepository = (namespaceOrRepository: Namespace | Repository): namespaceOrRepository is Repository => {
|
||||
return (namespaceOrRepository as Repository).name !== undefined;
|
||||
};
|
||||
|
||||
const SinglePermission: FC<Props> = ({
|
||||
namespaceOrRepository,
|
||||
availableRoles,
|
||||
@@ -129,6 +133,7 @@ const SinglePermission: FC<Props> = ({
|
||||
selectedVerbs={selectedVerbs || []}
|
||||
onClose={() => setShowAdvancedDialog(false)}
|
||||
onSubmit={handleVerbsChange}
|
||||
entityType={isRepository(namespaceOrRepository) ? "repository" : "namespace"}
|
||||
/>
|
||||
) : null}
|
||||
</VCenteredTd>
|
||||
|
||||
@@ -27,10 +27,13 @@ package sonia.scm.api.v2.resources;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("java:S2160") // we don't need equals and hashcode
|
||||
public class NamespaceDto extends HalRepresentation {
|
||||
|
||||
private String namespace;
|
||||
|
||||
@@ -24,32 +24,48 @@
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.mapstruct.InjectionStrategy;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.ObjectFactory;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.NamespaceManager;
|
||||
import sonia.scm.repository.NamespacePermissions;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SearchableType;
|
||||
import sonia.scm.web.EdisonHalAppender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Link.linkBuilder;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
|
||||
class NamespaceToNamespaceDtoMapper {
|
||||
|
||||
private final ResourceLinks links;
|
||||
private final SearchEngine searchEngine;
|
||||
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
|
||||
@SuppressWarnings("squid:S3306")
|
||||
@Mapper
|
||||
public abstract class NamespaceToNamespaceDtoMapper extends BaseMapper<Namespace, NamespaceDto> {
|
||||
|
||||
@Inject
|
||||
NamespaceToNamespaceDtoMapper(ResourceLinks links, SearchEngine searchEngine) {
|
||||
this.links = links;
|
||||
this.searchEngine = searchEngine;
|
||||
}
|
||||
protected ResourceLinks links;
|
||||
@Inject
|
||||
protected SearchEngine searchEngine;
|
||||
@Inject
|
||||
protected NamespaceManager namespaceManager;
|
||||
|
||||
NamespaceDto map(String namespace) {
|
||||
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
|
||||
public abstract NamespaceDto map(String namespace);
|
||||
|
||||
@ObjectFactory
|
||||
NamespaceDto createDto(String namespace) {
|
||||
Links.Builder linkingTo = linkingTo();
|
||||
linkingTo
|
||||
.self(links.namespace().self(namespace))
|
||||
@@ -61,9 +77,30 @@ class NamespaceToNamespaceDtoMapper {
|
||||
}
|
||||
linkingTo.array(searchLinks(namespace));
|
||||
linkingTo.single(link("searchableTypes", links.searchableTypes().searchableTypesForNamespace(namespace)));
|
||||
Optional<Namespace> optionalNamespace = namespaceManager.get(namespace);
|
||||
if (optionalNamespace.isPresent()) {
|
||||
Embedded.Builder embeddedBuilder = embeddedBuilder();
|
||||
applyEnrichers(new EdisonHalAppender(linkingTo, embeddedBuilder), optionalNamespace.get(), namespace);
|
||||
}
|
||||
|
||||
return new NamespaceDto(namespace, linkingTo.build());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setLinks(ResourceLinks links) {
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setSearchEngine(SearchEngine searchEngine) {
|
||||
this.searchEngine = searchEngine;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setNamespaceManager(NamespaceManager namespaceManager) {
|
||||
this.namespaceManager = namespaceManager;
|
||||
}
|
||||
|
||||
private List<Link> searchLinks(String namespace) {
|
||||
return searchEngine.getSearchableTypes().stream()
|
||||
.filter(SearchableType::limitableToNamespace)
|
||||
|
||||
@@ -28,6 +28,7 @@ import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
public class NamespaceDao {
|
||||
@@ -50,4 +51,8 @@ public class NamespaceDao {
|
||||
public void delete(String namespace) {
|
||||
store.remove(namespace);
|
||||
}
|
||||
|
||||
public Collection<Namespace> allWithPermissions() {
|
||||
return store.getAll().values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,35 +58,25 @@ import java.util.Set;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Extension
|
||||
public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
{
|
||||
public class DefaultAuthorizationCollector implements AuthorizationCollector {
|
||||
|
||||
/** Field description */
|
||||
private static final String CACHE_NAME = "sonia.cache.authorizing";
|
||||
|
||||
/**
|
||||
* the logger for DefaultAuthorizationCollector
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(DefaultAuthorizationCollector.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
* @param cacheManager
|
||||
* @param repositoryDAO
|
||||
* @param securitySystem
|
||||
* @param repositoryPermissionProvider
|
||||
* @param groupCollector
|
||||
* @param namespaceDao
|
||||
*/
|
||||
/** authorization cache */
|
||||
private final Cache<CacheKey, AuthorizationInfo> cache;
|
||||
|
||||
private final RepositoryDAO repositoryDAO;
|
||||
private final NamespaceDao namespaceDao;
|
||||
private final SecuritySystem securitySystem;
|
||||
private final RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private final GroupCollector groupCollector;
|
||||
|
||||
@Inject
|
||||
public DefaultAuthorizationCollector(CacheManager cacheManager,
|
||||
RepositoryDAO repositoryDAO, SecuritySystem securitySystem, RepositoryPermissionProvider repositoryPermissionProvider, GroupCollector groupCollector, NamespaceDao namespaceDao)
|
||||
@@ -99,14 +89,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
this.namespaceDao = namespaceDao;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@VisibleForTesting
|
||||
AuthorizationInfo collect()
|
||||
{
|
||||
@@ -125,13 +107,6 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
return authorizationInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
* @param principals
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public AuthorizationInfo collect(PrincipalCollection principals)
|
||||
{
|
||||
@@ -176,6 +151,35 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
}
|
||||
}
|
||||
|
||||
private void collectNamespacePermissions(Builder<String> builder, User user, Set<String> groups) {
|
||||
for (Namespace namespace : namespaceDao.allWithPermissions()) {
|
||||
collectNamespacePermissions(builder, namespace, user, groups);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectNamespacePermissions(Builder<String> builder, Namespace namespace, User user, Set<String> groups) {
|
||||
for (RepositoryPermission permission : namespace.getPermissions()) {
|
||||
if (isUserPermitted(user, groups, permission)) {
|
||||
addNamespacePermission(builder, namespace, user, permission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addNamespacePermission(Builder<String> builder, Namespace namespace, User user, RepositoryPermission permission) {
|
||||
Collection<String> verbs = getVerbs(permission);
|
||||
if (!verbs.isEmpty())
|
||||
{
|
||||
String perm = "namespace:" + String.join(",", verbs) + ":" + namespace.getId();
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("add namespace permission {} for user {} at namespace {}",
|
||||
perm, user.getName(), namespace.getNamespace());
|
||||
}
|
||||
|
||||
builder.add(perm);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectRepositoryPermissions(Builder<String> builder, User user,
|
||||
Set<String> groups)
|
||||
{
|
||||
@@ -245,6 +249,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
|
||||
collectGlobalPermissions(builder, user, groups);
|
||||
collectRepositoryPermissions(builder, user, groups);
|
||||
collectNamespacePermissions(builder, user, groups);
|
||||
builder.add(canReadOwnUser(user));
|
||||
if (!Authentications.isSubjectAnonymous(user.getName())) {
|
||||
builder.add(getUserAutocompletePermission());
|
||||
@@ -254,7 +259,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
builder.add(getPublicKeyPermission(user));
|
||||
}
|
||||
|
||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER));
|
||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(Set.of(Role.USER));
|
||||
info.addStringPermissions(builder.build());
|
||||
|
||||
return info;
|
||||
@@ -314,24 +319,17 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cache key.
|
||||
*/
|
||||
private static class CacheKey
|
||||
{
|
||||
private final Set<String> groupnames;
|
||||
private final String username;
|
||||
|
||||
private CacheKey(String username, Set<String> groupnames)
|
||||
{
|
||||
this.username = username;
|
||||
this.groupnames = groupnames;
|
||||
}
|
||||
|
||||
//~--- methods ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
@@ -351,36 +349,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
&& Objects.equal(groupnames, other.groupnames);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(username, groupnames);
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/** group names */
|
||||
private final Set<String> groupnames;
|
||||
|
||||
/** username */
|
||||
private final String username;
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** authorization cache */
|
||||
private final Cache<CacheKey, AuthorizationInfo> cache;
|
||||
|
||||
/** repository dao */
|
||||
private final RepositoryDAO repositoryDAO;
|
||||
|
||||
/** security system */
|
||||
private final SecuritySystem securitySystem;
|
||||
|
||||
private final RepositoryPermissionProvider repositoryPermissionProvider;
|
||||
private final GroupCollector groupCollector;
|
||||
private final NamespaceDao namespaceDao;
|
||||
}
|
||||
|
||||
@@ -183,6 +183,12 @@
|
||||
"displayName": "Alle Repository Rechte",
|
||||
"description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen."
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"*": {
|
||||
"displayName": "Alle Namespace Rechte",
|
||||
"description": "Darf im Namespace und allen darin enthaltenen Repositories alles ausführen. Dies beinhaltet alle Namespace und Repository Berechtigungen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -183,6 +183,12 @@
|
||||
"displayName": "own repository",
|
||||
"description": "May change everything for the repository (includes all other permissions)"
|
||||
}
|
||||
},
|
||||
"namespace": {
|
||||
"*": {
|
||||
"displayName": "own namespace",
|
||||
"description": "May change everything for the namespace and all its repositories (includes all other permissions)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -98,7 +98,10 @@ class NamespaceRootResourceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUpResources() {
|
||||
NamespaceToNamespaceDtoMapper namespaceMapper = new NamespaceToNamespaceDtoMapper(links, searchEngine);
|
||||
NamespaceToNamespaceDtoMapper namespaceMapper = new NamespaceToNamespaceDtoMapperImpl();
|
||||
namespaceMapper.setLinks(links);
|
||||
namespaceMapper.setSearchEngine(searchEngine);
|
||||
namespaceMapper.setNamespaceManager(namespaceManager);
|
||||
NamespaceCollectionToDtoMapper namespaceCollectionToDtoMapper = new NamespaceCollectionToDtoMapper(namespaceMapper, links);
|
||||
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(repositoryPermissionToRepositoryPermissionDtoMapper, links);
|
||||
RepositoryPermissionDtoToRepositoryPermissionMapperImpl dtoToModelMapper = new RepositoryPermissionDtoToRepositoryPermissionMapperImpl();
|
||||
|
||||
@@ -96,7 +96,7 @@ class PermissionOverviewToPermissionOverviewDtoMapperTest {
|
||||
|
||||
@BeforeEach
|
||||
void initNamespaceMapper() {
|
||||
when(namespaceToNamespaceDtoMapper.map(any()))
|
||||
when(namespaceToNamespaceDtoMapper.map(any(String.class)))
|
||||
.thenAnswer(invocation -> new NamespaceDto(invocation.getArgument(0, String.class), Links.emptyLinks()));
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.anyObject;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -239,12 +239,24 @@ public class DefaultAuthorizationCollectorTest {
|
||||
when(repositoryDAO.getAll()).thenReturn(newArrayList(heartOfGold, puzzle42));
|
||||
when(namespaceDao.get(heartOfGold.getNamespace())).thenReturn(of(heartOfGoldNamespace));
|
||||
when(namespaceDao.get(puzzle42.getNamespace())).thenReturn(of(puzzleNamespace));
|
||||
when(namespaceDao.allWithPermissions()).thenReturn(asList(heartOfGoldNamespace, puzzleNamespace));
|
||||
|
||||
// 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", "user:changeApiKeys:trillian", "user:changePublicKeys:trillian"));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder(
|
||||
"user:autocomplete",
|
||||
"group:autocomplete",
|
||||
"user:changePassword:trillian",
|
||||
"repository:read,pull:one",
|
||||
"repository:read,pull,push:two",
|
||||
"namespace:read,pull:hitchhiker",
|
||||
"namespace:read,pull,push:guide",
|
||||
"user:read:trillian",
|
||||
"user:changeApiKeys:trillian",
|
||||
"user:changePublicKeys:trillian")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user