160 lines
6.5 KiB
C#
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();
|
|
}
|
|
}
|