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

Chore/graphql permissions #177

Merged
merged 4 commits into from
Jul 7, 2023
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
137 changes: 0 additions & 137 deletions backend/LexBoxApi/GraphQL/LexMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,141 +14,4 @@ namespace LexBoxApi.GraphQL;
[MutationType]
public class LexMutations
{
[Error<DbError>]
[UseMutationConvention]
public async Task<Project?> CreateProject(
LoggedInContext loggedInContext,
CreateProjectInput input,
[Service] ProjectService projectService,
LexBoxDbContext dbContext)
{
var projectId = await projectService.CreateProject(input, loggedInContext.User.Id);
return await dbContext.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
public async Task<Project> AddProjectMember(AddProjectMemberInput input,
LexBoxDbContext dbContext)
{
var user = await dbContext.Users.FirstOrDefaultAsync(u =>
u.Username == input.UserEmail || u.Email == input.UserEmail);
if (user is null) throw new NotFoundException("Member not found");
dbContext.ProjectUsers.Add(
new ProjectUsers { Role = input.Role, ProjectId = input.ProjectId, UserId = user.Id });
await dbContext.SaveChangesAsync();
return await dbContext.Projects.Where(p => p.Id == input.ProjectId).FirstAsync();
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
public async Task<ProjectUsers> ChangeProjectMemberRole(ChangeProjectMemberRoleInput input,
LexBoxDbContext dbContext)
{
var projectUser = await dbContext.ProjectUsers.FirstOrDefaultAsync(u => u.ProjectId == input.ProjectId && u.UserId == input.UserId);
if (projectUser is null) throw new NotFoundException("Project member not found");
projectUser.Role = input.Role;
await dbContext.SaveChangesAsync();
return projectUser;
}

[Error<NotFoundException>]
[Error<DbError>]
[Error<RequiredException>]
[UseMutationConvention]
public async Task<Project> ChangeProjectName(ChangeProjectNameInput input,
LexBoxDbContext dbContext)
{
if (input.Name.IsNullOrEmpty()) throw new RequiredException("Project name cannot be empty");

var project = await dbContext.Projects.FindAsync(input.ProjectId);
if (project is null) throw new NotFoundException("Project not found");

project.Name = input.Name;
await dbContext.SaveChangesAsync();
return project;
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
public async Task<Project> ChangeProjectDescription(ChangeProjectDescriptionInput input,
LexBoxDbContext dbContext)
{
var project = await dbContext.Projects.FindAsync(input.ProjectId);
if (project is null) throw new NotFoundException("Project not found");

project.Description = input.Description;
await dbContext.SaveChangesAsync();
return project;
}

[UseFirstOrDefault]
[UseProjection]
public async Task<IExecutable<Project>> RemoveProjectMember(RemoveProjectMemberInput input,
LexBoxDbContext dbContext)
{
await dbContext.ProjectUsers.Where(pu => pu.ProjectId == input.ProjectId && pu.UserId == input.UserId)
.ExecuteDeleteAsync();
return dbContext.Projects.Where(p => p.Id == input.ProjectId).AsExecutable();
}

[Error<NotFoundException>]
[Error<DbError>]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[UseMutationConvention]
public async Task<User> ChangeUserAccountData(
LoggedInContext loggedInContext,
ChangeUserAccountDataInput input,
LexBoxDbContext dbContext)
{
var user = await dbContext.Users.FindAsync(input.UserId);
if (user is null) throw new NotFoundException("User not found");
if (loggedInContext.User.Id != input.UserId) throw new UnauthorizedAccessException();
// below works to change email
// minimum email = [email protected]
// if (input.Email is not null && input.Email != ""){
// if (input.Email.Contains("@") == false || input.Email.Length < 3){
// throw new RequiredException("Email does not match requirements");
// }
// user.Email = input.Email;
// }

if (!String.IsNullOrEmpty(input.Name)){
user.Name = input.Name;
}
await dbContext.SaveChangesAsync();
return user;
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
[AdminRequired]
public async Task<User> ChangeUserAccountByAdmin(ChangeUserAccountByAdminInput input, LexBoxDbContext dbContext)
{
var user = await dbContext.Users.FindAsync(input.UserId);
if (user is null) throw new NotFoundException("User not found");
if (!String.IsNullOrEmpty(input.Name)){
user.Name = input.Name;
}
if (!String.IsNullOrEmpty(input.Email)){
user.Email = input.Email;
}
await dbContext.SaveChangesAsync();
return user;
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
[AdminRequired]
public async Task<User> DeleteUserByAdmin(DeleteUserByAdminInput input, LexBoxDbContext dbContext){
var User = await dbContext.Users.FindAsync(input.UserId);
var user = dbContext.Users.Where(u => u.Id == input.UserId);
await user.ExecuteDeleteAsync();
return User;
}
}
3 changes: 2 additions & 1 deletion backend/LexBoxApi/GraphQL/LexQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public IQueryable<Project> Projects(LexBoxDbContext context)

[UseSingleOrDefault]
[UseProjection]
public IQueryable<Project> ProjectByCode(LexBoxDbContext context, string code)
public IQueryable<Project> ProjectByCode(LexBoxDbContext context, LoggedInContext loggedInContext, string code)
{
loggedInContext.User.AssertCanAccessProject(code);
return context.Projects.Where(p => p.Code == code);
}

Expand Down
106 changes: 106 additions & 0 deletions backend/LexBoxApi/GraphQL/ProjectMutations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using LexBoxApi.Auth;
using LexBoxApi.Models.Project;
using LexBoxApi.Services;
using LexCore.Entities;
using LexCore.Exceptions;
using LexData;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;

namespace LexBoxApi.GraphQL;

[MutationType]
public class ProjectMutations
{
[Error<DbError>]
[UseMutationConvention]
public async Task<Project?> CreateProject(
LoggedInContext loggedInContext,
CreateProjectInput input,
[Service] ProjectService projectService,
LexBoxDbContext dbContext)
{
var projectId = await projectService.CreateProject(input, loggedInContext.User.Id);
return await dbContext.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
public async Task<Project> AddProjectMember(LoggedInContext loggedInContext, AddProjectMemberInput input, LexBoxDbContext dbContext)
{
loggedInContext.User.AssertCanManageProject(input.ProjectId);
var user = await dbContext.Users.FirstOrDefaultAsync(u =>
u.Username == input.UserEmail || u.Email == input.UserEmail);
if (user is null) throw new NotFoundException("Member not found");
dbContext.ProjectUsers.Add(
new ProjectUsers { Role = input.Role, ProjectId = input.ProjectId, UserId = user.Id });
await dbContext.SaveChangesAsync();
return await dbContext.Projects.Where(p => p.Id == input.ProjectId).FirstAsync();
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
public async Task<ProjectUsers> ChangeProjectMemberRole(
ChangeProjectMemberRoleInput input,
LoggedInContext loggedInContext,
LexBoxDbContext dbContext)
{
loggedInContext.User.AssertCanManageProject(input.ProjectId);
var projectUser =
await dbContext.ProjectUsers.FirstOrDefaultAsync(u =>
u.ProjectId == input.ProjectId && u.UserId == input.UserId);
if (projectUser is null) throw new NotFoundException("Project member not found");
projectUser.Role = input.Role;
await dbContext.SaveChangesAsync();
return projectUser;
}

[Error<NotFoundException>]
[Error<DbError>]
[Error<RequiredException>]
[UseMutationConvention]
public async Task<Project> ChangeProjectName(ChangeProjectNameInput input,
LoggedInContext loggedInContext,
LexBoxDbContext dbContext)
{
loggedInContext.User.AssertCanManageProject(input.ProjectId);
if (input.Name.IsNullOrEmpty()) throw new RequiredException("Project name cannot be empty");

var project = await dbContext.Projects.FindAsync(input.ProjectId);
if (project is null) throw new NotFoundException("Project not found");

project.Name = input.Name;
await dbContext.SaveChangesAsync();
return project;
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
public async Task<Project> ChangeProjectDescription(ChangeProjectDescriptionInput input,
LoggedInContext loggedInContext,
LexBoxDbContext dbContext)
{
loggedInContext.User.AssertCanManageProject(input.ProjectId);
var project = await dbContext.Projects.FindAsync(input.ProjectId);
if (project is null) throw new NotFoundException("Project not found");

project.Description = input.Description;
await dbContext.SaveChangesAsync();
return project;
}

[UseFirstOrDefault]
[UseProjection]
public async Task<IExecutable<Project>> RemoveProjectMember(RemoveProjectMemberInput input,
LoggedInContext loggedInContext,
LexBoxDbContext dbContext)
{
loggedInContext.User.AssertCanManageProject(input.ProjectId);
await dbContext.ProjectUsers.Where(pu => pu.ProjectId == input.ProjectId && pu.UserId == input.UserId)
.ExecuteDeleteAsync();
return dbContext.Projects.Where(p => p.Id == input.ProjectId).AsExecutable();
}
}
77 changes: 77 additions & 0 deletions backend/LexBoxApi/GraphQL/UserMutations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using LexBoxApi.Auth;
using LexBoxApi.Models.Project;
using LexCore.Entities;
using LexCore.Exceptions;
using LexData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace LexBoxApi.GraphQL;

[MutationType]
public class UserMutations
{
[Error<NotFoundException>]
[Error<DbError>]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[UseMutationConvention]
public async Task<User> ChangeUserAccountData(
LoggedInContext loggedInContext,
ChangeUserAccountDataInput input,
LexBoxDbContext dbContext)
{
if (loggedInContext.User.Id != input.UserId) throw new UnauthorizedAccessException();
var user = await dbContext.Users.FindAsync(input.UserId);
if (user is null) throw new NotFoundException("User not found");
// below works to change email
// minimum email = [email protected]
// if (input.Email is not null && input.Email != ""){
// if (input.Email.Contains("@") == false || input.Email.Length < 3){
// throw new RequiredException("Email does not match requirements");
// }
// user.Email = input.Email;
// }

if (!String.IsNullOrEmpty(input.Name))
{
user.Name = input.Name;
}

await dbContext.SaveChangesAsync();
return user;
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
[AdminRequired]
public async Task<User> ChangeUserAccountByAdmin(ChangeUserAccountByAdminInput input, LexBoxDbContext dbContext)
{
var user = await dbContext.Users.FindAsync(input.UserId);
if (user is null) throw new NotFoundException("User not found");
if (!String.IsNullOrEmpty(input.Name))
{
user.Name = input.Name;
}

if (!String.IsNullOrEmpty(input.Email))
{
user.Email = input.Email;
}

await dbContext.SaveChangesAsync();
return user;
}

[Error<NotFoundException>]
[Error<DbError>]
[UseMutationConvention]
[AdminRequired]
public async Task<User> DeleteUserByAdmin(DeleteUserByAdminInput input, LexBoxDbContext dbContext)
{
var User = await dbContext.Users.FindAsync(input.UserId);
var user = dbContext.Users.Where(u => u.Id == input.UserId);
await user.ExecuteDeleteAsync();
return User;
}
}
24 changes: 22 additions & 2 deletions backend/LexCore/Auth/LexAuthUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public LexAuthUser(User user)
Email = user.Email;
Role = user.IsAdmin ? UserRole.admin : UserRole.user;
Name = user.Name;
Projects = user.Projects.Select(p => new AuthUserProject(p.Project.Code, p.Role)).ToArray();
Projects = user.Projects.Select(p => new AuthUserProject(p.Project.Code, p.Role, p.ProjectId)).ToArray();
}

[JsonPropertyName(LexAuthConstants.IdClaimType)]
Expand Down Expand Up @@ -117,9 +117,29 @@ public ClaimsPrincipal GetPrincipal(string authenticationType)
LexAuthConstants.EmailClaimType,
LexAuthConstants.RoleClaimType));
}

public bool CanManageProject(Guid projectId)
{
return Role == UserRole.admin || Projects.Any(p => p.ProjectId == projectId && p.Role == ProjectRole.Manager);
}

public bool CanManageProject(string projectCode)
{
return Role == UserRole.admin || Projects.Any(p => p.Code == projectCode && p.Role == ProjectRole.Manager);
}

public void AssertCanManageProject(Guid projectId)
{
if (!CanManageProject(projectId)) throw new UnauthorizedAccessException();
}

public void AssertCanAccessProject(string projectCode)
{
if (Role != UserRole.admin && Projects.All(p => p.Code != projectCode)) throw new UnauthorizedAccessException();
}
}

public record AuthUserProject(string Code, ProjectRole Role);
public record AuthUserProject(string Code, ProjectRole Role, Guid ProjectId);

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UserRole
Expand Down
2 changes: 1 addition & 1 deletion backend/Testing/LexCore/LexAuthUserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class LexAuthUserTests
Name = "test",
Projects = new[]
{
new AuthUserProject("test-flex", ProjectRole.Manager)
new AuthUserProject("test-flex", ProjectRole.Manager, Guid.NewGuid())
}
};

Expand Down
Loading