Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
+200
@@ -0,0 +1,200 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ItWorx.KnowledgeHub.Application.Operations;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.Operations;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
|
||||
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Npgsql;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.IntegrationTests.Operations;
|
||||
|
||||
[Collection(PostgresTestGroup.Name)]
|
||||
public sealed class TableIdentifierSafetyTests : OwnerSetupTestBase
|
||||
{
|
||||
private readonly PostgresContainerFixture _postgres;
|
||||
|
||||
public TableIdentifierSafetyTests(PostgresContainerFixture postgres) : base(postgres)
|
||||
{
|
||||
_postgres = postgres;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unsafe\";SELECT 1;--")]
|
||||
[InlineData("unsafe';SELECT 1;--")]
|
||||
[InlineData("unsafe;SELECT 1;--")]
|
||||
public async Task Backup_rejects_catalog_identifiers_before_building_copy_sql(string tableName)
|
||||
{
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<KnowledgeHubDbContext>();
|
||||
await db.Database.OpenConnectionAsync();
|
||||
var connection = (NpgsqlConnection)db.Database.GetDbConnection();
|
||||
var quotedName = QuoteIdentifier(tableName);
|
||||
await using (var create = new NpgsqlCommand($"CREATE TABLE public.{quotedName} (id integer)", connection))
|
||||
await create.ExecuteNonQueryAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var backups = scope.ServiceProvider.GetRequiredService<IBackupService>();
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
backups.CreateBackupAsync(CancellationToken.None));
|
||||
Assert.Contains("identificatie", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await using var drop = new NpgsqlCommand($"DROP TABLE IF EXISTS public.{quotedName}", connection);
|
||||
await drop.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("unsafe\";SELECT 1;--")]
|
||||
[InlineData("unsafe';SELECT 1;--")]
|
||||
[InlineData("unsafe;SELECT 1;--")]
|
||||
public async Task Restore_rejects_a_validly_signed_manifest_identifier_before_target_connection(string tableName)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "kh-identifier-" + Guid.NewGuid().ToString("N"));
|
||||
var backupsRoot = Path.Combine(root, "backups");
|
||||
var backupId = "signed-identifier-test";
|
||||
var backupDirectory = Path.Combine(backupsRoot, backupId);
|
||||
var keysRoot = Path.Combine(root, "keys");
|
||||
var attachmentsRoot = Path.Combine(root, "source-attachments");
|
||||
var targetAttachments = Path.Combine(root, "target-attachments");
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
Directory.CreateDirectory(keysRoot);
|
||||
Directory.CreateDirectory(attachmentsRoot);
|
||||
Directory.CreateDirectory(targetAttachments);
|
||||
|
||||
try
|
||||
{
|
||||
await WriteSignedManifestAsync(backupDirectory, keysRoot, backupId, tableName);
|
||||
var paths = new BackupPaths(backupsRoot, attachmentsRoot, keysRoot);
|
||||
var evidence = new FileBackupEvidenceStore(paths, TimeProvider.System);
|
||||
Assert.NotNull((await evidence.GetAsync(backupId, CancellationToken.None))!.IntegrityVerifiedAt);
|
||||
var restore = new PostgresRestoreService(paths, evidence);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => restore.RestoreAsync(
|
||||
backupId,
|
||||
new RestoreTarget(
|
||||
"Host=127.0.0.1;Port=1;Database=must-not-connect;Username=test;Password=test;Timeout=1",
|
||||
targetAttachments),
|
||||
CancellationToken.None));
|
||||
|
||||
Assert.Contains("identificatie", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Empty(Directory.EnumerateFileSystemEntries(targetAttachments));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_rejects_duplicate_signed_manifest_tables_before_target_connection()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "kh-identifier-" + Guid.NewGuid().ToString("N"));
|
||||
var backupsRoot = Path.Combine(root, "backups");
|
||||
var backupId = "duplicate-table-test";
|
||||
var backupDirectory = Path.Combine(backupsRoot, backupId);
|
||||
var keysRoot = Path.Combine(root, "keys");
|
||||
var attachmentsRoot = Path.Combine(root, "source-attachments");
|
||||
var targetAttachments = Path.Combine(root, "target-attachments");
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
Directory.CreateDirectory(keysRoot);
|
||||
Directory.CreateDirectory(attachmentsRoot);
|
||||
Directory.CreateDirectory(targetAttachments);
|
||||
|
||||
try
|
||||
{
|
||||
await WriteSignedManifestAsync(backupDirectory, keysRoot, backupId, "pages", duplicateTable: true);
|
||||
var paths = new BackupPaths(backupsRoot, attachmentsRoot, keysRoot);
|
||||
var evidence = new FileBackupEvidenceStore(paths, TimeProvider.System);
|
||||
Assert.NotNull((await evidence.GetAsync(backupId, CancellationToken.None))!.IntegrityVerifiedAt);
|
||||
var restore = new PostgresRestoreService(paths, evidence);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => restore.RestoreAsync(
|
||||
backupId,
|
||||
new RestoreTarget(
|
||||
"Host=127.0.0.1;Port=1;Database=must-not-connect;Username=test;Password=test;Timeout=1",
|
||||
targetAttachments),
|
||||
CancellationToken.None));
|
||||
|
||||
Assert.Contains("dubbele tabelnamen", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Empty(Directory.EnumerateFileSystemEntries(targetAttachments));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_rejects_unsafe_identity_column_names_before_committing_target_rows()
|
||||
{
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var backups = scope.ServiceProvider.GetRequiredService<IBackupService>();
|
||||
var backup = await backups.CreateBackupAsync(CancellationToken.None);
|
||||
await using var target = await RestoreTargetDatabase.CreateAsync(_postgres);
|
||||
|
||||
var unsafeColumn = QuoteIdentifier("unsafe';SELECT 1;--");
|
||||
await using (var connection = new NpgsqlConnection(target.ConnectionString))
|
||||
{
|
||||
await connection.OpenAsync();
|
||||
await using var create = new NpgsqlCommand(
|
||||
$"CREATE TABLE public.safe_identity_test ({unsafeColumn} integer GENERATED BY DEFAULT AS IDENTITY)", connection);
|
||||
await create.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
var restore = scope.ServiceProvider.GetRequiredService<IRestoreService>();
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => restore.RestoreAsync(
|
||||
backup.BackupId,
|
||||
new RestoreTarget(target.ConnectionString, target.AttachmentsDirectory),
|
||||
CancellationToken.None));
|
||||
|
||||
Assert.Contains("identificatie", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
await using var verify = new NpgsqlConnection(target.ConnectionString);
|
||||
await verify.OpenAsync();
|
||||
await using var count = new NpgsqlCommand("SELECT count(*) FROM public.workspaces", verify);
|
||||
Assert.Equal(0L, (long)(await count.ExecuteScalarAsync() ?? 0L));
|
||||
Assert.Empty(Directory.EnumerateFileSystemEntries(target.AttachmentsDirectory));
|
||||
}
|
||||
|
||||
private static async Task WriteSignedManifestAsync(
|
||||
string backupDirectory, string keysRoot, string backupId, string tableName, bool duplicateTable = false)
|
||||
{
|
||||
var table = new BackupTableManifestEntry(tableName, "rows.bin", 0, 0, new string('0', 64));
|
||||
BackupTableManifestEntry[] tables = duplicateTable ? [table, table] : [table];
|
||||
var manifest = new BackupManifest(
|
||||
backupId, DateTimeOffset.UtcNow, "ItWorx Knowledge Hub", "1.0", "test", "schema",
|
||||
tables, [], 0);
|
||||
var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(manifest);
|
||||
await File.WriteAllBytesAsync(Path.Combine(backupDirectory, "manifest.json"), manifestBytes);
|
||||
|
||||
var key = RandomNumberGenerator.GetBytes(32);
|
||||
await File.WriteAllBytesAsync(Path.Combine(keysRoot, "backup-integrity.key"), key);
|
||||
if (!OperatingSystem.IsWindows())
|
||||
File.SetUnixFileMode(Path.Combine(keysRoot, "backup-integrity.key"),
|
||||
UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var manifestHash = Convert.ToHexStringLower(SHA256.HashData(manifestBytes));
|
||||
var payload = string.Join('\n',
|
||||
"itworx-knowledge-hub-backup-evidence-v1", "integrity", backupId, manifestHash,
|
||||
completedAt.UtcTicks.ToString(CultureInfo.InvariantCulture), "1");
|
||||
var receipt = new
|
||||
{
|
||||
Version = 1,
|
||||
BackupId = backupId,
|
||||
ManifestSha256 = manifestHash,
|
||||
CompletedAt = completedAt,
|
||||
Success = true,
|
||||
HmacSha256 = Convert.ToHexStringLower(HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(payload)))
|
||||
};
|
||||
await File.WriteAllTextAsync(Path.Combine(backupDirectory, ".integrity.json"), JsonSerializer.Serialize(receipt));
|
||||
CryptographicOperations.ZeroMemory(key);
|
||||
}
|
||||
|
||||
private static string QuoteIdentifier(string identifier) => $"\"{identifier.Replace("\"", "\"\"", StringComparison.Ordinal)}\"";
|
||||
}
|
||||
Reference in new issue
Block a user