Skip to content

Commit

Permalink
Hide members of confidential projects (#1031)
Browse files Browse the repository at this point in the history
* Hide members of confidential projects

Only the following people can see the list of members if a project is
confidential: the manager(s) of the project, site admins, and the org
admin(s) of the org(s) the project belongs to, if any. Everyone else,
including people who are themselves members of the project, cannot see
its membership.

* Hide members list of confidential projects

GQL query will return a users list consisting of only one entry, the
project member currently viewing the page, if he's not allowed to see
the other project members.

* Change confidentiality default for project members

For project members, and ONLY for project members, we will default to
showing them for projects whose confidentiality has not been set. This
isn't the most secure default, but it will avoid what would likely be a
lot of user confusion.

---------

Co-authored-by: Tim Haasdyk <tim_haasdyk@sil.org>
  • Loading branch information
rmunn and myieye authored Sep 27, 2024
1 parent 13d70f2 commit fc5232d
Show file tree
Hide file tree
Showing 11 changed files with 59 additions and 51 deletions.
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

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).ToList();
}
}
}
}
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 @@ -428,6 +428,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

0 comments on commit fc5232d

Please sign in to comment.