using Microsoft.Data.SqlClient;
namespace SQLInsight.IntegrationTests;
///
/// Runs db/permissions/grant-least-privilege.sql and proves that the agent login can read everything it needs,
/// and nothing else.
///
[Collection(SqlServerTestGroup.Name)]
[Trait("Category", "Integration")]
public class LeastPrivilegeTests(SqlServerFixture sql) : IAsyncLifetime
{
private const string Login = "sqlinsight_agent";
private const string Password = "Agent-Pa55word!";
private const string Database = "SQLInsightQsTest";
public async Task InitializeAsync()
{
await sql.ExecuteAsSaAsync($"""
IF SUSER_ID(N'{Login}') IS NULL CREATE LOGIN [{Login}] WITH PASSWORD = N'{Password}', CHECK_POLICY = OFF;
IF DB_ID(N'{Database}') IS NULL CREATE DATABASE [{Database}];
""");
await sql.ExecuteAsSaAsync($"""
USE [{Database}];
ALTER DATABASE [{Database}] SET QUERY_STORE = ON;
IF OBJECT_ID(N'dbo.CustomerData') IS NULL
BEGIN
CREATE TABLE dbo.CustomerData (Id int NOT NULL PRIMARY KEY, Secret nvarchar(50) NOT NULL);
INSERT dbo.CustomerData VALUES (1, N'fortroligt');
END;
""");
var script = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Scripts", "grant-least-privilege.sql"));
script = script.Replace(@"N'DOMÆNE\svc-sqlinsight$'", $"N'{Login}'", StringComparison.Ordinal);
await sql.ExecuteAsSaAsync(script);
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Agent_login_is_not_sysadmin()
{
Assert.Equal(0, await ScalarAsync("SELECT IS_SRVROLEMEMBER('sysadmin')"));
}
[Theory]
[InlineData("SELECT COUNT(*) FROM sys.dm_os_wait_stats")]
[InlineData("SELECT COUNT(*) FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t")]
[InlineData("SELECT COUNT(*) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) p")]
[InlineData("SELECT COUNT(*) FROM sys.dm_io_virtual_file_stats(NULL, NULL)")]
[InlineData("SELECT COUNT(*) FROM sys.dm_os_ring_buffers")]
[InlineData("SELECT COUNT(*) FROM sys.dm_xe_sessions WHERE name = 'system_health'")]
[InlineData("SELECT COUNT(*) FROM sys.configurations")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.backupset")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.sysjobs")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.sysjobhistory")]
[InlineData("SELECT COUNT(*) FROM msdb.dbo.suspect_pages")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.database_query_store_options")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.query_store_runtime_stats")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.indexes WHERE object_id = OBJECT_ID(N'[{Database}].dbo.CustomerData')")]
[InlineData($"SELECT COUNT(*) FROM [{Database}].sys.dm_db_index_usage_stats")]
[InlineData($"SELECT COUNT(*) WHERE DATABASEPROPERTYEX(N'{Database}', 'LastGoodCheckDbTime') IS NOT NULL")]
public async Task Agent_can_read_monitoring_sources(string query)
{
Assert.True(await ScalarAsync(query) >= 0);
}
[Fact]
public async Task Agent_cannot_read_customer_data()
{
var ex = await Assert.ThrowsAsync(
() => ScalarAsync($"SELECT COUNT(*) FROM [{Database}].dbo.CustomerData"));
Assert.Equal(229, ex.Number); // The SELECT permission was denied.
}
[Fact]
public async Task Agent_cannot_change_anything()
{
var ex = await Assert.ThrowsAsync(
() => ScalarAsync($"USE [{Database}]; CREATE TABLE dbo.ShouldNotExist (Id int); SELECT 1"));
Assert.Equal(262, ex.Number); // CREATE TABLE permission denied.
}
[Fact]
public async Task Script_is_idempotent()
{
var script = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Scripts", "grant-least-privilege.sql"));
script = script.Replace(@"N'DOMÆNE\svc-sqlinsight$'", $"N'{Login}'", StringComparison.Ordinal);
await sql.ExecuteAsSaAsync(script);
}
private async Task ScalarAsync(string query)
{
var builder = new SqlConnectionStringBuilder(sql.SaConnectionString)
{
UserID = Login,
Password = Password,
InitialCatalog = "master",
};
await using var connection = new SqlConnection(builder.ConnectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = query;
return (T)Convert.ChangeType((await command.ExecuteScalarAsync())!, typeof(T), System.Globalization.CultureInfo.InvariantCulture);
}
}