Skip to content

Commit

Permalink
Merge branch 'develop' into crdt-demo
Browse files Browse the repository at this point in the history
  • Loading branch information
hahn-kev authored Sep 27, 2024
2 parents 9fefc49 + e75aa06 commit 9de8d9a
Show file tree
Hide file tree
Showing 43 changed files with 418 additions and 188 deletions.
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.5",
"version": "8.0.8",
"commands": [
"dotnet-ef"
]
}
}
}
}
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 SIL International
Copyright (c) 2024 SIL Global

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ includes:

tasks:
setup:
deps: [ setup-win, setup-unix, setup-local-env ]
deps: [ setup-win, setup-unix ]
cmds:
- git config blame.ignoreRevsFile .git-blame-ignore-revs
- kubectl --context=docker-desktop apply -f deployment/setup/namespace.yaml
Expand Down
6 changes: 0 additions & 6 deletions backend/FixFwData/FixFwData.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>FixFwData</RootNamespace>
<Description>FixFwData</Description>
<Company>SIL International</Company>
<Authors>SIL International</Authors>
<Product>LexBoxApi Testing</Product>
<Copyright>Copyright © 2023 SIL International</Copyright>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down
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.

11 changes: 10 additions & 1 deletion backend/LexBoxApi/Controllers/LoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ public async Task<ActionResult> LoginRedirect(

await HttpContext.SignInAsync(User,
new AuthenticationProperties { IsPersistent = true });
return Redirect(returnTo);
var destination = ValidateRedirectUrl(returnTo);
return Redirect(destination);
}

private string ValidateRedirectUrl(string url)
{
// Redirect URLs must be relative, to avoid phishing attacks where user is redirected to
// a lookalike site. So we strip off the host if there is one.
var uri = new Uri(url, UriKind.RelativeOrAbsolute);
return uri.IsAbsoluteUri ? uri.PathAndQuery : uri.ToString();
}

[HttpGet("google")]
Expand Down
9 changes: 9 additions & 0 deletions backend/LexBoxApi/Controllers/TestingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using LexBoxApi.Services;
using LexCore.Auth;
using LexCore.Exceptions;
using LexCore.ServiceInterfaces;
using LexData;
using LexData.Entities;
using Microsoft.AspNetCore.Authorization;
Expand All @@ -16,6 +17,7 @@ namespace LexBoxApi.Controllers;
public class TestingController(
LexAuthService lexAuthService,
LexBoxDbContext lexBoxDbContext,
IHgService hgService,
SeedingData seedingData)
: ControllerBase
{
Expand Down Expand Up @@ -84,4 +86,11 @@ public ActionResult ThrowsException()
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public ActionResult Test500NoError() => StatusCode(500);

[HttpGet("test-cleanup-reset-backups")]
[AdminRequired]
public async Task<string[]> TestCleanupResetBackups(bool dryRun = true)
{
return await hgService.CleanupResetBackups(dryRun);
}
}
79 changes: 71 additions & 8 deletions backend/LexBoxApi/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ await HttpContext.SignInAsync(user.GetPrincipal("Registration"),
return Ok(user);
}

[HttpGet("acceptInvitation")]
[RequireAudience(LexboxAudience.RegisterAccount, true)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> HandleInviteLink()
{
var user = _loggedInContext.User;
if (user.Email is null)
{
// Malformed JWT, exit early
return Redirect("/login");
}
var dbUser = await _lexBoxDbContext.Users
.Where(u => u.Email == user.Email)
.Include(u => u.Projects)
.Include(u => u.Organizations)
.FirstOrDefaultAsync();
if (dbUser is null)
{
// Go to frontend to fill out registration page
var queryString = QueryString.Create("email", user.Email);
var returnTo = new UriBuilder { Path = "/acceptInvitation", Query = queryString.Value }.Uri.PathAndQuery;
return Redirect(returnTo);
}
else
{
// No need to re-register users that already exist
UpdateUserMemberships(user, dbUser);
await _lexBoxDbContext.SaveChangesAsync();
var loginUser = new LexAuthUser(dbUser);
await HttpContext.SignInAsync(loginUser.GetPrincipal("Invitation"),
new AuthenticationProperties { IsPersistent = true });
return Redirect("/");
}
}

[HttpPost("acceptInvitation")]
[RequireAudience(LexboxAudience.RegisterAccount, true)]
[ProducesResponseType(StatusCodes.Status200OK)]
Expand All @@ -99,18 +135,29 @@ public async Task<ActionResult<LexAuthUser>> AcceptEmailInvitation(RegisterAccou
}

var jwtUser = _loggedInContext.User;
if (jwtUser.Email != accountInput.Email)
{
// Changing email address in invite links is not allowed; this prevents someone from trying to reuse a JWT belonging to somebody else
ModelState.AddModelError<RegisterAccountInput>(r => r.Email, "email address mismatch in invitation link");
return ValidationProblem(ModelState);
}

var hasExistingUser = await _lexBoxDbContext.Users.FilterByEmailOrUsername(accountInput.Email).AnyAsync();
acceptActivity?.AddTag("app.email_available", !hasExistingUser);
if (hasExistingUser)
var userEntity = await _lexBoxDbContext.Users.FindByEmailOrUsername(accountInput.Email);
acceptActivity?.AddTag("app.email_available", userEntity is null);
if (userEntity is null)
{
userEntity = CreateUserEntity(accountInput, jwtUser);
_lexBoxDbContext.Users.Add(userEntity);
}
else
{
// Multiple invitations accepted by the same account should no longer go through this method, so return an error if the account already exists
// That can only happen if an admin created the user's account while the user was still on the registration page: very unlikely
ModelState.AddModelError<RegisterAccountInput>(r => r.Email, "email already in use");
return ValidationProblem(ModelState);
}

var userEntity = CreateUserEntity(accountInput, jwtUser);
acceptActivity?.AddTag("app.user.id", userEntity.Id);
_lexBoxDbContext.Users.Add(userEntity);
await _lexBoxDbContext.SaveChangesAsync();

var user = new LexAuthUser(userEntity);
Expand Down Expand Up @@ -139,16 +186,32 @@ private User CreateUserEntity(RegisterAccountInput input, LexAuthUser? jwtUser,
Locked = false,
CanCreateProjects = false
};
UpdateUserMemberships(jwtUser, userEntity);
return userEntity;
}
private void UpdateUserMemberships(LexAuthUser? jwtUser, User userEntity)
{
// This audience check is redundant now because of [RequireAudience(LexboxAudience.RegisterAccount, true)], but let's leave it in for safety
if (jwtUser?.Audience == LexboxAudience.RegisterAccount && jwtUser.Projects.Length > 0)
{
userEntity.Projects = jwtUser.Projects.Select(p => new ProjectUsers { Role = p.Role, ProjectId = p.ProjectId }).ToList();
foreach (var p in jwtUser.Projects)
{
if (!userEntity.Projects.Exists(proj => proj.ProjectId == p.ProjectId))
{
userEntity.Projects.Add(new ProjectUsers { Role = p.Role, ProjectId = p.ProjectId });
}
}
}
if (jwtUser?.Audience == LexboxAudience.RegisterAccount && jwtUser.Orgs.Length > 0)
{
userEntity.Organizations = jwtUser.Orgs.Select(o => new OrgMember { Role = o.Role, OrgId = o.OrgId }).ToList();
foreach (var o in jwtUser.Orgs)
{
if (!userEntity.Organizations.Exists(org => org.OrgId == o.OrgId))
{
userEntity.Organizations.Add(new OrgMember { Role = o.Role, OrgId = o.OrgId });
}
}
}
return userEntity;
}

[HttpPost("sendVerificationEmail")]
Expand Down
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();
}
}
}
}
3 changes: 1 addition & 2 deletions backend/LexBoxApi/GraphQL/ProjectMutations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -402,14 +402,13 @@ public async Task<IQueryable<Project>> UpdateProjectLanguageList(string code,
public async Task<IQueryable<Project>> UpdateLangProjectId(string code,
IPermissionService permissionService,
[Service] ProjectService projectService,
[Service] IHgService hgService,
LexBoxDbContext dbContext)
{
var projectId = await projectService.LookupProjectId(code);
await permissionService.AssertCanManageProject(projectId);
var project = await dbContext.Projects.FindAsync(projectId);
NotFoundException.ThrowIfNull(project);
await hgService.GetProjectIdOfFlexProject(code);
await projectService.UpdateProjectLangProjectId(projectId);
return dbContext.Projects.Where(p => p.Id == projectId);
}

Expand Down
3 changes: 1 addition & 2 deletions backend/LexBoxApi/Jobs/CleanupResetBackupJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ protected override async Task ExecuteJob(IJobExecutionContext context)
{
logger.LogInformation("Starting cleanup reset backup job");

//todo implement job
await Task.Delay(TimeSpan.FromSeconds(1));
await hgService.CleanupResetBackups();

logger.LogInformation("Finished cleanup reset backup job");
}
Expand Down
8 changes: 5 additions & 3 deletions backend/LexBoxApi/Services/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ private async Task SendInvitationEmail(
var httpContext = httpContextAccessor.HttpContext;
ArgumentNullException.ThrowIfNull(httpContext);

var queryString = QueryString.Create("email", emailAddress);
var returnTo = new UriBuilder { Path = "/acceptInvitation", Query = queryString.Value }.Uri.PathAndQuery;
var returnTo = _linkGenerator.GetUriByAction(httpContext,
nameof(LexBoxApi.Controllers.UserController.HandleInviteLink),
"User");
var registerLink = _linkGenerator.GetUriByAction(httpContext,
"LoginRedirect",
"Login",
Expand Down Expand Up @@ -217,7 +218,8 @@ await RenderEmail(email,
}
public async Task SendUserAddedEmail(User user, string projectName, string projectCode)
{
var email = StartUserEmail(user) ?? throw new ArgumentNullException("emailAddress");
var email = StartUserEmail(user);
if (email is null) return; // Guest users have no email address, so we won't notify them by email and that's not an error
await RenderEmail(email, new UserAddedEmail(user.Name, user.Email!, projectName, projectCode), user.LocalizationCode);
await SendEmailWithRetriesAsync(email);
}
Expand Down
Loading

0 comments on commit 9de8d9a

Please sign in to comment.