Skip to content

Commit ebb44db

Browse files
committed
Query tracks and update tracks with least privileges.
1 parent aaed0c4 commit ebb44db

16 files changed

+256
-25
lines changed

src/FoxIDs.Control/Controllers/Tracks/TTrackController.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
namespace FoxIDs.Controllers
1818
{
19-
[TenantScopeAuthorize]
19+
[TenantScopeAuthorize(Constants.ControlApi.Segment.AnyTrack)]
2020
public class TTrackController : ApiController
2121
{
2222
private readonly FoxIDsControlSettings settings;
@@ -53,6 +53,7 @@ public TTrackController(FoxIDsControlSettings settings, TelemetryScopedLogger lo
5353
{
5454
if (!ModelState.TryValidateRequiredParameter(name, nameof(name))) return BadRequest(ModelState);
5555
name = name?.ToLower();
56+
HttpContext.TenantScopeGrantAccessToTrackName(name);
5657

5758
var mTrack = await tenantDataRepository.GetTrackByNameAsync(new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = name});
5859
return Ok(mapper.Map<Api.Track>(mTrack));
@@ -81,6 +82,7 @@ public TTrackController(FoxIDsControlSettings settings, TelemetryScopedLogger lo
8182
{
8283
if (!await ModelState.TryValidateObjectAsync(track)) return BadRequest(ModelState);
8384
track.Name = await GetTrackNameAsync(track.Name);
85+
HttpContext.TenantScopeGrantAccessToTrackName(track.Name);
8486

8587
if (track.Name == Constants.Routes.ControlSiteName || track.Name == Constants.Routes.HealthController)
8688
{
@@ -131,6 +133,7 @@ public TTrackController(FoxIDsControlSettings settings, TelemetryScopedLogger lo
131133
{
132134
if (!await ModelState.TryValidateObjectAsync(track)) return BadRequest(ModelState);
133135
track.Name = await GetTrackNameAsync(track.Name);
136+
HttpContext.TenantScopeGrantAccessToTrackName(track.Name);
134137

135138
var trackIdKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = track.Name };
136139
var mTrack = await tenantDataRepository.GetTrackByNameAsync(trackIdKey);
@@ -180,6 +183,7 @@ public async Task<IActionResult> DeleteTrack(string name)
180183
{
181184
if (!ModelState.TryValidateRequiredParameter(name, nameof(name))) return BadRequest(ModelState);
182185
name = name?.ToLower();
186+
HttpContext.TenantScopeGrantAccessToTrackName(name);
183187

184188
var trackIdKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = name };
185189
var mTrack = await tenantDataRepository.GetTrackByNameAsync(trackIdKey);

src/FoxIDs.Control/Controllers/Tracks/TTrackResourceSettingController.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Microsoft.AspNetCore.Http;
77
using Microsoft.AspNetCore.Mvc;
88
using System.Threading.Tasks;
9-
using System.Net;
109
using FoxIDs.Logic;
1110
using FoxIDs.Infrastructure.Security;
1211

src/FoxIDs.Control/Controllers/Tracks/TTracksController.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace FoxIDs.Controllers
1616
{
17-
[TenantScopeAuthorize]
17+
[TenantScopeAuthorize(Constants.ControlApi.Segment.AnyTrack)]
1818
public class TTracksController : ApiController
1919
{
2020
private const string dataType = Constants.Models.DataType.Track;
@@ -41,11 +41,7 @@ public TTracksController(TelemetryScopedLogger logger, IMapper mapper, ITenantDa
4141
{
4242
try
4343
{
44-
var idKey = new Track.IdKey { TenantName = RouteBinding.TenantName, TrackName = RouteBinding.TrackName };
45-
(var mTracks, var nextPaginationToken) = filterName.IsNullOrWhiteSpace() ?
46-
await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataType.Equals(dataType), paginationToken: paginationToken) :
47-
await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataType.Equals(dataType) &&
48-
(p.Name.Contains(filterName, StringComparison.CurrentCultureIgnoreCase) || p.DisplayName.Contains(filterName, StringComparison.CurrentCultureIgnoreCase)), paginationToken: paginationToken);
44+
(var mTracks, var nextPaginationToken) = await GetFilterTrackInternalAsync(filterName, paginationToken);
4945

5046
var response = new Api.PaginationResponse<Api.Track>
5147
{
@@ -68,5 +64,30 @@ await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataTyp
6864
throw;
6965
}
7066
}
67+
68+
private async Task<(IReadOnlyCollection<Track> mTracks, string nextPaginationToken)> GetFilterTrackInternalAsync(string filterName, string paginationToken)
69+
{
70+
var idKey = new Track.IdKey { TenantName = RouteBinding.TenantName };
71+
72+
if (HttpContext.GetTenantScopeAccessToAnyTrack())
73+
{
74+
return filterName.IsNullOrWhiteSpace() ?
75+
await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataType.Equals(dataType), paginationToken: paginationToken) :
76+
await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataType.Equals(dataType) &&
77+
(p.Name.Contains(filterName, StringComparison.CurrentCultureIgnoreCase) || p.DisplayName.Contains(filterName, StringComparison.CurrentCultureIgnoreCase)), paginationToken: paginationToken);
78+
}
79+
80+
var accessToTrackNames = HttpContext.GetTenantScopeAccessToTrackNames();
81+
if (accessToTrackNames?.Count() > 0)
82+
{
83+
return filterName.IsNullOrWhiteSpace() ?
84+
await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataType.Equals(dataType) && accessToTrackNames.Any(at => at == p.Name), paginationToken: paginationToken) :
85+
await tenantDataRepository.GetListAsync<Track>(idKey, whereQuery: p => p.DataType.Equals(dataType) && accessToTrackNames.Any(at => at == p.Name) &&
86+
(p.Name.Contains(filterName, StringComparison.CurrentCultureIgnoreCase) || p.DisplayName.Contains(filterName, StringComparison.CurrentCultureIgnoreCase)), paginationToken: paginationToken);
87+
88+
}
89+
90+
return (new List<Track>(), null);
91+
}
7192
}
7293
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Microsoft.AspNetCore.Http;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace FoxIDs
7+
{
8+
public static class TenantScopeAuthorizeExtension
9+
{
10+
public static bool GetTenantScopeAccessToAnyTrack(this HttpContext httpContext)
11+
{
12+
return httpContext.Items.ContainsKey(Constants.ControlApi.AccessToAnyTrackKey);
13+
}
14+
15+
public static IEnumerable<string> GetTenantScopeAccessToTrackNames(this HttpContext httpContext)
16+
{
17+
if (httpContext.Items.ContainsKey(Constants.ControlApi.AccessToTrackNamesKey))
18+
{
19+
return httpContext.Items[Constants.ControlApi.AccessToTrackNamesKey] as IEnumerable<string>;
20+
}
21+
return null;
22+
}
23+
24+
public static void TenantScopeGrantAccessToTrackName(this HttpContext httpContext, string trackName)
25+
{
26+
if (httpContext.GetTenantScopeAccessToAnyTrack())
27+
{
28+
return;
29+
}
30+
31+
var accessToTrackNames = httpContext.GetTenantScopeAccessToTrackNames();
32+
if (accessToTrackNames?.Count() > 0)
33+
{
34+
if (accessToTrackNames.Any(at => at == trackName))
35+
{
36+
return;
37+
}
38+
39+
}
40+
41+
throw new UnauthorizedAccessException($"Users scope and role do not grant access to the {trackName} track.");
42+
}
43+
}
44+
}

src/FoxIDs.Control/FoxIDs.Control.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFramework>net9.0</TargetFramework>
5-
<Version>1.17.4</Version>
5+
<Version>1.17.5</Version>
66
<RootNamespace>FoxIDs</RootNamespace>
77
<Authors>Anders Revsgaard</Authors>
88
<Company>FoxIDs</Company>

src/FoxIDs.Control/Infrastructure/Security/BaseAuthorizationRequirement.cs

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
using System.Collections.Generic;
77
using System.Linq;
88
using System.Net.Http;
9+
using System.Text.RegularExpressions;
910
using System.Threading.Tasks;
1011

1112
namespace FoxIDs.Infrastructure.Security
1213
{
1314
public abstract class BaseAuthorizationRequirement<Tar, Tsc> : AuthorizationHandler<Tar>, IAuthorizationRequirement where Tar : IAuthorizationRequirement where Tsc : BaseScopeAuthorizeAttribute
1415
{
16+
private static Regex accessToTracksRegex = new Regex(@"((?::track\[(?<track>[\w-]+)\])|(?::track\[(?<trackget>[\w-]+)\].read)|(?::track\[(?<trackpost>[\w-]+)\].create)|(?::track\[(?<trackput>[\w-]+)\].update)|(?::track\[(?<trackdelete>[\w-]+)\].delete))$", RegexOptions.Compiled);
17+
protected bool supportAccessToTracks = false;
18+
1519
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Tar requirement)
1620
{
1721
if (context.User != null && context.Resource is HttpContext httpContext)
@@ -31,10 +35,12 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
3135
var userRoles = context.User.Claims.Where(c => string.Equals(c.Type, JwtClaimTypes.Role, StringComparison.OrdinalIgnoreCase)).Select(c => c.Value).ToList();
3236
if (userScopes?.Count() > 0 && userRoles?.Count > 0)
3337
{
34-
(var acceptedScopes, var acceptedRoles) = GetAcceptedScopesAndRoles(scopeAuthorizeAttribute.Segments, httpContext.GetRouteBinding().TrackName, httpContext.Request?.Method);
38+
var accessToTrackNames = GetAccessToTracks(userScopes, userRoles, httpContext.Request?.Method);
39+
(var acceptedScopes, var acceptedRoles) = GetAcceptedScopesAndRoles(scopeAuthorizeAttribute.Segments, httpContext.GetRouteBinding().TrackName, httpContext.Request?.Method, accessToTrackNames);
3540

3641
if (userScopes.Where(us => acceptedScopes.Any(s => s.Equals(us, StringComparison.Ordinal))).Any() && userRoles.Where(ur => acceptedRoles.Any(r => r.Equals(ur, StringComparison.Ordinal))).Any())
3742
{
43+
AddAccessToTracksRequestItems(httpContext, userScopes.Where(us => acceptedScopes.Any(s => s.Equals(us, StringComparison.Ordinal))), userRoles.Where(ur => acceptedRoles.Any(r => r.Equals(ur, StringComparison.Ordinal))), httpContext.Request?.Method);
3844
context.Succeed(requirement);
3945
}
4046
else
@@ -60,7 +66,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
6066
return Task.CompletedTask;
6167
}
6268

63-
protected abstract (List<string> acceptedScopes, List<string> acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable<string> segments, string trackName, string httpMethod);
69+
protected abstract (List<string> acceptedScopes, List<string> acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable<string> segments, string trackName, string httpMethod, IEnumerable<string> accessToTrackNames = null);
6470

6571
protected void AddScopeAndRole(List<string> acceptedScopes, List<string> acceptedRoles, string httpMethod, string scope, string role, string segment = "")
6672
{
@@ -93,5 +99,106 @@ protected void AddScopeAndRole(List<string> acceptedScopes, List<string> accepte
9399
}
94100
}
95101
}
102+
103+
private IEnumerable<string> GetAccessToTracks(IEnumerable<string> userScopes, IEnumerable<string> userRoles, string httpMethod)
104+
{
105+
if (!supportAccessToTracks)
106+
{
107+
return null;
108+
}
109+
110+
var accessToTrackNames = new List<string>();
111+
accessToTrackNames.AddRange(GetAccessToTracks(userScopes, httpMethod));
112+
accessToTrackNames.ConcatOnce(GetAccessToTracks(userRoles, httpMethod));
113+
return accessToTrackNames;
114+
}
115+
116+
private IEnumerable<string> GetAccessToTracks(IEnumerable<string> userScopesOrRole, string httpMethod)
117+
{
118+
foreach (var item in userScopesOrRole)
119+
{
120+
var itemMatch = accessToTracksRegex.Match(item);
121+
if (itemMatch.Success)
122+
{
123+
if (itemMatch.Groups["track"].Success)
124+
{
125+
yield return itemMatch.Groups["track"].Value;
126+
}
127+
128+
if (httpMethod == HttpMethod.Get.Method)
129+
{
130+
if (itemMatch.Groups["trackget"].Success)
131+
{
132+
yield return itemMatch.Groups["trackget"].Value;
133+
}
134+
}
135+
else if (httpMethod == HttpMethod.Post.Method)
136+
{
137+
if (itemMatch.Groups["trackpost"].Success)
138+
{
139+
yield return itemMatch.Groups["trackpost"].Value;
140+
}
141+
142+
}
143+
else if (httpMethod == HttpMethod.Put.Method)
144+
{
145+
if (itemMatch.Groups["trackput"].Success)
146+
{
147+
yield return itemMatch.Groups["trackput"].Value;
148+
}
149+
150+
}
151+
else if (httpMethod == HttpMethod.Delete.Method)
152+
{
153+
if (itemMatch.Groups["trackdelete"].Success)
154+
{
155+
yield return itemMatch.Groups["trackdelete"].Value;
156+
}
157+
}
158+
}
159+
}
160+
}
161+
162+
private void AddAccessToTracksRequestItems(HttpContext httpContext, IEnumerable<string> accessUserScopes, IEnumerable<string> accessUserRoles, string httpMethod)
163+
{
164+
if (!supportAccessToTracks)
165+
{
166+
return;
167+
}
168+
169+
var scopesGrantAccessToAnyTrack = !accessToTracksRegex.Match(accessUserScopes.First()).Success;
170+
var rolesGrantAccessToAnyTrack = !accessToTracksRegex.Match(accessUserRoles.First()).Success;
171+
172+
if (scopesGrantAccessToAnyTrack && rolesGrantAccessToAnyTrack)
173+
{
174+
httpContext.Items[Constants.ControlApi.AccessToAnyTrackKey] = true;
175+
return;
176+
}
177+
178+
var scopesGrantAccessToTrackNames = GetAccessToTracks(accessUserScopes, httpMethod);
179+
var rolesGrantAccessToTrackNames = GetAccessToTracks(accessUserRoles, httpMethod);
180+
181+
var accessToTracks = GetLimitedGrantedAccessToTracks(scopesGrantAccessToAnyTrack, rolesGrantAccessToAnyTrack, scopesGrantAccessToTrackNames, rolesGrantAccessToTrackNames);
182+
if (accessToTracks.Count() > 0)
183+
{
184+
httpContext.Items[Constants.ControlApi.AccessToTrackNamesKey] = accessToTracks;
185+
}
186+
}
187+
188+
private IEnumerable<string> GetLimitedGrantedAccessToTracks(bool scopesGrantAccessToAnyTrack, bool rolesGrantAccessToAnyTrack, IEnumerable<string> scopesGrantAccessToTrackNames, IEnumerable<string> rolesGrantAccessToTrackNames)
189+
{
190+
if (scopesGrantAccessToAnyTrack)
191+
{
192+
return rolesGrantAccessToTrackNames;
193+
}
194+
else if (rolesGrantAccessToAnyTrack)
195+
{
196+
return scopesGrantAccessToTrackNames;
197+
}
198+
else
199+
{
200+
return scopesGrantAccessToTrackNames.Where(us => rolesGrantAccessToTrackNames.Any(s => s.Equals(us, StringComparison.Ordinal)));
201+
}
202+
}
96203
}
97204
}

src/FoxIDs.Control/Infrastructure/Security/MasterAuthorizationRequirement.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23

34
namespace FoxIDs.Infrastructure.Security
45
{
56
public class MasterAuthorizationRequirement : BaseAuthorizationRequirement<MasterAuthorizationRequirement, MasterScopeAuthorizeAttribute>
67
{
7-
protected override (List<string> acceptedScopes, List<string> acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable<string> segments, string trackName, string httpMethod)
8+
protected override (List<string> acceptedScopes, List<string> acceptedRoles) GetAcceptedScopesAndRoles(IEnumerable<string> segments, string trackName, string httpMethod, IEnumerable<string> accessToTrackNames = null)
89
{
10+
if (accessToTrackNames != null)
11+
{
12+
throw new NotSupportedException("Access to track names not supported in master authorization.");
13+
}
14+
915
var acceptedScopes = new List<string>();
1016
var acceptedRoles = new List<string>();
1117

@@ -14,6 +20,11 @@ protected override (List<string> acceptedScopes, List<string> acceptedRoles) Get
1420

1521
foreach (var segment in segments)
1622
{
23+
if (segment == Constants.ControlApi.Segment.Basic || segment == Constants.ControlApi.Segment.AnyTrack)
24+
{
25+
throw new NotSupportedException($"Segment {segment}' not supported in master authorization.");
26+
}
27+
1728
var scope = $"{Constants.ControlApi.ResourceAndScope.Master}{segment}";
1829
var role = $"{Constants.ControlApi.Access.Tenant}{segment}";
1930
AddScopeAndRole(acceptedScopes, acceptedRoles, httpMethod, scope, role, segment);

0 commit comments

Comments
 (0)