forked from gongxuegit/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
250
Tiku.IntegrationTests/Api/RbacAuthorizationTests.cs
Normal file
250
Tiku.IntegrationTests/Api/RbacAuthorizationTests.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
|
||||
public sealed class RbacAuthorizationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task TenantPolicy_RequiresCurrentMembershipPermissionAndMfa()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
var snapshot = Snapshot(
|
||||
userId,
|
||||
tenantId,
|
||||
tenantPermissions: [BackendPermissions.TenantRoleManage]);
|
||||
await using var provider = Services(snapshot);
|
||||
var authorization = provider.GetRequiredService<IAuthorizationService>();
|
||||
|
||||
var allowed = await authorization.AuthorizeAsync(
|
||||
Principal(userId, "tenant", tenantId, hasMfa: true),
|
||||
null,
|
||||
BackendPermissions.TenantRoleManage);
|
||||
var missingMfa = await authorization.AuthorizeAsync(
|
||||
Principal(userId, "tenant", tenantId, hasMfa: false),
|
||||
null,
|
||||
BackendPermissions.TenantRoleManage);
|
||||
var wrongTenant = await authorization.AuthorizeAsync(
|
||||
Principal(userId, "tenant", Guid.NewGuid(), hasMfa: true),
|
||||
null,
|
||||
BackendPermissions.TenantRoleManage);
|
||||
|
||||
Assert.True(allowed.Succeeded);
|
||||
Assert.False(missingMfa.Succeeded);
|
||||
Assert.False(wrongTenant.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlatformPolicy_RejectsTenantRealmEvenWhenUserHasPlatformPermission()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
var snapshot = Snapshot(
|
||||
userId,
|
||||
tenantId,
|
||||
platformPermissions: [BackendPermissions.PlatformRoleManage]);
|
||||
await using var provider = Services(snapshot);
|
||||
var authorization = provider.GetRequiredService<IAuthorizationService>();
|
||||
|
||||
var tenantRealm = await authorization.AuthorizeAsync(
|
||||
Principal(userId, "tenant", tenantId, hasMfa: true),
|
||||
null,
|
||||
BackendPermissions.PlatformRoleManage);
|
||||
var platformRealm = await authorization.AuthorizeAsync(
|
||||
Principal(userId, "platform", null, hasMfa: true),
|
||||
null,
|
||||
BackendPermissions.PlatformRoleManage);
|
||||
|
||||
Assert.False(tenantRealm.Succeeded);
|
||||
Assert.True(platformRealm.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PermissionPolicy_DoesNotUseJwtRoleClaims()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
var snapshot = Snapshot(userId, tenantId);
|
||||
await using var provider = Services(snapshot);
|
||||
var authorization = provider.GetRequiredService<IAuthorizationService>();
|
||||
var principal = Principal(userId, "tenant", tenantId, hasMfa: true);
|
||||
((ClaimsIdentity)principal.Identity!).AddClaim(new Claim(ClaimTypes.Role, "TenantOwner"));
|
||||
|
||||
var result = await authorization.AuthorizeAsync(
|
||||
principal,
|
||||
null,
|
||||
BackendPermissions.TenantRoleManage);
|
||||
|
||||
Assert.False(result.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllScopePolicy_RejectsRestrictedOrSelfDataScope()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
var principal = Principal(userId, "tenant", tenantId, hasMfa: true);
|
||||
var selfSnapshot = Snapshot(
|
||||
userId,
|
||||
tenantId,
|
||||
tenantPermissions: [BackendPermissions.TenantContentManage]);
|
||||
await using var selfProvider = Services(selfSnapshot);
|
||||
var denied = await selfProvider.GetRequiredService<IAuthorizationService>().AuthorizeAsync(
|
||||
principal,
|
||||
null,
|
||||
TikuPolicies.TenantContentManageAllScope);
|
||||
|
||||
var allScope = new CurrentDataScope(DataScopeMode.All, new HashSet<Guid>(), new HashSet<Guid>(), true);
|
||||
var allSnapshot = Snapshot(
|
||||
userId,
|
||||
tenantId,
|
||||
tenantPermissions: [BackendPermissions.TenantContentManage],
|
||||
dataScope: allScope);
|
||||
await using var allProvider = Services(allSnapshot);
|
||||
var allowed = await allProvider.GetRequiredService<IAuthorizationService>().AuthorizeAsync(
|
||||
principal,
|
||||
null,
|
||||
TikuPolicies.TenantContentManageAllScope);
|
||||
|
||||
Assert.False(denied.Succeeded);
|
||||
Assert.True(allowed.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResourceRequirement_UsesOwnerRegionClassAndTenantBoundary()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
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 },
|
||||
true);
|
||||
await using var provider = Services(Snapshot(userId, tenantId, dataScope: scope));
|
||||
var authorization = provider.GetRequiredService<IAuthorizationService>();
|
||||
var principal = Principal(userId, "tenant", tenantId, hasMfa: true);
|
||||
var requirement = new TenantResourceAccessRequirement();
|
||||
|
||||
var own = await authorization.AuthorizeAsync(
|
||||
principal,
|
||||
new TenantResourceAuthorizationResource(tenantId, OwnerUserId: userId),
|
||||
requirement);
|
||||
var region = await authorization.AuthorizeAsync(
|
||||
principal,
|
||||
new TenantResourceAuthorizationResource(tenantId, RegionId: regionId),
|
||||
requirement);
|
||||
var @class = await authorization.AuthorizeAsync(
|
||||
principal,
|
||||
new TenantResourceAuthorizationResource(tenantId, ClassId: classId),
|
||||
requirement);
|
||||
var outside = await authorization.AuthorizeAsync(
|
||||
principal,
|
||||
new TenantResourceAuthorizationResource(tenantId, RegionId: Guid.NewGuid()),
|
||||
requirement);
|
||||
var otherTenant = await authorization.AuthorizeAsync(
|
||||
principal,
|
||||
new TenantResourceAuthorizationResource(Guid.NewGuid(), OwnerUserId: userId),
|
||||
requirement);
|
||||
|
||||
Assert.True(own.Succeeded);
|
||||
Assert.True(region.Succeeded);
|
||||
Assert.True(@class.Succeeded);
|
||||
Assert.False(outside.Succeeded);
|
||||
Assert.False(otherTenant.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BackofficeBootstrapPolicies_RequireCurrentRealmAndEffectiveAccess()
|
||||
{
|
||||
var tenantId = Guid.NewGuid();
|
||||
var userId = Guid.NewGuid();
|
||||
var tenantSnapshot = Snapshot(userId, tenantId);
|
||||
await using var tenantProvider = Services(tenantSnapshot);
|
||||
var tenantAuthorization = tenantProvider.GetRequiredService<IAuthorizationService>();
|
||||
var tenantAllowed = await tenantAuthorization.AuthorizeAsync(
|
||||
Principal(userId, "tenant", tenantId, hasMfa: true),
|
||||
null,
|
||||
TikuPolicies.TenantBackofficeBootstrap);
|
||||
|
||||
var platformSnapshot = Snapshot(
|
||||
userId,
|
||||
null,
|
||||
platformPermissions: [BackendPermissions.PlatformDashboardView]);
|
||||
await using var platformProvider = Services(platformSnapshot);
|
||||
var platformAuthorization = platformProvider.GetRequiredService<IAuthorizationService>();
|
||||
var platformAllowed = await platformAuthorization.AuthorizeAsync(
|
||||
Principal(userId, "platform", null, hasMfa: true),
|
||||
null,
|
||||
TikuPolicies.PlatformBackofficeBootstrap);
|
||||
var tenantRealmDenied = await platformAuthorization.AuthorizeAsync(
|
||||
Principal(userId, "tenant", tenantId, hasMfa: true),
|
||||
null,
|
||||
TikuPolicies.PlatformBackofficeBootstrap);
|
||||
|
||||
Assert.True(tenantAllowed.Succeeded);
|
||||
Assert.True(platformAllowed.Succeeded);
|
||||
Assert.False(tenantRealmDenied.Succeeded);
|
||||
}
|
||||
|
||||
private static ServiceProvider Services(CurrentAccessSnapshot snapshot)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddSingleton<ICurrentAccessContext>(new StubCurrentAccessContext(snapshot));
|
||||
services.AddTikuRbacAuthorization();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal Principal(
|
||||
Guid userId,
|
||||
string realm,
|
||||
Guid? tenantId,
|
||||
bool hasMfa)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(TikuClaimTypes.UserId, userId.ToString()),
|
||||
new(TikuClaimTypes.Realm, realm)
|
||||
};
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
claims.Add(new Claim(TikuClaimTypes.TenantId, tenantId.Value.ToString()));
|
||||
}
|
||||
|
||||
if (hasMfa)
|
||||
{
|
||||
claims.Add(new Claim(TikuClaimTypes.Mfa, "totp"));
|
||||
}
|
||||
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||
}
|
||||
|
||||
private static CurrentAccessSnapshot Snapshot(
|
||||
Guid userId,
|
||||
Guid? tenantId,
|
||||
IEnumerable<string>? tenantPermissions = null,
|
||||
IEnumerable<string>? platformPermissions = null,
|
||||
CurrentDataScope? dataScope = null)
|
||||
{
|
||||
return new CurrentAccessSnapshot(
|
||||
userId,
|
||||
tenantId,
|
||||
true,
|
||||
tenantId.HasValue,
|
||||
(tenantPermissions ?? []).ToHashSet(StringComparer.Ordinal),
|
||||
(platformPermissions ?? []).ToHashSet(StringComparer.Ordinal),
|
||||
dataScope ?? CurrentDataScope.Self);
|
||||
}
|
||||
|
||||
private sealed class StubCurrentAccessContext(CurrentAccessSnapshot snapshot) : ICurrentAccessContext
|
||||
{
|
||||
public Task<CurrentAccessSnapshot> GetAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(snapshot);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user