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()}"; } }