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

Fix JWT out-of-sync detection middleware and move it into LexQueries #1218

Merged
merged 11 commits into from
Nov 13, 2024
3 changes: 2 additions & 1 deletion backend/LexBoxApi/GraphQL/CustomTypes/OrgGqlConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ protected override void Configure(IObjectTypeDescriptor<Organization> descriptor
{
descriptor.Field(o => o.CreatedDate).IsProjected();
descriptor.Field(o => o.Id).IsProjected(); // Needed for jwt refresh
descriptor.Field(o => o.Id).Use<RefreshJwtOrgMembershipMiddleware>();
//only admins can query members list and projects, custom logic is used for getById
descriptor.Field(o => o.Members).AdminRequired();
descriptor.Field(o => o.Projects).AdminRequired();
Expand All @@ -26,9 +25,11 @@ public class OrgByIdGqlConfiguration : ObjectType<Organization>
protected override void Configure(IObjectTypeDescriptor<Organization> descriptor)
{
descriptor.Name("OrgById");
descriptor.Field(o => o.Id).IsProjected(); // Needed for jwt refresh
descriptor.Field(o => o.Members).Type(ListType<OrgMember>(memberDescriptor =>
{
memberDescriptor.Name("OrgByIdMember");
memberDescriptor.Field(member => member.UserId).IsProjected(); // Needed for jwt refresh
memberDescriptor.Field(member => member.User).Type(ObjectType<User>(userDescriptor =>
{
userDescriptor.Name("OrgByIdUser");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ protected override void Configure(IObjectTypeDescriptor<OrgMember> descriptor)
{
descriptor.Field(f => f.User).Type<NonNullType<UserGqlConfiguration>>();
descriptor.Field(f => f.Organization).Type<NonNullType<ProjectGqlConfiguration>>();
descriptor.Field(f => f.UserId).IsProjected(); // Needed for jwt refresh (not really, but that's a complicated detail)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using LexBoxApi.Auth.Attributes;
using LexCore.Entities;
using LexCore.Entities;

namespace LexBoxApi.GraphQL.CustomTypes;

Expand All @@ -10,8 +9,8 @@ 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>().Use<ProjectMembersVisibilityMiddleware>();
descriptor.Field(p => p.Id).IsProjected(); // Needed for jwt refresh
descriptor.Field(p => p.Users).Use<ProjectMembersVisibilityMiddleware>();
// descriptor.Field("userCount").Resolve(ctx => ctx.Parent<Project>().UserCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class ProjectUsersGqlConfiguration : ObjectType<ProjectUsers>
{
protected override void Configure(IObjectTypeDescriptor<ProjectUsers> descriptor)
{
descriptor.Field(f => f.UserId).IsProjected(); // Needed for jwt refresh
descriptor.Field(f => f.User).Type<NonNullType<UserGqlConfiguration>>();
descriptor.Field(f => f.Project).Type<NonNullType<ProjectGqlConfiguration>>();
}
Expand Down

This file was deleted.

This file was deleted.

68 changes: 68 additions & 0 deletions backend/LexBoxApi/GraphQL/LexAuthUserOutOfSyncExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using LexCore.Auth;
using LexCore.Entities;

namespace LexBoxApi.GraphQL;

public static class LexAuthUserOutOfSyncExtensions
{
public static bool IsOutOfSyncWithMyProjects(this LexAuthUser user, List<Project> myProjects)
{
if (user.IsAdmin) return false; // admins don't have projects in their token
if (user.Projects.Length != myProjects.Count) return true; // different number of projects
return myProjects.Any(proj => user.IsOutOfSyncWithProject(proj, isMyProject: true));
}

public static bool IsOutOfSyncWithMyOrgs(this LexAuthUser user, List<Organization> myOrgs)
{
if (user.IsAdmin) return false; // admins don't have orgs in their token
if (user.Orgs.Length != myOrgs.Count) return true; // different number of orgs
return myOrgs.Any(org => user.IsOutOfSyncWithOrg(org, isMyOrg: true));
}

public static bool IsOutOfSyncWithProject(this LexAuthUser user, Project project, bool isMyProject = false)
{
if (user.IsAdmin) return false; // admins don't have projects in their token

var tokenMembership = user.Projects.SingleOrDefault(p => p.ProjectId == project.Id);

if (project.Users is null)
{
if (tokenMembership is null && isMyProject) return true; // we know we're supposed to be a member
return false; // otherwise, we can't detect differences without users available
}

var dbMembership = project.Users.SingleOrDefault(u => u.UserId == user.Id);

if (tokenMembership is null && dbMembership is null) return false; // both null: they're the same
if (tokenMembership is null || dbMembership is null) return true; // only 1 is null: they're different

var projectRolesAvailable = project.Users.Any(u => u.Role is not ProjectRole.Unknown);
if (!projectRolesAvailable) return false; // we can't detect differences without roles available

return tokenMembership.Role != dbMembership.Role;
}

public static bool IsOutOfSyncWithOrg(this LexAuthUser user, Organization org, bool isMyOrg = false)
{
if (user.IsAdmin) return false; // admins don't have orgs in their token
if (org.Projects?.Any(project => user.IsOutOfSyncWithProject(project)) ?? false) return true;

var tokenMembership = user.Orgs.SingleOrDefault(o => o.OrgId == org.Id);

if (org.Members is null)
{
if (tokenMembership is null && isMyOrg) return true; // we know we're supposed to be a member
return false; // otherwise, we can't detect differences without members available
}

var dbMembership = org.Members.SingleOrDefault(m => m.UserId == user.Id);

if (tokenMembership is null && dbMembership is null) return false; // both null: they're the same
if (tokenMembership is null || dbMembership is null) return true; // only 1 is null: they're different

var orgRolesAvailable = org.Members.Any(u => u.Role is not OrgRole.Unknown);
if (!orgRolesAvailable) return false; // we can't detect differences without roles available

return tokenMembership.Role != dbMembership.Role;
}
}
Loading