forked from gongxuegit/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
92
Tiku.UnitTests/Security/CurrentDataScopeTests.cs
Normal file
92
Tiku.UnitTests/Security/CurrentDataScopeTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.UnitTests.Security;
|
||||
|
||||
public sealed class CurrentDataScopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Merge_EmptyOrInvalidScopes_DefaultsToSelf()
|
||||
{
|
||||
var result = CurrentDataScope.Merge(
|
||||
[
|
||||
JsonSerializer.SerializeToElement(new { }),
|
||||
JsonSerializer.SerializeToElement("invalid")
|
||||
]);
|
||||
|
||||
Assert.Equal(DataScopeMode.Self, result.Mode);
|
||||
Assert.True(result.IncludesSelf);
|
||||
Assert.Empty(result.RegionIds);
|
||||
Assert.Empty(result.ClassIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_RestrictedRoles_UnionsResourceIdsAndSelfAccess()
|
||||
{
|
||||
var firstRegion = Guid.NewGuid();
|
||||
var secondRegion = Guid.NewGuid();
|
||||
var classId = Guid.NewGuid();
|
||||
|
||||
var result = CurrentDataScope.Merge(
|
||||
[
|
||||
JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
mode = "Restricted",
|
||||
regionIds = new[] { firstRegion },
|
||||
classIds = new[] { classId }
|
||||
}),
|
||||
JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
mode = "Restricted",
|
||||
regionIds = new[] { secondRegion },
|
||||
includesSelf = true
|
||||
})
|
||||
]);
|
||||
|
||||
Assert.Equal(DataScopeMode.Restricted, result.Mode);
|
||||
Assert.True(result.IncludesSelf);
|
||||
Assert.True(result.RegionIds.SetEquals([firstRegion, secondRegion]));
|
||||
Assert.True(result.ClassIds.SetEquals([classId]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Merge_AllScope_OverridesRestrictedScopes()
|
||||
{
|
||||
var result = CurrentDataScope.Merge(
|
||||
[
|
||||
JsonSerializer.SerializeToElement(new { mode = "Restricted", regionIds = new[] { Guid.NewGuid() } }),
|
||||
JsonSerializer.SerializeToElement(new { type = "All" })
|
||||
]);
|
||||
|
||||
Assert.Equal(DataScopeMode.All, result.Mode);
|
||||
Assert.True(result.IncludesSelf);
|
||||
Assert.Empty(result.RegionIds);
|
||||
Assert.Empty(result.ClassIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PermissionCatalog_UsesUniqueRealmScopedCodes()
|
||||
{
|
||||
Assert.All(BackendPermissions.Tenant, code => Assert.StartsWith("tenant:", code, StringComparison.Ordinal));
|
||||
Assert.All(BackendPermissions.Platform, code => Assert.StartsWith("platform:", code, StringComparison.Ordinal));
|
||||
Assert.Empty(BackendPermissions.Tenant.Intersect(BackendPermissions.Platform, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowsResource_UsesOwnerRegionAndClassWithoutCrossScopeFallback()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var regionId = Guid.NewGuid();
|
||||
var classId = Guid.NewGuid();
|
||||
var scope = new CurrentDataScope(
|
||||
DataScopeMode.Restricted,
|
||||
new HashSet<Guid> { regionId },
|
||||
new HashSet<Guid> { classId },
|
||||
false);
|
||||
|
||||
Assert.True(scope.AllowsResource(userId, regionId: regionId));
|
||||
Assert.True(scope.AllowsResource(userId, classId: classId));
|
||||
Assert.False(scope.AllowsResource(userId, ownerUserId: userId));
|
||||
Assert.False(scope.AllowsResource(userId, regionId: Guid.NewGuid(), classId: Guid.NewGuid()));
|
||||
}
|
||||
}
|
||||
94
Tiku.UnitTests/Security/DataProtectionKeyRingOptionsTests.cs
Normal file
94
Tiku.UnitTests/Security/DataProtectionKeyRingOptionsTests.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.UnitTests.Security;
|
||||
|
||||
public sealed class DataProtectionKeyRingOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Development_allows_an_unencrypted_key_ring()
|
||||
{
|
||||
var options = new DataProtectionKeyRingOptions();
|
||||
|
||||
Assert.True(DataProtectionKeyRingOptions.BeValid(options, requireCertificate: false));
|
||||
Assert.Null(options.LoadCertificate(requireCertificate: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Production_requires_a_certificate_path()
|
||||
{
|
||||
var options = new DataProtectionKeyRingOptions();
|
||||
|
||||
Assert.False(DataProtectionKeyRingOptions.BeValid(options, requireCertificate: true));
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
options.LoadCertificate(requireCertificate: true));
|
||||
Assert.Contains("required outside Development", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Application_name_is_always_required()
|
||||
{
|
||||
var options = new DataProtectionKeyRingOptions
|
||||
{
|
||||
ApplicationName = " ",
|
||||
CertificatePath = "/configured/key-ring.pfx"
|
||||
};
|
||||
|
||||
Assert.False(DataProtectionKeyRingOptions.BeValid(options, requireCertificate: false));
|
||||
Assert.False(DataProtectionKeyRingOptions.BeValid(options, requireCertificate: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configured_certificate_file_must_be_loadable()
|
||||
{
|
||||
var options = new DataProtectionKeyRingOptions
|
||||
{
|
||||
CertificatePath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"missing-data-protection-{Guid.NewGuid():N}.pfx")
|
||||
};
|
||||
|
||||
Assert.True(DataProtectionKeyRingOptions.BeValid(options, requireCertificate: true));
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
options.LoadCertificate(requireCertificate: true));
|
||||
Assert.Contains("could not be loaded", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Password_protected_pkcs12_certificate_with_private_key_is_loaded()
|
||||
{
|
||||
const string password = "unit-test-certificate-password";
|
||||
var certificatePath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"data-protection-{Guid.NewGuid():N}.pfx");
|
||||
|
||||
try
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest(
|
||||
"CN=Tiku Data Protection Unit Test",
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
using var certificate = request.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||
DateTimeOffset.UtcNow.AddDays(1));
|
||||
File.WriteAllBytes(certificatePath, certificate.Export(X509ContentType.Pfx, password));
|
||||
|
||||
var options = new DataProtectionKeyRingOptions
|
||||
{
|
||||
CertificatePath = certificatePath,
|
||||
CertificatePassword = password
|
||||
};
|
||||
|
||||
using var loaded = options.LoadCertificate(requireCertificate: true);
|
||||
Assert.NotNull(loaded);
|
||||
Assert.True(loaded.HasPrivateKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(certificatePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user