Skip to content

Commit

Permalink
fix bug with project members query permissions (#1229)
Browse files Browse the repository at this point in the history
* write permission test to ensure that users can't query project members they don't have access to
  • Loading branch information
hahn-kev authored Nov 12, 2024
1 parent fa56386 commit 502df9d
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 13 deletions.
2 changes: 2 additions & 0 deletions backend/LexBoxApi/Services/PermissionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public async ValueTask<bool> CanViewProjectMembers(Guid projectId)
if (User is not null && User.Role == UserRole.admin) return true;
// Project managers can view members of their own projects, even confidential ones
if (await CanManageProject(projectId)) return true;
if (User is null || !User.Projects.Any(p => p.ProjectId == projectId)) return false;

var isConfidential = await projectService.LookupProjectConfidentiality(projectId);
// In this specific case (only), we assume public unless explicitly set to private
return !(isConfidential ?? false);
Expand Down
2 changes: 2 additions & 0 deletions backend/Testing/ApiTests/ApiTestBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using LexCore.Auth;
using Microsoft.Extensions.Http.Resilience;
using Polly;
using Shouldly;
Expand All @@ -15,6 +16,7 @@ public class ApiTestBase
private readonly SocketsHttpHandler _httpClientHandler;
public readonly HttpClient HttpClient;
public string? CurrJwt { get; private set; }
public LexAuthUser CurrentUser => JwtHelper.ToLexAuthUser(CurrJwt!);

public ApiTestBase()
{
Expand Down
6 changes: 1 addition & 5 deletions backend/Testing/ApiTests/FlexJwtTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ namespace Testing.ApiTests;
[Trait("Category", "Integration")]
public class FlexJwtTests : ApiTestBase
{
private static readonly JwtSecurityTokenHandler TokenHandler = new();

private async Task<string> GetFlexJwt()
{
var userJwt = await JwtHelper.GetJwtForUser(new SendReceiveAuth("manager",
Expand All @@ -23,9 +21,7 @@ private async Task<string> GetFlexJwt()

private LexAuthUser ParseUserToken(string jwt)
{
var outputJwt = TokenHandler.ReadJwtToken(jwt);
var principal = new ClaimsPrincipal(new ClaimsIdentity(outputJwt.Claims, "Testing"));
return LexAuthUser.FromClaimsPrincipal(principal) ?? throw new NullReferenceException("User was null");
return JwtHelper.ToLexAuthUser(jwt);
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion backend/Testing/ApiTests/GqlMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@ await Task.WhenAll(
var myProjects = json["data"]!["myProjects"]!.AsArray();
var ids = myProjects.Select(p => p!["id"]!.GetValue<Guid>());

projects.Select(p => p.id).ShouldBeSubsetOf(ids);
projects.Select(p => p.Id).ShouldBeSubsetOf(ids);
}
}
138 changes: 138 additions & 0 deletions backend/Testing/ApiTests/ProjectPermissionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Text.Json.Nodes;
using Shouldly;
using Testing.Services;

namespace Testing.ApiTests;

[Trait("Category", "Integration")]
public class ProjectPermissionTests : ApiTestBase
{
private async Task<JsonObject> QueryProject(string projectCode, bool expectGqlError = false)
{
var json = await ExecuteGql(
$$"""
query {
projectByCode(code: "{{projectCode}}") {
id
name
users {
user {
id
name
}
}
}
}
""",
expectGqlError);
return json;
}

private async Task AddUserToProject(Guid projectId, string username)
{
await ExecuteGql(
$$"""
mutation {
addProjectMember(input: {
projectId: "{{projectId}}",
usernameOrEmail: "{{username}}",
role: EDITOR,
canInvite: false
}) {
project {
id
}
errors {
__typename
... on Error {
message
}
}
}
}
""");
}

private JsonObject GetProject(JsonObject json)
{
var project = json["data"]!["projectByCode"]?.AsObject();
project.ShouldNotBeNull();
return project;
}

private void MustHaveMembers(JsonObject project, int? count = null)
{
var members = project["users"]!.AsArray();
members.ShouldNotBeNull().ShouldNotBeEmpty();
if (count is not null) members.Count.ShouldBe(count.Value);
}

private void MustNotHaveMembers(JsonObject project)
{
var users = project["users"]!.AsArray();
users.ShouldBeEmpty();
}

private void MustHaveOnlyUserAsMember(JsonObject project, Guid userId)
{
var users = project["users"]!.AsArray();
users.ShouldContain(node => node!["user"]!["id"]!.GetValue<Guid>() == userId,
"user list " + users.ToJsonString());
}

[Fact]
public async Task MemberCanSeeProjectMembers()
{
await LoginAs("manager");
await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig());
//refresh jwt
await LoginAs("manager");
var json = GetProject(await QueryProject(project.Code));
MustHaveMembers(json);
}

[Fact]
public async Task NonMemberCannotSeeProjectMembers()
{
await LoginAs("manager");
await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig());
await LoginAs("user");
var json = GetProject(await QueryProject(project.Code));
MustNotHaveMembers(json);
}

[Fact]
public async Task ConfidentialProject_ManagerCanSeeProjectMembers()
{
await LoginAs("manager");
await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig(isConfidential: true));
await LoginAs("manager");
var json = GetProject(await QueryProject(project.Code));
MustHaveMembers(json);
}

[Fact]
public async Task ConfidentialProject_NonManagerCannotSeeProjectMembers()
{
await LoginAs("manager");
await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig(isConfidential: true));
await LoginAs("manager");
await AddUserToProject(project.Id, "editor");
MustHaveMembers(GetProject(await QueryProject(project.Code)), count: 2);
await LoginAs("editor");
var json = GetProject(await QueryProject(project.Code));
MustHaveOnlyUserAsMember(json, CurrentUser.Id);
}

[Fact]
public async Task ConfidentialProject_NonMemberCannotSeeProject()
{
await LoginAs("manager");
await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig(isConfidential: true));
await LoginAs("user");
var json = await QueryProject(project.Code, expectGqlError: true);
var error = json["errors"]!.AsArray().First()?.AsObject();
error.ShouldNotBeNull();
error["extensions"]?["code"]?.GetValue<string>().ShouldBe("AUTH_NOT_AUTHORIZED");
}
}
12 changes: 12 additions & 0 deletions backend/Testing/Services/JwtHelper.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Json;
using LexBoxApi.Auth;
using LexCore.Auth;
using Microsoft.Extensions.Http.Resilience;
using Mono.Unix.Native;
using Polly;
using Shouldly;
using Testing.ApiTests;
Expand Down Expand Up @@ -72,4 +76,12 @@ public static void ClearCookies(SocketsHttpHandler httpClientHandler)
cookie.Expired = true;
}
}

private static readonly JwtSecurityTokenHandler TokenHandler = new();
public static LexAuthUser ToLexAuthUser(string jwt)
{
var outputJwt = TokenHandler.ReadJwtToken(jwt);
var principal = new ClaimsPrincipal(new ClaimsIdentity(outputJwt.Claims, "Testing"));
return LexAuthUser.FromClaimsPrincipal(principal) ?? throw new NullReferenceException("User was null");
}
}
24 changes: 17 additions & 7 deletions backend/Testing/Services/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public static ProjectConfig GetNewProjectConfig(HgProtocol? protocol = null, boo
return new ProjectConfig(id, projectName, projectCode, dir, isConfidential, owningOrgId);
}

public static async Task<LexboxProject> RegisterProjectInLexBox(
this ApiTestBase apiTester,
ProjectConfig config,
bool waitForRepoReady = false)
{
return await RegisterProjectInLexBox(config, apiTester, waitForRepoReady);
}

public static async Task<LexboxProject> RegisterProjectInLexBox(
ProjectConfig config,
ApiTestBase apiTester,
Expand Down Expand Up @@ -65,7 +73,7 @@ ... on DbError {
}
""");
if (waitForRepoReady) await WaitForHgRefreshIntervalAsync();
return new LexboxProject(apiTester, config.Id);
return new LexboxProject(apiTester, config);
}

public static async Task AddMemberToProject(
Expand Down Expand Up @@ -135,20 +143,22 @@ private static string GetNewProjectDir(string projectCode,

public record LexboxProject : IAsyncDisposable
{
public readonly Guid id;
private static string? _jwt;
public Guid Id => _config.Id;
public string Code => _config.Code;
private readonly ProjectConfig _config;
private readonly ApiTestBase _apiTester;
private readonly string _jwt;

public LexboxProject(ApiTestBase apiTester, Guid id)
public LexboxProject(ApiTestBase apiTester, ProjectConfig config)
{
this.id = id;
_config = config;
_apiTester = apiTester;
_jwt = apiTester.CurrJwt ?? throw new InvalidOperationException("No JWT found");
}

public async ValueTask DisposeAsync()
{
var response = await _apiTester.HttpClient.DeleteAsync($"api/project/{id}?jwt={_jwt}");
_jwt ??= await JwtHelper.GetJwtForUser(AdminAuth);
var response = await _apiTester.HttpClient.DeleteAsync($"api/project/{Id}?jwt={_jwt}");
response.EnsureSuccessStatusCode();
}
}
Expand Down

0 comments on commit 502df9d

Please sign in to comment.