Skip to content

Support allow-list for SOP classes. #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
93c014f
gh-28 Remove Newtonsoft.Json where possible
mocsharp Jun 2, 2022
50601e1
gh-28 Use JsonStringEnumMemberConverter in IG client
mocsharp Jun 3, 2022
199c694
gh-28 Fix integration test failure due to JSON serializer change
mocsharp Jun 3, 2022
adbb737
gh-28 Fix unit test failure due to JSON serializer change
mocsharp Jun 3, 2022
eb3afce
gh-28 Add DICOMweb STOW API
mocsharp May 21, 2022
888a369
gh-28 Unit test for DICOMweb STOW-RS
mocsharp Jun 2, 2022
eb1c94c
gh-28 Remove Newtonsoft.Json where possible
mocsharp Jun 2, 2022
7a77c8a
gh-28 Use JsonStringEnumMemberConverter in IG client
mocsharp Jun 3, 2022
c3a9366
gh-28 Fix integration test failure due to JSON serializer change
mocsharp Jun 3, 2022
215890e
gh-28 Fix unit test failure due to JSON serializer change
mocsharp Jun 3, 2022
a20dcd0
gh-28 Add DICOMweb STOW integration test feature
mocsharp Jun 6, 2022
66b2d06
gh-28 Fix unit test & integration test
mocsharp Jun 6, 2022
7d7b2ef
gh-28 Fix warnings
mocsharp Jun 6, 2022
891f7cc
gh-28 Handles zero length stream and no content requests
mocsharp Jun 8, 2022
8834ffb
gh-28 Update user guide
mocsharp Jun 8, 2022
4bc07ec
gh-28 Update changelog
mocsharp Jun 8, 2022
bd1865e
gh-28 Fix unit test
mocsharp Jun 8, 2022
893e350
gh-28 Restore default temp storage path
mocsharp Jun 14, 2022
eaf830d
gh-28 Ignore instances if StudyInstanceUIDs don't match
mocsharp Jun 22, 2022
086e963
Fix merge conflicts
mocsharp Jun 22, 2022
44db9cd
gh-28 Add DICOMweb STOW API
mocsharp May 21, 2022
fcc3303
gh-28 Unit test for DICOMweb STOW-RS
mocsharp Jun 2, 2022
b70bf84
gh-28 Add DICOMweb STOW integration test feature
mocsharp Jun 6, 2022
f7ecb0b
gh-33 Support allow-list for SOP classes.
mocsharp Jun 13, 2022
0e6245c
gh-33 Update DB migration
mocsharp Jun 14, 2022
d1fb223
Merge remote-tracking branch 'origin/develop' into vchang/33-allowed-…
mocsharp Jun 28, 2022
5b49600
Fix merge conflicts
mocsharp Jun 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/api/rest/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ curl --location --request GET 'http://localhost:5000/config/ae'
"workflows": ["brain-tumor", "b75cd27a-068a-4f9c-b3da-e5d4ea08c55a"],
"grouping": "0020,000D",
"timeout": 5,
"ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"]
"ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"],
"allowedSopClasses": ["1.2.840.10008.5.1.4.1.1.1.2"]
},
{
"name": "liver-seg",
Expand Down
13 changes: 13 additions & 0 deletions src/Api/MonaiApplicationEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Monai.Deploy.InformaticsGateway.Api
/// "workflows": [ "EXAM", "Delta", "b75cd27a-068a-4f9c-b3da-e5d4ea08c55a"],
/// "grouping": [ "0010,0020"],
/// "ignoredSopClasses": ["1.2.840.10008.5.1.4.1.1.1.1"],
/// "allowedSopClasses": ["1.2.840.10008.5.1.4.1.1.1.2"],
/// "timeout": 300
/// }
/// </code>
Expand Down Expand Up @@ -58,9 +59,16 @@ public class MonaiApplicationEntity

/// <summary>
/// Optional field to specify SOP Class UIDs to ignore.
/// <see cref="IgnoredSopClasses"/> and <see cref="AllowedSopClasses"/> are mutually exclusive.
/// </summary>
public List<string> IgnoredSopClasses { get; set; }

/// <summary>
/// Optional field to specify accepted SOP Class UIDs.
/// <see cref="IgnoredSopClasses"/> and <see cref="AllowedSopClasses"/> are mutually exclusive.
/// </summary>
public List<string> AllowedSopClasses { get; set; }

/// <summary>
/// Timeout, in seconds, to wait for instances before notifying other subsystems of data arrival
/// for the specified data group.
Expand Down Expand Up @@ -89,6 +97,11 @@ public void SetDefaultValues()
{
IgnoredSopClasses = new List<string>();
}

if (AllowedSopClasses is null)
{
AllowedSopClasses = new List<string>();
}
}
}
}
27 changes: 21 additions & 6 deletions src/CLI/Commands/AetCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Rendering;
using System.CommandLine.Rendering.Views;
using System.Linq;
Expand Down Expand Up @@ -65,20 +65,24 @@ private void SetupAddAetCommand()
addCommand.AddOption(groupingOption);
var timeoutOption = new Option<uint>(new string[] { "-t", "--timeout" }, getDefaultValue: () => 5, "Timeout, in seconds, to wait for instances") { IsRequired = false };
addCommand.AddOption(timeoutOption);
var workflowsOption = new Option<string[]>(new string[] { "-w", "--workflows" }, () => Array.Empty<string>(), "A space separated list of workflow names or IDs to be associated with the SCP AE Title")
var workflowsOption = new Option<List<string>>(new string[] { "-w", "--workflows" }, description: "A space separated list of workflow names or IDs to be associated with the SCP AE Title")
{
AllowMultipleArgumentsPerToken = true,
IsRequired = false,
Name = "--workflows"
};
addCommand.AddOption(workflowsOption);
var ignoredSopsOption = new Option<string[]>(new string[] { "-i", "--ignored-sops" }, () => Array.Empty<string>(), "A space separated list of SOP Class UIDs to be ignoredS")
var ignoredSopsOption = new Option<List<string>>(new string[] { "-i", "--ignored-sop-classes" }, description: "A space separated list of SOP Class UIDs to be ignored")
{
AllowMultipleArgumentsPerToken = true,
IsRequired = false,
Name = "--ignored-sops"
};
addCommand.AddOption(ignoredSopsOption);
var allowedSopsOption = new Option<List<string>>(new string[] { "-s", "--allowed-sop-classes" }, description: "A space separated list of SOP Class UIDs to be accepted")
{
AllowMultipleArgumentsPerToken = true,
IsRequired = false,
};
addCommand.AddOption(allowedSopsOption);

addCommand.Handler = CommandHandler.Create<MonaiApplicationEntity, IHost, bool, CancellationToken>(AddAeTitlehandlerAsync);
}
Expand Down Expand Up @@ -139,8 +143,14 @@ private async Task<int> ListAeTitlehandlerAsync(IHost host, bool verbose, Cancel
};
table.AddColumn(p => p.Name, new ContentView("Name".Underline()));
table.AddColumn(p => p.AeTitle, new ContentView("AE Title".Underline()));
table.AddColumn(p => p.Timeout, new ContentView("Timeout".Underline()));
table.AddColumn(p => p.Grouping, new ContentView("Grouping".Underline()));
table.AddColumn(p => p.Workflows.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.Workflows), new ContentView("Workflows".Underline()));
table.AddColumn(p => p.AllowedSopClasses.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.AllowedSopClasses), new ContentView("Accepted SOP Classes".Underline()));
table.AddColumn(p => p.IgnoredSopClasses.IsNullOrEmpty() ? "n/a" : string.Join(", ", p.IgnoredSopClasses), new ContentView("Ignored SOP Classes".Underline()));
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());

logger.ListedNItems(items.Count);
}
return ExitCodes.Success;
}
Expand Down Expand Up @@ -213,7 +223,12 @@ private async Task<int> AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IH
if (result.IgnoredSopClasses.Any())
{
logger.MonaiAeIgnoredSops(string.Join(',', result.IgnoredSopClasses));
logger.IgnoreSopClassesWarning();
logger.IgnoredSopClassesWarning();
}
if (result.AllowedSopClasses.Any())
{
logger.MonaiAeAllowedSops(string.Join(',', result.AllowedSopClasses));
logger.AcceptedSopClassesWarning();
}
}
catch (ConfigurationException ex)
Expand Down
2 changes: 1 addition & 1 deletion src/CLI/Commands/ConfigCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
Expand Down
3 changes: 3 additions & 0 deletions src/CLI/Commands/DestinationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Rendering;
using System.CommandLine.Rendering.Views;
using System.Linq;
Expand Down Expand Up @@ -130,6 +131,8 @@ private async Task<int> ListDestinationHandlerAsync(DestinationApplicationEntity
table.AddColumn(p => p.HostIp, new ContentView("Host/IP Address".Underline()));
table.AddColumn(p => p.Port, new ContentView("Port".Underline()));
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());

logger.ListedNItems(items.Count);
}
return ExitCodes.Success;
}
Expand Down
2 changes: 1 addition & 1 deletion src/CLI/Commands/RestartCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache License 2.0

using System;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
Expand Down
3 changes: 3 additions & 0 deletions src/CLI/Commands/SourceCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.CommandLine.Rendering;
using System.CommandLine.Rendering.Views;
using System.Linq;
Expand Down Expand Up @@ -126,6 +127,8 @@ private async Task<int> ListSourceHandlerAsync(SourceApplicationEntity entity, I
table.AddColumn(p => p.AeTitle, new ContentView("AE Title".Underline()));
table.AddColumn(p => p.HostIp, new ContentView("Host/IP Address".Underline()));
table.Render(consoleRenderer, consoleRegion.GetDefaultConsoleRegion());

logger.ListedNItems(items.Count);
}
return ExitCodes.Success;
}
Expand Down
1 change: 1 addition & 0 deletions src/CLI/Commands/StartCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
Expand Down
1 change: 1 addition & 0 deletions src/CLI/Commands/StatusCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
Expand Down
1 change: 1 addition & 0 deletions src/CLI/Commands/StopCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
Expand Down
16 changes: 14 additions & 2 deletions src/CLI/Logging/Log.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static partial class Log
public static partial void MonaiAeIgnoredSops(this ILogger logger, string ignoredSopClasses);

[LoggerMessage(EventId = 30014, Level = LogLevel.Warning, Message = "Instances with matching SOP class UIDs are accepted but dropped.")]
public static partial void IgnoreSopClassesWarning(this ILogger logger);
public static partial void IgnoredSopClassesWarning(this ILogger logger);

[LoggerMessage(EventId = 30015, Level = LogLevel.Critical, Message = "Error creating MONAI SCP AE Title {aeTitle}: {message}.")]
public static partial void MonaiAeCreateCritical(this ILogger logger, string aeTitle, string message);
Expand Down Expand Up @@ -111,7 +111,7 @@ public static partial class Log
[LoggerMessage(EventId = 30040, Level = LogLevel.Information, Message = "\t\t{name}: {status}")]
public static partial void ServiceStatusItem(this ILogger logger, string name, ServiceStatus status);

[LoggerMessage(EventId = 30041, Level = LogLevel.Warning, Message = "Action cancelled.")]
[LoggerMessage(EventId = 30041, Level = LogLevel.Warning, Message = "Action canceled.")]
public static partial void ActionCancelled(this ILogger logger);

[LoggerMessage(EventId = 30042, Level = LogLevel.Critical, Message = "Error restarting {applicationName}: {message}.")]
Expand All @@ -138,6 +138,18 @@ public static partial class Log
[LoggerMessage(EventId = 30049, Level = LogLevel.Information, Message = "Configuration updated successfully.")]
public static partial void ConfigurationUpdated(this ILogger logger);

[LoggerMessage(EventId = 30050, Level = LogLevel.Information, Message = "\tAccepted SOP Classes: {alowedSopClasses}")]
public static partial void MonaiAeAllowedSops(this ILogger logger, string alowedSopClasses);

[LoggerMessage(EventId = 30051, Level = LogLevel.Warning, Message = "Instances without matching SOP class UIDs are accepted but dropped.")]
public static partial void AllowedSopClassesWarning(this ILogger logger);

[LoggerMessage(EventId = 30052, Level = LogLevel.Warning, Message = "Only instances with matching SOP class UIDs are accepted and stored.")]
public static partial void AcceptedSopClassesWarning(this ILogger logger);

[LoggerMessage(EventId = 30053, Level = LogLevel.Information, Message = "\n\nFound {count} items.")]
public static partial void ListedNItems(this ILogger logger, int count);

// Docker Runner
[LoggerMessage(EventId = 31000, Level = LogLevel.Debug, Message = "Checking for existing {applicationName} ({version}) containers...")]
public static partial void CheckingExistingAppContainer(this ILogger logger, string applicationName, string version);
Expand Down
8 changes: 4 additions & 4 deletions src/CLI/Monai.Deploy.InformaticsGateway.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SPDX-License-Identifier: Apache License 2.0
<SelfContained>true</SelfContained>
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<AssemblyName>mig-cli</AssemblyName>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand Down Expand Up @@ -48,9 +48,9 @@ SPDX-License-Identifier: Apache License 2.0
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21308.1" />
<PackageReference Include="System.CommandLine.Hosting" Version="0.3.0-alpha.21216.1" />
<PackageReference Include="System.CommandLine.Rendering" Version="0.3.0-alpha.21216.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.22272.1" />
<PackageReference Include="System.CommandLine.Rendering" Version="0.4.0-alpha.22272.1" />
<PackageReference Include="System.IO.Abstractions" Version="17.0.18" />
</ItemGroup>
</Project>
26 changes: 13 additions & 13 deletions src/CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ private static async Task<int> Main(string[] args)
internal static Parser BuildParser()
{
var verboseOption = new Option<bool>(new[] { "--verbose", "-v" }, () => false, "Show verbose output");
return new CommandLineBuilder(new RootCommand($"{Strings.ApplicationName} CLI"))
var commandLineBuilder = new CommandLineBuilder(new RootCommand($"{Strings.ApplicationName} CLI"))
.UseHost(
_ => Host.CreateDefaultBuilder(),
host =>
{
_ = host.ConfigureLogging((context, logging) =>
{
var invocationContext = context.GetInvocationContext();
var verboseEnabled = invocationContext.ParseResult.ValueForOption(verboseOption);
var verboseEnabled = invocationContext.ParseResult.GetValueForOption(verboseOption);
logging.ClearProviders();

_ = logging.AddInformaticsGatewayConsole(options => options.MinimumLogLevel = verboseEnabled ? LogLevel.Trace : LogLevel.Information)
Expand All @@ -60,22 +60,22 @@ internal static Parser BuildParser()
services.AddTransient<IDockerClient>(p => new DockerClientConfiguration().CreateClient());
});
})
.AddGlobalOption(verboseOption)
.AddCommand(new ConfigCommand())
.AddCommand(new StartCommand())
.AddCommand(new StopCommand())
.AddCommand(new RestartCommand())
.AddCommand(new AetCommand())
.AddCommand(new SourceCommand())
.AddCommand(new DestinationCommand())
.AddCommand(new StatusCommand())
.UseAnsiTerminalWhenAvailable()
.UseExceptionHandler((exception, context) =>
{
Console.Out.WriteLineAsync(Crayon.Output.Bright.Red($"Exception: {exception.Message}"));
})
.UseDefaults()
.Build();
.UseDefaults();
commandLineBuilder.Command.AddGlobalOption(verboseOption);
commandLineBuilder.Command.AddCommand(new ConfigCommand());
commandLineBuilder.Command.AddCommand(new StartCommand());
commandLineBuilder.Command.AddCommand(new StopCommand());
commandLineBuilder.Command.AddCommand(new RestartCommand());
commandLineBuilder.Command.AddCommand(new AetCommand());
commandLineBuilder.Command.AddCommand(new SourceCommand());
commandLineBuilder.Command.AddCommand(new DestinationCommand());
commandLineBuilder.Command.AddCommand(new StatusCommand());
return commandLineBuilder.Build();
}
}
}
52 changes: 50 additions & 2 deletions src/CLI/Test/AetCommandTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public AetCommandTest()
services.AddSingleton<IInformaticsGatewayClient>(p => _informaticsGatewayClient.Object);
services.AddSingleton<IConfigurationService>(p => _configurationService.Object);
});
})
.AddCommand(new AetCommand());
});
_commandLineBuilder.Command.AddCommand(new AetCommand());
_paser = _commandLineBuilder.Build();

_loggerFactory.Setup(p => p.CreateLogger(It.IsAny<string>())).Returns(_logger.Object);
Expand Down Expand Up @@ -110,6 +110,54 @@ public async Task AetAdd_Command()
It.IsAny<CancellationToken>()), Times.Once());
}

[Fact(DisplayName = "aet add comand with allowed & ignored SOP classes")]
public async Task AetAdd_Command_AllowedIgnoredSopClasses()
{
var command = "aet add -n MyName -a MyAET --workflows App MyCoolApp TheApp -i A B C -s D E F";
var result = _paser.Parse(command);
Assert.Equal(ExitCodes.Success, result.Errors.Count);

var entity = new MonaiApplicationEntity()
{
Name = result.CommandResult.Children[0].Tokens[0].Value,
AeTitle = result.CommandResult.Children[1].Tokens[0].Value,
Workflows = result.CommandResult.Children[2].Tokens.Select(p => p.Value).ToList(),
IgnoredSopClasses = result.CommandResult.Children[3].Tokens.Select(p => p.Value).ToList(),
AllowedSopClasses = result.CommandResult.Children[4].Tokens.Select(p => p.Value).ToList(),
};
Assert.Equal("MyName", entity.Name);
Assert.Equal("MyAET", entity.AeTitle);
Assert.Collection(entity.Workflows,
item => item.Equals("App"),
item => item.Equals("MyCoolApp"),
item => item.Equals("TheApp"));
Assert.Collection(entity.AllowedSopClasses,
item => item.Equals("D"),
item => item.Equals("E"),
item => item.Equals("F"));
Assert.Collection(entity.IgnoredSopClasses,
item => item.Equals("A"),
item => item.Equals("B"),
item => item.Equals("C"));

_informaticsGatewayClient.Setup(p => p.MonaiScpAeTitle.Create(It.IsAny<MonaiApplicationEntity>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(entity);

int exitCode = await _paser.InvokeAsync(command);

Assert.Equal(ExitCodes.Success, exitCode);

_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny<Uri>()), Times.Once());
_informaticsGatewayClient.Verify(
p => p.MonaiScpAeTitle.Create(
It.Is<MonaiApplicationEntity>(o => o.AeTitle == entity.AeTitle &&
o.Name == entity.Name &&
Enumerable.SequenceEqual(o.Workflows, entity.Workflows) &&
Enumerable.SequenceEqual(o.AllowedSopClasses, entity.AllowedSopClasses) &&
Enumerable.SequenceEqual(o.IgnoredSopClasses, entity.IgnoredSopClasses)),
It.IsAny<CancellationToken>()), Times.Once());
}

[Fact(DisplayName = "aet add comand exception")]
public async Task AetAdd_Command_Exception()
{
Expand Down
Loading