using System.Text;
namespace SQLInsight.Core.DynamicsAx;
///
/// The optional read access to AX tables. The agent normally reads only metadata (grant-least-privilege.sql); the AX
/// data checks need SELECT on a few AX system tables (batch jobs, number sequences, RecId allocation, companies), and
/// the customer decides whether to give it, per database. No customer data tables, never more than SELECT.
/// db/permissions/grant-dynamics-ax-read.sql does the same for any database; makes the exact
/// statements for one database, with its schema (bmssa in AX 3.0) and the agent's login filled in.
///
public static class AxReadAccess
{
public const string ScriptPath = "db/permissions/grant-dynamics-ax-read.sql";
public const string RevokeScriptPath = "db/permissions/revoke-dynamics-ax-read.sql";
/// Used in the GRANT text when the agent's login is not known yet.
public const string LoginPlaceholder = @"DOMÆNE\svc-sqlinsight$";
/// The tables the read access covers for a version (those that exist in the database).
public static IReadOnlyList Tables(AxDatabase database)
{
ArgumentNullException.ThrowIfNull(database);
return AxSchemaAdapter.For(database.Version) is { } adapter
? adapter.ReadTables.Where(database.HasTable).ToList()
: [];
}
/// The tables the agent may not read yet.
public static IReadOnlyList Missing(AxDatabase database) => Tables(database).Where(t => !database.CanRead(t)).ToList();
///
/// The exact statements that give the agent SELECT on (all the version's tables when null)
/// in this database. Run by a sysadmin or db_owner; SQLInsight never runs it.
///
public static string GrantSql(AxDatabase database, string? login, IEnumerable? tables = null)
{
ArgumentNullException.ThrowIfNull(database);
login = string.IsNullOrWhiteSpace(login) ? LoginPlaceholder : login.Trim();
var user = database.DatabaseUser ?? login;
var sql = new StringBuilder()
.Append("USE ").Append(AxSql.Identifier(database.Database)).Append(";\r\n");
if (database.DatabaseUser is null)
{
sql.Append("IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE sid = SUSER_SID(").Append(AxSql.Literal(login)).Append("))\r\n")
.Append(" CREATE USER ").Append(AxSql.Identifier(login)).Append(" FOR LOGIN ").Append(AxSql.Identifier(login)).Append(";\r\n");
}
foreach (var table in (tables ?? Tables(database)).Distinct(StringComparer.OrdinalIgnoreCase))
{
sql.Append("GRANT SELECT ON ").Append(database.Qualified(table)).Append(" TO ").Append(AxSql.Identifier(user)).Append(";\r\n");
}
return sql.ToString();
}
/// db/permissions/grant-dynamics-ax-read.sql as shipped.
public static string Template()
{
using var stream = typeof(AxReadAccess).Assembly.GetManifestResourceStream("SQLInsight.Core.grant-dynamics-ax-read.sql")
?? throw new InvalidOperationException("grant-dynamics-ax-read.sql mangler i programmet.");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
/// The shipped script with the database and the agent's login filled in.
public static string Script(string database, string? login)
{
ArgumentNullException.ThrowIfNull(database);
return Template()
.Replace("SET @DatabaseName = N'AXDB';", "SET @DatabaseName = " + AxSql.Literal(database) + ";", StringComparison.Ordinal)
.Replace("SET @LoginName = N'" + LoginPlaceholder + "';", "SET @LoginName = " + AxSql.Literal(string.IsNullOrWhiteSpace(login) ? LoginPlaceholder : login.Trim()) + ";",
StringComparison.Ordinal);
}
/// "Ikke målbar" for a check that needs , with the exact GRANT text.
public static string NotMeasurable(AxDatabase database, IReadOnlyList tables, string? login = null)
{
ArgumentNullException.ThrowIfNull(database);
ArgumentNullException.ThrowIfNull(tables);
return $"Ikke målbar: agenten må ikke læse {string.Join(", ", tables.Select(t => database.Schema + "." + t))} i {database.Database} "
+ $"(valgfri AX-læseadgang). Kør som sysadmin eller db_owner: {GrantSql(database, login ?? database.AgentLogin, tables).Replace("\r\n", " ", StringComparison.Ordinal).Trim()}";
}
}