Skip to content

Commit ae40dc9

Browse files
committed
Bulk roles/permissions.
1 parent 758dddf commit ae40dc9

File tree

27 files changed

+401
-78
lines changed

27 files changed

+401
-78
lines changed

README.md

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,3 @@ An Identity and Access Management (IAM) platform.
55
# Documentation
66

77
Please visit the [Shuttle.Access documentation](https://www.pendel.co.za/shuttle-access/home.html) for more information
8-
9-
- move "allow identity/password" setting to back-end
10-
- add description to identity (for OID scenarios)
11-
- logging of tokens as option
12-
- configuration.json:
13-
14-
```json
15-
{
16-
"Roles": [
17-
{
18-
"Name": "Admin",
19-
"Description": "Administrator role",
20-
"Permissions": [
21-
"system://name/permission-a"
22-
]
23-
},
24-
{
25-
"Name": "User",
26-
"Description": "User role"
27-
}
28-
],
29-
"Permissions": [
30-
"system://name/permission-a",
31-
"system://name/permission-b",
32-
]
33-
}
34-
```

Shuttle.Access.Application/.package/package.nuspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<tags>iam identity authorization authentication security permissions</tags>
1818
<dependencies>
1919
<dependency id="Microsoft.CSharp" version="4.7.0" />
20+
<dependency id="Shuttle.Access" version="7.3.0" />
2021
<dependency id="Shuttle.Core.Contract" version="20.0.1" />
2122
<dependency id="Shuttle.Core.Mediator" version="20.0.0" />
2223
<dependency id="Shuttle.Esb" version="20.0.0" />

Shuttle.Access.Application/ConfigureApplicationParticipant.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,7 @@ public async Task ProcessMessageAsync(IParticipantContext<ConfigureApplication>
8484
{
8585
_logger.LogDebug("[role/registration] : name = 'Administrator'");
8686

87-
var registerRoleMessage = new RequestResponseMessage<RegisterRole, RoleRegistered>(new()
88-
{
89-
Name = "Administrator"
90-
});
87+
var registerRoleMessage = new RequestResponseMessage<RegisterRole, RoleRegistered>(new("Administrator"));
9188

9289
await _mediator.SendAsync(registerRoleMessage);
9390

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Collections.Generic;
2+
using Shuttle.Core.Contract;
3+
4+
namespace Shuttle.Access.Application;
5+
6+
public class RegisterRole
7+
{
8+
private readonly List<string> _permissions = [];
9+
10+
public string Name { get; }
11+
public bool HasMissingPermissions { get; private set; }
12+
13+
public RegisterRole(string name)
14+
{
15+
Name = Guard.AgainstEmpty(name);
16+
}
17+
18+
public RegisterRole AddPermissions(IEnumerable<string> permissions)
19+
{
20+
Guard.AgainstNull(permissions);
21+
22+
foreach (var permission in permissions)
23+
{
24+
if (!_permissions.Contains(permission))
25+
{
26+
_permissions.Add(permission);
27+
}
28+
}
29+
30+
return this;
31+
}
32+
33+
public IEnumerable<string> GetPermissions() => _permissions.AsReadOnly();
34+
35+
public RegisterRole MissingPermissions()
36+
{
37+
HasMissingPermissions = true;
38+
return this;
39+
}
40+
}

Shuttle.Access.Application/RegisterRoleParticipant.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Threading.Tasks;
5+
using Shuttle.Access.DataAccess;
36
using Shuttle.Access.Messages.v1;
47
using Shuttle.Core.Contract;
58
using Shuttle.Core.Mediator;
@@ -10,13 +13,15 @@ namespace Shuttle.Access.Application;
1013

1114
public class RegisterRoleParticipant : IParticipant<RequestResponseMessage<RegisterRole, RoleRegistered>>
1215
{
16+
private readonly IPermissionQuery _permissionQuery;
1317
private readonly IEventStore _eventStore;
1418
private readonly IIdKeyRepository _idKeyRepository;
1519

16-
public RegisterRoleParticipant(IEventStore eventStore, IIdKeyRepository idKeyRepository)
20+
public RegisterRoleParticipant(IEventStore eventStore, IIdKeyRepository idKeyRepository, IPermissionQuery permissionQuery)
1721
{
1822
_eventStore = Guard.AgainstNull(eventStore);
1923
_idKeyRepository = Guard.AgainstNull(idKeyRepository);
24+
_permissionQuery = Guard.AgainstNull(permissionQuery);
2025
}
2126

2227
public async Task ProcessMessageAsync(IParticipantContext<RequestResponseMessage<RegisterRole, RoleRegistered>> context)
@@ -25,6 +30,24 @@ public async Task ProcessMessageAsync(IParticipantContext<RequestResponseMessage
2530

2631
var message = context.Message.Request;
2732

33+
var permissionIds = new List<Guid>();
34+
35+
foreach (var permission in message.GetPermissions())
36+
{
37+
var permissionId = (await _permissionQuery.SearchAsync(new DataAccess.Permission.Specification().AddName(permission))).FirstOrDefault()?.Id;
38+
39+
if (permissionId.HasValue)
40+
{
41+
permissionIds.Add(permissionId.Value);
42+
}
43+
else
44+
{
45+
46+
message.MissingPermissions();
47+
return;
48+
}
49+
}
50+
2851
var key = Role.Key(message.Name);
2952

3053
if (await _idKeyRepository.ContainsAsync(key))
@@ -41,6 +64,14 @@ public async Task ProcessMessageAsync(IParticipantContext<RequestResponseMessage
4164

4265
stream.Add(role.Register(message.Name));
4366

67+
foreach (var permissionId in permissionIds)
68+
{
69+
if (!role.HasPermission(permissionId))
70+
{
71+
stream.Add(role.AddPermission(permissionId));
72+
}
73+
}
74+
4475
context.Message.WithResponse(new()
4576
{
4677
Id = id,

Shuttle.Access.Application/Shuttle.Access.Application.csproj

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,14 @@
1616

1717
<ItemGroup>
1818
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
19+
<PackageReference Include="Shuttle.Access" Version="7.3.0" />
1920
<PackageReference Include="Shuttle.Core.Contract" Version="20.0.1" />
2021
<PackageReference Include="Shuttle.Core.Mediator" Version="20.0.0" />
2122
<PackageReference Include="Shuttle.Esb" Version="20.0.0" />
2223
<PackageReference Include="Shuttle.Recall" Version="20.0.0" />
2324
<PackageReference Include="Shuttle.Recall.Sql.Storage" Version="20.0.0" />
2425
</ItemGroup>
2526

26-
<ItemGroup>
27-
<ProjectReference Include="..\Shuttle.Access\Shuttle.Access.csproj" />
28-
</ItemGroup>
29-
3027
<ItemGroup>
3128
<Compile Update="Resources.Designer.cs">
3229
<DesignTime>True</DesignTime>

Shuttle.Access.AspNetCore/.package/package.nuspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<tags>iam middleware authorization permissions</tags>
1818
<dependencies>
1919
<dependency id="Microsoft.IdentityModel.JsonWebTokens" version="8.7.0" />
20-
<dependency id="Shuttle.Access" version="7.2.3" />
20+
<dependency id="Shuttle.Access" version="7.3.0" />
2121
<dependency id="Shuttle.Core.Contract" version="20.0.1" />
2222
</dependencies>
2323
</metadata>

Shuttle.Access.AspNetCore/Authentication/RoutingAuthenticationHandler.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
using Microsoft.AspNetCore.Authentication;
33
using Microsoft.Extensions.Logging;
44
using Microsoft.Extensions.Options;
5+
using Shuttle.Core.Contract;
56

67
namespace Shuttle.Access.AspNetCore.Authentication;
78

89
public class RoutingAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
910
{
11+
private readonly ILogger _logger;
12+
private readonly AccessOptions _accessOptions;
1013
public const string AuthenticationScheme = "Routing";
1114

12-
public RoutingAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
15+
public RoutingAuthenticationHandler(IOptions<AccessOptions> accessOptions, IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
1316
{
17+
_accessOptions = Guard.AgainstNull(Guard.AgainstNull(accessOptions).Value);
18+
_logger = logger.CreateLogger(GetType());
1419
}
1520

1621
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()

Shuttle.Access.Messages/v1/RegisterIdentity.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
2-
3-
namespace Shuttle.Access.Messages.v1;
1+
namespace Shuttle.Access.Messages.v1;
42

53
public class RegisterIdentity
64
{
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
namespace Shuttle.Access.Messages.v1;
1+
using System.Collections.Generic;
2+
3+
namespace Shuttle.Access.Messages.v1;
24

35
public class RegisterRole
46
{
57
public string Name { get; set; } = default!;
8+
public List<string> Permissions { get; set; } = [];
9+
public int WaitCount { get; set; }
610
}

Shuttle.Access.RestClient/.package/package.nuspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
<dependencies>
1919
<dependency id="Refit" version="8.0.0" />
2020
<dependency id="Refit.HttpClientFactory" version="8.0.0" />
21-
<dependency id="Shuttle.Access" version="7.2.3" />
22-
<dependency id="Shuttle.Access.AspNetCore" version="7.2.3" />
21+
<dependency id="Shuttle.Access" version="7.3.0" />
22+
<dependency id="Shuttle.Access.AspNetCore" version="7.3.0" />
2323
<dependency id="Shuttle.Core.Contract" version="20.0.1" />
2424
</dependencies>
2525
</metadata>

Shuttle.Access.Server/v1/MessageHandlers/RoleHandler.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Threading.Tasks;
1+
using System;
2+
using System.Threading.Tasks;
23
using Shuttle.Access.Messages.v1;
34
using Shuttle.Core.Contract;
45
using Shuttle.Core.Data;
@@ -34,14 +35,32 @@ public async Task ProcessMessageAsync(IHandlerContext<RegisterRole> context)
3435
return;
3536
}
3637

37-
var requestResponse = new RequestResponseMessage<RegisterRole, RoleRegistered>(message);
38+
var registerRole = new Application.RegisterRole(message.Name);
39+
40+
registerRole.AddPermissions(message.Permissions);
41+
42+
var requestResponse = new RequestResponseMessage<Application.RegisterRole, RoleRegistered>(registerRole);
3843

3944
using (new DatabaseContextScope())
4045
await using (_databaseContextFactory.Create())
4146
{
4247
await _mediator.SendAsync(requestResponse);
4348
}
4449

50+
if (registerRole.HasMissingPermissions)
51+
{
52+
if (message.WaitCount < 5)
53+
{
54+
message.WaitCount++;
55+
56+
await context.SendAsync(message, builder => builder.Defer(DateTime.Now.AddSeconds(5)).Local());
57+
58+
return;
59+
}
60+
61+
throw new UnrecoverableHandlerException("Maximum permission wait count reached.");
62+
}
63+
4564
if (requestResponse.Response != null)
4665
{
4766
await context.PublishAsync(requestResponse.Response);

Shuttle.Access.Sql/Identity/IdentityQueryFactory.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,7 @@ @IdentityId is null
291291
OR
292292
i.Id = @IdentityId
293293
)
294-
{(columns ? @"
295-
ORDER BY
296-
i.[Name]
297-
" : string.Empty)}
294+
{(columns ? @"ORDER BY i.[Name]" : string.Empty)}
298295
")
299296
.AddParameter(Columns.RoleName, specification.RoleName)
300297
.AddParameter(Columns.Name, specification.Name)

Shuttle.Access.Sql/Permission/PermissionQueryFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ INNER JOIN
142142
RolePermission rp ON (rp.PermissionId = p.Id)
143143
")}
144144
{Where(specification)}
145+
{(columns ? "ORDER BY p.[Name]" : string.Empty)}
145146
")
146147
.AddParameter(Columns.NameMatch, specification.NameMatch);
147148
}

Shuttle.Access.Tests/Participants/RegisterRoleParticipantFixture.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Moq;
44
using NUnit.Framework;
55
using Shuttle.Access.Application;
6+
using Shuttle.Access.DataAccess;
67
using Shuttle.Access.Events.Role.v1;
78
using Shuttle.Access.Messages.v1;
89
using Shuttle.Core.Mediator;
@@ -22,14 +23,14 @@ public async Task Should_be_able_to_add_role_async()
2223
idKeyRepository.Setup(m => m.ContainsAsync(It.IsAny<string>(), default)).ReturnsAsync(await ValueTask.FromResult(false));
2324

2425
var participant =
25-
new RegisterRoleParticipant(eventStore, idKeyRepository.Object);
26+
new RegisterRoleParticipant(eventStore, idKeyRepository.Object, new Mock<IPermissionQuery>().Object);
2627

27-
var addRole = new RegisterRole { Name = "role-name" };
28+
var addRole = new Application.RegisterRole("role-name");
2829

2930
var requestResponseMessage =
30-
new RequestResponseMessage<RegisterRole, RoleRegistered>(addRole);
31+
new RequestResponseMessage<Application.RegisterRole, RoleRegistered>(addRole);
3132

32-
await participant.ProcessMessageAsync(new ParticipantContext<RequestResponseMessage<RegisterRole, RoleRegistered>>(requestResponseMessage, CancellationToken.None));
33+
await participant.ProcessMessageAsync(new ParticipantContext<RequestResponseMessage<Application.RegisterRole, RoleRegistered>>(requestResponseMessage, CancellationToken.None));
3334

3435
Assert.That(requestResponseMessage.Response, Is.Not.Null);
3536

Shuttle.Access.Vue/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"last-administrator": "You cannot remove the administrator role as this identity is the last administrator.",
2727
"messages": {
2828
"invalid-credentials": "Invalid credentials.",
29+
"invalid-json": "Invalid JSON.",
2930
"invalid-session": "Invalid session.",
3031
"missing-credentials": "Incomplete credentials specified.",
3132
"no-items": "There are no items to display.",
@@ -47,6 +48,7 @@
4748
"password": "Password",
4849
"password-mismatch": "Your new password must must the new confirmation password.",
4950
"permission": "Permission",
51+
"permission-json": "JSON Permissions",
5052
"permission-required": "You do not have permission to access the requested route.",
5153
"permissions": "Permissions",
5254
"permissions-button": "permissions",
@@ -57,6 +59,7 @@
5759
"removed": "Removed",
5860
"rename": "Rename",
5961
"role": "Role",
62+
"role-json": "JSON Roles",
6063
"roles": "Roles",
6164
"role-permissions": "Permissions",
6265
"role-name": "Role Name",

Shuttle.Access.Vue/src/router/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ const routes: Array<RouteRecordRaw> = [
9797
permission: Permissions.Permissions.Manage,
9898
},
9999
},
100+
{
101+
path: "permission/json",
102+
name: "permission-json",
103+
component: () => import("../views/PermissionJson.vue"),
104+
meta: {
105+
permission: Permissions.Permissions.Manage,
106+
},
107+
},
100108
{
101109
path: ":id/rename",
102110
name: "permission-rename",
@@ -124,6 +132,14 @@ const routes: Array<RouteRecordRaw> = [
124132
permission: Permissions.Roles.Manage,
125133
},
126134
},
135+
{
136+
path: "role/json",
137+
name: "role-json",
138+
component: () => import("../views/RoleJson.vue"),
139+
meta: {
140+
permission: Permissions.Roles.Manage,
141+
},
142+
},
127143
{
128144
path: "role/:id/rename",
129145
name: "role-rename",

0 commit comments

Comments
 (0)