Skip to content

Commit 379e03f

Browse files
committed
Reconcile configured admin role permissions
1 parent f1e2a09 commit 379e03f

4 files changed

Lines changed: 113 additions & 8 deletions

File tree

src/modules/Elsa.Identity/HostedServices/AdminUserInitializer.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,21 @@ public override async Task ExecuteAsync(CancellationToken cancellationToken)
4848
}
4949
else
5050
{
51-
logger.LogInformation("Admin role '{RoleName}' already exists. Skipping creation.", adminRoleName);
51+
var missingPermissions = adminRolePermissions.Except(existingRole.Permissions, StringComparer.Ordinal).ToArray();
52+
53+
if (missingPermissions.Length == 0)
54+
{
55+
logger.LogInformation("Admin role '{RoleName}' already exists with all configured permissions.", adminRoleName);
56+
}
57+
else
58+
{
59+
existingRole.Permissions = existingRole.Permissions.Concat(missingPermissions).ToList();
60+
await roleStore.SaveAsync(existingRole, cancellationToken);
61+
logger.LogInformation(
62+
"Admin role '{RoleName}' updated successfully with {PermissionCount} missing configured permissions.",
63+
adminRoleName,
64+
missingPermissions.Length);
65+
}
5266
}
5367

5468
var roleToAssign = adminRoleName;

test/integration/Elsa.ExternalAuthentication.IntegrationTests/Identity/JustInTimeProvisioningTests.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ public async Task InMemoryProvisionerRetriesAGeneratedUserNameCollision()
116116
Assert.Null(user.HashedPasswordSalt);
117117
}
118118

119+
[Fact]
120+
public async Task InMemoryProvisionerAssignsConfiguredDefaultRolesToNewUser()
121+
{
122+
var userStore = new MemoryUserStore(new MemoryStore<User>());
123+
var roleStore = new MemoryRoleStore(new MemoryStore<Role>());
124+
await roleStore.AddAsync(new Role { Id = "admin", Name = "Administrator", TenantId = "tenant-a", Permissions = ["*"] });
125+
var provisioner = CreateInMemoryProvisioner(userStore, roleProvider: new StoreBasedRoleProvider(roleStore));
126+
127+
var result = await provisioner.CreateLinkOrGetExistingAsync(CreateProvisioningRequest(defaultRoleIds: ["admin"]));
128+
var user = await userStore.FindAsync(new Elsa.Identity.Models.UserFilter { Id = result.UserId });
129+
130+
Assert.NotNull(user);
131+
Assert.Equal(["admin"], user.Roles);
132+
}
133+
119134
[Fact]
120135
public async Task InMemoryProvisionerTreatsEachExternalIdentityTupleAsDistinct()
121136
{
@@ -165,20 +180,27 @@ public async Task InMemoryProvisionerSharesTupleStateAcrossDependencyInjectionSc
165180
[policy],
166181
Microsoft.Extensions.Options.Options.Create(new ExternalAuthenticationOptions()));
167182

168-
private static InMemoryExternalIdentityProvisioner CreateInMemoryProvisioner(IUserStore userStore, IIdentityGenerator? identityGenerator = null) => new(
183+
private static InMemoryExternalIdentityProvisioner CreateInMemoryProvisioner(
184+
IUserStore userStore,
185+
IIdentityGenerator? identityGenerator = null,
186+
IRoleProvider? roleProvider = null) => new(
169187
userStore,
170188
new StoreBasedUserProvider(userStore),
171-
NSubstitute.Substitute.For<IRoleProvider>(),
189+
roleProvider ?? NSubstitute.Substitute.For<IRoleProvider>(),
172190
identityGenerator ?? new Elsa.Workflows.GuidIdentityGenerator(),
173191
new Elsa.Common.Services.SystemClock(),
174192
new HmacExternalAuthenticationHandleHasher(),
175193
new InMemoryExternalIdentityProvisionerState());
176194

177-
private static ProvisioningRequest CreateProvisioningRequest(string? existingUserId = null, string connectionKey = "contoso", string issuer = "https://issuer.example") => new(
195+
private static ProvisioningRequest CreateProvisioningRequest(
196+
string? existingUserId = null,
197+
string connectionKey = "contoso",
198+
string issuer = "https://issuer.example",
199+
IReadOnlyCollection<string>? defaultRoleIds = null) => new(
178200
"tenant-a",
179201
connectionKey,
180202
new ExternalIdentity(issuer, "subject-a", new Dictionary<string, IReadOnlyCollection<string>>()),
181-
existingUserId is null ? new UserCreationProposal("external") : null,
203+
existingUserId is null ? new UserCreationProposal("external", DefaultRoleIds: defaultRoleIds) : null,
182204
existingUserId);
183205

184206
private static ExternalIdentityResolutionContext CreateContext(string policyType = "create-user", IReadOnlyDictionary<string, IReadOnlyCollection<string>>? claims = null)

test/integration/Elsa.ExternalAuthentication.IntegrationTests/Sessions/ExternalRefreshPermissionTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public async Task RefreshRetainsBoundedExternalSnapshotAndReevaluatesCurrentElsa
9191
registry.FindByKeyAsync("tenant-a", "contoso", Arg.Any<CancellationToken>())
9292
.Returns(ValueTask.FromResult<EffectiveIdentityProviderConnection?>(effective));
9393
var user = new User { Id = "user-a", Name = "alice", TenantId = "tenant-a", Roles = ["role-a"] };
94-
var role = new Role { Id = "role-a", Name = "Operators", TenantId = "tenant-a", Permissions = ["workflows:read"] };
94+
var role = new Role { Id = "role-a", Name = "Operators", TenantId = "tenant-a", Permissions = ["*"] };
9595
var users = Substitute.For<IUserProvider>();
9696
users.FindAsync(Arg.Any<UserFilter>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult<User?>(user));
9797
var roles = Substitute.For<IRoleProvider>();
@@ -129,9 +129,9 @@ public async Task RefreshRetainsBoundedExternalSnapshotAndReevaluatesCurrentElsa
129129
await issuer.RefreshAsync("studio", refreshToken);
130130

131131
Assert.Equal(2, issuanceContexts.Count);
132-
Assert.Equal(["workflows:read", "reports:view"], issuanceContexts[0].Permissions);
132+
Assert.Equal(["*", "reports:view"], issuanceContexts[0].Permissions);
133133
Assert.Equal(["workflows:manage", "reports:view"], issuanceContexts[1].Permissions);
134-
Assert.DoesNotContain("workflows:read", issuanceContexts[1].Permissions);
134+
Assert.DoesNotContain("*", issuanceContexts[1].Permissions);
135135
Assert.Equal("session-a", issuanceContexts[1].ExternalAuthenticationSessionId);
136136
}
137137

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using Elsa.Common.Services;
2+
using Elsa.Identity.Contracts;
3+
using Elsa.Identity.Entities;
4+
using Elsa.Identity.HostedServices;
5+
using Elsa.Identity.Models;
6+
using Elsa.Identity.Options;
7+
using Elsa.Identity.Services;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
using NSubstitute;
10+
11+
namespace Elsa.Identity.UnitTests.HostedServices;
12+
13+
public class AdminUserInitializerTests
14+
{
15+
[Fact]
16+
public async Task ExecuteAsyncAddsMissingConfiguredPermissionsToExistingAdminRole()
17+
{
18+
var roleStore = await CreateRoleStoreAsync(["custom"]);
19+
var initializer = CreateInitializer(roleStore, ["*"]);
20+
21+
await initializer.ExecuteAsync(CancellationToken.None);
22+
23+
var role = await roleStore.FindAsync(new RoleFilter { Id = "admin" });
24+
Assert.NotNull(role);
25+
Assert.Equal(["custom", "*"], role.Permissions);
26+
}
27+
28+
[Fact]
29+
public async Task ExecuteAsyncDoesNotDuplicateExistingConfiguredPermissions()
30+
{
31+
var roleStore = await CreateRoleStoreAsync(["*", "custom"]);
32+
var initializer = CreateInitializer(roleStore, ["*"]);
33+
34+
await initializer.ExecuteAsync(CancellationToken.None);
35+
await initializer.ExecuteAsync(CancellationToken.None);
36+
37+
var role = await roleStore.FindAsync(new RoleFilter { Id = "admin" });
38+
Assert.NotNull(role);
39+
Assert.Equal(["*", "custom"], role.Permissions);
40+
}
41+
42+
private static async Task<MemoryRoleStore> CreateRoleStoreAsync(ICollection<string> permissions)
43+
{
44+
var roleStore = new MemoryRoleStore(new MemoryStore<Role>());
45+
await roleStore.AddAsync(new Role
46+
{
47+
Id = "admin",
48+
Name = "Administrator",
49+
Permissions = permissions
50+
});
51+
return roleStore;
52+
}
53+
54+
private static AdminUserInitializer CreateInitializer(IRoleStore roleStore, ICollection<string> permissions)
55+
{
56+
var options = Microsoft.Extensions.Options.Options.Create(new DefaultAdminUserOptions
57+
{
58+
AdminRoleName = "admin",
59+
AdminRolePermissions = permissions
60+
});
61+
return new(
62+
Substitute.For<IUserStore>(),
63+
roleStore,
64+
Substitute.For<IUserManager>(),
65+
Substitute.For<IRoleManager>(),
66+
options,
67+
NullLogger<AdminUserInitializer>.Instance);
68+
}
69+
}

0 commit comments

Comments
 (0)