mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-01 09:50:48 +01:00
Add API for updating RepositoryPermissions
Squash commits of branch feature/permission-updater: - Add API for updating RepositoryPermissions - Add licensing and logging - Fix assertion depending on the order of item serialization - Replace manual logger setup with @Slf4j annotation
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import sonia.scm.repository.RepositoryPermissionHolder;
|
||||
|
||||
public interface RepositoryPermissionUpdater {
|
||||
void removePermission(RepositoryPermissionHolder permissionHolder, String permissionName);
|
||||
}
|
||||
@@ -62,8 +62,10 @@ import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
import sonia.scm.store.JAXBDataStoreFactory;
|
||||
import sonia.scm.store.JAXBPropertyFileAccess;
|
||||
import sonia.scm.update.BlobDirectoryAccess;
|
||||
import sonia.scm.update.DefaultRepositoryPermissionUpdater;
|
||||
import sonia.scm.update.NamespaceUpdateIterator;
|
||||
import sonia.scm.update.PropertyFileAccess;
|
||||
import sonia.scm.update.RepositoryPermissionUpdater;
|
||||
import sonia.scm.update.RepositoryUpdateIterator;
|
||||
import sonia.scm.update.StoreUpdateStepUtilFactory;
|
||||
import sonia.scm.update.UpdateStepRepositoryMetadataAccess;
|
||||
@@ -121,6 +123,7 @@ public class BootstrapModule extends AbstractModule {
|
||||
bind(RepositoryUpdateIterator.class, FileRepositoryUpdateIterator.class);
|
||||
bind(NamespaceUpdateIterator.class, FileNamespaceUpdateIterator.class);
|
||||
bind(StoreUpdateStepUtilFactory.class, FileStoreUpdateStepUtilFactory.class);
|
||||
bind(RepositoryPermissionUpdater.class, DefaultRepositoryPermissionUpdater.class);
|
||||
bind(new TypeLiteral<UpdateStepRepositoryMetadataAccess<Path>>() {}).to(new TypeLiteral<MetadataStore>() {});
|
||||
|
||||
// bind metrics
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.xml.bind.JAXBContext;
|
||||
import jakarta.xml.bind.JAXBException;
|
||||
import jakarta.xml.bind.Marshaller;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.RepositoryPermissionHolder;
|
||||
import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class DefaultRepositoryPermissionUpdater implements RepositoryPermissionUpdater {
|
||||
|
||||
private final RepositoryLocationResolver locationResolver;
|
||||
private final DataStore<Namespace> namespaceStore;
|
||||
private final JAXBContext jaxbRepositoryContext;
|
||||
|
||||
@Inject
|
||||
public DefaultRepositoryPermissionUpdater(RepositoryLocationResolver locationResolver,
|
||||
DataStoreFactory dataStoreFactory) {
|
||||
this.locationResolver = locationResolver;
|
||||
this.namespaceStore = dataStoreFactory.withType(Namespace.class).withName("namespaces").build();
|
||||
this.jaxbRepositoryContext = createJAXBContext();
|
||||
}
|
||||
|
||||
private JAXBContext createJAXBContext() {
|
||||
try {
|
||||
return JAXBContext.newInstance(Repository.class);
|
||||
} catch (JAXBException e) {
|
||||
throw new UpdateException("could not create Repository XML marshaller", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePermission(RepositoryPermissionHolder permissionHolder, String permissionName) {
|
||||
List<RepositoryPermission> permissionsToUpdate = permissionHolder
|
||||
.getPermissions()
|
||||
.stream()
|
||||
.filter(permission -> shouldPermissionBeRemoved(permission, permissionName))
|
||||
.toList();
|
||||
permissionsToUpdate.forEach(permissionHolder::removePermission);
|
||||
|
||||
List<RepositoryPermission> updatedPermissions = permissionsToUpdate
|
||||
.stream()
|
||||
.map(permission -> removePermissionFromVerbs(permission, permissionName))
|
||||
.toList();
|
||||
updatedPermissions.forEach(permission -> {
|
||||
|
||||
if (permissionHolder instanceof Repository repository) {
|
||||
log.debug(
|
||||
"removing permission {} from {} inside repository {}",
|
||||
permissionName,
|
||||
permission.getName(),
|
||||
repository
|
||||
);
|
||||
}
|
||||
|
||||
if (permissionHolder instanceof Namespace namespace) {
|
||||
log.debug(
|
||||
"removing permission {} from {} inside namespace {}",
|
||||
permissionName,
|
||||
permission.getName(),
|
||||
namespace.getNamespace());
|
||||
}
|
||||
|
||||
permissionHolder.addPermission(permission);
|
||||
});
|
||||
|
||||
if (permissionHolder instanceof Repository repository) {
|
||||
Path path = locationResolver.forClass(Path.class).getLocation(repository.getId());
|
||||
this.writeRepository(repository, path);
|
||||
}
|
||||
|
||||
if (permissionHolder instanceof Namespace namespace) {
|
||||
this.writeNamespace(namespace);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldPermissionBeRemoved(RepositoryPermission permission, String permissionName) {
|
||||
return permission.getVerbs().contains(permissionName);
|
||||
}
|
||||
|
||||
private RepositoryPermission removePermissionFromVerbs(RepositoryPermission permission, String permissionName) {
|
||||
return new RepositoryPermission(
|
||||
permission.getName(),
|
||||
permission
|
||||
.getVerbs()
|
||||
.stream()
|
||||
.filter(verb -> !verb.equals(permissionName))
|
||||
.collect(Collectors.toUnmodifiableSet()),
|
||||
permission.isGroupPermission()
|
||||
);
|
||||
}
|
||||
|
||||
private void writeRepository(Repository repository, Path repositoryPath) {
|
||||
try {
|
||||
Marshaller marshaller = jaxbRepositoryContext.createMarshaller();
|
||||
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
|
||||
marshaller.marshal(repository, repositoryPath.resolve("metadata.xml").toFile());
|
||||
} catch (JAXBException e) {
|
||||
throw new UpdateException("could not write repository structure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeNamespace(Namespace namespace) {
|
||||
this.namespaceStore.put(namespace.getId(), namespace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
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.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Namespace;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.InMemoryByteDataStoreFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultRepositoryPermissionUpdaterTest {
|
||||
|
||||
private final Repository repository = new Repository(
|
||||
"1",
|
||||
"git",
|
||||
"Kanto",
|
||||
"Saffron City",
|
||||
"Sabrina",
|
||||
"City",
|
||||
new RepositoryPermission("Trainer Red", Set.of("read", "pull", "readCIStatus"), false)
|
||||
);
|
||||
private final InMemoryByteDataStoreFactory dataStoreFactory = new InMemoryByteDataStoreFactory();
|
||||
@Mock
|
||||
private RepositoryLocationResolver locationResolver;
|
||||
@Mock
|
||||
private RepositoryLocationResolver.RepositoryLocationResolverInstance<Path> resolverInstance;
|
||||
private DefaultRepositoryPermissionUpdater updater;
|
||||
|
||||
@TempDir
|
||||
private Path tempDir;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
updater = new DefaultRepositoryPermissionUpdater(locationResolver, dataStoreFactory);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class RemovePermission {
|
||||
|
||||
@Test
|
||||
void shouldRemovePermissionFromUserAndGroupsForNamespace() {
|
||||
RepositoryPermission userPermission = new RepositoryPermission(
|
||||
"Trainer Red", Set.of("read", "pull", "readCIStatus"), false
|
||||
);
|
||||
RepositoryPermission groupPermission = new RepositoryPermission(
|
||||
"Elite Four", Set.of("read", "pull", "readCIStatus"), true
|
||||
);
|
||||
|
||||
DataStore<Namespace> namespaceStore = dataStoreFactory.withType(Namespace.class).withName("namespaces").build();
|
||||
Namespace namespace = new Namespace("Kanto");
|
||||
namespace.setPermissions(Set.of(userPermission, groupPermission));
|
||||
namespaceStore.put(namespace.getId(), namespace);
|
||||
|
||||
updater.removePermission(namespace, "readCIStatus");
|
||||
|
||||
assertThat(namespaceStore.get(namespace.getId()).getPermissions()).containsOnly(
|
||||
new RepositoryPermission("Trainer Red", Set.of("read", "pull"), false),
|
||||
new RepositoryPermission("Elite Four", Set.of("read", "pull"), true)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemovePermissionFromUserAndGroupsForRepository() throws IOException {
|
||||
URL metadataUrl = Resources.getResource(
|
||||
"sonia/scm/update/repository/metadataWithPermissionsToRemove.xml"
|
||||
);
|
||||
Files.copy(metadataUrl.openStream(), tempDir.resolve("metadata.xml"));
|
||||
when(locationResolver.forClass(Path.class)).thenReturn(resolverInstance);
|
||||
when(resolverInstance.getLocation(repository.getId())).thenReturn(tempDir);
|
||||
|
||||
updater.removePermission(repository, "readCIStatus");
|
||||
|
||||
List<String> newMetadata = Files.readAllLines(tempDir.resolve("metadata.xml"));
|
||||
assertThat(newMetadata.stream().map(String::trim))
|
||||
.contains(
|
||||
"<permission>",
|
||||
"<groupPermission>false</groupPermission>",
|
||||
"<name>Trainer Red</name>",
|
||||
"<verb>read</verb>",
|
||||
"<verb>pull</verb>",
|
||||
"</permission>"
|
||||
).doesNotContain("<verb>readCIStatus</verb>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!--
|
||||
Copyright (c) 2020 - present Cloudogu GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, version 3.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
-->
|
||||
|
||||
<repositories>
|
||||
<properties/>
|
||||
<id>1</id>
|
||||
<namespace>Kanto</namespace>
|
||||
<name>Saffron City</name>
|
||||
<type>git</type>
|
||||
<description>City</description>
|
||||
<contact>Sabrina</contact>
|
||||
<creationDate>1738596766353</creationDate>
|
||||
<lastModified>1740736889695</lastModified>
|
||||
<permission>
|
||||
<groupPermission>false</groupPermission>
|
||||
<name>Trainer Red</name>
|
||||
<verb>read</verb>
|
||||
<verb>pull</verb>
|
||||
<verb>readCIStatus</verb>
|
||||
</permission>
|
||||
<archived>false</archived>
|
||||
</repositories>
|
||||
Reference in New Issue
Block a user