Skip to content

Commit 9e05d83

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

10 files changed

Lines changed: 84 additions & 14 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace Resgrid.Web.Services.Controllers.v4
2626
[Route("api/v{VersionId:apiVersion}/[controller]")]
2727
[ApiVersion("4.0")]
2828
[ApiExplorerSettings(GroupName = "v4")]
29+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
2930
public class CallFilesController : V4AuthenticatedApiControllerbaseSystemAuth
3031
{
3132
#region Members and Constructors

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ namespace Resgrid.Web.Services.Controllers.v4
3232
[Route("api/v{VersionId:apiVersion}/[controller]")]
3333
[ApiVersion("4.0")]
3434
[ApiExplorerSettings(GroupName = "v4")]
35+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
3536
public class CallsController : V4AuthenticatedApiControllerbaseSystemAuth
3637
{
3738
#region Members and Constructors
@@ -560,6 +561,10 @@ public async Task<ActionResult<SaveCallResult>> SaveCall([FromBody] NewCallInput
560561
return BadRequest();
561562

562563
var department = await _departmentsService.GetDepartmentByIdAsync(effectiveDepartmentId);
564+
565+
if (department == null)
566+
return BadRequest($"Department not found: {effectiveDepartmentId}");
567+
563568
var activeUsers = await _departmentsService.GetAllMembersForDepartmentAsync(effectiveDepartmentId);
564569
var groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(effectiveDepartmentId);
565570
var roles = await _personnelRolesService.GetAllRolesForDepartmentAsync(effectiveDepartmentId);

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using System.Security.Claims;
1818
using System.Threading;
1919
using System.Threading.Tasks;
20+
using System.Text;
21+
using System.Security.Cryptography;
2022
using Resgrid.Providers.Claims;
2123
using static OpenIddict.Abstractions.OpenIddictConstants;
2224

@@ -252,9 +254,9 @@ public async Task<IActionResult> Token()
252254
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
253255
}
254256

255-
// First, check system-level credentials
257+
// First, check system-level credentials (timing-safe comparison)
256258
if (Config.SecurityConfig.SystemLoginCredentials.ContainsKey(request.ClientId) &&
257-
Config.SecurityConfig.SystemLoginCredentials[request.ClientId] == request.ClientSecret)
259+
FixedTimeSecretEquals(Config.SecurityConfig.SystemLoginCredentials[request.ClientId], request.ClientSecret))
258260
{
259261
audit.Successful = true;
260262
await _systemAuditsService.SaveSystemAuditAsync(audit);
@@ -305,7 +307,7 @@ public async Task<IActionResult> Token()
305307
}
306308

307309
if (string.IsNullOrWhiteSpace(department.SharedSecret) ||
308-
department.SharedSecret != request.ClientSecret)
310+
!FixedTimeSecretEquals(department.SharedSecret, request.ClientSecret))
309311
{
310312
await _systemAuditsService.SaveSystemAuditAsync(audit);
311313

@@ -913,5 +915,18 @@ private static void AddAllResourceClaims(ClaimsIdentity identity)
913915
}
914916
}
915917
}
918+
/// <summary>
919+
/// Performs a timing-safe comparison of two secret strings to prevent timing attacks.
920+
/// </summary>
921+
private static bool FixedTimeSecretEquals(string stored, string provided)
922+
{
923+
if (stored == null || provided == null)
924+
return false;
925+
926+
var storedBytes = Encoding.UTF8.GetBytes(stored);
927+
var providedBytes = Encoding.UTF8.GetBytes(provided);
928+
929+
return CryptographicOperations.FixedTimeEquals(storedBytes, providedBytes);
930+
}
916931
}
917932
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace Resgrid.Web.Services.Controllers.v4
1717
[Route("api/v{VersionId:apiVersion}/[controller]")]
1818
[ApiVersion("4.0")]
1919
[ApiExplorerSettings(GroupName = "v4")]
20+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
2021
public class DepartmentsController : V4AuthenticatedApiControllerbaseSystemAuth
2122
{
2223
#region Members and Constructors

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace Resgrid.Web.Services.Controllers.v4
1717
[Route("api/v{VersionId:apiVersion}/[controller]")]
1818
[ApiVersion("4.0")]
1919
[ApiExplorerSettings(GroupName = "v4")]
20+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
2021
public class GroupsController : V4AuthenticatedApiControllerbaseSystemAuth
2122
{
2223
#region Members and Constructors
@@ -138,8 +139,14 @@ public async Task<ActionResult<GroupResult>> GetGroupByDispatchCode(string code,
138139
if (group.DepartmentId != DepartmentId)
139140
return Unauthorized();
140141
}
141-
else if (!string.IsNullOrWhiteSpace(departmentId) && int.TryParse(departmentId, out var deptId) && deptId > 0)
142+
else if (!string.IsNullOrWhiteSpace(departmentId))
142143
{
144+
// DepartmentId was provided explicitly — must be valid and match
145+
if (!int.TryParse(departmentId, out var deptId) || deptId <= 0)
146+
{
147+
ResponseHelper.PopulateV4ResponseNotFound(result);
148+
return result;
149+
}
143150
if (group.DepartmentId != deptId)
144151
{
145152
ResponseHelper.PopulateV4ResponseNotFound(result);
@@ -193,8 +200,14 @@ public async Task<ActionResult<GroupResult>> GetGroupByMessageCode(string code,
193200
if (group.DepartmentId != DepartmentId)
194201
return Unauthorized();
195202
}
196-
else if (!string.IsNullOrWhiteSpace(departmentId) && int.TryParse(departmentId, out var deptId) && deptId > 0)
203+
else if (!string.IsNullOrWhiteSpace(departmentId))
197204
{
205+
// DepartmentId was provided explicitly — must be valid and match
206+
if (!int.TryParse(departmentId, out var deptId) || deptId <= 0)
207+
{
208+
ResponseHelper.PopulateV4ResponseNotFound(result);
209+
return result;
210+
}
198211
if (group.DepartmentId != deptId)
199212
{
200213
ResponseHelper.PopulateV4ResponseNotFound(result);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Resgrid.Web.Services.Controllers.v4
1818
[Route("api/v{VersionId:apiVersion}/[controller]")]
1919
[ApiVersion("4.0")]
2020
[ApiExplorerSettings(GroupName = "v4")]
21+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
2122
public class RolesController : V4AuthenticatedApiControllerbaseSystemAuth
2223
{
2324
#region Members and Constructors

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace Resgrid.Web.Services.Controllers.v4
2424
[Route("api/v{VersionId:apiVersion}/[controller]")]
2525
[ApiVersion("4.0")]
2626
[ApiExplorerSettings(GroupName = "v4")]
27+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
2728
public class UnitsController : V4AuthenticatedApiControllerbaseSystemAuth
2829
{
2930
#region Members and Constructors
@@ -276,6 +277,12 @@ public async Task<ActionResult<UnitResult>> GetUnitByName(string name, [FromQuer
276277
return result;
277278
}
278279

280+
if (!await _authorizationService.CanUserViewUnitViaMatrixAsync(unit.UnitId, UserId, DepartmentId))
281+
{
282+
ResponseHelper.PopulateV4ResponseNotFound(result);
283+
return result;
284+
}
285+
279286
result.Data = ConvertUnitsData(unit, null, null, TimeZone);
280287
result.PageSize = 1;
281288
result.Status = ResponseHelper.Success;

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using Microsoft.AspNetCore.Authorization;
2-
using Microsoft.AspNetCore.Mvc;
3-
using OpenIddict.Server.AspNetCore;
1+
using Microsoft.AspNetCore.Mvc;
42
using System.Linq;
53
using Resgrid.Web.ServicesCore.Helpers;
64

@@ -17,14 +15,13 @@ namespace Resgrid.Web.Services.Controllers.v4
1715
/// </summary>
1816
[ApiController]
1917
[Produces("application/json")]
20-
[Authorize(AuthenticationSchemes = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme + ",SystemApiKey")]
2118
public class V4AuthenticatedApiControllerbaseSystemAuth : ControllerBase
2219
{
2320
/// <summary>
2421
/// Returns the current user ID. In SystemApiKey mode returns a synthetic identifier.
2522
/// </summary>
2623
protected string UserId => IsSystemApiKeyRequest
27-
? "smpt_relay_system"
24+
? "smtp_relay_system"
2825
: ClaimsAuthorizationHelper.GetUserId();
2926

3027
/// <summary>

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Security.Claims;
4+
using System.Security.Cryptography;
5+
using System.Text;
46
using System.Text.Encodings.Web;
57
using System.Threading.Tasks;
68
using Microsoft.AspNetCore.Authentication;
@@ -45,9 +47,9 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
4547
if (string.IsNullOrWhiteSpace(apiKey))
4648
return Task.FromResult(AuthenticateResult.NoResult());
4749

48-
// Validate the API key against the configured system API key
50+
// Validate the API key against the configured system API key (timing-safe comparison)
4951
if (string.IsNullOrWhiteSpace(Config.SecurityConfig.SystemApiKey) ||
50-
!string.Equals(apiKey, Config.SecurityConfig.SystemApiKey, StringComparison.Ordinal))
52+
!FixedTimeApiKeyEquals(Config.SecurityConfig.SystemApiKey, apiKey))
5153
{
5254
return Task.FromResult(AuthenticateResult.Fail("Invalid System API Key"));
5355
}
@@ -56,14 +58,14 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
5658
var claims = new List<Claim>
5759
{
5860
new Claim(ClaimTypes.Name, "SMTP Relay System"),
59-
new Claim(ClaimTypes.PrimarySid, "smpt_relay_system"),
61+
new Claim(ClaimTypes.PrimarySid, "smtp_relay_system"),
6062
new Claim(ClaimTypes.PrimaryGroupSid, "0"),
6163
new Claim(ClaimTypes.GivenName, "SMTP Relay"),
6264
new Claim(ClaimTypes.Email, "smtp-relay@resgrid.local"),
6365
// Data claims
6466
new Claim(ResgridClaimTypes.Data.TimeZone, "UTC"),
6567
new Claim(ResgridClaimTypes.Data.DisplayName, "SMTP Relay"),
66-
new Claim(ResgridClaimTypes.Data.UserId, "smpt_relay_system")
68+
new Claim(ResgridClaimTypes.Data.UserId, "smtp_relay_system")
6769
};
6870

6971
// Add all resource claims for full cross-department access
@@ -130,5 +132,21 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
130132

131133
return Task.FromResult(AuthenticateResult.Success(ticket));
132134
}
135+
136+
/// <summary>
137+
/// Performs a timing-safe comparison of two API key strings to prevent timing attacks.
138+
/// Unlike string.Equals with Ordinal, this compares every byte regardless of where
139+
/// the first mismatch occurs.
140+
/// </summary>
141+
private static bool FixedTimeApiKeyEquals(string stored, string provided)
142+
{
143+
if (stored == null || provided == null)
144+
return false;
145+
146+
var storedBytes = Encoding.UTF8.GetBytes(stored);
147+
var providedBytes = Encoding.UTF8.GetBytes(provided);
148+
149+
return CryptographicOperations.FixedTimeEquals(storedBytes, providedBytes);
150+
}
133151
}
134152
}

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

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)