Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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