sln build fix (again), tests fixes, audit work and doctors work
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using System.Globalization;
|
||||
using Npgsql;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Builders;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user