diff --git a/Backend.Tests/Mocks/UserRoleRepositoryMock.cs b/Backend.Tests/Mocks/UserRoleRepositoryMock.cs index 6232145373..13edd049b5 100644 --- a/Backend.Tests/Mocks/UserRoleRepositoryMock.cs +++ b/Backend.Tests/Mocks/UserRoleRepositoryMock.cs @@ -27,7 +27,8 @@ public Task> GetAllUserRoles(string projectId) { try { - var foundUserRole = _userRoles.Single(userRole => userRole.Id == userRoleId); + var foundUserRole = _userRoles.Single( + userRole => userRole.ProjectId == projectId && userRole.Id == userRoleId); return Task.FromResult(foundUserRole.Clone()); } catch (InvalidOperationException) diff --git a/Backend.Tests/Services/PermissionServiceTests.cs b/Backend.Tests/Services/PermissionServiceTests.cs index 8a1bd6ffd8..0d0f1a1a7f 100644 --- a/Backend.Tests/Services/PermissionServiceTests.cs +++ b/Backend.Tests/Services/PermissionServiceTests.cs @@ -14,9 +14,9 @@ public class PermissionServiceTests private IUserRepository _userRepo = null!; private IUserRoleRepository _userRoleRepo = null!; private IPermissionService _permService = null!; - private const string UserId = "mock-user-id"; + private const string ProjId = "mock-proj-id"; - private HttpContext createHttpContextWithUser(User user) + private HttpContext CreateHttpContextWithUser(User user) { var longEnoughString = "12345678901234567890123456789012"; Environment.SetEnvironmentVariable("COMBINE_JWT_SECRET_KEY", longEnoughString); @@ -46,19 +46,19 @@ public void MakeJwtTestReturnsUser() [Test] public void GetUserIdTestReturnsNonemptyId() { - Assert.That(String.IsNullOrEmpty(_permService.GetUserId(createHttpContextWithUser(new User()))), Is.False); + Assert.That(String.IsNullOrEmpty(_permService.GetUserId(CreateHttpContextWithUser(new User()))), Is.False); } [Test] public void IsUserIdAuthorizedTestFalse() { - Assert.That(_permService.IsUserIdAuthorized(createHttpContextWithUser(new User()), "other-id"), Is.False); + Assert.That(_permService.IsUserIdAuthorized(CreateHttpContextWithUser(new User()), "other-id"), Is.False); } [Test] public void IsUserIdAuthorizedTestTrue() { - var httpContext = createHttpContextWithUser(new User()); + var httpContext = CreateHttpContextWithUser(new User()); var userId = _userRepo.GetAllUsers().Result.First().Id; Assert.That(_permService.IsUserIdAuthorized(httpContext, userId), Is.True); } @@ -66,34 +66,92 @@ public void IsUserIdAuthorizedTestTrue() [Test] public void IsCurrentUserAuthorizedTestTrue() { - Assert.That(_permService.IsCurrentUserAuthorized(createHttpContextWithUser(new User())), Is.True); + Assert.That(_permService.IsCurrentUserAuthorized(CreateHttpContextWithUser(new User())), Is.True); } [Test] public void IsSiteAdminTestFalse() { - Assert.That(_permService.IsSiteAdmin(createHttpContextWithUser(new User())).Result, Is.False); + Assert.That(_permService.IsSiteAdmin(CreateHttpContextWithUser(new User())).Result, Is.False); } [Test] public void IsSiteAdminTestTrue() { - var httpContext = createHttpContextWithUser(new User { IsAdmin = true }); + var httpContext = CreateHttpContextWithUser(new User { IsAdmin = true }); Assert.That(_permService.IsSiteAdmin(httpContext).Result, Is.True); } [Test] public void HasProjectPermissionTestAdmin() { - var httpContext = createHttpContextWithUser(new User { IsAdmin = true }); - Assert.That(_permService.HasProjectPermission(httpContext, Permission.Archive, "ProjId").Result, Is.True); + var httpContext = CreateHttpContextWithUser(new User { IsAdmin = true }); + Assert.That(_permService.HasProjectPermission(httpContext, Permission.Archive, ProjId).Result, Is.True); + } + + [Test] + public void HasProjectPermissionTestNoProjectRole() + { + var httpContext = CreateHttpContextWithUser(new User()); + Assert.That(_permService.HasProjectPermission(httpContext, Permission.WordEntry, ProjId).Result, Is.False); + } + + [Test] + public void HasProjectPermissionTestProjectPermFalse() + { + var user = new User(); + var httpContext = CreateHttpContextWithUser(user); + var userRole = _userRoleRepo.Create(new UserRole { ProjectId = ProjId, Role = Role.Harvester }).Result; + user.ProjectRoles[ProjId] = userRole.Id; + _ = _userRepo.Update(user.Id, user).Result; + Assert.That(_permService.HasProjectPermission(httpContext, Permission.Import, ProjId).Result, Is.False); + } + + [Test] + public void HasProjectPermissionTestProjectPermTrue() + { + var user = new User(); + var httpContext = CreateHttpContextWithUser(user); + var userRole = _userRoleRepo.Create(new UserRole { ProjectId = ProjId, Role = Role.Owner }).Result; + user.ProjectRoles[ProjId] = userRole.Id; + _ = _userRepo.Update(user.Id, user).Result; + Assert.That(_permService.HasProjectPermission(httpContext, Permission.Import, ProjId).Result, Is.True); } [Test] public void ContainsProjectRoleTestAdmin() { - var httpContext = createHttpContextWithUser(new User { IsAdmin = true }); - Assert.That(_permService.ContainsProjectRole(httpContext, Role.Owner, "project-id").Result, Is.True); + var httpContext = CreateHttpContextWithUser(new User { IsAdmin = true }); + Assert.That(_permService.ContainsProjectRole(httpContext, Role.Owner, ProjId).Result, Is.True); + } + + [Test] + public void ContainsProjectRoleTestNoProjectRole() + { + var httpContext = CreateHttpContextWithUser(new User()); + Assert.That(_permService.ContainsProjectRole(httpContext, Role.Harvester, ProjId).Result, Is.False); + } + + [Test] + public void ContainsProjectRoleTestProjectRoleFalse() + { + var user = new User(); + var httpContext = CreateHttpContextWithUser(user); + var userRole = _userRoleRepo.Create(new UserRole { ProjectId = ProjId, Role = Role.Harvester }).Result; + user.ProjectRoles[ProjId] = userRole.Id; + _ = _userRepo.Update(user.Id, user).Result; + Assert.That(_permService.ContainsProjectRole(httpContext, Role.Editor, ProjId).Result, Is.False); + } + + [Test] + public void ContainsProjectRoleTestProjectRoleTrue() + { + var user = new User(); + var httpContext = CreateHttpContextWithUser(user); + var userRole = _userRoleRepo.Create(new UserRole { ProjectId = ProjId, Role = Role.Owner }).Result; + user.ProjectRoles[ProjId] = userRole.Id; + _ = _userRepo.Update(user.Id, user).Result; + Assert.That(_permService.ContainsProjectRole(httpContext, Role.Harvester, ProjId).Result, Is.True); } } } diff --git a/Backend/Models/UserRole.cs b/Backend/Models/UserRole.cs index 7551f72f18..1ef39749d0 100644 --- a/Backend/Models/UserRole.cs +++ b/Backend/Models/UserRole.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; @@ -108,6 +109,12 @@ public override int GetHashCode() return HashCode.Combine(ProjectId, Role); } + public static bool RoleContainsRole(Role roleA, Role roleB) + { + var permsA = RolePermissions(roleA); + return RolePermissions(roleB).All(perm => permsA.Contains(perm)); + } + public static List RolePermissions(Role role) { return role switch diff --git a/Backend/Services/PermissionService.cs b/Backend/Services/PermissionService.cs index 1cc4609ff9..4d0f8de6be 100644 --- a/Backend/Services/PermissionService.cs +++ b/Backend/Services/PermissionService.cs @@ -1,18 +1,14 @@ using System; -using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Runtime.Serialization; using System.Security.Claims; using System.Text; -using System.Text.Json; using System.Threading.Tasks; using BackendFramework.Helper; using BackendFramework.Interfaces; using BackendFramework.Models; using Microsoft.AspNetCore.Http; using Microsoft.IdentityModel.Tokens; -using MongoDB.Bson; namespace BackendFramework.Services { @@ -21,16 +17,13 @@ public class PermissionService : IPermissionService private readonly IUserRepository _userRepo; private readonly IUserRoleRepository _userRoleRepo; - // TODO: This appears intrinsic to mongodb implementation and is brittle. - private const int ProjIdLength = 24; - private const string ProjPath = "projects/"; - public PermissionService(IUserRepository userRepo, IUserRoleRepository userRoleRepo) { _userRepo = userRepo; _userRoleRepo = userRoleRepo; } + /// Extracts the JWT token from the given HTTP context. private static SecurityToken GetJwt(HttpContext request) { // Get authorization header (i.e. JWT token) @@ -46,46 +39,28 @@ private static SecurityToken GetJwt(HttpContext request) return jsonToken; } + /// Checks whether the given user is authorized. public bool IsUserIdAuthorized(HttpContext request, string userId) { var currentUserId = GetUserId(request); return userId == currentUserId; } - /// - /// Checks whether the current user is authorized. - /// + /// Checks whether the current user is authorized. public bool IsCurrentUserAuthorized(HttpContext request) { var userId = GetUserId(request); return IsUserIdAuthorized(request, userId); } - private static List GetProjectPermissions(HttpContext request) - { - var jsonToken = GetJwt(request); - var userRoleInfo = ((JwtSecurityToken)jsonToken).Payload["UserRoleInfo"].ToString(); - // If unable to parse permissions, return empty permissions. - if (userRoleInfo is null) - { - return new List(); - } - - var permissions = JsonSerializer.Deserialize>(userRoleInfo); - return permissions ?? new List(); - } - + /// Checks whether the current user is a site admin. public async Task IsSiteAdmin(HttpContext request) { - var userId = GetUserId(request); - var user = await _userRepo.GetUser(userId); - if (user is null) - { - return false; - } - return user.IsAdmin; + var user = await _userRepo.GetUser(GetUserId(request)); + return user is not null && user.IsAdmin; } + /// Checks whether the current user has the given project permission. public async Task HasProjectPermission(HttpContext request, Permission permission, string projectId) { var user = await _userRepo.GetUser(GetUserId(request)); @@ -100,10 +75,22 @@ public async Task HasProjectPermission(HttpContext request, Permission per return true; } - return GetProjectPermissions(request).Any( - p => p.ProjectId == projectId && p.Permissions.Contains(permission)); + user.ProjectRoles.TryGetValue(projectId, out var userRoleId); + if (userRoleId is null) + { + return false; + } + + var userRole = await _userRoleRepo.GetUserRole(projectId, userRoleId); + if (userRole is null) + { + return false; + } + + return ProjectRole.RolePermissions(userRole.Role).Contains(permission); } + /// Checks whether the current user has all permissions of the given project role. public async Task ContainsProjectRole(HttpContext request, Role role, string projectId) { var user = await _userRepo.GetUser(GetUserId(request)); @@ -118,21 +105,23 @@ public async Task ContainsProjectRole(HttpContext request, Role role, stri return true; } - // Retrieve JWT token from HTTP request and convert to object - var projectPermissionsList = GetProjectPermissions(request); + user.ProjectRoles.TryGetValue(projectId, out var userRoleId); + if (userRoleId is null) + { + return false; + } - // Assert that the user has all permissions in the specified role - foreach (var projPermissions in projectPermissionsList) + var userRole = await _userRoleRepo.GetUserRole(projectId, userRoleId); + if (userRole is null) { - if (projPermissions.ProjectId != projectId) - { - continue; - } - return ProjectRole.RolePermissions(role).All(p => projPermissions.Permissions.Contains(p)); + return false; } - return false; + + return ProjectRole.RoleContainsRole(userRole.Role, role); } + /// Checks whether the given project user edit is a mismatch with the current user. + /// bool: true if a the userEditIds don't match. public async Task IsViolationEdit(HttpContext request, string userEditId, string projectId) { var userId = GetUserId(request); @@ -179,6 +168,7 @@ public string GetUserId(HttpContext request) return PasswordHash.ValidatePassword(hashedPassword, password) ? await MakeJwt(user) : null; } + /// Creates a JWT token for the given user. public async Task MakeJwt(User user) { const int hoursUntilExpires = 4; @@ -186,34 +176,10 @@ public string GetUserId(HttpContext request) var secretKey = Environment.GetEnvironmentVariable("COMBINE_JWT_SECRET_KEY")!; var key = Encoding.ASCII.GetBytes(secretKey); - // Fetch the projects Id and the roles for each Id - var projectPermissionMap = new List(); - - foreach (var (projectRoleKey, projectRoleValue) in user.ProjectRoles) - { - // Convert each userRoleId to its respective role and add to the mapping - var userRole = await _userRoleRepo.GetUserRole(projectRoleKey, projectRoleValue); - if (userRole is null) - { - return null; - } - - var permissions = ProjectRole.RolePermissions(userRole.Role); - var validEntry = new ProjectPermissions(projectRoleKey, permissions); - projectPermissionMap.Add(validEntry); - } - - var claimString = projectPermissionMap.ToJson(); var tokenDescriptor = new SecurityTokenDescriptor { - Subject = new ClaimsIdentity(new[] - { - new Claim("UserId", user.Id), - new Claim("UserRoleInfo", claimString) - }), - + Subject = new ClaimsIdentity(new[] { new Claim("UserId", user.Id) }), Expires = DateTime.UtcNow.AddHours(hoursUntilExpires), - SigningCredentials = new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; @@ -245,16 +211,5 @@ protected InvalidJwtTokenException(SerializationInfo info, StreamingContext cont : base(info, context) { } } } - - public class ProjectPermissions - { - public ProjectPermissions(string projectId, List permissions) - { - ProjectId = projectId; - Permissions = permissions; - } - public string ProjectId { get; } - public List Permissions { get; } - } }