using Npgsql; using StellaOps.Doctor.Models; using StellaOps.Doctor.Plugins; using StellaOps.Doctor.Plugins.Builders; using System.Globalization; namespace StellaOps.Doctor.Plugins.Database.Checks; /// /// Verifies the database user has appropriate permissions. /// public sealed class DatabasePermissionsCheck : DatabaseCheckBase { /// public override string CheckId => "check.db.permissions"; /// public override string Name => "Database Permissions"; /// public override string Description => "Verifies the database user has appropriate permissions"; /// public override IReadOnlyList Tags => ["database", "security", "permissions"]; /// protected override async Task 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(); } }