Add minimum security level validation to the compile step.

Only serves as a one off warning at the moment. To be integrated with compile jobs and expanded upon at a later date. At the moment, this also allows for more verbosity during DMAPI validation.
This commit is contained in:
Cyberboss
2018-09-17 16:21:43 -04:00
parent 49658a64e8
commit 5a0acf7266
6 changed files with 106 additions and 22 deletions
+1 -1
View File
@@ -68,7 +68,7 @@
if(cached_json["apiValidateOnly"])
TGS_INFO_LOG("Validating API and exiting...")
Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = minimum_required_security_level))
Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]"))
del(world)
chat_channels_json_path = cached_json["chatChannelsJson"]
@@ -125,8 +125,8 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
/// <param name="portToUse">The port to use for API validation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns>
async Task<bool> VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
{
logger.LogTrace("Verifying DMAPI...");
var launchParameters = new DreamDaemonLaunchParameters
@@ -156,13 +156,27 @@ namespace Tgstation.Server.Host.Components.Compiler
if (!controller.Lifetime.IsCompleted)
{
logger.LogDebug("API validation timed out!");
return false;
var validationStatus = controller.ApiValidationStatus;
logger.LogTrace("API validation status: {0}", validationStatus);
switch (validationStatus)
{
case ApiValidationStatus.Validated:
return;
case ApiValidationStatus.NeverValidated:
break;
case ApiValidationStatus.BadValidationRequest:
throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!");
case ApiValidationStatus.RequiresSafe:
throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!");
case ApiValidationStatus.RequiresTrusted:
throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!");
case ApiValidationStatus.UnaskedValidationRequest:
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
}
}
var validated = controller.ApiValidated;
logger.LogTrace("API valid: {0}", validated);
return validated;
throw new JobException("DMAPI validation timed out!");
}
}
@@ -356,13 +370,18 @@ namespace Tgstation.Server.Host.Components.Compiler
var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
var apiValidated = exitCode == 0 && await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
try
{
if (exitCode != 0)
throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
if (!apiValidated)
await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
}
catch (JobException)
{
//server never validated or compile failed
await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
throw;
}
logger.LogTrace("Running post compile event...");
@@ -0,0 +1,33 @@
namespace Tgstation.Server.Host.Components.Watchdog
{
/// <summary>
/// Status of DMAPI validation
/// </summary>
enum ApiValidationStatus
{
/// <summary>
/// The DMAPI never contacted the server for validation
/// </summary>
NeverValidated,
/// <summary>
/// The server was contacted for validation but it was never requested
/// </summary>
UnaskedValidationRequest,
/// <summary>
/// The game must be run with a minimum security level of <see cref="Api.Models.DreamDaemonSecurity.Safe"/>
/// </summary>
RequiresSafe,
/// <summary>
/// The game must be run with a security level of <see cref="Api.Models.DreamDaemonSecurity.Trusted"/>
/// </summary>
RequiresTrusted,
/// <summary>
/// The validation request was malformed
/// </summary>
BadValidationRequest,
/// <summary>
/// The DMAPI validated successfully
/// </summary>
Validated
}
}
@@ -25,9 +25,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
bool TerminationWasRequested { get; }
/// <summary>
/// If the DMAPI was validated. This field may only be access once <see cref="IProcessBase.Lifetime"/> completes
/// The DMAPI <see cref="Components.Watchdog.ApiValidationStatus"/>
/// </summary>
bool ApiValidated { get; }
ApiValidationStatus ApiValidationStatus { get; }
/// <summary>
/// The <see cref="IDmbProvider"/> being used
@@ -29,13 +29,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
/// <inheritdoc />
public bool ApiValidated
public ApiValidationStatus ApiValidationStatus
{
get
{
if (!Lifetime.IsCompleted)
throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
return apiValidated;
return apiValidationStatus;
}
}
@@ -126,6 +126,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
readonly ILogger<SessionController> logger;
/// <summary>
/// The <see cref="DreamDaemonSecurity"/> level the <see cref="process"/> was launched with
/// </summary>
readonly DreamDaemonSecurity? launchSecurityLevel;
/// <summary>
/// The <see cref="TaskCompletionSource{TResult}"/> <see cref="SetPort(ushort, CancellationToken)"/> waits on when DreamDaemon currently has it's ports closed
/// </summary>
@@ -151,9 +156,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
bool disposed;
/// <summary>
/// If the DMAPI was validated
/// The <see cref="ApiValidationStatus"/> for the <see cref="SessionController"/>
/// </summary>
bool apiValidated;
ApiValidationStatus apiValidationStatus;
/// <summary>
/// If <see cref="process"/> should be kept alive instead
@@ -171,8 +176,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="chat">The value of <see cref="chat"/></param>
/// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="launchSecurityLevel">The value of <see cref="launchSecurityLevel"/></param>
/// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param>
public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, uint? startupTimeout)
public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout)
{
this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
@@ -183,11 +189,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.launchSecurityLevel = launchSecurityLevel;
interopContext.RegisterHandler(this);
portClosedForReboot = false;
disposed = false;
apiValidated = false;
apiValidationStatus = ApiValidationStatus.NeverValidated;
released = false;
rebootTcs = new TaskCompletionSource<object>();
@@ -289,6 +297,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
/////UHHHH
logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
break;
}
@@ -314,7 +323,30 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
break;
case Constants.DMCommandApiValidate:
apiValidated = true;
if (!launchSecurityLevel.HasValue)
{
logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
content = new ErrorMessage { Message = "Invalid API validation request!" };
break;
}
if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevel) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevel, out var minimumSecurityLevel))
apiValidationStatus = ApiValidationStatus.BadValidationRequest;
else
switch (minimumSecurityLevel)
{
case DreamDaemonSecurity.Safe:
apiValidationStatus = launchSecurityLevel == DreamDaemonSecurity.Ultrasafe ? ApiValidationStatus.RequiresSafe : ApiValidationStatus.Validated;
break;
case DreamDaemonSecurity.Ultrasafe:
apiValidationStatus = ApiValidationStatus.Validated;
break;
case DreamDaemonSecurity.Trusted:
apiValidationStatus = launchSecurityLevel == DreamDaemonSecurity.Trusted ? ApiValidationStatus.Validated : ApiValidationStatus.RequiresTrusted;
break;
default:
throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
}
break;
case Constants.DMCommandWorldReboot:
if (ClosePortOnReboot)
@@ -200,7 +200,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
ChatChannelsJson = interopInfo.ChatChannelsJson,
ChatCommandsJson = interopInfo.ChatCommandsJson,
ServerCommandsJson = interopInfo.ServerCommandsJson,
}, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), launchParameters.StartupTimeout);
}, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout);
}
catch
{
@@ -247,7 +247,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
var process = processExecutor.GetProcess(reattachInformation.ProcessId);
try
{
return new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), null);
return new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), null, null);
}
catch
{