Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Database/Checks/DatabasePermissionsCheck.cs
2026-02-01 21:37:40 +02:00

160 lines
6.5 KiB
C#

using Npgsql;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
using System.Globalization;
namespace StellaOps.Doctor.Plugins.Database.Checks;
/// <summary>
/// Verifies the database user has appropriate permissions.
/// </summary>
public sealed class DatabasePermissionsCheck : DatabaseCheckBase
{
/// <inheritdoc />
public override string CheckId => "check.db.permissions";
/// <inheritdoc />
public override string Name => "Database Permissions";
/// <inheritdoc />
public override string Description => "Verifies the database user has appropriate permissions";
/// <inheritdoc />
public override IReadOnlyList<string> Tags => ["database", "security", "permissions"];
/// <inheritdoc />
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
DoctorPluginContext context,
string connectionString,
CheckResultBuilder result,
CancellationToken ct)
{
await using var connection = await CreateConnectionAsync(connectionString, ct);
var currentUser = string.Empty;
var currentDatabase = string.Empty;
var isSuperuser = false;
var canCreateDb = false;
var canCreateRole = false;
// Get current user info
await using var userCmd = new NpgsqlCommand(@"
SELECT
current_user,
current_database(),
usesuper,
usecreatedb
FROM pg_user
WHERE usename = current_user",
connection);
await using var reader = await userCmd.ExecuteReaderAsync(ct);
if (await reader.ReadAsync(ct))
{
currentUser = reader.GetString(0);
currentDatabase = reader.GetString(1);
isSuperuser = reader.GetBoolean(2);
canCreateDb = reader.GetBoolean(3);
}
await reader.CloseAsync();
// Check role creation privilege
await using var roleCmd = new NpgsqlCommand(@"
SELECT rolcreaterole
FROM pg_roles
WHERE rolname = current_user",
connection);
var roleResult = await roleCmd.ExecuteScalarAsync(ct);
canCreateRole = roleResult is bool b && b;
// Check schema permissions
var schemaPermissions = new List<(string Schema, bool CanSelect, bool CanInsert, bool CanCreate)>();
await using var schemaCmd = new NpgsqlCommand(@"
SELECT
n.nspname,
has_schema_privilege(current_user, n.nspname, 'USAGE') AS can_use,
has_schema_privilege(current_user, n.nspname, 'CREATE') AS can_create
FROM pg_namespace n
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
ORDER BY n.nspname",
connection);
await using var schemaReader = await schemaCmd.ExecuteReaderAsync(ct);
while (await schemaReader.ReadAsync(ct))
{
var schema = schemaReader.GetString(0);
var canUse = schemaReader.GetBoolean(1);
var canCreate = schemaReader.GetBoolean(2);
schemaPermissions.Add((schema, canUse, canUse, canCreate));
}
await schemaReader.CloseAsync();
// Security check: warn if superuser
if (isSuperuser)
{
return result
.Warn("Application is running as superuser - security risk")
.WithEvidence("User permissions", e =>
{
e.Add("User", currentUser);
e.Add("Database", currentDatabase);
e.Add("IsSuperuser", "true");
e.Add("CanCreateDb", canCreateDb.ToString(CultureInfo.InvariantCulture));
e.Add("CanCreateRole", canCreateRole.ToString(CultureInfo.InvariantCulture));
e.Add("SchemaCount", schemaPermissions.Count.ToString(CultureInfo.InvariantCulture));
})
.WithCauses(
"Connection string using postgres user",
"User granted superuser privilege unnecessarily")
.WithRemediation(r => r
.AddManualStep(1, "Create dedicated user", "CREATE USER stellaops WITH PASSWORD 'secure_password'")
.AddManualStep(2, "Grant minimal permissions", "GRANT CONNECT ON DATABASE stellaops TO stellaops")
.AddManualStep(3, "Update connection string", "Change user in connection string to dedicated user"))
.WithVerification("stella doctor --check check.db.permissions")
.Build();
}
// Check if user has access to public schema
var publicSchema = schemaPermissions.FirstOrDefault(s => s.Schema == "public");
if (publicSchema == default || !publicSchema.CanSelect)
{
return result
.Fail("User lacks basic permissions on public schema")
.WithEvidence("User permissions", e =>
{
e.Add("User", currentUser);
e.Add("Database", currentDatabase);
e.Add("PublicSchemaAccess", "false");
})
.WithCauses(
"User not granted USAGE on public schema",
"Restrictive default privileges")
.WithRemediation(r => r
.AddManualStep(1, "Grant schema access", $"GRANT USAGE ON SCHEMA public TO {currentUser}")
.AddManualStep(2, "Grant table access", $"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO {currentUser}"))
.WithVerification("stella doctor --check check.db.permissions")
.Build();
}
return result
.Pass($"User '{currentUser}' has appropriate permissions")
.WithEvidence("User permissions", e =>
{
e.Add("User", currentUser);
e.Add("Database", currentDatabase);
e.Add("IsSuperuser", "false");
e.Add("CanCreateDb", canCreateDb.ToString(CultureInfo.InvariantCulture));
e.Add("CanCreateRole", canCreateRole.ToString(CultureInfo.InvariantCulture));
e.Add("AccessibleSchemas", schemaPermissions.Count.ToString(CultureInfo.InvariantCulture));
foreach (var perm in schemaPermissions.Take(5))
{
e.Add($"Schema_{perm.Schema}", $"use={perm.CanSelect}, create={perm.CanCreate}");
}
})
.Build();
}
}