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

Hide members of confidential projects #1031

Merged
merged 8 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 0 additions & 6 deletions backend/LexBoxApi/Auth/Attributes/LexAuthPolicies.cs

This file was deleted.

3 changes: 0 additions & 3 deletions backend/LexBoxApi/Auth/AuthKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public static void AddLexBoxAuth(IServiceCollection services,

services.AddScoped<LexAuthService>();
services.AddSingleton<IAuthorizationHandler, AudienceRequirementHandler>();
services.AddScoped<IAuthorizationHandler, AccessProjectUsersRequirementHandler>();
services.AddSingleton<IAuthorizationHandler, ValidateUserUpdatedHandler>();
services.AddAuthorization(options =>
{
Expand All @@ -54,8 +53,6 @@ public static void AddLexBoxAuth(IServiceCollection services,
options.AddPolicy(AdminRequiredAttribute.PolicyName,
builder => builder.RequireDefaultLexboxAuth()
.RequireAssertion(context => context.User.IsInRole(UserRole.admin.ToString())));
options.AddPolicy(LexAuthPolicies.CanAccessProjectUsers,
builder => builder.RequireDefaultLexboxAuth().AddRequirements(new AccessProjectUsersRequirement()));
options.AddPolicy(VerifiedEmailRequiredAttribute.PolicyName,
builder => builder.RequireDefaultLexboxAuth()
.RequireAssertion(context => !context.User.HasClaim(LexAuthConstants.EmailUnverifiedClaimType, "true")));
Expand Down
myieye marked this conversation as resolved.
Outdated
Show resolved Hide resolved

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected override void Configure(IObjectTypeDescriptor<Project> descriptor)
descriptor.Field(p => p.Code).IsProjected();
descriptor.Field(p => p.CreatedDate).IsProjected();
descriptor.Field(p => p.Id).Use<RefreshJwtProjectMembershipMiddleware>();
descriptor.Field(p => p.Users).Use<RefreshJwtProjectMembershipMiddleware>().Authorize(LexAuthPolicies.CanAccessProjectUsers);
descriptor.Field(p => p.Users).Use<RefreshJwtProjectMembershipMiddleware>().Use<ProjectMembersVisibilityMiddleware>();
// descriptor.Field("userCount").Resolve(ctx => ctx.Parent<Project>().UserCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using HotChocolate.Resolvers;
using LexBoxApi.Auth;
using LexCore.Entities;
using LexCore.Exceptions;
using LexCore.ServiceInterfaces;

namespace LexBoxApi.GraphQL.CustomTypes;

public class ProjectMembersVisibilityMiddleware(FieldDelegate next)
{
public async Task InvokeAsync(IMiddlewareContext context, IPermissionService permissionService, LoggedInContext loggedInContext)
{
await next(context);
if (context.Result is IEnumerable<ProjectUsers> projectUsers)
{
var contextProject = context.Parent<Project>();
var projId = contextProject?.Id ?? throw new RequiredException("Must include project ID in query if querying users");
if (!await permissionService.CanViewProjectMembers(projId))
{
// Confidential project, and user doesn't have permission to see its users, so only show the current user's membership
context.Result = projectUsers.Where(pu => pu.User?.Id == loggedInContext.MaybeUser?.Id);
myieye marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
10 changes: 10 additions & 0 deletions backend/LexBoxApi/Services/PermissionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ public async ValueTask AssertCanViewProject(string projectCode)
if (!await CanViewProject(projectCode)) throw new UnauthorizedAccessException();
}

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;
var isConfidential = await projectService.LookupProjectConfidentiality(projectId);
// In this specific case (only), we assume public unless explicitly set to private
return !(isConfidential ?? false);
}

public async ValueTask<bool> CanManageProject(Guid projectId)
{
if (User is null) return false;
Expand Down
1 change: 1 addition & 0 deletions backend/LexCore/ServiceInterfaces/IPermissionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public interface IPermissionService
ValueTask AssertCanViewProject(Guid projectId);
ValueTask<bool> CanViewProject(string projectCode);
ValueTask AssertCanViewProject(string projectCode);
ValueTask<bool> CanViewProjectMembers(Guid projectId);
ValueTask<bool> CanManageProject(Guid projectId);
ValueTask<bool> CanManageProject(string projectCode);
ValueTask AssertCanManageProject(Guid projectId);
Expand Down
4 changes: 2 additions & 2 deletions frontend/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
schema {
schema {
query: Query
mutation: Mutation
}
Expand Down Expand Up @@ -357,7 +357,7 @@ type Project {
code: String!
createdDate: DateTime!
id: UUID!
users: [ProjectUsers!]! @authorize(policy: "CanAccessProjectUsers")
users: [ProjectUsers!]!
changesets: [Changeset!]!
hasAbandonedTransactions: Boolean!
isLanguageForgeProject: Boolean!
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ the [Linguistics Institute at Payap University](https://li.payap.ac.th/) in Chia
},
"members": {
"title": "Members",
"membership_confidentail": "Membership is confidential",
"filter_members_placeholder": "Filter members...",
"show_all": "Show all...",
"show_less": "Show less",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@
loadingLanguageList = false;
}

// Mirrors PermissionService.CanViewProjectMembers() in C#
$: canViewProjectMembers = user.isAdmin
|| user.orgs.find((o) => project.organizations?.find((org) => org.id === o.orgId))?.role === OrgRole.Admin
|| project?.users?.find((u) => u.user.id == user.id)?.role == ProjectRole.Manager;

let resetProjectModal: ResetProjectModal;
async function resetProject(): Promise<void> {
await resetProjectModal.open(project.code, project.resetStatus);
Expand Down Expand Up @@ -444,6 +449,7 @@
{members}
canManageMember={(member) => canManage && (member.user?.id !== userId || user.isAdmin)}
canManageList={canManage}
canViewMembers={canViewProjectMembers}
on:openUserModal={(event) => userModal.open(event.detail.user)}
on:deleteProjectUser={(event) => deleteProjectUser(event.detail)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
export let canManageMember: (member: Member) => boolean;
export let canManageList: boolean;
export let projectId: string;
export let canViewMembers: boolean;

const dispatch = createEventDispatcher<{
openUserModal: Member;
Expand Down Expand Up @@ -77,16 +78,25 @@
</script>

<div>
<p class="text-2xl mb-4 flex items-baseline gap-4 max-sm:flex-col">
{$t('project_page.members.title')}
<div class="text-2xl mb-4 flex items-baseline gap-4 max-sm:flex-col">
<h2>
{$t('project_page.members.title')}
{#if !canViewMembers}
<span
class="tooltip tooltip-warning text-warning shrink-0 leading-0"
data-tip={$t('project_page.members.membership_confidentail')}>
<Icon icon="i-mdi-shield-lock-outline" size="text-xl" />
</span>
{/if}
</h2>
{#if members?.length > TRUNCATED_MEMBER_COUNT}
<div class="form-control max-w-full w-96">
<PlainInput
placeholder={$t('project_page.members.filter_members_placeholder')}
bind:value={memberSearch} />
</div>
{/if}
</p>
</div>

<BadgeList grid={showMembers.length > TRUNCATED_MEMBER_COUNT}>

Expand Down
Loading