Add explicit client version disable mechanism to API (#1548)

This commit is contained in:
Leendert de Borst
2026-01-31 15:56:18 +01:00
parent c2177d0c8d
commit 62b4ec8643
4 changed files with 148 additions and 5 deletions
@@ -110,10 +110,11 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
if (AppInfo.MinimumClientVersions.TryGetValue(platform, out var minimumVersion))
{
if (VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion))
{
clientSupported = true;
}
// Check if version meets minimum requirement AND is not in blocked list
var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion);
var isBlocked = VersionHelper.IsVersionBlocked(platform, clientVersion, AppInfo.UnsupportedClientVersions);
clientSupported = meetsMinimum && !isBlocked;
}
else
{
@@ -59,4 +59,36 @@ public static class VersionHelper
// Compare the versions
return v1 >= v2;
}
/// <summary>
/// Checks if a version is blocked for a specific platform.
/// Checks both platform-specific blocks and global blocks (using "*" key).
/// </summary>
/// <param name="platform">The platform to check (e.g., "chrome", "ios").</param>
/// <param name="version">The version to check.</param>
/// <param name="blockedVersions">Dictionary of platform to blocked versions. Use "*" for global blocks.</param>
/// <returns>True if the version is blocked for this platform, false otherwise.</returns>
public static bool IsVersionBlocked(string platform, string version, IReadOnlyDictionary<string, HashSet<string>> blockedVersions)
{
if (string.IsNullOrEmpty(version) || blockedVersions == null || blockedVersions.Count == 0)
{
return false;
}
// Check global blocks (applies to all platforms)
if (blockedVersions.TryGetValue("*", out var globalBlocked) && globalBlocked.Contains(version))
{
return true;
}
// Check platform-specific blocks
if (!string.IsNullOrEmpty(platform) &&
blockedVersions.TryGetValue(platform, out var platformBlocked) &&
platformBlocked.Contains(version))
{
return true;
}
return false;
}
}
@@ -42,7 +42,7 @@ public static class AppInfo
/// for all clients as we are using a monorepo to build all clients from the same source code. But it's
/// possible to override the minimum client version for a specific client if needed.
/// </summary>
public const string MinimumClientVersion = "0.26.1";
public const string MinimumClientVersion = "0.12.0";
/// <summary>
/// Gets a dictionary of minimum supported client versions that the WebApi supports.
@@ -68,6 +68,24 @@ public static class AppInfo
{ "android", MinimumClientVersion },
}.AsReadOnly();
/// <summary>
/// Gets a dictionary of specific client versions that are explicitly unsupported (blocked) per platform.
/// This is useful for blocking specific versions with (critical) bugs while still allowing
/// older versions that predate the bug to continue working.
/// For example: if 0.26.0 has a critical bug fixed in 0.26.1, we can block only 0.26.0
/// without affecting users on 0.25.x who may still have compatible vaults.
/// Use "*" as the platform key to block a version across all platforms.
/// </summary>
public static IReadOnlyDictionary<string, HashSet<string>> UnsupportedClientVersions { get; } = new Dictionary<string, HashSet<string>>
{
// Block version across all platforms: "*" applies to all clients.
{ "*", ["0.26.0"] }, // Version with vault migration bug, fixed in 0.26.1
// Platform-specific blocks (examples):
// { "chrome", ["0.25.0"] },
// { "ios", ["0.24.0", "0.24.1"] },
};
/// <summary>
/// Gets the build number, typically used in CI/CD pipelines.
/// Can be overridden at build time.
@@ -68,4 +68,96 @@ public class VersionTests
var version2 = "1.1.0";
Assert.That(VersionHelper.IsVersionEqualOrNewer(version1, version2), Is.True);
}
/// <summary>
/// Test that a version in the global blocked list is correctly identified as blocked.
/// </summary>
[Test]
public void VersionBlockedReturnsTrueForGloballyBlockedVersion()
{
var blockedVersions = new Dictionary<string, HashSet<string>>
{
{ "*", ["0.26.0", "0.27.0"] },
};
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", blockedVersions), Is.True);
Assert.That(VersionHelper.IsVersionBlocked("ios", "0.27.0", blockedVersions), Is.True);
Assert.That(VersionHelper.IsVersionBlocked("android", "0.26.0", blockedVersions), Is.True);
}
/// <summary>
/// Test that a version in the platform-specific blocked list is correctly identified as blocked.
/// </summary>
[Test]
public void VersionBlockedReturnsTrueForPlatformSpecificBlockedVersion()
{
var blockedVersions = new Dictionary<string, HashSet<string>>
{
{ "chrome", ["0.25.0"] },
{ "ios", ["0.24.0"] },
};
// Platform-specific blocks should work
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.0", blockedVersions), Is.True);
Assert.That(VersionHelper.IsVersionBlocked("ios", "0.24.0", blockedVersions), Is.True);
// Other platforms should not be blocked
Assert.That(VersionHelper.IsVersionBlocked("firefox", "0.25.0", blockedVersions), Is.False);
Assert.That(VersionHelper.IsVersionBlocked("android", "0.24.0", blockedVersions), Is.False);
}
/// <summary>
/// Test that global and platform-specific blocks work together.
/// </summary>
[Test]
public void VersionBlockedCombinesGlobalAndPlatformSpecific()
{
var blockedVersions = new Dictionary<string, HashSet<string>>
{
{ "*", ["0.26.0"] },
{ "chrome", ["0.25.0"] },
};
// Global block applies to all platforms for exact version
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", blockedVersions), Is.True);
Assert.That(VersionHelper.IsVersionBlocked("ios", "0.26.0", blockedVersions), Is.True);
Assert.That(VersionHelper.IsVersionBlocked("ios", "0.26.1", blockedVersions), Is.False);
// Platform-specific block only applies to that platform
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.0", blockedVersions), Is.True);
Assert.That(VersionHelper.IsVersionBlocked("firefox", "0.25.0", blockedVersions), Is.False);
}
/// <summary>
/// Test that a version not in the blocked list is correctly identified as not blocked.
/// </summary>
[Test]
public void VersionBlockedReturnsFalseForNonBlockedVersion()
{
var blockedVersions = new Dictionary<string, HashSet<string>>
{
{ "*", ["0.26.0"] },
};
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.25.3", blockedVersions), Is.False);
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.1", blockedVersions), Is.False);
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.27.0", blockedVersions), Is.False);
}
/// <summary>
/// Test that empty or null inputs are handled correctly.
/// </summary>
[Test]
public void VersionBlockedHandlesEmptyInputs()
{
var blockedVersions = new Dictionary<string, HashSet<string>>
{
{ "*", ["0.26.0"] },
};
var emptyBlockedVersions = new Dictionary<string, HashSet<string>>();
Assert.That(VersionHelper.IsVersionBlocked("chrome", string.Empty, blockedVersions), Is.False);
Assert.That(VersionHelper.IsVersionBlocked("chrome", "0.26.0", emptyBlockedVersions), Is.False);
}
}