feat(deploy): fetch options + per-deployment token helper
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates and compares the single-purpose, short-TTL token that authorizes a
|
||||||
|
/// site's HTTP fetch of one deployment's flattened config. URL-safe; compared in
|
||||||
|
/// constant time to avoid timing oracles.
|
||||||
|
/// </summary>
|
||||||
|
public static class DeploymentFetchToken
|
||||||
|
{
|
||||||
|
/// <summary>Generates a URL-safe random token (256 bits of entropy).</summary>
|
||||||
|
public static string Generate() =>
|
||||||
|
Convert.ToBase64String(RandomNumberGenerator.GetBytes(32))
|
||||||
|
.Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||||
|
|
||||||
|
/// <summary>Constant-time string comparison (no early-out on first mismatch).</summary>
|
||||||
|
public static bool ConstantTimeEquals(string a, string b) =>
|
||||||
|
CryptographicOperations.FixedTimeEquals(
|
||||||
|
Encoding.UTF8.GetBytes(a), Encoding.UTF8.GetBytes(b));
|
||||||
|
}
|
||||||
@@ -58,4 +58,17 @@ public class CommunicationOptions
|
|||||||
|
|
||||||
/// <summary>Akka.Remote transport failure detection threshold.</summary>
|
/// <summary>Akka.Remote transport failure detection threshold.</summary>
|
||||||
public TimeSpan TransportFailureThreshold { get; set; } = TimeSpan.FromSeconds(15);
|
public TimeSpan TransportFailureThreshold { get; set; } = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base URL (Traefik/LB) the SITE uses to fetch deploy configs from central,
|
||||||
|
/// e.g. "https://central.example:9000". Carried in RefreshDeploymentCommand so
|
||||||
|
/// sites need no new standing config. Empty disables notify-and-fetch fallback.
|
||||||
|
/// </summary>
|
||||||
|
public string CentralFetchBaseUrl { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long a staged PendingDeployment (and its fetch token) stays valid. Must
|
||||||
|
/// comfortably cover both site nodes' fetches within one deploy window.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan PendingDeploymentTtl { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Deployment;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests;
|
||||||
|
|
||||||
|
public class DeploymentFetchTokenTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Generate_ReturnsUrlSafeTokenOfAdequateLength()
|
||||||
|
{
|
||||||
|
var token = DeploymentFetchToken.Generate();
|
||||||
|
|
||||||
|
Assert.True(token.Length >= 32, $"Expected length >= 32 but got {token.Length}");
|
||||||
|
Assert.DoesNotContain('+', token);
|
||||||
|
Assert.DoesNotContain('/', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Generate_TwoCallsReturnDifferentValues()
|
||||||
|
{
|
||||||
|
var a = DeploymentFetchToken.Generate();
|
||||||
|
var b = DeploymentFetchToken.Generate();
|
||||||
|
|
||||||
|
Assert.NotEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstantTimeEquals_SameToken_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var token = DeploymentFetchToken.Generate();
|
||||||
|
|
||||||
|
Assert.True(DeploymentFetchToken.ConstantTimeEquals(token, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstantTimeEquals_TokenWithSuffix_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var token = DeploymentFetchToken.Generate();
|
||||||
|
|
||||||
|
Assert.False(DeploymentFetchToken.ConstantTimeEquals(token, token + "x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConstantTimeEquals_BothEmpty_ReturnsTrue()
|
||||||
|
{
|
||||||
|
Assert.True(DeploymentFetchToken.ConstantTimeEquals("", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user