Review 1.3: redact logins and user names in the configuration API
API v1 returns logins and people's names as [udeladt] unless ApiIncludesSensitiveEvidence is on, but /monitored-instances and /agent-requests returned the SQL login, createdBy/updatedBy/requestedBy/selectionChangedBy, the login the agent arrived as and 'Login failed for user ...' texts, and the GRANT script filled in with the login. They are now redacted the same way (the GRANT script falls back to the shipped template). ChooseAsync also answers 404 for an unknown instance instead of 422. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cade91aa4a
commit
356e102126
3 changed files with 125 additions and 45 deletions
|
|
@ -73,6 +73,8 @@ Webbrugerfladens login har stadig kun `db_datareader` og ellers præcis det, den
|
|||
|
||||
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`.
|
||||
|
||||
Som i resten af API v1 vises logins (SQL-login'et, det login agenten ankom som, og logins i fejltekster) og navnene på dem, der har ændret noget, som `[udeladt]`, medmindre `SQLInsight:Web:ApiIncludesSensitiveEvidence` er `true`; GRANT-scriptet er så det leverede script med pladsholderen i `@LoginName`. Adgangskoder returneres aldrig. Beder man om en forbindelsestest eller søgning, mens den samme stadig venter på agenten, får man den ventende anmodning igen.
|
||||
|
||||
| Endpoint | |
|
||||
|---|---|
|
||||
| `GET /api/v1/monitored-instances` | Overvågede servere |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SQLInsight.Core.Configuration;
|
||||
using SQLInsight.Repository;
|
||||
|
|
@ -31,23 +32,27 @@ public sealed record InstanceDiscoveryBody(string? Host);
|
|||
|
||||
/// <summary>
|
||||
/// 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 (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.
|
||||
/// Reading needs the scope read; changing needs config:write for tokens, or for Windows users membership of
|
||||
/// SQLInsight:Web:AdminGroups (the web server's local Administrators while that is empty). 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. Logins (the SQL login, the login the agent arrived as) and the names of
|
||||
/// the people who made changes are customer data and returned as "[udeladt]" unless
|
||||
/// SQLInsight:Web:ApiIncludesSensitiveEvidence is on, as in the rest of API v1.
|
||||
/// </summary>
|
||||
public static class ConfigurationApiEndpoints
|
||||
public static partial class ConfigurationApiEndpoints
|
||||
{
|
||||
public static void Map(IEndpointRouteBuilder app)
|
||||
{
|
||||
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 for Windows users SQLInsight:Web:AdminGroups (empty: the web server's local Administrators).";
|
||||
const string redacted = " Logins and user names are \"[udeladt]\" unless SQLInsight:Web:ApiIncludesSensitiveEvidence is on.";
|
||||
|
||||
api.MapGet("/monitored-instances", ListAsync).WithSummary("Overvågede instanser / Monitored instances (configuration)");
|
||||
api.MapGet("/monitored-instances", ListAsync).WithSummary("Overvågede instanser / Monitored instances (configuration)").WithDescription(redacted.Trim());
|
||||
api.MapPost("/monitored-instances", AddAsync)
|
||||
.WithSummary("Tilføj en instans / Add an instance")
|
||||
.WithDescription("201 Created. The agent tests the connection and lists the databases at once (connectionTestRequestId)." + write);
|
||||
api.MapGet("/monitored-instances/{id:int}", GetAsync).WithSummary("Én overvåget instans / One monitored instance");
|
||||
api.MapGet("/monitored-instances/{id:int}", GetAsync).WithSummary("Én overvåget instans / One monitored instance").WithDescription(redacted.Trim());
|
||||
api.MapPatch("/monitored-instances/{id:int}", PatchAsync)
|
||||
.WithSummary("Sæt på pause, genoptag, overvåg nye databaser / Pause, resume, monitor new databases").WithDescription(write.Trim());
|
||||
api.MapPut("/monitored-instances/{id:int}/connection", UpdateConnectionAsync)
|
||||
|
|
@ -58,30 +63,50 @@ public static class ConfigurationApiEndpoints
|
|||
.WithDescription("204. Collected data is kept. 409 for an instance from the agent's configuration file (pause it instead)." + write);
|
||||
api.MapPost("/monitored-instances/{id:int}/connection-test", (int id, HttpContext http, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct) =>
|
||||
QueueAsync(id, AgentRequestKind.ConnectionTest, http, repository, options, ct))
|
||||
.WithSummary("Test forbindelsen fra agenten / Test the connection from the agent").WithDescription("202 Accepted with requestId." + write);
|
||||
.WithSummary("Test forbindelsen fra agenten / Test the connection from the agent")
|
||||
.WithDescription("202 Accepted with requestId. A test already waiting for the agent is returned instead of a new one." + write);
|
||||
api.MapGet("/monitored-instances/{id:int}/connection-test", LatestTestAsync)
|
||||
.WithSummary("Seneste forbindelsestest med manglende rettigheder / Latest connection test and missing permissions");
|
||||
.WithSummary("Seneste forbindelsestest med manglende rettigheder / Latest connection test and missing permissions").WithDescription(redacted.Trim());
|
||||
api.MapPost("/monitored-instances/{id:int}/discovery", (int id, HttpContext http, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct) =>
|
||||
QueueAsync(id, AgentRequestKind.DatabaseDiscovery, http, repository, options, ct))
|
||||
.WithSummary("Søg efter databaser / Discover databases").WithDescription("202 Accepted with requestId; the result is in GET …/databases." + write);
|
||||
api.MapGet("/monitored-instances/{id:int}/databases", DatabasesAsync).WithSummary("Fundne databaser og valg / Discovered databases and choices");
|
||||
.WithSummary("Søg efter databaser / Discover databases")
|
||||
.WithDescription("202 Accepted with requestId; the result is in GET …/databases. A discovery already waiting is returned instead of a new one." + write);
|
||||
api.MapGet("/monitored-instances/{id:int}/databases", DatabasesAsync).WithSummary("Fundne databaser og valg / Discovered databases and choices").WithDescription(redacted.Trim());
|
||||
api.MapPut("/monitored-instances/{id:int}/databases", ChooseAsync)
|
||||
.WithSummary("Vælg databaser og AX-fund til hubben / Choose databases and AX findings to the hub")
|
||||
.WithDescription("Databases not named keep their choice. 422 with the names that were not found." + write);
|
||||
api.MapGet("/monitored-instances/{id:int}/grant-script", GrantScriptAsync)
|
||||
.WithSummary("Least-privilege GRANT-script til agentens login / Least-privilege GRANT script for the agent's login");
|
||||
.WithSummary("Least-privilege GRANT-script til agentens login / Least-privilege GRANT script for the agent's login")
|
||||
.WithDescription("With the login filled in only when SQLInsight:Web:ApiIncludesSensitiveEvidence is on; otherwise the shipped script with its placeholder.");
|
||||
api.MapPost("/instance-discovery", FindAsync)
|
||||
.WithSummary("Find SQL Server-instanser på en computer / Find SQL Server instances on a computer")
|
||||
.WithDescription("202 Accepted with requestId. The agent asks SQL Server Browser (UDP 1434) and, on its own computer, the registry." + write);
|
||||
api.MapGet("/agent-requests/{id:long}", RequestAsync).WithSummary("Status og resultat af en anmodning til agenten / Status and result of an agent request");
|
||||
api.MapGet("/agent-requests/{id:long}", RequestAsync)
|
||||
.WithSummary("Status og resultat af en anmodning til agenten / Status and result of an agent request").WithDescription(redacted.Trim());
|
||||
}
|
||||
|
||||
/// <summary>Logins and people's names, "[udeladt]" unless SQLInsight:Web:ApiIncludesSensitiveEvidence is on.</summary>
|
||||
internal sealed class Sensitive(bool include)
|
||||
{
|
||||
public static Sensitive For(IOptions<WebOptions> options) => new(options.Value.ApiIncludesSensitiveEvidence);
|
||||
|
||||
public bool Include => include;
|
||||
|
||||
public string? Value(string? value) => include || value is null ? value : ApiV1Endpoints.Redacted;
|
||||
|
||||
/// <summary>An error text from SQL Server or the agent, with the login in it (Login failed for user '…') left out.</summary>
|
||||
public string? Text(string? value) => include || value is null ? value : LoginInText().Replace(value, m => m.Groups[1].Value + "'" + ApiV1Endpoints.Redacted + "'");
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(\b(?:user|login|bruger|login'et)\s+)'[^'\r\n]*'", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex LoginInText();
|
||||
|
||||
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 (er den tom: den lokale gruppe Administratorer på webserveren).");
|
||||
|
||||
private static IResult Invalid(string detail) => Results.Problem(statusCode: StatusCodes.Status422UnprocessableEntity, title: "Ugyldig", detail: detail);
|
||||
|
||||
private static object Json(ConfiguredInstanceRow i) => new
|
||||
private static object Json(ConfiguredInstanceRow i, Sensitive s) => new
|
||||
{
|
||||
Id = i.ConfiguredInstanceId,
|
||||
i.Name,
|
||||
|
|
@ -89,7 +114,7 @@ public static class ConfigurationApiEndpoints
|
|||
i.Port,
|
||||
i.DataSource,
|
||||
i.Authentication,
|
||||
i.UserName,
|
||||
UserName = s.Value(i.UserName),
|
||||
i.HasPassword,
|
||||
i.Encryption,
|
||||
i.TrustServerCertificate,
|
||||
|
|
@ -97,15 +122,15 @@ public static class ConfigurationApiEndpoints
|
|||
i.Paused,
|
||||
i.MonitorNewDatabases,
|
||||
i.CreatedAt,
|
||||
i.CreatedBy,
|
||||
CreatedBy = s.Value(i.CreatedBy),
|
||||
i.UpdatedAt,
|
||||
i.UpdatedBy,
|
||||
UpdatedBy = s.Value(i.UpdatedBy),
|
||||
i.LastDiscoveryAt,
|
||||
i.LastDiscoveryError,
|
||||
i.CredentialError,
|
||||
LastDiscoveryError = s.Text(i.LastDiscoveryError),
|
||||
CredentialError = s.Text(i.CredentialError),
|
||||
};
|
||||
|
||||
private static object Json(ConfiguredDatabaseRow d) => new
|
||||
private static object Json(ConfiguredDatabaseRow d, Sensitive s) => new
|
||||
{
|
||||
Name = d.DatabaseName,
|
||||
d.Monitored,
|
||||
|
|
@ -124,31 +149,51 @@ public static class ConfigurationApiEndpoints
|
|||
d.FirstSeenAt,
|
||||
d.LastSeenAt,
|
||||
d.SelectionChangedAt,
|
||||
d.SelectionChangedBy,
|
||||
SelectionChangedBy = s.Value(d.SelectionChangedBy),
|
||||
};
|
||||
|
||||
private static object Json(AgentRequestRow r) => new
|
||||
private static object Json(AgentRequestRow r, Sensitive s) => new
|
||||
{
|
||||
Id = r.RequestId,
|
||||
r.Kind,
|
||||
InstanceId = r.ConfiguredInstanceId,
|
||||
r.Status,
|
||||
r.RequestedBy,
|
||||
RequestedBy = s.Value(r.RequestedBy),
|
||||
r.RequestedAt,
|
||||
r.StartedAt,
|
||||
r.CompletedAt,
|
||||
r.Error,
|
||||
Result = r.Result is { Length: > 0 } result ? JsonDocument.Parse(result).RootElement.Clone() : (JsonElement?)null,
|
||||
Error = s.Text(r.Error),
|
||||
Result = ResultJson(r, s),
|
||||
};
|
||||
|
||||
/// <summary>The agent's result; a connection test's login and error text are redacted like the rest.</summary>
|
||||
private static JsonElement? ResultJson(AgentRequestRow r, Sensitive s)
|
||||
{
|
||||
if (r.Result is not { Length: > 0 } result)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!s.Include && r.Kind == AgentRequestKind.ConnectionTest && AgentRequestJson.Deserialize<ConnectionTestResult>(result) is { } test)
|
||||
{
|
||||
result = AgentRequestJson.Serialize(test with { Login = s.Value(test.Login), Error = s.Text(test.Error) });
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(result);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
private static InstanceInput Input(MonitoredInstanceBody body) => new(body.Name, body.Server, body.Port, body.Authentication, body.UserName, body.Password,
|
||||
body.Encryption, body.TrustServerCertificate ?? false, body.MonitorNewDatabases ?? false);
|
||||
|
||||
private static async Task<IResult> ListAsync(InsightRepository repository, CancellationToken ct) =>
|
||||
Results.Ok((await repository.GetConfiguredInstancesAsync(ct).ConfigureAwait(false)).Select(Json));
|
||||
private static async Task<IResult> ListAsync(InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
{
|
||||
var s = Sensitive.For(options);
|
||||
return Results.Ok((await repository.GetConfiguredInstancesAsync(ct).ConfigureAwait(false)).Select(i => Json(i, s)));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetAsync(int id, InsightRepository repository, CancellationToken ct) =>
|
||||
await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false) is { } row ? Results.Ok(Json(row)) : Results.NotFound();
|
||||
private static async Task<IResult> GetAsync(int id, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct) =>
|
||||
await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false) is { } row ? Results.Ok(Json(row, Sensitive.For(options))) : Results.NotFound();
|
||||
|
||||
private static async Task<IResult> AddAsync(MonitoredInstanceBody body, HttpContext http, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
{
|
||||
|
|
@ -164,7 +209,7 @@ public static class ConfigurationApiEndpoints
|
|||
}
|
||||
|
||||
var row = (await repository.GetConfiguredInstanceAsync(outcome.Id!.Value, ct).ConfigureAwait(false))!;
|
||||
return Results.Created($"/api/v1/monitored-instances/{row.ConfiguredInstanceId}", new { Instance = Json(row), ConnectionTestRequestId = outcome.RequestId });
|
||||
return Results.Created($"/api/v1/monitored-instances/{row.ConfiguredInstanceId}", new { Instance = Json(row, Sensitive.For(options)), ConnectionTestRequestId = outcome.RequestId });
|
||||
}
|
||||
|
||||
private static async Task<IResult> PatchAsync(int id, MonitoredInstancePatch body, HttpContext http, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
|
|
@ -190,7 +235,7 @@ public static class ConfigurationApiEndpoints
|
|||
await repository.SetMonitorNewDatabasesAsync(id, monitorNew, by, DateTimeOffset.UtcNow, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Results.Ok(Json((await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false))!));
|
||||
return Results.Ok(Json((await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false))!, Sensitive.For(options)));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateConnectionAsync(int id, MonitoredInstanceBody body, HttpContext http, InsightRepository repository, IOptions<WebOptions> options,
|
||||
|
|
@ -214,7 +259,11 @@ public static class ConfigurationApiEndpoints
|
|||
: Invalid(error);
|
||||
}
|
||||
|
||||
return Results.Ok(new { Instance = Json((await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false))!), ConnectionTestRequestId = outcome.RequestId });
|
||||
return Results.Ok(new
|
||||
{
|
||||
Instance = Json((await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false))!, Sensitive.For(options)),
|
||||
ConnectionTestRequestId = outcome.RequestId,
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> RemoveAsync(int id, HttpContext http, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
|
|
@ -247,10 +296,10 @@ public static class ConfigurationApiEndpoints
|
|||
}
|
||||
|
||||
var request = (await repository.GetAgentRequestAsync(outcome.RequestId!.Value, ct).ConfigureAwait(false))!;
|
||||
return Results.Accepted($"/api/v1/agent-requests/{request.RequestId}", Json(request));
|
||||
return Results.Accepted($"/api/v1/agent-requests/{request.RequestId}", Json(request, Sensitive.For(options)));
|
||||
}
|
||||
|
||||
private static async Task<IResult> LatestTestAsync(int id, InsightRepository repository, CancellationToken ct)
|
||||
private static async Task<IResult> LatestTestAsync(int id, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
{
|
||||
if (await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false) is null)
|
||||
{
|
||||
|
|
@ -264,17 +313,18 @@ public static class ConfigurationApiEndpoints
|
|||
}
|
||||
|
||||
var result = AgentRequestJson.Deserialize<ConnectionTestResult>(test.Result);
|
||||
return Results.Ok(new { Request = Json(test), MissingPermissions = result?.MissingRequired.Select(p => p.Name).ToList() ?? [] });
|
||||
return Results.Ok(new { Request = Json(test, Sensitive.For(options)), MissingPermissions = result?.MissingRequired.Select(p => p.Name).ToList() ?? [] });
|
||||
}
|
||||
|
||||
private static async Task<IResult> DatabasesAsync(int id, InsightRepository repository, CancellationToken ct)
|
||||
private static async Task<IResult> DatabasesAsync(int id, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
{
|
||||
if (await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false) is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok((await repository.GetConfiguredDatabasesAsync(id, ct).ConfigureAwait(false)).Select(Json));
|
||||
var s = Sensitive.For(options);
|
||||
return Results.Ok((await repository.GetConfiguredDatabasesAsync(id, ct).ConfigureAwait(false)).Select(d => Json(d, s)));
|
||||
}
|
||||
|
||||
private static async Task<IResult> ChooseAsync(int id, DatabaseChoicesBody body, HttpContext http, InsightRepository repository, IOptions<WebOptions> options,
|
||||
|
|
@ -285,6 +335,11 @@ public static class ConfigurationApiEndpoints
|
|||
return Forbidden();
|
||||
}
|
||||
|
||||
if (await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false) is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var known = (await repository.GetConfiguredDatabasesAsync(id, ct).ConfigureAwait(false)).ToDictionary(d => d.DatabaseName, StringComparer.OrdinalIgnoreCase);
|
||||
var requested = body.Databases ?? [];
|
||||
if (requested.Any(d => string.IsNullOrWhiteSpace(d.Name)))
|
||||
|
|
@ -292,20 +347,23 @@ public static class ConfigurationApiEndpoints
|
|||
return Invalid("Hver database skal have et navn.");
|
||||
}
|
||||
|
||||
var unknown = requested.Where(d => !known.ContainsKey(d.Name!)).Select(d => d.Name!).ToList();
|
||||
var unknown = requested.Where(d => !known.ContainsKey(d.Name!)).Select(d => d.Name!).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
if (unknown.Count > 0)
|
||||
{
|
||||
return Invalid("Agenten har ikke fundet disse databaser: " + string.Join(", ", unknown) + ". Start en søgning (POST …/discovery), og prøv igen.");
|
||||
return Invalid("Agenten har ikke fundet disse databaser: " + string.Join(", ", unknown.Take(20)) + (unknown.Count > 20 ? $" og {unknown.Count - 20} flere" : string.Empty)
|
||||
+ ". Start en søgning (POST …/discovery), og prøv igen.");
|
||||
}
|
||||
|
||||
var choices = requested.Select(d => (Body: d, Current: known[d.Name!]))
|
||||
.Select(x => new DatabaseChoice(x.Current.DatabaseName, x.Body.Monitored ?? x.Current.Monitored, x.Body.SendDynamicsAxToHub ?? x.Current.SendDynamicsAxToHub))
|
||||
.DistinctBy(c => c.DatabaseName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var (_, found) = await InstanceSetup.ChooseDatabasesAsync(repository, id, choices, body.MonitorNewDatabases, http.User.Identity?.Name, ct).ConfigureAwait(false);
|
||||
return found ? Results.Ok((await repository.GetConfiguredDatabasesAsync(id, ct).ConfigureAwait(false)).Select(Json)) : Results.NotFound();
|
||||
var s = Sensitive.For(options);
|
||||
return found ? Results.Ok((await repository.GetConfiguredDatabasesAsync(id, ct).ConfigureAwait(false)).Select(d => Json(d, s))) : Results.NotFound();
|
||||
}
|
||||
|
||||
private static async Task<IResult> GrantScriptAsync(int id, InsightRepository repository, CancellationToken ct)
|
||||
private static async Task<IResult> GrantScriptAsync(int id, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct)
|
||||
{
|
||||
if (await repository.GetConfiguredInstanceAsync(id, ct).ConfigureAwait(false) is not { } row)
|
||||
{
|
||||
|
|
@ -313,6 +371,17 @@ public static class ConfigurationApiEndpoints
|
|||
}
|
||||
|
||||
var (login, script, note) = await InstanceSetup.GrantScriptAsync(repository, row, ct).ConfigureAwait(false);
|
||||
if (!Sensitive.For(options).Include && login is not null)
|
||||
{
|
||||
// The script names the login; without sensitive data it is the shipped script with its placeholder.
|
||||
return Results.Ok(new
|
||||
{
|
||||
Login = ApiV1Endpoints.Redacted,
|
||||
Script = GrantScript.Template(),
|
||||
Note = "Login'et er udeladt (SQLInsight:Web:ApiIncludesSensitiveEvidence er slået fra). Ret @LoginName i scriptet, eller se det udfyldte script under Servere og databaser.",
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new { Login = login, Script = script, Note = note });
|
||||
}
|
||||
|
||||
|
|
@ -330,9 +399,9 @@ public static class ConfigurationApiEndpoints
|
|||
}
|
||||
|
||||
var request = (await repository.GetAgentRequestAsync(outcome.RequestId!.Value, ct).ConfigureAwait(false))!;
|
||||
return Results.Accepted($"/api/v1/agent-requests/{request.RequestId}", Json(request));
|
||||
return Results.Accepted($"/api/v1/agent-requests/{request.RequestId}", Json(request, Sensitive.For(options)));
|
||||
}
|
||||
|
||||
private static async Task<IResult> RequestAsync(long id, InsightRepository repository, CancellationToken ct) =>
|
||||
await repository.GetAgentRequestAsync(id, ct).ConfigureAwait(false) is { } request ? Results.Ok(Json(request)) : Results.NotFound();
|
||||
private static async Task<IResult> RequestAsync(long id, InsightRepository repository, IOptions<WebOptions> options, CancellationToken ct) =>
|
||||
await repository.GetAgentRequestAsync(id, ct).ConfigureAwait(false) is { } request ? Results.Ok(Json(request, Sensitive.For(options))) : Results.NotFound();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,9 +113,18 @@ public sealed class InstanceSetupApiTests(SqlServerFixture sql) : IAsyncLifetime
|
|||
Assert.True((await _repository.GetConfiguredInstanceAsync(id))!.MonitorNewDatabases);
|
||||
Assert.Equal(2, (await JsonAsync(await read.GetAsync($"/api/v1/monitored-instances/{id}/databases"))).GetArrayLength());
|
||||
|
||||
// Logins and people's names are redacted as in the rest of API v1 (ApiIncludesSensitiveEvidence is off).
|
||||
var grant = await JsonAsync(await read.GetAsync($"/api/v1/monitored-instances/{id}/grant-script"));
|
||||
Assert.Equal("mon", grant.GetProperty("login").GetString());
|
||||
Assert.Contains("SET @LoginName = N'mon';", grant.GetProperty("script").GetString(), StringComparison.Ordinal);
|
||||
Assert.Equal(ApiV1Endpoints.Redacted, grant.GetProperty("login").GetString());
|
||||
Assert.DoesNotContain("N'mon'", grant.GetProperty("script").GetString(), StringComparison.Ordinal);
|
||||
Assert.Contains("SET @LoginName = N'", grant.GetProperty("script").GetString(), StringComparison.Ordinal);
|
||||
var shown = await JsonAsync(await read.GetAsync($"/api/v1/monitored-instances/{id}"));
|
||||
Assert.Equal(ApiV1Endpoints.Redacted, shown.GetProperty("userName").GetString());
|
||||
|
||||
// Asking again while the agent has not taken the test yet returns the waiting one; the queue does not grow.
|
||||
var again = await JsonAsync(await write.PostAsync($"/api/v1/monitored-instances/{id}/connection-test", Body(new { })));
|
||||
var once = await JsonAsync(await write.PostAsync($"/api/v1/monitored-instances/{id}/connection-test", Body(new { })));
|
||||
Assert.Equal(again.GetProperty("id").GetInt64(), once.GetProperty("id").GetInt64());
|
||||
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await write.PostAsync("/api/v1/instance-discovery", Body(new { host = @"sql01\PROD" }))).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.Accepted, (await write.PostAsync("/api/v1/instance-discovery", Body(new { host = "sql01.firma.local" }))).StatusCode);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue