Skip to content

Commit

Permalink
Update project permissions without having to log out and back in (#2952)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Apr 17, 2024
1 parent 278c72d commit e72e2eb
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 93 deletions.
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; }
}
}

0 comments on commit e72e2eb

Please sign in to comment.