Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update project permissions without having to log out and back in #2952

Merged
merged 9 commits into from
Apr 17, 2024
3 changes: 2 additions & 1 deletion Backend.Tests/Mocks/UserRoleRepositoryMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public Task<List<UserRole>> 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<UserRole?>(foundUserRole.Clone());
}
catch (InvalidOperationException)
Expand Down
82 changes: 70 additions & 12 deletions Backend.Tests/Services/PermissionServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -46,54 +46,112 @@ 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);
}

[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);
}
}
}
7 changes: 7 additions & 0 deletions Backend/Models/UserRole.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Permission> RolePermissions(Role role)
{
return role switch
Expand Down
115 changes: 35 additions & 80 deletions Backend/Services/PermissionService.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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;
}

/// <summary> Extracts the JWT token from the given HTTP context. </summary>
private static SecurityToken GetJwt(HttpContext request)
{
// Get authorization header (i.e. JWT token)
Expand All @@ -46,46 +39,28 @@ private static SecurityToken GetJwt(HttpContext request)
return jsonToken;
}

/// <summary> Checks whether the given user is authorized. </summary>
public bool IsUserIdAuthorized(HttpContext request, string userId)
{
var currentUserId = GetUserId(request);
return userId == currentUserId;
}

/// <summary>
/// Checks whether the current user is authorized.
/// </summary>
/// <summary> Checks whether the current user is authorized. </summary>
public bool IsCurrentUserAuthorized(HttpContext request)
{
var userId = GetUserId(request);
return IsUserIdAuthorized(request, userId);
}

private static List<ProjectPermissions> 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<ProjectPermissions>();
}

var permissions = JsonSerializer.Deserialize<List<ProjectPermissions>>(userRoleInfo);
return permissions ?? new List<ProjectPermissions>();
}

/// <summary> Checks whether the current user is a site admin. </summary>
public async Task<bool> 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;
}

/// <summary> Checks whether the current user has the given project permission. </summary>
public async Task<bool> HasProjectPermission(HttpContext request, Permission permission, string projectId)
{
var user = await _userRepo.GetUser(GetUserId(request));
Expand All @@ -100,10 +75,22 @@ public async Task<bool> 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);
}

/// <summary> Checks whether the current user has all permissions of the given project role. </summary>
public async Task<bool> ContainsProjectRole(HttpContext request, Role role, string projectId)
{
var user = await _userRepo.GetUser(GetUserId(request));
Expand All @@ -118,21 +105,23 @@ public async Task<bool> 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);
}

/// <summary> Checks whether the given project user edit is a mismatch with the current user. </summary>
/// <returns> bool: true if a the userEditIds don't match. </returns>
public async Task<bool> IsViolationEdit(HttpContext request, string userEditId, string projectId)
{
var userId = GetUserId(request);
Expand Down Expand Up @@ -179,41 +168,18 @@ public string GetUserId(HttpContext request)
return PasswordHash.ValidatePassword(hashedPassword, password) ? await MakeJwt(user) : null;
}

/// <summary> Creates a JWT token for the given user. </summary>
public async Task<User?> MakeJwt(User user)
{
const int hoursUntilExpires = 4;
var tokenHandler = new JwtSecurityTokenHandler();
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<ProjectPermissions>();

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)
};
Expand Down Expand Up @@ -245,16 +211,5 @@ protected InvalidJwtTokenException(SerializationInfo info, StreamingContext cont
: base(info, context) { }
}
}

public class ProjectPermissions
{
public ProjectPermissions(string projectId, List<Permission> permissions)
{
ProjectId = projectId;
Permissions = permissions;
}
public string ProjectId { get; }
public List<Permission> Permissions { get; }
}
}

Loading