Skip to content

Commit d1b8c03

Browse files
Access control: implement scoped expiring/revokable access tokens
1 parent b4f157e commit d1b8c03

File tree

7 files changed

+230
-86
lines changed

7 files changed

+230
-86
lines changed

src/Certify.Core/Management/Access/AccessControl.cs

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Security.Cryptography;
55
using System.Text;
66
using System.Threading.Tasks;
7+
using Certify.Models.Config;
78
using Certify.Models.Hub;
89
using Certify.Models.Providers;
910
using Certify.Providers;
@@ -198,31 +199,56 @@ public async Task<SecurityPrinciple> GetSecurityPrincipleByUsername(string conte
198199
return list?.SingleOrDefault(sp => sp.Username?.ToLowerInvariant() == username.ToLowerInvariant());
199200
}
200201

201-
public async Task<bool> IsAuthorised(string contextUserId, string principleId, string roleId, string resourceType, string actionId, string identifier)
202+
/// <summary>
203+
/// Check if a security principle has access to the given resource action
204+
/// </summary>
205+
/// <param name="contextUserId">Security principle performing access check</param>
206+
/// <param name="principleId">Security principle to check access for</param>
207+
/// <param name="resourceType">resource type being accessed</param>
208+
/// <param name="actionId">resource action required</param>
209+
/// <param name="identifier">optional resource identifier, if access is limited by specific resource</param>
210+
/// <param name="scopedAssignedRoles">optional scoped assigned roles to limit access to (for scoped access token checks etc)</param>
211+
/// <returns></returns>
212+
public async Task<bool> IsAuthorised(string contextUserId, string principleId, string resourceType, string actionId, string identifier = null, List<string> scopedAssignedRoles = null)
202213
{
203214
// to determine is a principle has access to perform a particular action
204215
// for each group the principle is part of
205216

206-
// TODO: cache results for performance
217+
// TODO: cache results for performance based on last update of access control config, which will be largely static
207218

219+
// get all assigned roles (all users)
208220
var allAssignedRoles = await _store.GetItems<AssignedRole>(nameof(AssignedRole));
209221

210-
var spAssigned = allAssignedRoles.Where(a => a.SecurityPrincipleId == principleId);
211-
222+
// get all defined roles
212223
var allRoles = await _store.GetItems<Role>(nameof(Role));
213224

214-
var spAssignedRoles = allRoles.Where(r => spAssigned.Any(t => t.RoleId == r.Id));
225+
// get all defined policies
226+
var allPolicies = await _store.GetItems<ResourcePolicy>(nameof(ResourcePolicy));
215227

216-
var spSpecificAssignedRoles = spAssigned.Where(a => spAssignedRoles.Any(r => r.Id == a.RoleId));
228+
// get the assigned roles for this specific security principle
229+
var spAssignedRoles = allAssignedRoles.Where(a => a.SecurityPrincipleId == principleId);
217230

218-
var allPolicies = await _store.GetItems<ResourcePolicy>(nameof(ResourcePolicy));
231+
// if scoped assigned role ID specified (access token check etc), reduce scope of assigned roles to check
232+
if (scopedAssignedRoles?.Any() == true)
233+
{
234+
spAssignedRoles = spAssignedRoles.Where(a => scopedAssignedRoles.Contains(a.Id));
235+
}
219236

220-
var spAssignedPolicies = allPolicies.Where(r => spAssignedRoles.Any(p => p.Policies.Contains(r.Id)));
237+
// get all role definitions included in the principles assigned roles
238+
var spAssignedRoleDefinitions = allRoles.Where(r => spAssignedRoles.Any(t => t.RoleId == r.Id));
221239

240+
var spSpecificAssignedRoles = spAssignedRoles.Where(a => spAssignedRoleDefinitions.Any(r => r.Id == a.RoleId));
241+
242+
// get all resource policies included in the principles assigned roles
243+
var spAssignedPolicies = allPolicies.Where(r => spAssignedRoleDefinitions.Any(p => p.Policies.Contains(r.Id)));
244+
245+
// check an assigned policy allows the required resource action
222246
if (spAssignedPolicies.Any(a => a.ResourceActions.Contains(actionId)))
223247
{
224-
// if any of the service principles assigned roles are restricted by the type of resource type,
248+
249+
// if any of the service principles assigned roles are restricted by resource type,
225250
// check for identifier matches (e.g. role assignment restricted on domains )
251+
226252
if (spSpecificAssignedRoles.Any(a => a.IncludedResources?.Any(r => r.ResourceType == resourceType) == true))
227253
{
228254
var allIncludedResources = spSpecificAssignedRoles.SelectMany(a => a.IncludedResources).Distinct();
@@ -263,6 +289,36 @@ public async Task<bool> IsAuthorised(string contextUserId, string principleId, s
263289
}
264290
}
265291

292+
public async Task<ActionResult> IsAccessTokenAuthorised(string contextUserId, AccessToken accessToken, string resourceType, string actionId, string identifier)
293+
{
294+
// resolve security principle from access token
295+
296+
var assignedTokens = await _store.GetItems<AssignedAccessToken>(nameof(AssignedAccessToken));
297+
298+
// check if a non-expired/non-revoked access token exists matching the given client ID
299+
var knownAssignedToken = assignedTokens.SingleOrDefault(t => t.AccessTokens.Any(a => a.ClientId == accessToken.ClientId && a.Secret == accessToken.Secret && a.DateRevoked == null && (a.DateExpiry == null || a.DateExpiry >= DateTimeOffset.UtcNow)));
300+
301+
if (knownAssignedToken == null)
302+
{
303+
return new ActionResult("Access token unknown, expired or revoked.", false);
304+
}
305+
306+
// check related principle has access
307+
308+
var isAuthorised = await IsAuthorised(contextUserId, knownAssignedToken.SecurityPrincipleId, resourceType, actionId, identifier, knownAssignedToken.ScopedAssignedRoles);
309+
310+
if (isAuthorised)
311+
{
312+
// TODO: check token scope restrictions
313+
314+
return new ActionResult("OK", true);
315+
}
316+
else
317+
{
318+
return new ActionResult("Access token not authorized or invalid for action, resource or identifier", false);
319+
}
320+
}
321+
266322
/// <summary>
267323
/// Check security principle is in a given role at the system level
268324
/// </summary>
@@ -398,6 +454,11 @@ public async Task AddAssignedRole(AssignedRole r)
398454
await _store.Add(nameof(AssignedRole), r);
399455
}
400456

457+
public async Task AddAssignedAccessToken(AssignedAccessToken t)
458+
{
459+
await _store.Add(nameof(AssignedAccessToken), t);
460+
}
461+
401462
public async Task AddResourceAction(ResourceAction action)
402463
{
403464
await _store.Add(nameof(ResourceAction), action);

src/Certify.Core/Management/Access/IAccessControl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public interface IAccessControl
1717
/// </summary>
1818
/// <returns></returns>
1919
Task<List<Role>> GetRoles();
20-
Task<bool> IsAuthorised(string contextUserId, string principleId, string roleId, string resourceType, string actionId, string identifier);
20+
Task<bool> IsAuthorised(string contextUserId, string principleId, string resourceType, string actionId, string identifier = null, List<string> scopedAssignedRoles = null);
2121
Task<bool> IsPrincipleInRole(string contextUserId, string id, string roleId);
2222
Task<List<AssignedRole>> GetAssignedRoles(string contextUserId, string id);
2323
Task<RoleStatus> GetSecurityPrincipleRoleStatus(string contextUserId, string id);
Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
11
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
2-
<PropertyGroup>
3-
<TargetFrameworks>netstandard2.0;net462;net9.0</TargetFrameworks>
4-
<RootNamespace>Certify</RootNamespace>
5-
<Platforms>AnyCPU</Platforms>
6-
<LangVersion>10.0</LangVersion>
7-
<Nullable>enable</Nullable>
8-
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
9-
<Description>Certify Certificate Manager API Models</Description>
10-
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
11-
<EnableNETAnalyzers>True</EnableNETAnalyzers>
12-
<AnalysisLevel>latest-recommended</AnalysisLevel>
13-
<Product>Certify The Web - Certify Certificate Manager</Product>
14-
<PackageReadmeFile>readme.md</PackageReadmeFile>
15-
</PropertyGroup>
16-
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
17-
<PlatformTarget>AnyCPU</PlatformTarget>
18-
</PropertyGroup>
19-
<ItemGroup>
20-
<PackageReference Include="Fody" Version="6.9.1">
21-
<PrivateAssets>all</PrivateAssets>
22-
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
23-
</PackageReference>
24-
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
25-
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.12.0">
26-
<PrivateAssets>all</PrivateAssets>
27-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
28-
</PackageReference>
29-
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
30-
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
31-
<PrivateAssets>all</PrivateAssets>
32-
</PackageReference>
33-
<PackageReference Include="System.Text.Json" Version="9.0.1" />
34-
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
35-
</ItemGroup>
2+
<PropertyGroup>
3+
<TargetFrameworks>netstandard2.0;net462;net9.0</TargetFrameworks>
4+
<RootNamespace>Certify</RootNamespace>
5+
<Platforms>AnyCPU</Platforms>
6+
<LangVersion>latest</LangVersion>
7+
<Nullable>enable</Nullable>
8+
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
9+
<Description>Certify Certificate Manager API Models</Description>
10+
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
11+
<EnableNETAnalyzers>True</EnableNETAnalyzers>
12+
<AnalysisLevel>latest-recommended</AnalysisLevel>
13+
<Product>Certify The Web - Certify Certificate Manager</Product>
14+
<PackageReadmeFile>readme.md</PackageReadmeFile>
15+
</PropertyGroup>
16+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
17+
<PlatformTarget>AnyCPU</PlatformTarget>
18+
</PropertyGroup>
19+
<ItemGroup>
20+
<PackageReference Include="Fody" Version="6.9.1">
21+
<PrivateAssets>all</PrivateAssets>
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
23+
</PackageReference>
24+
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
25+
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.12.0">
26+
<PrivateAssets>all</PrivateAssets>
27+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
28+
</PackageReference>
29+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
30+
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
31+
<PrivateAssets>all</PrivateAssets>
32+
</PackageReference>
33+
<PackageReference Include="System.Text.Json" Version="9.0.1" />
34+
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
35+
</ItemGroup>
3636

37-
<ItemGroup>
38-
<ProjectReference Include="..\Certify.Locales\Certify.Locales.csproj" />
39-
</ItemGroup>
37+
<ItemGroup>
38+
<ProjectReference Include="..\Certify.Locales\Certify.Locales.csproj" />
39+
</ItemGroup>
4040

41-
<ItemGroup>
42-
<None Include="readme.md">
43-
<Pack>True</Pack>
44-
<PackagePath>\</PackagePath>
45-
</None>
46-
</ItemGroup>
41+
<ItemGroup>
42+
<None Include="readme.md">
43+
<Pack>True</Pack>
44+
<PackagePath>\</PackagePath>
45+
</None>
46+
</ItemGroup>
4747

4848
</Project>

src/Certify.Models/Hub/AccessControl.cs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33

44
namespace Certify.Models.Hub
55
{
6+
public enum SecurityPrincipleType
7+
{
8+
User = 1,
9+
Application = 2,
10+
Group
11+
}
12+
13+
public enum SecurityPermissionType
14+
{
15+
ALLOW = 1,
16+
DENY = 0
17+
}
618

719
/// <summary>
820
/// A Security Principle is a user or service account which can be assigned roles and other permissions
@@ -24,7 +36,7 @@ public class SecurityPrinciple : ConfigurationStoreItem
2436
/// </summary>
2537
public string? ExternalIdentifier { get; set; }
2638

27-
public SecurityPrincipleType? PrincipleType { get; set; }
39+
public SecurityPrincipleType PrincipleType { get; set; } = SecurityPrincipleType.User;
2840

2941
public string? AuthKey { get; set; }
3042

@@ -58,14 +70,38 @@ public class AssignedRole : ConfigurationStoreItem
5870
/// <summary>
5971
/// Defines the role to be assigned
6072
/// </summary>
61-
public string? RoleId { get; set; }
73+
public string RoleId { get; set; } = default!;
6274

6375
/// <summary>
6476
/// Specific security principle assigned to the role
6577
/// </summary>
66-
public string? SecurityPrincipleId { get; set; }
78+
public string SecurityPrincipleId { get; set; } = default!;
6779

68-
public List<Resource>? IncludedResources { get; set; }
80+
public List<Resource>? IncludedResources { get; set; } = [];
81+
}
82+
83+
public class AccessToken : ConfigurationStoreItem
84+
{
85+
public string TokenType { get; set; }
86+
public string Secret { get; set; }
87+
public string ClientId { get; set; }
88+
public DateTimeOffset? DateCreated { get; set; }
89+
public DateTimeOffset? DateExpiry { get; set; }
90+
public DateTimeOffset? DateRevoked { get; set; }
91+
}
92+
public class AssignedAccessToken : ConfigurationStoreItem
93+
{
94+
public string SecurityPrincipleId { get; set; } = default!;
95+
96+
/// <summary>
97+
/// Optional list of Assigned Roles this access token is scoped to
98+
/// </summary>
99+
public List<string> ScopedAssignedRoles { get; set; } = [];
100+
101+
/// <summary>
102+
/// List of access tokens assigned
103+
/// </summary>
104+
public List<AccessToken> AccessTokens { get; set; } = [];
69105
}
70106

71107
/// <summary>
@@ -76,12 +112,12 @@ public class Resource : ConfigurationStoreItem
76112
/// <summary>
77113
/// Type of this resource
78114
/// </summary>
79-
public string? ResourceType { get; set; }
115+
public string ResourceType { get; set; } = default!;
80116

81117
/// <summary>
82118
/// Identifier for this resource, can include wildcards for domains etc
83119
/// </summary>
84-
public string? Identifier { get; set; }
120+
public string Identifier { get; set; } = default!;
85121
}
86122

87123
public class ResourcePolicy : ConfigurationStoreItem

src/Certify.Models/Hub/AccessControlConfig.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,6 @@
22

33
namespace Certify.Models.Hub
44
{
5-
public enum SecurityPrincipleType
6-
{
7-
User = 1,
8-
Application = 2,
9-
Group
10-
}
11-
12-
public enum SecurityPermissionType
13-
{
14-
ALLOW = 1,
15-
DENY = 0
16-
}
17-
185
public class StandardRoles
196
{
207
public static Role Administrator { get; } = new Role("sysadmin", "Administrator", "Certify Server Administrator",

src/Certify.Server/Certify.Server.Core/Certify.Server.Core/Controllers/AccessController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public async Task<bool> CheckSecurityPrincipleHasAccess(string id, string resour
107107
{
108108
var accessControl = await _certifyManager.GetCurrentAccessControl();
109109

110-
return await accessControl.IsAuthorised(GetContextUserId(), id, null, resourceType, actionId: resourceAction, identifier);
110+
return await accessControl.IsAuthorised(GetContextUserId(), id, resourceType, actionId: resourceAction, identifier);
111111
}
112112

113113
[HttpGet, Route("securityprinciple/{id}/assignedroles")]

0 commit comments

Comments
 (0)