Skip to content

Commit ffb5e50

Browse files
committed
RE1-T116 PR#351 fixes
1 parent 9e05d83 commit ffb5e50

6 files changed

Lines changed: 58 additions & 35 deletions

File tree

Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ public static class Data
2424
public const string TimeZone = "TimeZone";
2525
public const string DisplayName = "DisplayName";
2626
public const string UserId = "UserId";
27+
28+
/// <summary>
29+
/// Claim added to service-account tokens (client_credentials, system API keys)
30+
/// so downstream controllers can identify them as non-user principals that bypass
31+
/// per-user authorization checks and support cross-department operation.
32+
/// </summary>
33+
public const string ServiceAccount = "ServiceAccount";
2734
}
2835

2936
public static class Resources

Web/Resgrid.Web.Services/Controllers/v4/ConnectController.cs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -255,44 +255,54 @@ public async Task<IActionResult> Token()
255255
}
256256

257257
// First, check system-level credentials (timing-safe comparison)
258-
if (Config.SecurityConfig.SystemLoginCredentials.ContainsKey(request.ClientId) &&
259-
FixedTimeSecretEquals(Config.SecurityConfig.SystemLoginCredentials[request.ClientId], request.ClientSecret))
260-
{
261-
audit.Successful = true;
262-
await _systemAuditsService.SaveSystemAuditAsync(audit);
258+
if (Config.SecurityConfig.SystemLoginCredentials.ContainsKey(request.ClientId) &&
259+
FixedTimeSecretEquals(Config.SecurityConfig.SystemLoginCredentials[request.ClientId], request.ClientSecret))
260+
{
261+
audit.Successful = true;
262+
await _systemAuditsService.SaveSystemAuditAsync(audit);
263263

264-
// Create a system-level service principal with all claims
265-
var identity = new ClaimsIdentity(
266-
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
267-
Claims.Name,
268-
Claims.Role);
264+
// Create a system-level service principal with all claims
265+
var identity = new ClaimsIdentity(
266+
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
267+
Claims.Name,
268+
Claims.Role);
269269

270-
identity.AddClaim(new Claim(Claims.Subject, $"system_{request.ClientId}")
271-
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
272-
identity.AddClaim(new Claim(Claims.Name, request.ClientId)
273-
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
270+
identity.AddClaim(new Claim(Claims.Subject, $"system_{request.ClientId}")
271+
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
272+
identity.AddClaim(new Claim(Claims.Name, $"System Account ({request.ClientId})")
273+
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
274+
identity.AddClaim(new Claim(ClaimTypes.PrimarySid, $"system_{request.ClientId}")
275+
.SetDestinations(Destinations.AccessToken));
276+
identity.AddClaim(new Claim(ClaimTypes.PrimaryGroupSid, "0")
277+
.SetDestinations(Destinations.AccessToken));
278+
identity.AddClaim(new Claim(ClaimTypes.GivenName, "SMTP Relay System")
279+
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
280+
identity.AddClaim(new Claim(ResgridClaimTypes.Data.DisplayName, "SMTP Relay System")
281+
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
282+
identity.AddClaim(new Claim(ResgridClaimTypes.Data.ServiceAccount, "true")
283+
.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken));
274284

275-
// Add all resource claims for full access
276-
AddAllResourceClaims(identity);
285+
// Add all resource claims for full access
286+
AddAllResourceClaims(identity);
277287

278-
var principal = new ClaimsPrincipal(identity);
288+
var principal = new ClaimsPrincipal(identity);
279289

280-
principal.SetScopes(new[]
281-
{
282-
Scopes.OpenId,
283-
Scopes.Email,
284-
Scopes.Profile
285-
}.Intersect(request.GetScopes()));
290+
principal.SetScopes(new[]
291+
{
292+
Scopes.OpenId,
293+
Scopes.Email,
294+
Scopes.Profile
295+
}.Intersect(request.GetScopes()));
286296

287-
principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(OidcConfig.AccessTokenExpiryMinutes));
297+
principal.SetAccessTokenLifetime(TimeSpan.FromMinutes(OidcConfig.AccessTokenExpiryMinutes));
288298

289-
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
290-
}
299+
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
300+
}
291301

292-
// Second, try department-level credentials via department code + shared secret
293-
var department = await _departmentsService.GetDepartmentByNameAsync(request.ClientId);
302+
// Second, try department-level credentials via department code + shared secret
303+
var department = await _departmentsService.GetDepartmentByNameAsync(request.ClientId);
294304

295-
if (department == null)
305+
if (department == null)
296306
{
297307
await _systemAuditsService.SaveSystemAuditAsync(audit);
298308

Web/Resgrid.Web.Services/Controllers/v4/UnitsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ public async Task<ActionResult<UnitResult>> GetUnitByName(string name, [FromQuer
277277
return result;
278278
}
279279

280-
if (!await _authorizationService.CanUserViewUnitViaMatrixAsync(unit.UnitId, UserId, DepartmentId))
280+
if (!IsSystemApiKeyRequest && !await _authorizationService.CanUserViewUnitViaMatrixAsync(unit.UnitId, UserId, DepartmentId))
281281
{
282282
ResponseHelper.PopulateV4ResponseNotFound(result);
283283
return result;

Web/Resgrid.Web.Services/Controllers/v4/V4AuthenticatedApiControllerbaseSystemAuth.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using Microsoft.AspNetCore.Mvc;
22
using System.Linq;
3+
using System.Security.Claims;
4+
using Resgrid.Providers.Claims;
35
using Resgrid.Web.ServicesCore.Helpers;
46

57
namespace Resgrid.Web.Services.Controllers.v4
@@ -41,10 +43,12 @@ public class V4AuthenticatedApiControllerbaseSystemAuth : ControllerBase
4143
protected string TimeZone => ClaimsAuthorizationHelper.GetTimeZone();
4244

4345
/// <summary>
44-
/// Returns true if the current request was authenticated via the SystemApiKey scheme.
46+
/// Returns true if the current request was authenticated via the SystemApiKey scheme
47+
/// or carries a service-account marker claim set by the ConnectController client_credentials flow.
4548
/// </summary>
4649
protected bool IsSystemApiKeyRequest =>
47-
HttpContext.User.Identities.Any(i => i.AuthenticationType == "SystemApiKey");
50+
HttpContext.User.Identities.Any(i => i.AuthenticationType == "SystemApiKey") ||
51+
HttpContext.User.HasClaim(ResgridClaimTypes.Data.ServiceAccount, "true");
4852

4953
/// <summary>
5054
/// Returns the effective department ID for the current request.

Web/Resgrid.Web.Services/Middleware/SystemApiKeyAuthHandler.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
6565
// Data claims
6666
new Claim(ResgridClaimTypes.Data.TimeZone, "UTC"),
6767
new Claim(ResgridClaimTypes.Data.DisplayName, "SMTP Relay"),
68-
new Claim(ResgridClaimTypes.Data.UserId, "smtp_relay_system")
68+
new Claim(ResgridClaimTypes.Data.UserId, "smtp_relay_system"),
69+
new Claim(ResgridClaimTypes.Data.ServiceAccount, "true")
6970
};
7071

7172
// Add all resource claims for full cross-department access

Web/Resgrid.Web.Services/Resgrid.Web.Services.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)