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:
Konstantin Schaper
2023-05-25 18:51:29 +02:00
parent 9f9aebf1d5
commit b812922142
27 changed files with 302 additions and 116 deletions

View File

@@ -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.
![Repository-Settings-PermissionOverview](assets/repository-settings-permissionOverview.png)
Für individuelle Berechtigungen kann man über "Erweitert" einen Dialog öffnen, um jede Berechtigung einzeln zu vergeben.

View File

@@ -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.
![Repository-Settings-PermissionOverview](assets/repository-settings-permissionOverview.png)
To manage permissions individually, an "Advanced" dialog can be opened to manage every single permission.

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}
}
);
}

View File

@@ -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
*

View File

@@ -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)

View File

@@ -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)
);

View File

@@ -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>

View File

@@ -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();

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 = {

View File

@@ -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")}

View File

@@ -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}
/>
));

View File

@@ -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}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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();

View File

@@ -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()));
}

View File

@@ -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")
);
}
/**