feat: strengthen P0 security and operations
This commit is contained in:
30
Tiku.Application/Assets/IAssetSecurityScanner.cs
Normal file
30
Tiku.Application/Assets/IAssetSecurityScanner.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Tiku.Application.Assets;
|
||||
|
||||
public enum AssetSecurityScanVerdict
|
||||
{
|
||||
Clean,
|
||||
Infected
|
||||
}
|
||||
|
||||
public sealed record AssetSecurityScanResult(
|
||||
AssetSecurityScanVerdict Verdict,
|
||||
string Provider,
|
||||
string? Signature,
|
||||
long BytesScanned,
|
||||
string RawResponse);
|
||||
|
||||
public interface IAssetSecurityScanner
|
||||
{
|
||||
Task<AssetSecurityScanResult> ScanAsync(
|
||||
Stream content,
|
||||
long? declaredLength,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> CheckHealthAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class AssetSecurityScannerException(string code, string message, Exception? innerException = null)
|
||||
: Exception(message, innerException)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
@@ -99,6 +99,41 @@ public sealed record PasswordChangeChallengeRequest(
|
||||
string? IpAddress,
|
||||
string? UserAgent);
|
||||
|
||||
public sealed record PasswordResetCodeRequest(
|
||||
Guid TenantId,
|
||||
string Phone,
|
||||
string? IpAddress,
|
||||
string? UserAgent,
|
||||
string? DeviceId = null);
|
||||
|
||||
public sealed record PasswordResetRequest(
|
||||
Guid TenantId,
|
||||
string Phone,
|
||||
string Code,
|
||||
string NewPassword,
|
||||
string? IpAddress,
|
||||
string? UserAgent);
|
||||
|
||||
public sealed record AuthenticatedPasswordChangeRequest(
|
||||
Guid UserId,
|
||||
Guid SessionId,
|
||||
string CurrentPassword,
|
||||
string NewPassword,
|
||||
string? IpAddress,
|
||||
string? UserAgent);
|
||||
|
||||
public sealed record AuthSessionSummary(
|
||||
Guid SessionFamilyId,
|
||||
AuthRealm Realm,
|
||||
Guid? TenantId,
|
||||
string Provider,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset LastRotatedAt,
|
||||
DateTimeOffset ExpiresAt,
|
||||
string? IpAddressMasked,
|
||||
string? UserAgent,
|
||||
bool IsCurrent);
|
||||
|
||||
public sealed record SmsSendResult(
|
||||
Guid VerificationId,
|
||||
DateTimeOffset ExpiresAt);
|
||||
|
||||
@@ -25,3 +25,9 @@ public sealed class AuthSecurityUnavailableException()
|
||||
|
||||
public sealed class InvalidAuthChallengeException(string code = "invalid_auth_challenge")
|
||||
: AuthException(code, "The authentication challenge is invalid, consumed, or expired.");
|
||||
|
||||
public sealed class AuthSessionNotFoundException()
|
||||
: AuthException("auth_session_not_found", "The requested authentication session was not found.");
|
||||
|
||||
public sealed class CurrentAuthSessionCannotBeRevokedException()
|
||||
: AuthException("current_auth_session_cannot_be_revoked", "Use logout to revoke the current authentication session.");
|
||||
|
||||
15
Tiku.Application/Auth/IAuthAdministrationService.cs
Normal file
15
Tiku.Application/Auth/IAuthAdministrationService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Tiku.Application.Auth;
|
||||
|
||||
public sealed record AdministrativePasswordResetRequest(
|
||||
Guid ActorUserId,
|
||||
Guid TargetUserId,
|
||||
Guid? TenantId,
|
||||
string TemporaryPassword,
|
||||
string Reason);
|
||||
|
||||
public interface IAuthAdministrationService
|
||||
{
|
||||
Task ResetPasswordAsync(
|
||||
AdministrativePasswordResetRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -31,4 +31,16 @@ public interface IAuthService
|
||||
Task<AuthenticationResult> ChangeRequiredPasswordAsync(
|
||||
PasswordChangeChallengeRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<SmsSendResult> RequestPasswordResetAsync(
|
||||
PasswordResetCodeRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task ResetPasswordAsync(
|
||||
PasswordResetRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AuthenticationResult> ChangePasswordAsync(
|
||||
AuthenticatedPasswordChangeRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,25 @@ public interface IAuthSessionStore
|
||||
Guid? tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AuthSessionValidationResult?> ResolveActiveSessionAsync(
|
||||
Guid sessionId,
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task RevokeFamilyAsync(string refreshToken, string reason, CancellationToken cancellationToken = default);
|
||||
Task RevokeRealmAsync(Guid userId, AuthRealm realm, Guid? tenantId, string reason, CancellationToken cancellationToken = default);
|
||||
Task RevokeAllAsync(Guid userId, string reason, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyCollection<AuthSessionSummary>> ListActiveAsync(
|
||||
Guid userId,
|
||||
Guid currentSessionId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task RevokeOwnedFamilyAsync(
|
||||
Guid userId,
|
||||
Guid currentSessionId,
|
||||
Guid sessionFamilyId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed record AuthSessionIssueRequest(
|
||||
|
||||
@@ -3,23 +3,34 @@ using Tiku.Domain.Operations;
|
||||
|
||||
namespace Tiku.Application.Jobs;
|
||||
|
||||
public sealed class BackgroundJobException(string code, string message) : Exception(message)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
|
||||
public sealed record CreateBackgroundJobCommand(
|
||||
Guid TenantId,
|
||||
string JobType,
|
||||
JsonElement Payload,
|
||||
DateTimeOffset? RunAfter = null,
|
||||
int MaxRetries = 3);
|
||||
int MaxRetries = 3,
|
||||
string? IdempotencyKey = null,
|
||||
bool IsSystemJob = false);
|
||||
|
||||
public sealed record BackgroundJobItem(
|
||||
Guid Id,
|
||||
Guid TenantId,
|
||||
string JobType,
|
||||
string? IdempotencyKey,
|
||||
BackgroundJobStatus Status,
|
||||
int RetryCount,
|
||||
int MaxRetries,
|
||||
DateTimeOffset? RunAfter,
|
||||
DateTimeOffset? StartedAt,
|
||||
DateTimeOffset? CompletedAt,
|
||||
DateTimeOffset? CancellationRequestedAt,
|
||||
Guid? CancellationRequestedBy,
|
||||
string? CancellationReason,
|
||||
string? LastError,
|
||||
Guid? OutputAssetId,
|
||||
JsonElement Result);
|
||||
@@ -48,4 +59,29 @@ public interface IBackgroundJobService
|
||||
string? jobType = null,
|
||||
int limit = 50,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<BackgroundJobItem?> GetAsync(
|
||||
Guid jobId,
|
||||
Guid? tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyCollection<BackgroundJobItem>> ListPlatformAsync(
|
||||
Guid? tenantId = null,
|
||||
string? jobType = null,
|
||||
BackgroundJobStatus? status = null,
|
||||
int limit = 100,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<BackgroundJobItem> RequestCancellationAsync(
|
||||
Guid jobId,
|
||||
Guid? tenantId,
|
||||
Guid actorUserId,
|
||||
string reason,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<BackgroundJobItem> RetryAsync(
|
||||
Guid jobId,
|
||||
Guid? tenantId,
|
||||
Guid actorUserId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ public static class BackendPermissions
|
||||
public const string PlatformSmsWrite = "platform:sms:write";
|
||||
public const string PlatformPaymentRead = "platform:payment:read";
|
||||
public const string PlatformPaymentWrite = "platform:payment:write";
|
||||
public const string PlatformOperationsView = "platform:operations:view";
|
||||
public const string PlatformOperationsManage = "platform:operations:manage";
|
||||
|
||||
public static readonly IReadOnlySet<string> Tenant = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
@@ -73,7 +75,9 @@ public static class BackendPermissions
|
||||
PlatformSmsRead,
|
||||
PlatformSmsWrite,
|
||||
PlatformPaymentRead,
|
||||
PlatformPaymentWrite
|
||||
PlatformPaymentWrite,
|
||||
PlatformOperationsView,
|
||||
PlatformOperationsManage
|
||||
};
|
||||
|
||||
public static void EnsureTenant(string permissionCode)
|
||||
|
||||
@@ -83,6 +83,7 @@ public static class PermissionModuleCatalog
|
||||
["platform_crm"] = null,
|
||||
["platform_sms"] = null,
|
||||
["platform_payment"] = null,
|
||||
["platform_operations"] = null,
|
||||
["commerce"] = SaasFeatureCatalog.StudentStore
|
||||
};
|
||||
|
||||
@@ -114,6 +115,7 @@ public static class PermissionModuleCatalog
|
||||
BackendPermissions.PlatformCrmRead or BackendPermissions.PlatformCrmWrite => "platform_crm",
|
||||
BackendPermissions.PlatformSmsRead or BackendPermissions.PlatformSmsWrite => "platform_sms",
|
||||
BackendPermissions.PlatformPaymentRead or BackendPermissions.PlatformPaymentWrite => "platform_payment",
|
||||
BackendPermissions.PlatformOperationsView or BackendPermissions.PlatformOperationsManage => "platform_operations",
|
||||
_ when permissionCode.StartsWith("commerce:", StringComparison.Ordinal) => "commerce",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(permissionCode), permissionCode, "Permission module mapping is missing.")
|
||||
};
|
||||
|
||||
@@ -26,4 +26,9 @@ public interface IObjectStorageService
|
||||
Task<ObjectStorageMetadata> HeadObjectAsync(
|
||||
ObjectStorageHeadRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Stream> OpenReadAsync(
|
||||
ObjectStorageReadRequest request,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromException<Stream>(new NotSupportedException("Object storage read streaming is not configured."));
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ public sealed record ObjectStorageHeadRequest(
|
||||
long? DeclaredFileSizeBytes = null,
|
||||
string? DeclaredChecksumSha256 = null);
|
||||
|
||||
public sealed record ObjectStorageReadRequest(
|
||||
Guid TenantId,
|
||||
string Provider,
|
||||
string Bucket,
|
||||
string ObjectKey);
|
||||
|
||||
public sealed record ObjectStorageWriteRequest(
|
||||
Guid TenantId,
|
||||
string Provider,
|
||||
|
||||
39
Tiku.Application/Tenancy/TenantLifecycleModels.cs
Normal file
39
Tiku.Application/Tenancy/TenantLifecycleModels.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Domain.Operations;
|
||||
|
||||
namespace Tiku.Application.Tenancy;
|
||||
|
||||
public sealed record TenantArchivePreview(
|
||||
Guid TenantId,
|
||||
bool CanArchive,
|
||||
bool HasRecentSuccessfulExport,
|
||||
IReadOnlyCollection<string> Blockers);
|
||||
|
||||
public sealed record TenantLifecycleOperationItem(
|
||||
Guid Id,
|
||||
Guid TenantId,
|
||||
TenantLifecycleOperationType OperationType,
|
||||
TenantLifecycleOperationStatus Status,
|
||||
Guid RequestedBy,
|
||||
Guid? TargetUserId,
|
||||
Guid? ExportAssetId,
|
||||
string? Reason,
|
||||
string? LastError,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset? CompletedAt);
|
||||
|
||||
public interface ITenantLifecycleService
|
||||
{
|
||||
Task<TenantArchivePreview> PreviewArchiveAsync(Guid tenantId, CancellationToken cancellationToken = default);
|
||||
Task<TenantLifecycleOperationItem> CreateExportAsync(Guid tenantId, Guid actorUserId, CancellationToken cancellationToken = default);
|
||||
Task<TenantLifecycleOperationItem?> GetOperationAsync(Guid tenantId, Guid operationId, CancellationToken cancellationToken = default);
|
||||
Task<ObjectStorageSignedUrl> SignExportDownloadAsync(Guid tenantId, Guid operationId, CancellationToken cancellationToken = default);
|
||||
Task<TenantLifecycleOperationItem> ArchiveAsync(Guid tenantId, Guid actorUserId, string reason, CancellationToken cancellationToken = default);
|
||||
Task<TenantLifecycleOperationItem> RestoreAsync(Guid tenantId, Guid actorUserId, string reason, CancellationToken cancellationToken = default);
|
||||
Task<TenantLifecycleOperationItem> TransferOwnerAsync(Guid tenantId, Guid actorUserId, Guid targetUserId, string reason, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class TenantLifecycleException(string code, string message) : Exception(message)
|
||||
{
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
Reference in New Issue
Block a user