diff --git a/docs/installation.md b/docs/installation.md index 2546d85..bc85ee5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -23,6 +23,7 @@ msiexec /i SQLInsight-Pro-1.0.0-x64.msi /qn /l*v install.log ` REPOSITORY_CONNECTION="Server=sqlmon01;Database=SQLInsight;Integrated Security=true;Encrypt=true" ` MONITOR_SERVER="sql01" ` AGENT_ACCOUNT="DOMÆNE\svc-sqlinsight$" WEB_ACCOUNT="DOMÆNE\svc-sqlinsight-web$" WEB_PORT=5080 ` + ADMIN_GROUPS="DOMÆNE\SQL-Admins" ` ADDLOCAL=ALL ``` @@ -33,6 +34,7 @@ msiexec /i SQLInsight-Pro-1.0.0-x64.msi /qn /l*v install.log ` | `AGENT_ACCOUNT` | `NT SERVICE\SQLInsight Agent` | Tjenestekonto til agenten. Den virtuelle konto når andre servere som computerkontoen (`DOMÆNE\SERVER$`), så det er den, der skal have login og rettigheder på de overvågede instanser. En gMSA angives som `DOMÆNE\navn$` | | `WEB_ACCOUNT` | `NT SERVICE\SQLInsight Web` | Tjenestekonto til web. Skal have `db_datareader` på repositoriet | | `WEB_PORT` | `5080` | Port til webbrugerfladen. Installeren åbner den i Windows Firewall | +| `ADMIN_GROUPS` | (ingen) | Windows-grupper, der må ændre, hvilke servere og databaser der overvåges, adskilt af `;` (fx `DOMÆNE\SQL-Admins;DOMÆNE\DBA`). Skrives som `SQLInsight:Web:AdminGroups` i webbens `sqlinsight.ini`. Uden den må den lokale gruppe Administratorer på webserveren (se [servere-og-databaser.md](servere-og-databaser.md#hvem-må-ændre-opsætningen)) | | `ADDLOCAL` | `Agent,Web` | `Agent`, `Web` og `UpdateTask` (planlagt opgave, der installerer hentede opdateringer som SYSTEM, se [opdatering.md](opdatering.md)). `ALL` = alle tre | | `SKIP_REPOSITORY_SETUP` | (ingen) | `1` = installeren opretter ikke database, logins og rettigheder. Brug det, når en DBA gør det (trin 2 og 3), eller når installationen kører som SYSTEM (SCCM/Intune) uden rettigheder på SQL Server | diff --git a/docs/integration.md b/docs/integration.md index c5c659d..0b96605 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -100,7 +100,7 @@ Alt, hvad webbrugerfladen viser, kan hentes som JSON under `/api/v1/` med de sam "ApiTokens": { "itsm": { "hash": "sha256:…", "scopes": [ "read", "support:write" ] } } ``` -og oprettes med `SQLInsight.Web.exe new-api-token itsm --scope read,support:write`. Windows-brugere med adgang til webbrugerfladen må det samme som i webbrugerfladen. Scopes: `read`, `support:write` (supportsager) og `config:write` (hvilke servere og databaser der overvåges; for Windows-brugere kræver det medlemskab af `SQLInsight:Web:AdminGroups`). +og oprettes med `SQLInsight.Web.exe new-api-token itsm --scope read,support:write`. Windows-brugere med adgang til webbrugerfladen må det samme som i webbrugerfladen. Scopes: `read`, `support:write` (supportsager) og `config:write` (hvilke servere og databaser der overvåges; for Windows-brugere kræver det medlemskab af `SQLInsight:Web:AdminGroups`, eller af Administratorer på webserveren, når den er tom). PowerShell: diff --git a/docs/servere-og-databaser.md b/docs/servere-og-databaser.md index 2c091d8..27fa449 100644 --- a/docs/servere-og-databaser.md +++ b/docs/servere-og-databaser.md @@ -15,7 +15,9 @@ Alle med adgang til webbrugerfladen kan **se** opsætningen. Kun medlemmer af en } ``` -Er `AdminGroups` tom, kan ingen ændre noget i webbrugerfladen; siden viser så, hvor gruppen sættes. Genstart tjenesten `SQLInsight Web` efter en ændring. +Installeren kan sætte gruppen med egenskaben `ADMIN_GROUPS` (flere grupper adskilt af `;`, fx `ADMIN_GROUPS="DOMÆNE\SQL-Admins;DOMÆNE\DBA"`); den skrives i webbens `sqlinsight.ini`. Genstart tjenesten `SQLInsight Web` efter en ændring. + +Er `AdminGroups` tom (standard ved en installation uden `ADMIN_GROUPS`), kan medlemmer af den **lokale gruppe Administratorer på webserveren** (`BUILTIN\Administrators`, SID `S-1-5-32-544`) ændre opsætningen, så den første server kan tilføjes med det samme. Det afgøres ud fra brugerens Windows-grupper ved login. En Windows-login fra en anden computer (Negotiate/Kerberos) har den fulde token, så en domænekonto, der er administrator på webserveren (fx via Domain Admins), genkendes. Åbnes webbrugerfladen i en browser på selve webserveren, er brugerens token filtreret af UAC (Administratorer er kun *deny-only*), og så tæller det ikke; brug en anden computer, eller sæt `AdminGroups`. Så snart `AdminGroups` indeholder en gruppe, er det kun den, der gælder. Siden *Servere og databaser* viser, hvem der må ændre opsætningen. ## Tilføj en server @@ -69,7 +71,7 @@ Webbrugerfladens login har stadig kun `db_datareader` og ellers præcis det, den ## API -Alt på siden findes i API v1 (se [integration.md](integration.md#json-api)). Læsning kræver scope `read`; ændringer kræver scope `config:write` for tokens eller medlemskab af `AdminGroups` for Windows-brugere. Opret et token med `SQLInsight.Web.exe new-api-token automation --scope read,config:write`. +Alt på siden findes i API v1 (se [integration.md](integration.md#json-api)). Læsning kræver scope `read`; ændringer kræver scope `config:write` for tokens eller medlemskab af `AdminGroups` for Windows-brugere (er den tom: den lokale gruppe Administratorer på webserveren). Opret et token med `SQLInsight.Web.exe new-api-token automation --scope read,config:write`. | Endpoint | | |---|---| diff --git a/installer/SQLInsight.wxs b/installer/SQLInsight.wxs index f423021..e6557d7 100644 --- a/installer/SQLInsight.wxs +++ b/installer/SQLInsight.wxs @@ -10,6 +10,9 @@ which reaches other servers as the computer account (DOMAIN\SERVER$). A gMSA: DOMAIN\svc-sqlinsight$ WEB_ACCOUNT Account for SQLInsight Web. Default NT SERVICE\SQLInsight Web WEB_PORT Port for the web UI (default 5080) + ADMIN_GROUPS Windows groups that may change which servers and databases are monitored in the web UI, + separated by ';' (e.g. DOMAIN\SQL-Admins). Written to the web's sqlinsight.ini as + SQLInsight:Web:AdminGroups. Empty: the web server's local Administrators group may. SKIP_REPOSITORY_SETUP 1 = do not create the repository database, logins and rights (the admin does it, see docs/installation.md). Otherwise the installing user's Windows account does it. Features: Agent, Web (both by default) and UpdateTask under Agent (ADDLOCAL=ALL: installs downloaded updates hourly as SYSTEM). @@ -36,6 +39,7 @@ + @@ -135,6 +139,13 @@ + + + ADMIN_GROUPS + + + REPOSITORY_CONNECTION OR OLDVERSIONFOUND @@ -213,6 +224,7 @@ + diff --git a/installer/test-msi-wine.sh b/installer/test-msi-wine.sh index 1371d88..7d3835e 100755 --- a/installer/test-msi-wine.sh +++ b/installer/test-msi-wine.sh @@ -28,10 +28,11 @@ sed -i 's/"MaxParallelInstances": 4/"MaxParallelInstances": 7/' "$P/Agent/appset printf '[SQLInsight:Instances:1]\nName=manuel\nServer=sql02\n' >> "$P/Agent/sqlinsight.ini" files_before=$(find "$P" -type f | wc -l) -msi /i "$NEW" /qn +msi /i "$NEW" /qn ADMIN_GROUPS="FIRMA\SQL-Admins;FIRMA\DBA" check "opgradering" "[ $? = 0 ]" check "ændret appsettings.json er bevaret" "grep -q '\"MaxParallelInstances\": 7' '$P/Agent/appsettings.json'" check "ændret sqlinsight.ini er bevaret" "grep -q 'Server=sql02' '$P/Agent/sqlinsight.ini'" +check "ADMIN_GROUPS i webbens sqlinsight.ini" "grep -q 'SQL-Admins;FIRMA' '$P/Web/sqlinsight.ini'" entries=$(reg 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' /s | grep -c 'REG_SZ SQLInsight Pro') check "én post under Programmer ($entries)" "[ $entries = 1 ]" check "alle filer er der stadig" "[ \$(find '$P' -type f | wc -l) -ge $files_before ]" diff --git a/src/SQLInsight.Web/Api/ConfigurationApiEndpoints.cs b/src/SQLInsight.Web/Api/ConfigurationApiEndpoints.cs index fd7b9e4..159e7c0 100644 --- a/src/SQLInsight.Web/Api/ConfigurationApiEndpoints.cs +++ b/src/SQLInsight.Web/Api/ConfigurationApiEndpoints.cs @@ -31,8 +31,8 @@ public sealed record InstanceDiscoveryBody(string? Host); /// /// Which instances and databases are monitored, through the API: everything the page Servere og databaser does. -/// Reading needs the scope read; changing needs config:write for tokens, or membership of SQLInsight:Web:AdminGroups -/// for Windows users. Connection tests and discovery are queued for the agent (202 Accepted with a request id to +/// Reading needs the scope read; changing needs config:write for tokens, or membership of SQLInsight:Web:AdminGroups (the +/// web server's local Administrators while that is empty) for Windows users. Connection tests and discovery are queued for the agent (202 Accepted with a request id to /// follow at /api/v1/agent-requests/{id}). Passwords are write-only and never returned. /// public static class ConfigurationApiEndpoints @@ -41,7 +41,7 @@ public static class ConfigurationApiEndpoints { ArgumentNullException.ThrowIfNull(app); var api = app.MapGroup("/api/v1").DisableAntiforgery().WithTags("SQLInsight Pro API v1").RequireRead(); - const string write = " Needs scope config:write for tokens, or SQLInsight:Web:AdminGroups for Windows users."; + const string write = " Needs scope config:write for tokens, or for Windows users SQLInsight:Web:AdminGroups (empty: the web server's local Administrators)."; api.MapGet("/monitored-instances", ListAsync).WithSummary("Overvågede instanser / Monitored instances (configuration)"); api.MapPost("/monitored-instances", AddAsync) @@ -77,7 +77,7 @@ public static class ConfigurationApiEndpoints } private static IResult Forbidden() => Results.Problem(statusCode: StatusCodes.Status403Forbidden, title: "Ikke tilladt", - detail: "Ændringer kræver scope \"config:write\" (API-token) eller medlemskab af en gruppe i SQLInsight:Web:AdminGroups."); + detail: "Ændringer kræver scope \"config:write\" (API-token) eller medlemskab af en gruppe i SQLInsight:Web:AdminGroups (er den tom: den lokale gruppe Administratorer på webserveren)."); private static IResult Invalid(string detail) => Results.Problem(statusCode: StatusCodes.Status422UnprocessableEntity, title: "Ugyldig", detail: detail); diff --git a/src/SQLInsight.Web/Components/Pages/InstanceSetupPage.razor b/src/SQLInsight.Web/Components/Pages/InstanceSetupPage.razor index f9c8fc9..0d42629 100644 --- a/src/SQLInsight.Web/Components/Pages/InstanceSetupPage.razor +++ b/src/SQLInsight.Web/Components/Pages/InstanceSetupPage.razor @@ -24,13 +24,11 @@ } @if (!_mayChange) { -

- Du kan se opsætningen, men kun administratorer kan ændre den. - @if (Options.Value.AdminGroups.Count == 0) - { - Der er ikke sat nogen administratorgruppe op endnu: tilføj en Windows-gruppe under SQLInsight:Web:AdminGroups i SQLInsight Webs appsettings.json. - } -

+

Du kan se opsætningen, men kun administratorer kan ændre den. @InstanceSetup.WhoMayChangeText(Options.Value)

+} +else if (Options.Value.AdminGroupNames.Count == 0 && !string.Equals(Options.Value.Authentication, "None", StringComparison.OrdinalIgnoreCase)) +{ +

@InstanceSetup.WhoMayChangeText(Options.Value)

} @if (_agentKey is null) { diff --git a/src/SQLInsight.Web/InstanceSetup.cs b/src/SQLInsight.Web/InstanceSetup.cs index 9e5d143..9dbce72 100644 --- a/src/SQLInsight.Web/InstanceSetup.cs +++ b/src/SQLInsight.Web/InstanceSetup.cs @@ -27,7 +27,7 @@ public sealed record InstanceInput( public static class InstanceSetup { public const string NotAdminText = - "Kun administratorer kan ændre, hvilke servere og databaser der overvåges. Bed en administrator om at tilføje jeres gruppe under SQLInsight:Web:AdminGroups."; + "Kun administratorer kan ændre, hvilke servere og databaser der overvåges (SQLInsight:Web:AdminGroups, eller den lokale gruppe Administratorer på webserveren, når den er tom). Bed en administrator om at tilføje jeres gruppe."; public const string NoAgentKeyText = "Agenten har ikke meldt sig endnu, så adgangskoden kan ikke krypteres til den. Start tjenesten SQLInsight Agent (version 1.3 eller nyere), og prøv igen om lidt."; @@ -39,8 +39,14 @@ public static class InstanceSetup /// /// Whether the user may change the configuration: an API token needs the scope config:write; a Windows user must be - /// in one of SQLInsight:Web:AdminGroups. With authentication off (development only) everyone may. + /// in one of SQLInsight:Web:AdminGroups or, while that is empty, in the web server's local Administrators group. + /// With authentication off (development only) everyone may. /// + /// + /// Administrators is recognised by its well-known SID among the user's group SIDs (ClaimTypes.GroupSid). A remote + /// Windows (Negotiate) login carries the full token, so a member has the SID as a group; a UAC-filtered local token + /// has it only as deny-only (ClaimTypes.DenyOnlySid), which does not count. + /// public static bool MayChange(ClaimsPrincipal user, WebOptions options) { ArgumentNullException.ThrowIfNull(user); @@ -55,7 +61,21 @@ public static class InstanceSetup return true; } - return options.AdminGroups.Any(user.IsInRole); + var groups = options.AdminGroupNames; + return groups.Count > 0 + ? groups.Any(user.IsInRole) + : user.Identity?.IsAuthenticated == true && user.HasClaim(ClaimTypes.GroupSid, WebOptions.BuiltinAdministratorsSid); + } + + /// Who may change the configuration (Danish), for the page. + public static string WhoMayChangeText(WebOptions options) + { + ArgumentNullException.ThrowIfNull(options); + var groups = options.AdminGroupNames; + return groups.Count > 0 + ? "Opsætningen kan ændres af medlemmer af " + string.Join(", ", groups) + " (SQLInsight:Web:AdminGroups)." + : "SQLInsight:Web:AdminGroups er tom, så opsætningen kan ændres af den lokale gruppe Administratorer på webserveren (BUILTIN\\Administrators). " + + "Sæt en Windows-gruppe i AdminGroups i SQLInsight Webs appsettings.json (eller ADMIN_GROUPS i installeren) for at give andre adgang."; } private static string? Validate(InstanceInput input) diff --git a/src/SQLInsight.Web/WebOptions.cs b/src/SQLInsight.Web/WebOptions.cs index fb84a20..f782013 100644 --- a/src/SQLInsight.Web/WebOptions.cs +++ b/src/SQLInsight.Web/WebOptions.cs @@ -11,11 +11,20 @@ public sealed class WebOptions public IList AllowedGroups { get; } = []; /// - /// Windows groups that may change which servers and databases are monitored (Konfiguration og sikkerhed → Servere - /// og databaser), e.g. DOMAIN\SQL-Admins. Empty: nobody can change it in the UI; everyone with access can see it. + /// Windows groups that may change which servers and databases are monitored (Overvågning → Servere og databaser), + /// e.g. DOMAIN\SQL-Admins. An entry may hold several groups separated by ';' (the installer's ADMIN_GROUPS writes one + /// entry). Empty: the web server's local Administrators group (BUILTIN\Administrators) may change it. Everyone with + /// access to the UI can see it. /// public IList AdminGroups { get; } = []; + /// The well-known SID of BUILTIN\Administrators: who may change the configuration while AdminGroups is empty. + public const string BuiltinAdministratorsSid = "S-1-5-32-544"; + + /// The groups in , with entries like "A;B" split. + public IReadOnlyList AdminGroupNames => + AdminGroups.SelectMany(g => (g ?? string.Empty).Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)).ToList(); + /// Time zone for display, e.g. Europe/Copenhagen. Defaults to the server's local time zone. public string? TimeZone { get; set; } diff --git a/tests/SQLInsight.IntegrationTests/InstanceSetupWebTests.cs b/tests/SQLInsight.IntegrationTests/InstanceSetupWebTests.cs index 226f91a..0c3eaf7 100644 --- a/tests/SQLInsight.IntegrationTests/InstanceSetupWebTests.cs +++ b/tests/SQLInsight.IntegrationTests/InstanceSetupWebTests.cs @@ -273,5 +273,27 @@ public sealed partial class InstanceSetupWebTests(SqlServerFixture sql) : IAsync Assert.False(InstanceSetup.MayChange(User(new Claim(ApiTokenHandler.ClaimType, "grafana"), new Claim(ApiTokenHandler.ScopeClaimType, ApiToken.Read), new Claim(ClaimTypes.Role, @"FIRMA\SQL-Admins")), windows)); } + + [Fact] + public void Without_admin_groups_the_local_administrators_may_change_the_configuration() + { + static ClaimsPrincipal User(params Claim[] claims) => new(new ClaimsIdentity(claims, "Negotiate", ClaimTypes.Name, ClaimTypes.Role)); + var empty = new WebOptions { Authentication = "Windows" }; + var administrator = User(new Claim(ClaimTypes.GroupSid, WebOptions.BuiltinAdministratorsSid)); + + // A remote Windows login carries the full token: Administrators is a group SID. + Assert.True(InstanceSetup.MayChange(administrator, empty)); + // A UAC-filtered token has it only as deny-only, and that does not count. + Assert.False(InstanceSetup.MayChange(User(new Claim(ClaimTypes.DenyOnlySid, WebOptions.BuiltinAdministratorsSid)), empty)); + Assert.False(InstanceSetup.MayChange(User(new Claim(ClaimTypes.GroupSid, "S-1-5-32-545")), empty)); + Assert.False(InstanceSetup.MayChange(new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.GroupSid, WebOptions.BuiltinAdministratorsSid)])), empty)); + + // Once groups are set they decide alone; ADMIN_GROUPS from the installer may list several separated by ';'. + var groups = new WebOptions { Authentication = "Windows" }; + groups.AdminGroups.Add(@"FIRMA\SQL-Admins; FIRMA\DBA"); + Assert.False(InstanceSetup.MayChange(administrator, groups)); + Assert.True(InstanceSetup.MayChange(User(new Claim(ClaimTypes.Role, @"FIRMA\DBA")), groups)); + Assert.Equal([@"FIRMA\SQL-Admins", @"FIRMA\DBA"], groups.AdminGroupNames); + } }