Skip to content

Commit ebd1b6e

Browse files
authored
Merge pull request #351 from Resgrid/develop
RE1-T116 Updating API to support SMTP Relay
2 parents af8b2c9 + ffb5e50 commit ebd1b6e

16 files changed

Lines changed: 1581 additions & 549 deletions

Core/Resgrid.Config/SecurityConfig.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ public static class SecurityConfig
1616

1717
};
1818

19+
/// <summary>
20+
/// System-level API key used by the SMTP Relay in hosted multi-department mode.
21+
/// When the X-Resgrid-SystemApiKey header matches this value, the request bypasses
22+
/// OAuth 2.0 authentication and is granted full cross-department access.
23+
/// The department for each operation is determined by the DepartmentId field in the
24+
/// request body/query parameters, not by the auth token.
25+
/// </summary>
26+
public static string SystemApiKey = "";
27+
1928
// ── Encryption ───────────────────────────────────────────────────────────────
2029

2130
/// <summary>AES-256 master key used by IEncryptionService for system-wide encryption.</summary>

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/CallFilesController.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ namespace Resgrid.Web.Services.Controllers.v4
2626
[Route("api/v{VersionId:apiVersion}/[controller]")]
2727
[ApiVersion("4.0")]
2828
[ApiExplorerSettings(GroupName = "v4")]
29-
public class CallFilesController : V4AuthenticatedApiControllerbase
29+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
30+
public class CallFilesController : V4AuthenticatedApiControllerbaseSystemAuth
3031
{
3132
#region Members and Constructors
3233
private readonly ICallsService _callsService;
@@ -205,7 +206,9 @@ public async Task<ActionResult<SaveCallFileResult>> SaveCallFile(SaveCallFileInp
205206
return Ok(result);
206207
}
207208

208-
if (call.DepartmentId != DepartmentId)
209+
var effectiveDepartmentId = GetEffectiveDepartmentId(input.DepartmentId);
210+
211+
if (call.DepartmentId != effectiveDepartmentId)
209212
return Unauthorized();
210213

211214
if (call.State != (int)CallStates.Active)

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

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ namespace Resgrid.Web.Services.Controllers.v4
3232
[Route("api/v{VersionId:apiVersion}/[controller]")]
3333
[ApiVersion("4.0")]
3434
[ApiExplorerSettings(GroupName = "v4")]
35-
public class CallsController : V4AuthenticatedApiControllerbase
35+
[Authorize(AuthenticationSchemes = "BasicAuthentication,SystemApiKey")]
36+
public class CallsController : V4AuthenticatedApiControllerbaseSystemAuth
3637
{
3738
#region Members and Constructors
3839
private readonly ICallsService _callsService;
@@ -161,7 +162,7 @@ public async Task<ActionResult<ActiveCallsResult>> GetActiveCalls()
161162
[HttpGet("GetCall")]
162163
[ProducesResponseType(StatusCodes.Status200OK)]
163164
[Authorize(Policy = ResgridResources.Call_View)]
164-
public async Task<ActionResult<GetCallResult>> GetCall(string callId)
165+
public async Task<ActionResult<GetCallResult>> GetCall(string callId, [FromQuery] string departmentId = null)
165166
{
166167
if (String.IsNullOrWhiteSpace(callId))
167168
return BadRequest();
@@ -175,14 +176,16 @@ public async Task<ActionResult<GetCallResult>> GetCall(string callId)
175176
return Ok(result);
176177
}
177178

178-
if (c.DepartmentId != DepartmentId)
179+
var effectiveDepartmentId = GetEffectiveDepartmentId(departmentId);
180+
181+
if (c.DepartmentId != effectiveDepartmentId)
179182
return Unauthorized();
180183

181-
if (!await _authorizationService.CanUserViewCallAsync(UserId, int.Parse(callId)))
184+
if (!IsSystemApiKeyRequest && !await _authorizationService.CanUserViewCallAsync(UserId, int.Parse(callId)))
182185
return Unauthorized();
183186

184187
c = await _callsService.PopulateCallData(c, false, true, true, false, false, false, true, true, true);
185-
var destinationPoi = await GetValidatedDestinationPoiAsync(c.DestinationPoiId);
188+
var destinationPoi = await GetValidatedDestinationPoiAsync(c.DestinationPoiId, effectiveDepartmentId);
186189

187190
string address = "";
188191
if (String.IsNullOrWhiteSpace(c.Address) && c.HasValidGeolocationData())
@@ -209,7 +212,7 @@ public async Task<ActionResult<GetCallResult>> GetCall(string callId)
209212
result.Data = ConvertCall(c, protocols, address, TimeZone, destinationPoi);
210213

211214
// Populate UDF values
212-
var udfValues = await _userDefinedFieldsService.GetFieldValuesForEntityAsync(DepartmentId, (int)UdfEntityType.Call, c.CallId.ToString());
215+
var udfValues = await _userDefinedFieldsService.GetFieldValuesForEntityAsync(effectiveDepartmentId, (int)UdfEntityType.Call, c.CallId.ToString());
213216
if (udfValues != null && udfValues.Any())
214217
{
215218
result.Data.UdfValues = udfValues.Select(v => new UdfFieldValueResultData
@@ -545,27 +548,35 @@ public async Task<ActionResult<SaveCallResult>> SaveCall([FromBody] NewCallInput
545548
{
546549
var result = new SaveCallResult();
547550

548-
var canDoOperation = await _authorizationService.CanUserCreateCallAsync(UserId, DepartmentId);
551+
var effectiveDepartmentId = GetEffectiveDepartmentId(newCallInput.DepartmentId);
549552

550-
if (!canDoOperation)
551-
return Unauthorized();
553+
if (!IsSystemApiKeyRequest)
554+
{
555+
var canDoOperation = await _authorizationService.CanUserCreateCallAsync(UserId, effectiveDepartmentId);
556+
if (!canDoOperation)
557+
return Unauthorized();
558+
}
552559

553560
if (!ModelState.IsValid)
554561
return BadRequest();
555562

556-
var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId);
557-
var activeUsers = await _departmentsService.GetAllMembersForDepartmentAsync(DepartmentId);
558-
var groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(DepartmentId);
559-
var roles = await _personnelRolesService.GetAllRolesForDepartmentAsync(DepartmentId);
560-
var units = await _unitsService.GetUnitsForDepartmentAsync(DepartmentId);
561-
var destinationPoi = await GetValidatedDestinationPoiAsync(newCallInput.DestinationPoiId);
563+
var department = await _departmentsService.GetDepartmentByIdAsync(effectiveDepartmentId);
564+
565+
if (department == null)
566+
return BadRequest($"Department not found: {effectiveDepartmentId}");
567+
568+
var activeUsers = await _departmentsService.GetAllMembersForDepartmentAsync(effectiveDepartmentId);
569+
var groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(effectiveDepartmentId);
570+
var roles = await _personnelRolesService.GetAllRolesForDepartmentAsync(effectiveDepartmentId);
571+
var units = await _unitsService.GetUnitsForDepartmentAsync(effectiveDepartmentId);
572+
var destinationPoi = await GetValidatedDestinationPoiAsync(newCallInput.DestinationPoiId, effectiveDepartmentId);
562573

563574
if (newCallInput.DestinationPoiId.HasValue && newCallInput.DestinationPoiId.Value > 0 && destinationPoi == null)
564575
return BadRequest();
565576

566577
var call = new Call
567578
{
568-
DepartmentId = DepartmentId,
579+
DepartmentId = effectiveDepartmentId,
569580
ReportingUserId = UserId,
570581
Priority = newCallInput.Priority,
571582
Name = newCallInput.Name,
@@ -635,27 +646,27 @@ public async Task<ActionResult<SaveCallResult>> SaveCall([FromBody] NewCallInput
635646
call.CheckInTimersEnabled = newCallInput.CheckInTimersEnabled.Value;
636647
else
637648
{
638-
var autoEnable = await _departmentSettingsService.GetCheckInTimersAutoEnableForNewCallsAsync(DepartmentId);
649+
var autoEnable = await _departmentSettingsService.GetCheckInTimersAutoEnableForNewCallsAsync(effectiveDepartmentId);
639650
call.CheckInTimersEnabled = autoEnable;
640651
}
641652

642653
if (!String.IsNullOrWhiteSpace(newCallInput.Type) && newCallInput.Type != "No Type")
643654
{
644-
var callTypes = await _callsService.GetCallTypesForDepartmentAsync(DepartmentId);
655+
var callTypes = await _callsService.GetCallTypesForDepartmentAsync(effectiveDepartmentId);
645656
var type = callTypes.FirstOrDefault(x => x.Type == newCallInput.Type);
646657

647658
if (type != null)
648659
{
649660
call.Type = type.Type;
650661
}
651662
}
652-
var users = await _departmentsService.GetAllUsersForDepartmentAsync(DepartmentId);
663+
var users = await _departmentsService.GetAllUsersForDepartmentAsync(effectiveDepartmentId);
653664
call.Dispatches = new Collection<CallDispatch>();
654665
call.GroupDispatches = new List<CallDispatchGroup>();
655666
call.RoleDispatches = new List<CallDispatchRole>();
656667
call.UnitDispatches = new List<CallDispatchUnit>();
657668

658-
if (newCallInput.DispatchList == "0")
669+
if (!IsSystemApiKeyRequest && (newCallInput.DispatchList == "0" || string.IsNullOrWhiteSpace(newCallInput.DispatchList)))
659670
{
660671
// Use case, existing clients and non-ionic2 app this will be null dispatch all users. Or we've specified everyone (0).
661672
foreach (var u in users)
@@ -665,7 +676,7 @@ public async Task<ActionResult<SaveCallResult>> SaveCall([FromBody] NewCallInput
665676
call.Dispatches.Add(cd);
666677
}
667678
}
668-
else
679+
else if (!string.IsNullOrWhiteSpace(newCallInput.DispatchList) && newCallInput.DispatchList != "0")
669680
{
670681
var dispatch = newCallInput.DispatchList.Split(char.Parse("|"));
671682

@@ -751,7 +762,7 @@ public async Task<ActionResult<SaveCallResult>> SaveCall([FromBody] NewCallInput
751762

752763
//OutboundEventProvider handler = new OutboundEventProvider.CallAddedTopicHandler();
753764
//OutboundEventProvider..Handle(new CallAddedEvent() { DepartmentId = DepartmentId, Call = savedCall });
754-
_eventAggregator.SendMessage<CallAddedEvent>(new CallAddedEvent() { DepartmentId = DepartmentId, Call = savedCall });
765+
_eventAggregator.SendMessage<CallAddedEvent>(new CallAddedEvent() { DepartmentId = effectiveDepartmentId, Call = savedCall });
755766

756767
if (shouldDispatchNow && ((call.GroupDispatches != null && call.GroupDispatches.Any()) || (call.UnitDispatches != null && call.UnitDispatches.Any())))
757768
{
@@ -806,7 +817,7 @@ await _callDispatchStatusService.ApplyDispatchStatusesAsync(savedCall,
806817
// Save UDF field values if supplied
807818
if (newCallInput.UdfValues != null && newCallInput.UdfValues.Any())
808819
{
809-
bool isDeptAdmin = ClaimsAuthorizationHelper.IsUserDepartmentAdmin();
820+
bool isDeptAdmin = IsSystemApiKeyRequest || ClaimsAuthorizationHelper.IsUserDepartmentAdmin();
810821
bool isGroupAdmin = HttpContext.User.Claims
811822
.Any(c => c.Type.StartsWith(ResgridClaimTypes.Resources.Group + "/", StringComparison.Ordinal)
812823
&& c.Value == ResgridClaimTypes.Actions.Update);
@@ -816,7 +827,7 @@ await _callDispatchStatusService.ApplyDispatchStatusesAsync(savedCall,
816827
UdfFieldId = v.UdfFieldId,
817828
Value = v.Value
818829
}).ToList();
819-
await _userDefinedFieldsService.SaveFieldValuesForEntityAsync(DepartmentId, (int)UdfEntityType.Call, savedCall.CallId.ToString(), udfValues, UserId, isDeptAdmin, isGroupAdmin, cancellationToken);
830+
await _userDefinedFieldsService.SaveFieldValuesForEntityAsync(effectiveDepartmentId, (int)UdfEntityType.Call, savedCall.CallId.ToString(), udfValues, UserId, isDeptAdmin, isGroupAdmin, cancellationToken);
820831
}
821832

822833
result.Id = savedCall.CallId.ToString();
@@ -1819,12 +1830,13 @@ public static CallResultData ConvertCall(Call call, List<DispatchProtocol> proto
18191830
return callResult;
18201831
}
18211832

1822-
private async Task<Poi> GetValidatedDestinationPoiAsync(int? destinationPoiId)
1833+
private async Task<Poi> GetValidatedDestinationPoiAsync(int? destinationPoiId, int? departmentIdOverride = null)
18231834
{
18241835
if (!destinationPoiId.HasValue || destinationPoiId.Value <= 0)
18251836
return null;
18261837

1827-
return await _mappingService.GetDestinationPOIByIdAsync(DepartmentId, destinationPoiId.Value);
1838+
var deptId = departmentIdOverride ?? DepartmentId;
1839+
return await _mappingService.GetDestinationPOIByIdAsync(deptId, destinationPoiId.Value);
18281840
}
18291841
}
18301842
}

0 commit comments

Comments
 (0)