Skip to content

Commit 9842733

Browse files
committed
Add support for read-only role on backend
1 parent 22c5961 commit 9842733

File tree

12 files changed

+275
-38
lines changed

12 files changed

+275
-38
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System.Security.Claims;
2+
using System.Threading.Tasks;
3+
using Api.Database.Context;
4+
using Api.Database.Models;
5+
using Api.Services;
6+
using Api.Test.Database;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Testcontainers.PostgreSql;
10+
using Xunit;
11+
12+
namespace Api.Test.Services;
13+
14+
public class AccessRoleServiceTest : IAsyncLifetime
15+
{
16+
public required DatabaseUtilities DatabaseUtilities;
17+
public required PostgreSqlContainer Container;
18+
public required IAccessRoleService AccessRoleService;
19+
public required FlotillaDbContext Context;
20+
21+
public async Task InitializeAsync()
22+
{
23+
(Container, string cs, var _) = await TestSetupHelpers.ConfigurePostgreSqlDatabase();
24+
25+
var factory = TestSetupHelpers.ConfigureWebApplicationFactory(
26+
postgreSqlConnectionString: cs
27+
);
28+
var sp = TestSetupHelpers.ConfigureServiceProvider(factory);
29+
30+
Context = TestSetupHelpers.ConfigurePostgreSqlContext(cs);
31+
DatabaseUtilities = new DatabaseUtilities(Context);
32+
AccessRoleService = sp.GetRequiredService<IAccessRoleService>();
33+
34+
var http = sp.GetRequiredService<IHttpContextAccessor>();
35+
http.HttpContext = new DefaultHttpContext
36+
{
37+
User = new ClaimsPrincipal(
38+
new ClaimsIdentity([new Claim(ClaimTypes.Role, "Role.Admin")], "TestAuth")
39+
),
40+
};
41+
42+
var installationHUA = await DatabaseUtilities.NewInstallation("HUA");
43+
await AccessRoleService.Create(
44+
installationHUA,
45+
"Role.ReadOnly.HUA",
46+
RoleAccessLevel.READ_ONLY
47+
);
48+
await AccessRoleService.Create(installationHUA, "Role.User.HUA", RoleAccessLevel.USER);
49+
}
50+
51+
public Task DisposeAsync() => Task.CompletedTask;
52+
53+
[Fact]
54+
public async Task GetAllowedInstallationCodes_ReadMode_WithReadOnlyRole_ReturnsHUA()
55+
{
56+
var identity = new ClaimsIdentity(
57+
[new Claim(ClaimTypes.Role, "Role.ReadOnly.HUA")],
58+
"TestAuth"
59+
);
60+
61+
var user = new ClaimsPrincipal(identity);
62+
63+
var result = await AccessRoleService.GetAllowedInstallationCodes(user, AccessMode.Read);
64+
65+
Assert.Single(result);
66+
Assert.Equal("HUA", result[0]);
67+
}
68+
69+
[Fact]
70+
public async Task GetAllowedInstallationCodes_ReadMode_WithUserRole_ReturnsHUA()
71+
{
72+
var identity = new ClaimsIdentity(
73+
[new Claim(ClaimTypes.Role, "Role.User.HUA")],
74+
"TestAuth"
75+
);
76+
77+
var user = new ClaimsPrincipal(identity);
78+
79+
var result = await AccessRoleService.GetAllowedInstallationCodes(user, AccessMode.Read);
80+
81+
Assert.Single(result);
82+
Assert.Equal("HUA", result[0]);
83+
}
84+
85+
[Fact]
86+
public async Task GetAllowedInstallationCodes_WriteMode_WithUserRole_ReturnsHUA()
87+
{
88+
var identity = new ClaimsIdentity(
89+
[new Claim(ClaimTypes.Role, "Role.User.HUA")],
90+
"TestAuth"
91+
);
92+
93+
var user = new ClaimsPrincipal(identity);
94+
95+
var result = await AccessRoleService.GetAllowedInstallationCodes(user, AccessMode.Write);
96+
97+
Assert.Single(result);
98+
Assert.Equal("HUA", result[0]);
99+
}
100+
101+
[Fact]
102+
public async Task GetAllowedInstallationCodes_WriteMode_WithReadOnlyRole_ReturnsEmpty()
103+
{
104+
var identity = new ClaimsIdentity(
105+
[new Claim(ClaimTypes.Role, "Role.ReadOnly.HUA")],
106+
"TestAuth"
107+
);
108+
var user = new ClaimsPrincipal(identity);
109+
110+
var result = await AccessRoleService.GetAllowedInstallationCodes(user, AccessMode.Write);
111+
112+
Assert.Empty(result);
113+
}
114+
}

backend/api/Database/Context/InitDb.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,21 @@ private static List<Inspection> GetInspections()
2929

3030
private static List<AccessRole> GetAccessRoles()
3131
{
32-
var accessRole1 = new AccessRole
32+
var userAccessRole = new AccessRole
3333
{
3434
Installation = installations[0],
3535
AccessLevel = RoleAccessLevel.ADMIN,
3636
RoleName = "Role.User.HUA",
3737
};
3838

39-
return new List<AccessRole>([accessRole1]);
39+
var readOnlyAccessRole = new AccessRole
40+
{
41+
Installation = installations[0],
42+
AccessLevel = RoleAccessLevel.READ_ONLY,
43+
RoleName = "Role.ReadOnly.HUA",
44+
};
45+
46+
return new List<AccessRole>([userAccessRole, readOnlyAccessRole]);
4047
}
4148

4249
private static List<Installation> GetInstallations()

backend/api/Services/AccessRoleService.cs

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55

66
namespace Api.Services
77
{
8+
public enum AccessMode
9+
{
10+
Read,
11+
Write,
12+
}
13+
814
public interface IAccessRoleService
915
{
10-
public Task<List<string>> GetAllowedInstallationCodes();
11-
public Task<List<string>> GetAllowedInstallationCodes(ClaimsPrincipal user);
16+
public Task<List<string>> GetAllowedInstallationCodes(AccessMode accessMode);
17+
public Task<List<string>> GetAllowedInstallationCodes(
18+
ClaimsPrincipal user,
19+
AccessMode accessMode
20+
);
1221
public bool IsUserAdmin();
1322
public bool IsAuthenticationAvailable();
1423
public Task<AccessRole> Create(
@@ -33,14 +42,17 @@ private IQueryable<AccessRole> GetAccessRoles(bool readOnly = true)
3342
return readOnly ? context.AccessRoles.AsNoTracking() : context.AccessRoles.AsTracking();
3443
}
3544

36-
public async Task<List<string>> GetAllowedInstallationCodes()
45+
public async Task<List<string>> GetAllowedInstallationCodes(AccessMode accessMode)
3746
{
3847
var user = httpContextAccessor.HttpContext?.User;
3948

40-
return await GetAllowedInstallationCodes(user);
49+
return await GetAllowedInstallationCodes(user, accessMode);
4150
}
4251

43-
public async Task<List<string>> GetAllowedInstallationCodes(ClaimsPrincipal? user)
52+
public async Task<List<string>> GetAllowedInstallationCodes(
53+
ClaimsPrincipal? user,
54+
AccessMode accessMode
55+
)
4456
{
4557
if (user == null)
4658
return await context
@@ -58,18 +70,83 @@ public async Task<List<string>> GetAllowedInstallationCodes(ClaimsPrincipal? use
5870
.Claims.Where(c => c.Type == ClaimTypes.Role)
5971
.Select(c => c.Value)
6072
.ToList();
61-
return await GetAllowedInstallationCodes(userRoles);
73+
74+
var allowedInstallationCodes = await GetAllowedInstallationCodes(userRoles, accessMode);
75+
76+
return allowedInstallationCodes;
6277
}
6378

64-
public async Task<List<string>> GetAllowedInstallationCodes(List<string> roles)
79+
private async Task<List<string>> EnsureUserRolesExistInDatabase(List<string> roles)
6580
{
66-
return await GetAccessRoles(readOnly: true)
81+
var dbRoles = await GetAccessRoles(readOnly: true)
6782
.Include(r => r.Installation)
68-
.Where(r => roles.Contains(r.RoleName))
69-
.Select(r =>
70-
r.Installation != null ? r.Installation.InstallationCode.ToUpperInvariant() : ""
71-
)
83+
.Select(r => r.RoleName)
7284
.ToListAsync();
85+
86+
var intersection = roles.Intersect(dbRoles, StringComparer.OrdinalIgnoreCase).ToList();
87+
88+
return intersection;
89+
}
90+
91+
private static List<string> GetInstallationCodesByAccessMode(
92+
List<string> roles,
93+
AccessMode accessMode
94+
)
95+
{
96+
List<string> permittedInstallationCodes = [];
97+
98+
foreach (var role in roles)
99+
{
100+
switch (accessMode)
101+
{
102+
case AccessMode.Read:
103+
if (role.StartsWith("Role.ReadOnly.") || role.StartsWith("Role.User."))
104+
{
105+
var installationCode = TryExtractInstallationCode(role);
106+
if (installationCode is null)
107+
continue;
108+
permittedInstallationCodes.Add(installationCode);
109+
}
110+
break;
111+
112+
case AccessMode.Write:
113+
if (role.StartsWith("Role.User."))
114+
{
115+
var installationCode = TryExtractInstallationCode(role);
116+
if (installationCode is null)
117+
continue;
118+
permittedInstallationCodes.Add(installationCode);
119+
}
120+
break;
121+
}
122+
}
123+
124+
return [.. permittedInstallationCodes.Distinct()];
125+
}
126+
127+
private static string? TryExtractInstallationCode(string role)
128+
{
129+
var parts = role.Split('.');
130+
if (parts.Length != 3)
131+
return null;
132+
133+
var code = parts[2];
134+
return code.Length == 3 ? code : null;
135+
}
136+
137+
public async Task<List<string>> GetAllowedInstallationCodes(
138+
List<string> roles,
139+
AccessMode accessMode
140+
)
141+
{
142+
var rolesInUserAndDb = await EnsureUserRolesExistInDatabase(roles);
143+
144+
var permittedInstallationCodes = GetInstallationCodesByAccessMode(
145+
rolesInUserAndDb,
146+
accessMode
147+
);
148+
149+
return permittedInstallationCodes;
73150
}
74151

75152
private void ThrowExceptionIfNotAdmin()

backend/api/Services/ExclusionAreaService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public async Task<ExclusionArea> Update(ExclusionArea exclusionArea)
279279
private IQueryable<ExclusionArea> GetExclusionAreas(bool readOnly = true)
280280
{
281281
var accessibleInstallationCodes = accessRoleService
282-
.GetAllowedInstallationCodes()
282+
.GetAllowedInstallationCodes(AccessMode.Read)
283283
.Result;
284284
var query = context
285285
.ExclusionAreas.Include(p => p.Plant)
@@ -293,7 +293,9 @@ private IQueryable<ExclusionArea> GetExclusionAreas(bool readOnly = true)
293293

294294
private async Task ApplyDatabaseUpdate(Installation? installation)
295295
{
296-
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes();
296+
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(
297+
AccessMode.Write
298+
);
297299
if (
298300
installation == null
299301
|| accessibleInstallationCodes.Contains(

backend/api/Services/InspectionAreaService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ public async Task<InspectionArea> Update(InspectionArea inspectionArea)
282282
private IQueryable<InspectionArea> GetInspectionAreas(bool readOnly = true)
283283
{
284284
var accessibleInstallationCodes = accessRoleService
285-
.GetAllowedInstallationCodes()
285+
.GetAllowedInstallationCodes(AccessMode.Read)
286286
.Result;
287287
var query = context
288288
.InspectionAreas.Include(p => p.Plant)
@@ -296,7 +296,9 @@ private IQueryable<InspectionArea> GetInspectionAreas(bool readOnly = true)
296296

297297
private async Task ApplyDatabaseUpdate(Installation? installation)
298298
{
299-
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes();
299+
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(
300+
AccessMode.Write
301+
);
300302
if (
301303
installation == null
302304
|| accessibleInstallationCodes.Contains(

backend/api/Services/InspectionService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ AnalysisResult analysisResult
9191

9292
private async Task ApplyDatabaseUpdate(Installation? installation)
9393
{
94-
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes();
94+
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(
95+
AccessMode.Write
96+
);
9597
if (
9698
installation == null
9799
|| accessibleInstallationCodes.Contains(

backend/api/Services/InstallationService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ public async Task<IList<Installation>> ReadAll(bool readOnly = true)
5555

5656
private IQueryable<Installation> GetInstallations(bool readOnly = true)
5757
{
58-
var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes();
58+
var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(
59+
AccessMode.Read
60+
);
5961
var query = context.Installations.Where(i =>
6062
accessibleInstallationCodes.Result.Contains(i.InstallationCode.ToUpper())
6163
);
@@ -69,7 +71,9 @@ private async Task ApplyUnprotectedDatabaseUpdate()
6971

7072
private async Task ApplyDatabaseUpdate(Installation? installation)
7173
{
72-
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes();
74+
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(
75+
AccessMode.Write
76+
);
7377
if (
7478
installation == null
7579
|| accessibleInstallationCodes.Contains(

backend/api/Services/MissionDefinitionService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ public async Task<MissionDefinition> Update(MissionDefinition missionDefinition)
228228

229229
private async Task ApplyDatabaseUpdate(Installation? installation)
230230
{
231-
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes();
231+
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(
232+
AccessMode.Write
233+
);
232234
if (
233235
installation == null
234236
|| accessibleInstallationCodes.Contains(
@@ -246,7 +248,9 @@ private IQueryable<MissionDefinition> GetMissionDefinitionsWithSubModels(
246248
bool readOnly = true
247249
)
248250
{
249-
var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes();
251+
var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(
252+
AccessMode.Read
253+
);
250254
var query = context
251255
.MissionDefinitions.Include(missionDefinition =>
252256
missionDefinition.AutoScheduleFrequency

backend/api/Services/MissionRunService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,9 @@ public async Task UpdateWithInspections(MissionRun missionRun)
310310

311311
private IQueryable<MissionRun> GetMissionRunsWithSubModels(bool readOnly = true)
312312
{
313-
var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes();
313+
var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(
314+
AccessMode.Read
315+
);
314316
var query = context
315317
.MissionRuns.Include(missionRun => missionRun.InspectionArea)
316318
.ThenInclude(inspectionArea => inspectionArea != null ? inspectionArea.Plant : null)
@@ -346,7 +348,9 @@ protected virtual void OnMissionRunCreated(MissionRunCreatedEventArgs e)
346348

347349
private async Task ApplyDatabaseUpdate(Installation? installation)
348350
{
349-
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes();
351+
var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(
352+
AccessMode.Write
353+
);
350354
if (
351355
installation == null
352356
|| accessibleInstallationCodes.Contains(

0 commit comments

Comments
 (0)