Skip to content

Commit

Permalink
Add filesystem audit events for Panorama Public symlinks (#361)
Browse files Browse the repository at this point in the history
- Added filesystem audit events when symlinks are created / updated, or replaced with the target file.
- Added additional logging, and removed unused boolean parameter from moveAndSymLinkDirectory.
  • Loading branch information
vagisha authored Jul 13, 2023
1 parent 6e54ef7 commit 1bd3975
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,15 @@ public void process(@Nullable PipelineJob job, FolderImportContext ctx, VirtualF
Files.createDirectories(targetFiles.toPath());
}

log.info("Moving files and creating sym links in folder " + ctx.getContainer().getPath());
PanoramaPublicSymlinkManager.get().moveAndSymLinkDirectory(expJob, sourceFiles, targetFiles, false, log);
if (expJob.isMoveAndSymlink())
{
log.info("Moving files to folder " + expJob.getContainer().getPath() + " and creating symlinks");
}
else
{
log.info("Copying files to folder " + expJob.getContainer().getPath());
}
PanoramaPublicSymlinkManager.get().moveAndSymLinkDirectory(expJob, sourceFiles, targetFiles, log);

alignDataFileUrls(expJob.getUser(), ctx.getContainer(), log);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public void fileCreated(@NotNull File created, @Nullable User user, @Nullable Co
public int fileMoved(@NotNull File src, @NotNull File dest, @Nullable User user, @Nullable Container container)
{
// Update any symlinks targeting the file
PanoramaPublicSymlinkManager.get().fireSymlinkUpdate(src.toPath(), dest.toPath(), container);
PanoramaPublicSymlinkManager.get().fireSymlinkUpdate(src.toPath(), dest.toPath(), container, user);

ExpData data = ExperimentService.get().getExpDataByURL(src, null);
if (null != data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ public void containerDeleted(Container c, User user)
{
JournalManager.deleteProjectJournal(c, user);

PanoramaPublicSymlinkManager.get().beforeContainerDeleted(c);
PanoramaPublicSymlinkManager.get().beforeContainerDeleted(c, user);
}

@Override
public void containerMoved(Container c, Container oldParent, User user)
{
PanoramaPublicSymlinkManager.get().fireSymlinkUpdateContainer(oldParent, c);
PanoramaPublicSymlinkManager.get().fireSymlinkUpdateContainer(oldParent, c, user);
}

@Override
Expand Down Expand Up @@ -109,7 +109,7 @@ public void propertyChange(PropertyChangeEvent evt)
// ce.getOldValue() and ce.getNewValue() are just the names of the old and new containers. We need the full path.
Path oldPath = parentPath.resolve((String) ce.getOldValue());
Path newPath = parentPath.resolve((String) ce.getNewValue());
PanoramaPublicSymlinkManager.get().fireSymlinkUpdateContainer(oldPath.toString(), newPath.toString(), c);
PanoramaPublicSymlinkManager.get().fireSymlinkUpdateContainer(oldPath.toString(), newPath.toString(), c, ce.user);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.labkey.panoramapublic;

import org.labkey.api.data.Container;
import org.labkey.api.security.User;

import java.io.IOException;
import java.nio.file.Path;

public interface PanoramaPublicSymlinkHandler
{
void handleSymlink(Path link, Path target) throws IOException;
void handleSymlink(Path link, Path target, Container container, User user) throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.audit.AuditLogService;
import org.labkey.api.audit.provider.FileSystemAuditProvider;
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerManager;
import org.labkey.api.data.ContainerService;
import org.labkey.api.files.FileContentService;
import org.labkey.api.module.ModuleLoader;
import org.labkey.api.security.User;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.logging.LogHelper;
import org.labkey.panoramapublic.model.ExperimentAnnotations;
Expand Down Expand Up @@ -54,13 +57,13 @@ public static PanoramaPublicSymlinkManager get()
}


private void handleContainerSymlinks(File source, PanoramaPublicSymlinkHandler handler)
private void handleContainerSymlinks(File source, PanoramaPublicSymlinkHandler handler, Container container, User user)
{
for (File file : Objects.requireNonNull(source.listFiles()))
{
if (file.isDirectory())
{
handleContainerSymlinks(file, handler);
handleContainerSymlinks(file, handler, container, user);
}
else
{
Expand All @@ -69,7 +72,7 @@ private void handleContainerSymlinks(File source, PanoramaPublicSymlinkHandler h
{
try {
Path target = Files.readSymbolicLink(filePath);
handler.handleSymlink(filePath, target);
handler.handleSymlink(filePath, target, container, user);
} catch (IOException x) {
_log.error("Unable to resolve symlink target for symlink at " + filePath);
}
Expand All @@ -78,27 +81,27 @@ private void handleContainerSymlinks(File source, PanoramaPublicSymlinkHandler h
}
}

public void handleContainerSymlinks(Container container, PanoramaPublicSymlinkHandler handler)
public void handleContainerSymlinks(Container container, User user, PanoramaPublicSymlinkHandler handler)
{
FileContentService fcs = FileContentService.get();
if (null != fcs)
{
File root = fcs.getFileRoot(container);
if (null != root)
{
handleContainerSymlinks(root, handler);
handleContainerSymlinks(root, handler, container, user);
}
}
}

private void handleAllSymlinks(Set<Container> containers, PanoramaPublicSymlinkHandler handler)
private void handleAllSymlinks(Set<Container> containers, User user, PanoramaPublicSymlinkHandler handler)
{
for (Container container : containers)
{
Set<Container> tree = ContainerManager.getAllChildren(container);
for (Container node : tree)
{
handleContainerSymlinks(node, handler);
handleContainerSymlinks(node, user, handler);
}
}
}
Expand All @@ -112,7 +115,7 @@ private String normalizeContainerPath(String path)
return File.separator + path + File.separator;
}

public void beforeContainerDeleted(Container container)
public void beforeContainerDeleted(Container container, User user)
{
if (PanoramaPublicManager.canBeSymlinkTarget(container)) // Fire the event only if the container being deleted is in the Panorama Public project.
{
Expand All @@ -121,7 +124,7 @@ public void beforeContainerDeleted(Container container)
ExperimentAnnotations expAnnot = ExperimentAnnotationsManager.getExperimentIncludesContainer(container);
if (null != expAnnot)
{
fireSymlinkCopiedExperimentDelete(expAnnot, container);
fireSymlinkCopiedExperimentDelete(expAnnot, container, user);
}
}
}
Expand All @@ -132,7 +135,7 @@ public void beforeContainerDeleted(Container container)
* @param container container being deleted. This could be a subfolder of the experiment container if the experiment
* is configured to include subfolders.
*/
private void fireSymlinkCopiedExperimentDelete(ExperimentAnnotations expAnnot, Container container)
private void fireSymlinkCopiedExperimentDelete(ExperimentAnnotations expAnnot, Container container, User user)
{
if (expAnnot.getDataVersion() != null && !ExperimentAnnotationsManager.isCurrentVersion(expAnnot))
{
Expand All @@ -153,17 +156,19 @@ private void fireSymlinkCopiedExperimentDelete(ExperimentAnnotations expAnnot, C
if (nextHighestVersion != null)
{
Container versionContainer = nextHighestVersion.getContainer();
handleContainerSymlinks(versionContainer, (link, target) -> {
handleContainerSymlinks(versionContainer, user, (link, target, c, u) -> {
if (!target.startsWith(deletedContainerPath))
{
return;
}
Files.move(target, link, REPLACE_EXISTING); // Move the files back to the next highest version of the experiment
addReplaceSymlinkWithTargetAuditEvent(link, target, c, u);

_log.info("File moved from " + target + " to " + link);

// This should update the symlinks in the submitted folder as well as
// symlinks in versions older than this one to point to the files in the next highest version.
fireSymlinkUpdate(target, link, container);
fireSymlinkUpdate(target, link, container, user);
});
}
}
Expand All @@ -180,12 +185,14 @@ private void fireSymlinkCopiedExperimentDelete(ExperimentAnnotations expAnnot, C
}
if (null != sourceContainer)
{
handleContainerSymlinks(sourceContainer, (link, target) -> {
handleContainerSymlinks(sourceContainer, user, (link, target, c, u) -> {
if (!target.startsWith(deletedContainerPath))
{
return;
}
Files.move(target, link, REPLACE_EXISTING);
addReplaceSymlinkWithTargetAuditEvent(link, target, c, u);

_log.info("File moved from " + target + " to " + link);

// Symlinks in the source container point to -> current version container on Panorama Public
Expand All @@ -197,35 +204,36 @@ private void fireSymlinkCopiedExperimentDelete(ExperimentAnnotations expAnnot, C
}
}

public void fireSymlinkUpdateContainer(Container oldContainer, Container newContainer)
public void fireSymlinkUpdateContainer(Container oldContainer, Container newContainer, User user)
{
// Update symlinks to new target
FileContentService fcs = FileContentService.get();
if (fcs != null)
{
if (fcs.getFileRoot(oldContainer) != null && fcs.getFileRoot(newContainer) != null)
{
fireSymlinkUpdateContainer(fcs.getFileRoot(oldContainer).getPath(), fcs.getFileRoot(newContainer).getPath(), oldContainer);
fireSymlinkUpdateContainer(fcs.getFileRoot(oldContainer).getPath(), fcs.getFileRoot(newContainer).getPath(), oldContainer, user);
}
}
}

public void fireSymlinkUpdateContainer(String oldContainer, String newContainer, Container container)
public void fireSymlinkUpdateContainer(String oldContainer, String newContainer, Container container, User user)
{
if (PanoramaPublicManager.canBeSymlinkTarget(container))
{
String oldContainerPath = normalizeContainerPath(oldContainer);
String newContainerPath = normalizeContainerPath(newContainer);

Set<Container> containers = getSymlinkContainers(container);
handleAllSymlinks(containers, (link, target) -> {
handleAllSymlinks(containers, user, (link, target, c, u) -> {
if (String.valueOf(target).contains(oldContainerPath))
{
Path newTarget = Path.of(target.toString().replace(oldContainerPath, newContainerPath));
try
{
Files.delete(link);
Files.createSymbolicLink(link, newTarget);
addLinkUpdatedAuditEvent(link, newTarget, c, u);
}
catch (IOException e)
{
Expand All @@ -236,20 +244,53 @@ public void fireSymlinkUpdateContainer(String oldContainer, String newContainer,
}
}

public void fireSymlinkUpdate(Path oldTarget, Path newTarget, Container container)
private void addLinkUpdatedAuditEvent(Path link, Path newTarget, Container linkContainer, User user)
{
addFileAuditEvent(link, linkContainer, user, "Updated symlink target to " + newTarget);
}

private void addReplaceSymlinkWithTargetAuditEvent(Path link, Path target, Container linkContainer, User user)
{
addFileAuditEvent(link, linkContainer, user, "Replaced symlink with target file " + target);
}

private void addReplaceTargetWithSymlinkAuditEvent(Path link, Path target, Container container, User user)
{
addFileAuditEvent(link, container, user, "Replaced target file with symlink to " + target);
}

private void addFileAuditEvent(Path link, Container container, User user, String comment)
{
FileSystemAuditProvider.FileSystemAuditEvent event = new FileSystemAuditProvider.FileSystemAuditEvent(
container != null ? container.getId() : null, comment);
event.setFile(link.toString());
AuditLogService.get().addEvent(user, event);
}

public void fireSymlinkUpdate(Path oldTarget, Path newTarget, Container container, User user)
{
fireSymlinkUpdate(oldTarget, newTarget, container, user, null);
}

public void fireSymlinkUpdate(Path oldTarget, Path newTarget, Container container, User user, @Nullable Logger log)
{
if (PanoramaPublicManager.canBeSymlinkTarget(container)) // Only files in the Panorama Public project can be symlink targets.
{
Set<Container> containers = getSymlinkContainers(container);

handleAllSymlinks(containers, (link, target) -> {
handleAllSymlinks(containers, user, (link, target, c, u) -> {
if (!target.equals(oldTarget))
return;

try
{
Files.delete(link);
Files.createSymbolicLink(link, newTarget);
addLinkUpdatedAuditEvent(link, newTarget, c, u);
if (log != null)
{
log.info("Target for symlink " + link + " updated to " + newTarget);
}
}
catch (IOException e)
{
Expand All @@ -259,7 +300,7 @@ public void fireSymlinkUpdate(Path oldTarget, Path newTarget, Container containe
}
}

public void moveAndSymLinkDirectory(CopyExperimentPipelineJob job, File source, File target, boolean createSourceSymLinks, @Nullable Logger log) throws IOException
public void moveAndSymLinkDirectory(CopyExperimentPipelineJob job, File source, File target, @Nullable Logger log) throws IOException
{
if (null == log)
{
Expand All @@ -283,7 +324,7 @@ public void moveAndSymLinkDirectory(CopyExperimentPipelineJob job, File source,
log.debug("Directory created: " + targetPath);
}

moveAndSymLinkDirectory(job, file, targetPath.toFile(), createSourceSymLinks, log);
moveAndSymLinkDirectory(job, file, targetPath.toFile(), log);
}
else
{
Expand Down Expand Up @@ -313,6 +354,7 @@ public void moveAndSymLinkDirectory(CopyExperimentPipelineJob job, File source,

// Copy the file to panorama public
Files.copy(filePath, targetPath, REPLACE_EXISTING);
log.debug("Copied file " + filePath + " to " + targetPath);
fcs.fireFileCreateEvent(targetPath, job.getUser(), job.getContainer());

continue;
Expand All @@ -321,35 +363,50 @@ public void moveAndSymLinkDirectory(CopyExperimentPipelineJob job, File source,
// Symbolic link should move the target file over. This would be for a re-copy to public.
if (Files.isSymbolicLink(filePath))
{
log.debug("Source file is a symlink: " + filePath);

Path oldPath = Files.readSymbolicLink(filePath);
Files.move(oldPath, targetPath, REPLACE_EXISTING);
log.debug("Moved symlink target " + oldPath + " to " + targetPath);
fcs.fireFileCreateEvent(targetPath, job.getUser(), job.getContainer());

fireSymlinkUpdate(oldPath, targetPath, job.getContainer()); // job container is the target container on Panorama Public
log.debug("File moved from " + oldPath + " to " + targetPath);
fireSymlinkUpdate(oldPath, targetPath, job.getContainer(), // job container is the target container on Panorama Public
job.getUser(), log);

Path symlink = Files.createSymbolicLink(oldPath, targetPath);
log.debug("Symlink created: " + symlink);
Container oldTargetContainer = getContainerForFilePath(oldPath);
if (oldTargetContainer != null)
{
// The target of the symlink has been moved from a previous version on Panorama Public to the
// new version on Panorama Public, and a symink has been created in the previous version container.
// Add an audit event in the previous version container.
addReplaceTargetWithSymlinkAuditEvent(symlink, targetPath, oldTargetContainer, job.getUser());
}
log.debug("Replaced old target with symlink: " + symlink);
}
else
{
Files.move(filePath, targetPath, REPLACE_EXISTING);
log.debug("Moved file " + filePath + " to " + targetPath);
fcs.fireFileCreateEvent(targetPath, job.getUser(), job.getContainer());

Files.createSymbolicLink(filePath, targetPath);
log.debug("Created symlink " + filePath + " targeting " + targetPath);
// We don't need to update any symlinks here since the source container should not have any symlink targets.
}

if (createSourceSymLinks)
{
Path symlink = Files.createSymbolicLink(filePath, targetPath);
log.debug("Symlink created: " + symlink);
}
}
}
}
}

// Get the container for the given file path.
// Example: if the file path is C:\Users\vsharma\WORK\LabKey\build\deploy\files\Panorama Public\TestProject V.1\@files\Study9S_Site52_v1.sky.zip
// this will return the container for "/Panorama Public/TestProject V.1"
private Container getContainerForFilePath(Path path)
{
return FileContentService.get().getContainersForFilePath(path).stream().findFirst().orElse(null);
}

private void verifyFileTreeSymlinks(File source, Map<String, String> linkInvalidTarget, Map<String, String> linkWithSymlinkTarget) throws IOException
{
for (File file : Objects.requireNonNull(source.listFiles()))
Expand Down
Loading

0 comments on commit 1bd3975

Please sign in to comment.