Skip to content

Commit cb2ab21

Browse files
committed
3.6.1 patch
1 parent 757ca17 commit cb2ab21

6 files changed

Lines changed: 115 additions & 45 deletions

File tree

NpgsqlRest/NpgsqlRest.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
<GenerateDocumentationFile>true</GenerateDocumentationFile>
3030
<PackageReadmeFile>README.MD</PackageReadmeFile>
3131
<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
32-
<Version>3.6.0</Version>
33-
<AssemblyVersion>3.6.0</AssemblyVersion>
34-
<FileVersion>3.6.0</FileVersion>
35-
<PackageVersion>3.6.0</PackageVersion>
32+
<Version>3.6.1</Version>
33+
<AssemblyVersion>3.6.1</AssemblyVersion>
34+
<FileVersion>3.6.1</FileVersion>
35+
<PackageVersion>3.6.1</PackageVersion>
3636
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
3737
</PropertyGroup>
3838

NpgsqlRestClient/NpgsqlRestClient.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<PublishAot>true</PublishAot>
1111
<PublishTrimmed>true</PublishTrimmed>
1212
<TrimMode>full</TrimMode>
13-
<Version>3.6.0</Version>
13+
<Version>3.6.1</Version>
1414
</PropertyGroup>
1515

1616
<ItemGroup>

NpgsqlRestClient/Program.cs

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@
353353
var path = config.GetConfigStr("Path", healthCfg) ?? "/health";
354354
var readyPath = config.GetConfigStr("ReadyPath", healthCfg) ?? "/health/ready";
355355
var livePath = config.GetConfigStr("LivePath", healthCfg) ?? "/health/live";
356-
var requireAuthorization = config.GetConfigBool("RequireAuthorization", healthCfg);
356+
var healthRequireAuthorization = config.GetConfigBool("RequireAuthorization", healthCfg);
357357
var rateLimiterPolicy = config.GetConfigStr("RateLimiterPolicy", healthCfg);
358358

359359
var healthEndpoint = app.MapHealthChecks(path);
@@ -366,11 +366,20 @@
366366
Predicate = _ => false // Always healthy if app is running
367367
});
368368

369-
if (requireAuthorization)
369+
if (healthRequireAuthorization)
370370
{
371-
healthEndpoint.RequireAuthorization();
372-
readyEndpoint.RequireAuthorization();
373-
liveEndpoint.RequireAuthorization();
371+
healthEndpoint.AddEndpointFilter(AuthorizationFilter);
372+
readyEndpoint.AddEndpointFilter(AuthorizationFilter);
373+
liveEndpoint.AddEndpointFilter(AuthorizationFilter);
374+
375+
static async ValueTask<object?> AuthorizationFilter(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
376+
{
377+
if (context.HttpContext.User?.Identity?.IsAuthenticated is false)
378+
{
379+
return Results.Problem(statusCode: 401, title: "Unauthorized");
380+
}
381+
return await next(context);
382+
}
374383
}
375384

376385
if (rateLimiterPolicy is not null)
@@ -396,10 +405,11 @@
396405
var tablesPath = config.GetConfigStr("TablesStatsPath", statsCfg) ?? "/stats/tables";
397406
var indexesPath = config.GetConfigStr("IndexesStatsPath", statsCfg) ?? "/stats/indexes";
398407
var activityPath = config.GetConfigStr("ActivityPath", statsCfg) ?? "/stats/activity";
399-
var requireAuthorization = config.GetConfigBool("RequireAuthorization", statsCfg);
400-
var authorizedRoles = config.GetConfigEnumerable("AuthorizedRoles", statsCfg)?.ToArray();
408+
var statsRequireAuthorization = config.GetConfigBool("RequireAuthorization", statsCfg);
409+
var statsAuthorizedRoles = config.GetConfigEnumerable("AuthorizedRoles", statsCfg)?.ToArray();
410+
var statsRoleClaimType = authenticationOptions.DefaultRoleClaimType;
401411
var rateLimiterPolicy = config.GetConfigStr("RateLimiterPolicy", statsCfg);
402-
var outputFormat = config.GetConfigStr("OutputFormat", statsCfg) ?? "json";
412+
var outputFormat = config.GetConfigStr("OutputFormat", statsCfg) ?? "html";
403413
var schemaSimilarTo = config.GetConfigStr("SchemaSimilarTo", statsCfg);
404414

405415
// Resolve connection string for stats endpoints
@@ -411,34 +421,16 @@
411421
}
412422

413423
var routinesEndpoint = app.MapGet(routinesPath, async (HttpContext context) =>
414-
await StatsEndpoints.HandleRoutinesStats(context, statsConnectionString!, outputFormat, schemaSimilarTo, builder.ClientLogger));
424+
await StatsEndpoints.HandleRoutinesStats(context, statsConnectionString!, outputFormat, schemaSimilarTo, statsRequireAuthorization, statsAuthorizedRoles, statsRoleClaimType, builder.ClientLogger));
415425

416426
var tablesEndpoint = app.MapGet(tablesPath, async (HttpContext context) =>
417-
await StatsEndpoints.HandleTablesStats(context, statsConnectionString!, outputFormat, schemaSimilarTo, builder.ClientLogger));
427+
await StatsEndpoints.HandleTablesStats(context, statsConnectionString!, outputFormat, schemaSimilarTo, statsRequireAuthorization, statsAuthorizedRoles, statsRoleClaimType, builder.ClientLogger));
418428

419429
var indexesEndpoint = app.MapGet(indexesPath, async (HttpContext context) =>
420-
await StatsEndpoints.HandleIndexesStats(context, statsConnectionString!, outputFormat, schemaSimilarTo, builder.ClientLogger));
430+
await StatsEndpoints.HandleIndexesStats(context, statsConnectionString!, outputFormat, schemaSimilarTo, statsRequireAuthorization, statsAuthorizedRoles, statsRoleClaimType, builder.ClientLogger));
421431

422432
var activityEndpoint = app.MapGet(activityPath, async (HttpContext context) =>
423-
await StatsEndpoints.HandleActivityStats(context, statsConnectionString!, outputFormat, builder.ClientLogger));
424-
425-
if (requireAuthorization)
426-
{
427-
if (authorizedRoles is null || authorizedRoles.Length == 0)
428-
{
429-
routinesEndpoint.RequireAuthorization();
430-
tablesEndpoint.RequireAuthorization();
431-
indexesEndpoint.RequireAuthorization();
432-
activityEndpoint.RequireAuthorization();
433-
}
434-
else
435-
{
436-
routinesEndpoint.RequireAuthorization(policy => policy.RequireRole(authorizedRoles));
437-
tablesEndpoint.RequireAuthorization(policy => policy.RequireRole(authorizedRoles));
438-
indexesEndpoint.RequireAuthorization(policy => policy.RequireRole(authorizedRoles));
439-
activityEndpoint.RequireAuthorization(policy => policy.RequireRole(authorizedRoles));
440-
}
441-
}
433+
await StatsEndpoints.HandleActivityStats(context, statsConnectionString!, outputFormat, statsRequireAuthorization, statsAuthorizedRoles, statsRoleClaimType, builder.ClientLogger));
442434

443435
if (rateLimiterPolicy is not null)
444436
{

NpgsqlRestClient/StatsEndpoints.cs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Net;
12
using System.Text;
23
using System.Text.Json;
34
using Microsoft.AspNetCore.Http;
@@ -92,7 +93,11 @@ from pg_stat_user_indexes s
9293
ltrim(justify_interval(now() - state_change)::text, '@ ') as state_duration,
9394
ltrim(justify_interval(now() - backend_start)::text, '@ ') as connection_age,
9495
ltrim(justify_interval(now() - xact_start)::text, '@ ') as transaction_duration,
95-
ltrim(justify_interval(now() - query_start)::text, '@ ') as query_duration,
96+
case
97+
when state = 'active' then ltrim(justify_interval(now() - query_start)::text, '@ ')
98+
when state = 'idle' then 'finished ' || ltrim(justify_interval(now() - state_change)::text, '@ ') || ' ago'
99+
else state
100+
end as query_duration,
96101
left(query, 100) as query_preview,
97102
query
98103
from pg_stat_activity
@@ -103,35 +108,96 @@ order by state_duration desc nulls last
103108
/// <summary>
104109
/// Returns statistics for user-defined functions and procedures.
105110
/// </summary>
106-
public static async Task HandleRoutinesStats(HttpContext context, string connectionString, string outputFormat, string? schemaSimilarTo, ILogger? logger)
111+
public static async Task HandleRoutinesStats(HttpContext context, string connectionString, string outputFormat, string? schemaSimilarTo, bool requireAuthorization, string[]? authorizedRoles, string? roleClaimType, ILogger? logger)
107112
{
113+
if (await CheckAuthorization(context, requireAuthorization, authorizedRoles, roleClaimType) is false)
114+
{
115+
return;
116+
}
108117
await ExecuteQueryAndRespond(context, connectionString, RoutinesQuery, outputFormat, schemaSimilarTo, logger, "Stats.Routines", setupCommands: ["set local intervalstyle = 'postgres_verbose'"]);
109118
}
110119

111120
/// <summary>
112121
/// Returns statistics for user tables including size, tuple counts, and vacuum info.
113122
/// </summary>
114-
public static async Task HandleTablesStats(HttpContext context, string connectionString, string outputFormat, string? schemaSimilarTo, ILogger? logger)
123+
public static async Task HandleTablesStats(HttpContext context, string connectionString, string outputFormat, string? schemaSimilarTo, bool requireAuthorization, string[]? authorizedRoles, string? roleClaimType, ILogger? logger)
115124
{
125+
if (await CheckAuthorization(context, requireAuthorization, authorizedRoles, roleClaimType) is false)
126+
{
127+
return;
128+
}
116129
await ExecuteQueryAndRespond(context, connectionString, TablesQuery, outputFormat, schemaSimilarTo, logger, "Stats.Tables");
117130
}
118131

119132
/// <summary>
120133
/// Returns statistics for user indexes including scan counts and definitions.
121134
/// </summary>
122-
public static async Task HandleIndexesStats(HttpContext context, string connectionString, string outputFormat, string? schemaSimilarTo, ILogger? logger)
135+
public static async Task HandleIndexesStats(HttpContext context, string connectionString, string outputFormat, string? schemaSimilarTo, bool requireAuthorization, string[]? authorizedRoles, string? roleClaimType, ILogger? logger)
123136
{
137+
if (await CheckAuthorization(context, requireAuthorization, authorizedRoles, roleClaimType) is false)
138+
{
139+
return;
140+
}
124141
await ExecuteQueryAndRespond(context, connectionString, IndexesQuery, outputFormat, schemaSimilarTo, logger, "Stats.Indexes");
125142
}
126143

127144
/// <summary>
128145
/// Returns current database activity from pg_stat_activity.
129146
/// </summary>
130-
public static async Task HandleActivityStats(HttpContext context, string connectionString, string outputFormat, ILogger? logger)
147+
public static async Task HandleActivityStats(HttpContext context, string connectionString, string outputFormat, bool requireAuthorization, string[]? authorizedRoles, string? roleClaimType, ILogger? logger)
131148
{
149+
if (await CheckAuthorization(context, requireAuthorization, authorizedRoles, roleClaimType) is false)
150+
{
151+
return;
152+
}
132153
await ExecuteQueryAndRespond(context, connectionString, ActivityQuery, outputFormat, null, logger, "Stats.Activity", setupCommands: ["set local intervalstyle = 'postgres_verbose'"]);
133154
}
134155

156+
private static async Task<bool> CheckAuthorization(HttpContext context, bool requireAuthorization, string[]? authorizedRoles, string? roleClaimType)
157+
{
158+
if (requireAuthorization is false && authorizedRoles is null)
159+
{
160+
return true;
161+
}
162+
163+
if (context.User?.Identity?.IsAuthenticated is false)
164+
{
165+
await Results.Problem(
166+
type: null,
167+
statusCode: (int)HttpStatusCode.Unauthorized,
168+
title: "Unauthorized",
169+
detail: null).ExecuteAsync(context);
170+
return false;
171+
}
172+
173+
if (authorizedRoles is not null)
174+
{
175+
bool ok = false;
176+
foreach (var claim in context.User?.Claims ?? [])
177+
{
178+
if (string.Equals(claim.Type, roleClaimType, StringComparison.Ordinal))
179+
{
180+
if (authorizedRoles.Contains(claim.Value) is true)
181+
{
182+
ok = true;
183+
break;
184+
}
185+
}
186+
}
187+
if (ok is false)
188+
{
189+
await Results.Problem(
190+
type: null,
191+
statusCode: (int)HttpStatusCode.Forbidden,
192+
title: "Forbidden",
193+
detail: null).ExecuteAsync(context);
194+
return false;
195+
}
196+
}
197+
198+
return true;
199+
}
200+
135201
private static async Task ExecuteQueryAndRespond(
136202
HttpContext context,
137203
string connectionString,

NpgsqlRestTests/Setup/StatsTestFixture.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,20 @@ public StatsTestFixture()
4747

4848
// Map stats endpoints manually (simulating what Program.cs does)
4949
_app.MapGet(RoutinesPath, async (Microsoft.AspNetCore.Http.HttpContext context) =>
50-
await StatsEndpoints.HandleRoutinesStats(context, _connectionString, "json", null, null));
50+
await StatsEndpoints.HandleRoutinesStats(context, _connectionString, "json", null, false, null, null, null));
5151

5252
_app.MapGet(TablesPath, async (Microsoft.AspNetCore.Http.HttpContext context) =>
53-
await StatsEndpoints.HandleTablesStats(context, _connectionString, "json", null, null));
53+
await StatsEndpoints.HandleTablesStats(context, _connectionString, "json", null, false, null, null, null));
5454

5555
_app.MapGet(IndexesPath, async (Microsoft.AspNetCore.Http.HttpContext context) =>
56-
await StatsEndpoints.HandleIndexesStats(context, _connectionString, "json", null, null));
56+
await StatsEndpoints.HandleIndexesStats(context, _connectionString, "json", null, false, null, null, null));
5757

5858
_app.MapGet(ActivityPath, async (Microsoft.AspNetCore.Http.HttpContext context) =>
59-
await StatsEndpoints.HandleActivityStats(context, _connectionString, "json", null));
59+
await StatsEndpoints.HandleActivityStats(context, _connectionString, "json", false, null, null, null));
6060

6161
// Add HTML endpoints for testing HTML format
6262
_app.MapGet("/stats/tables-html", async (Microsoft.AspNetCore.Http.HttpContext context) =>
63-
await StatsEndpoints.HandleTablesStats(context, _connectionString, "html", null, null));
63+
await StatsEndpoints.HandleTablesStats(context, _connectionString, "html", null, false, null, null, null));
6464

6565
// Add NpgsqlRest for completeness
6666
_app.UseNpgsqlRest(new NpgsqlRestOptions(_connectionString)

changelog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ Note: The changelog for the older version can be found here: [Changelog Archive]
44

55
---
66

7+
## Version [3.6.1](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.6.1) (2025-02-02)
8+
9+
[Full Changelog](https://github.com/NpgsqlRest/NpgsqlRest/compare/3.6.0...3.6.1)
10+
11+
### Fixes
12+
13+
- Fixed `RequireAuthorization` on Stats and Health endpoints to use manual authorization check consistent with NpgsqlRest endpoints.
14+
- Fixed `ActivityQuery` in Stats endpoints.
15+
- Fixed `OutputFormat` default value in Stats endpoints.
16+
17+
---
18+
719
## Version [3.6.0](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.6.0) (2025-02-01)
820

921
[Full Changelog](https://github.com/NpgsqlRest/NpgsqlRest/compare/3.5.0...3.6.0)

0 commit comments

Comments
 (0)