Skip to content

Commit

Permalink
Soft link support (#164)
Browse files Browse the repository at this point in the history
* Done, only tests are missing

* Add test

---------

Co-authored-by: Vincent Wilms <[email protected]>
  • Loading branch information
Apollo3zehn and Vincent Wilms authored Dec 17, 2024
1 parent fc6b55d commit 5dceaba
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 116 deletions.
1 change: 0 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<LangVersion>preview</LangVersion>
<RestoreAdditionalProjectSources>
https://www.myget.org/F/apollo3zehn-dev/api/v3/index.json
</RestoreAdditionalProjectSources>
Expand Down
136 changes: 68 additions & 68 deletions src/Nexus/API/v1/CatalogsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ internal class CatalogsController(
/// <param name="resourcePaths">The list of resource paths.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpPost("search-items")]
public async Task<ActionResult<Dictionary<string, CatalogItem>>>
SearchCatalogItemsAsync(
[FromBody] string[] resourcePaths,
CancellationToken cancellationToken)
public async Task<ActionResult<Dictionary<string, CatalogItem>>> SearchCatalogItemsAsync(
[FromBody] string[] resourcePaths,
CancellationToken cancellationToken)
{
var root = _appState.CatalogState.Root;

Expand All @@ -70,8 +69,7 @@ public async Task<ActionResult<Dictionary<string, CatalogItem>>>
{
resourcePathAndRequests = await Task.WhenAll(resourcePaths.Distinct().Select(async resourcePath =>
{
var catalogItemRequest = await root
.TryFindAsync(resourcePath, cancellationToken)
var catalogItemRequest = await root.TryFindAsync(root, resourcePath, cancellationToken)
?? throw new ValidationException($"Could not find resource path {resourcePath}.");

return (resourcePath, catalogItemRequest);
Expand Down Expand Up @@ -110,10 +108,9 @@ public async Task<ActionResult<Dictionary<string, CatalogItem>>>
/// <param name="catalogId">The catalog identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}")]
public Task<ActionResult<ResourceCatalog>>
GetAsync(
string catalogId,
CancellationToken cancellationToken)
public Task<ActionResult<ResourceCatalog>> GetAsync(
string catalogId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);

Expand All @@ -134,8 +131,7 @@ public Task<ActionResult<ResourceCatalog>>
/// <param name="catalogId">The parent catalog identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/child-catalog-infos")]
public async Task<ActionResult<CatalogInfo[]>>
GetChildCatalogInfosAsync(
public async Task<ActionResult<CatalogInfo[]>> GetChildCatalogInfosAsync(
string catalogId,
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -215,10 +211,9 @@ public async Task<ActionResult<CatalogInfo[]>>
/// <param name="catalogId">The catalog identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/timerange")]
public Task<ActionResult<CatalogTimeRange>>
GetTimeRangeAsync(
string catalogId,
CancellationToken cancellationToken)
public Task<ActionResult<CatalogTimeRange>> GetTimeRangeAsync(
string catalogId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);

Expand All @@ -240,13 +235,12 @@ public Task<ActionResult<CatalogTimeRange>>
/// <param name="step">Step period.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/availability")]
public async Task<ActionResult<CatalogAvailability>>
GetAvailabilityAsync(
string catalogId,
[BindRequired] DateTime begin,
[BindRequired] DateTime end,
[BindRequired] TimeSpan step,
CancellationToken cancellationToken)
public async Task<ActionResult<CatalogAvailability>> GetAvailabilityAsync(
string catalogId,
[BindRequired] DateTime begin,
[BindRequired] DateTime end,
[BindRequired] TimeSpan step,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);
begin = begin.ToUniversalTime();
Expand Down Expand Up @@ -277,10 +271,9 @@ public async Task<ActionResult<CatalogAvailability>>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/license")]
[return: CanBeNull]
public async Task<ActionResult<string?>>
GetLicenseAsync(
string catalogId,
CancellationToken cancellationToken)
public async Task<ActionResult<string?>> GetLicenseAsync(
string catalogId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);

Expand Down Expand Up @@ -312,10 +305,9 @@ public async Task<ActionResult<string?>>
/// <param name="catalogId">The catalog identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/attachments")]
public Task<ActionResult<string[]>>
GetAttachmentsAsync(
string catalogId,
CancellationToken cancellationToken)
public Task<ActionResult<string[]>> GetAttachmentsAsync(
string catalogId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);

Expand All @@ -336,12 +328,11 @@ public Task<ActionResult<string[]>>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpPut("{catalogId}/attachments/{attachmentId}")]
[DisableRequestSizeLimit]
public Task<ActionResult>
UploadAttachmentAsync(
string catalogId,
string attachmentId,
[FromBody] Stream content,
CancellationToken cancellationToken)
public Task<ActionResult> UploadAttachmentAsync(
string catalogId,
string attachmentId,
[FromBody] Stream content,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);
attachmentId = WebUtility.UrlDecode(attachmentId);
Expand Down Expand Up @@ -385,11 +376,10 @@ public Task<ActionResult>
/// <param name="attachmentId">The attachment identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpDelete("{catalogId}/attachments/{attachmentId}")]
public Task<ActionResult>
DeleteAttachmentAsync(
string catalogId,
string attachmentId,
CancellationToken cancellationToken)
public Task<ActionResult> DeleteAttachmentAsync(
string catalogId,
string attachmentId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);
attachmentId = WebUtility.UrlDecode(attachmentId);
Expand All @@ -399,8 +389,8 @@ public Task<ActionResult>
try
{
_databaseService.DeleteAttachment(catalogId, attachmentId);
return Task.FromResult<ActionResult>(
Ok());

return Task.FromResult<ActionResult>(Ok());
}
catch (IOException ex)
{
Expand All @@ -419,11 +409,10 @@ public Task<ActionResult>
/// <param name="attachmentId">The attachment identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/attachments/{attachmentId}/content")]
public Task<ActionResult>
GetAttachmentStreamAsync(
string catalogId,
string attachmentId,
CancellationToken cancellationToken)
public Task<ActionResult> GetAttachmentStreamAsync(
string catalogId,
string attachmentId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);
attachmentId = WebUtility.UrlDecode(attachmentId);
Expand Down Expand Up @@ -460,10 +449,9 @@ public Task<ActionResult>
/// <param name="catalogId">The catalog identifier.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpGet("{catalogId}/metadata")]
public Task<ActionResult<CatalogMetadata>>
GetMetadataAsync(
string catalogId,
CancellationToken cancellationToken)
public Task<ActionResult<CatalogMetadata>> GetMetadataAsync(
string catalogId,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);

Expand All @@ -482,23 +470,20 @@ public Task<ActionResult<CatalogMetadata>>
/// <param name="metadata">The catalog metadata to set.</param>
/// <param name="cancellationToken">A token to cancel the current operation.</param>
[HttpPut("{catalogId}/metadata")]
public async Task<ActionResult<object?>>
SetMetadataAsync(
string catalogId,
[FromBody] CatalogMetadata metadata,
CancellationToken cancellationToken)
public async Task<ActionResult> SetMetadataAsync(
string catalogId,
[FromBody] CatalogMetadata metadata,
CancellationToken cancellationToken)
{
catalogId = WebUtility.UrlDecode(catalogId);

if (metadata.Overrides?.Id != catalogId)
return UnprocessableEntity("The catalog ID does not match the ID of the catalog to update.");

var response = await ProtectCatalogAsync<object?>(catalogId, ensureReadable: false, ensureWritable: true, async catalogContainer =>
var response = await ProtectCatalogNonGenericAsync(catalogId, ensureReadable: false, ensureWritable: true, async catalogContainer =>
{
await catalogContainer.UpdateMetadataAsync(metadata);

return default!;

return Ok();
}, cancellationToken);

return response;
Expand All @@ -511,11 +496,12 @@ private async Task<ActionResult<T>> ProtectCatalogAsync<T>(
Func<CatalogContainer, Task<ActionResult<T>>> action,
CancellationToken cancellationToken)
{
/* KEEP IN SYNC WITH ProtectCatalogNonGenericAsync! */
var root = _appState.CatalogState.Root;

var catalogContainer = catalogId == CatalogContainer.RootCatalogId
? root
: await root.TryFindCatalogContainerAsync(catalogId, cancellationToken);
: await root.TryFindCatalogContainerAsync(root, catalogId, cancellationToken);

if (catalogContainer is not null)
{
Expand Down Expand Up @@ -550,16 +536,30 @@ private async Task<ActionResult> ProtectCatalogNonGenericAsync(
Func<CatalogContainer, Task<ActionResult>> action,
CancellationToken cancellationToken)
{
/* KEEP IN SYNC WITH ProtectCatalogAsync! */
var root = _appState.CatalogState.Root;
var catalogContainer = await root.TryFindCatalogContainerAsync(catalogId, cancellationToken);

var catalogContainer = catalogId == CatalogContainer.RootCatalogId
? root
: await root.TryFindCatalogContainerAsync(root, catalogId, cancellationToken);

if (catalogContainer is not null)
{
if (ensureReadable && !AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User))
return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to read the catalog {catalogId}.");
if (ensureReadable && !AuthUtilities.IsCatalogReadable(
catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User))
{
return StatusCode(
StatusCodes.Status403Forbidden,
$"The current user is not permitted to read the catalog {catalogId}.");
}

if (ensureWritable && !AuthUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User))
return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}.");
if (ensureWritable && !AuthUtilities.IsCatalogWritable(
catalogContainer.Id, catalogContainer.Metadata, User))
{
return StatusCode(
StatusCodes.Status403Forbidden,
$"The current user is not permitted to modify the catalog {catalogId}.");
}

return await action.Invoke(catalogContainer);
}
Expand Down
6 changes: 4 additions & 2 deletions src/Nexus/API/v1/JobsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ public async Task<ActionResult<Job>> ExportAsync(
{
catalogItemRequests = await Task.WhenAll(parameters.ResourcePaths.Select(async resourcePath =>
{
var catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken) ?? throw new ValidationException($"Could not find resource path {resourcePath}.");
var catalogItemRequest = await root.TryFindAsync(root, resourcePath, cancellationToken)
?? throw new ValidationException($"Could not find resource path {resourcePath}.");

return catalogItemRequest;
}));
}
Expand Down Expand Up @@ -304,7 +306,7 @@ private async Task<ActionResult> ProtectCatalogNonGenericAsync(
CancellationToken cancellationToken)
{
var root = _appStateManager.AppState.CatalogState.Root;
var catalogContainer = await root.TryFindCatalogContainerAsync(catalogId, cancellationToken);
var catalogContainer = await root.TryFindCatalogContainerAsync(root, catalogId, cancellationToken);

if (catalogContainer is not null)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Nexus/Core/CatalogContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public CatalogContainer(
Id = catalogRegistration.Path;
Title = catalogRegistration.Title;
IsTransient = catalogRegistration.IsTransient;
LinkTarget = catalogRegistration.LinkTarget;
Owner = owner;
PipelineId = pipelineId;
Pipeline = pipeline;
Expand All @@ -61,6 +62,8 @@ public CatalogContainer(

public bool IsTransient { get; }

public string? LinkTarget { get; }

public ClaimsPrincipal? Owner { get; }

public string PhysicalName => Id.TrimStart('/').Replace('/', '_');
Expand Down
38 changes: 30 additions & 8 deletions src/Nexus/Core/CatalogContainerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ internal static class CatalogContainerExtensions
{
public static async Task<CatalogItemRequest?> TryFindAsync(
this CatalogContainer parent,
CatalogContainer root,
string resourcePath,
CancellationToken cancellationToken)
{
if (!DataModelUtilities.TryParseResourcePath(resourcePath, out var parseResult))
throw new Exception("The resource path is malformed.");

// find catalog
var catalogContainer = await parent.TryFindCatalogContainerAsync(parseResult.CatalogId, cancellationToken);
var catalogContainer = await parent.TryFindCatalogContainerAsync(root, parseResult.CatalogId, cancellationToken);

if (catalogContainer is null)
return default;
Expand Down Expand Up @@ -54,25 +55,46 @@ internal static class CatalogContainerExtensions

public static async Task<CatalogContainer?> TryFindCatalogContainerAsync(
this CatalogContainer parent,
CatalogContainer root,
string catalogId,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
int recursionCounter = 0)
{
var childCatalogContainers = await parent.GetChildCatalogContainersAsync(cancellationToken);
var catalogIdWithTrailingSlash = catalogId + "/"; /* the slashes are important to correctly find /A/D/E2 in the tests */
var catalogIdWithTrailingSlash = catalogId + "/"; /* The slashes are important to correctly find /A/D/E2 in the tests */

var catalogContainer = childCatalogContainers
.FirstOrDefault(current => catalogIdWithTrailingSlash.StartsWith(current.Id + "/"));

/* nothing found */
/* Nothing found */
if (catalogContainer is null)
return default;

/* catalogContainer is searched one */
/* CatalogContainer is the searched one */
else if (catalogContainer.Id == catalogId)
return catalogContainer;
{
if (catalogContainer.LinkTarget is null)
{
return catalogContainer;
}

/* It is a soft link */
else
{
if (recursionCounter >= 10)
return null;

return await root.TryFindCatalogContainerAsync(
root,
catalogContainer.LinkTarget,
cancellationToken,
++recursionCounter
);
}
}

/* catalogContainer is (grand)-parent of searched one */
/* CatalogContainer is (grand)-parent of the searched one */
else
return await catalogContainer.TryFindCatalogContainerAsync(catalogId, cancellationToken);
return await catalogContainer.TryFindCatalogContainerAsync(root, catalogId, cancellationToken);
}
}
Loading

0 comments on commit 5dceaba

Please sign in to comment.