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

Add Permission Authorization & Assign Role #18

Merged
merged 1 commit into from
Oct 25, 2024
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
4 changes: 3 additions & 1 deletion src/SearchBugs.Api/Endpoints/ProjectsEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using SearchBugs.Application.Projects;
using SearchBugs.Application.Projects.CreateProject;
using SearchBugs.Application.Projects.GetProjects;
using SearchBugs.Infrastructure.Authentication;

namespace SearchBugs.Api.Endpoints;

Expand All @@ -14,7 +16,7 @@ public record CreateProjectRequest(
public static void MapProjectsEndpoints(this IEndpointRouteBuilder app)
{
var projects = app.MapGroup("api/projects");
projects.MapPost("", CreateProject).WithName(nameof(CreateProject));
projects.MapPost("", CreateProject).WithName(nameof(CreateProject)).WithMetadata(new HasPermissionAttribute(ProjectPermission.Create));
projects.MapGet("", GetProjects).WithName(nameof(GetProjects));
}

Expand Down
11 changes: 11 additions & 0 deletions src/SearchBugs.Api/Endpoints/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,24 @@
{

public record UpdateUserRequest(string FirstName, string LastName);
public record AssignRoleRequest(Guid UserId, string Role);

public static void MapUserEndpoints(this IEndpointRouteBuilder app)
{
var users = app.MapGroup("api/users");
users.MapGet("", GetUsers).WithName(nameof(GetUsers));
users.MapGet("{id}", GetUserDetail).WithName(nameof(GetUserDetail));
users.MapPut("{id}", UpdateUser).WithName(nameof(UpdateUser));
users.MapPost("{id}/assign-role", AssignRole).WithName(nameof(AssignRole));
}

public static async Task<IResult> AssignRole(
AssignRoleRequest request,
ISender sender)
{
var command = new AssignRoleCommand(request.UserId, request.Role);

Check failure on line 27 in src/SearchBugs.Api/Endpoints/UserEndpoints.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'AssignRoleCommand' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 27 in src/SearchBugs.Api/Endpoints/UserEndpoints.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'AssignRoleCommand' could not be found (are you missing a using directive or an assembly reference?)
var result = await sender.Send(command);
return Results.Ok(result);
}

public static async Task<IResult> UpdateUser(
Expand Down
2 changes: 1 addition & 1 deletion src/SearchBugs.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private static void Main(string[] args)

// app.UseHttpsRedirection();
app.UseAuthentication();

app.UseStaticFiles();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.Run();
}
Expand Down
4 changes: 4 additions & 0 deletions src/SearchBugs.Api/SearchBugs.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>

</Project>
1 change: 0 additions & 1 deletion src/SearchBugs.Api/SearchBugs.Api.http

This file was deleted.

11 changes: 11 additions & 0 deletions src/SearchBugs.Application/Projects/ProjectPermission.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SearchBugs.Application.Projects;

public static class ProjectPermission
{
public const string Create = "Projects.Create";
public const string Read = "Projects.Read";
public const string Update = "Projects.Update";
public const string Delete = "Projects.Delete";
public const string Assign = "Projects.Assign";
public const string Manage = "Projects.Manage";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Shared.Messaging;

namespace SearchBugs.Application.Users.AssignRole;

public record AssignRoleCommand(Guid UserId, string Role) : ICommand;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using SearchBugs.Domain;
using SearchBugs.Domain.Roles;
using SearchBugs.Domain.Users;
using Shared.Messaging;
using Shared.Results;

namespace SearchBugs.Application.Users.AssignRole;

internal sealed class AssignRoleCommandHandler : ICommandHandler<AssignRoleCommand>
{

private readonly IUserRepository _userRepository;
private readonly IRoleRepository _roleRepository;
private readonly IUnitOfWork _unitOfWork;

public AssignRoleCommandHandler(IUserRepository userRepository, IUnitOfWork unitOfWork, IRoleRepository roleRepository)
{
_userRepository = userRepository;
_unitOfWork = unitOfWork;
_roleRepository = roleRepository;
}


public async Task<Result> Handle(AssignRoleCommand request, CancellationToken cancellationToken)
{

var user = await _userRepository.GetByIdAsync(new UserId(request.UserId), cancellationToken);
if (user.IsFailure)
{
return Result.Failure(user.Error);
}
var role = await _roleRepository.GetByNameAsync(request.Role, cancellationToken);

if (role.IsFailure)
{
return Result.Failure(role.Error);
}

user.Value.AddRole(role.Value);
await _unitOfWork.SaveChangesAsync(cancellationToken);

return Result.Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using FluentValidation;

namespace SearchBugs.Application.Users.AssignRole;

internal class AssignRoleCommandValidator : AbstractValidator<AssignRoleCommand>
{
public AssignRoleCommandValidator()
{
RuleFor(x => x.UserId).NotEmpty()
.Must(x => x != Guid.Empty);
RuleFor(x => x.Role).NotEmpty();
}
}
10 changes: 10 additions & 0 deletions src/SearchBugs.Domain/Roles/IRoleRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Shared.Results;

namespace SearchBugs.Domain.Roles;

public interface IRoleRepository
{
Task<Result<Role>> GetByIdAsync(int roleId, CancellationToken cancellationToken);

Task<Result<Role>> GetByNameAsync(string roleName, CancellationToken cancellationToken);
}
3 changes: 2 additions & 1 deletion src/SearchBugs.Domain/Roles/Role.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ public sealed class Role : Enumeration<Role>
public static readonly Role Reporter = new(4, "Reporter");
public static readonly Role Guest = new(5, "Guest");

public IReadOnlyCollection<User> Users { get; } = new List<User>();
public IReadOnlyCollection<User> Users { get; set; }

public ICollection<Permission> Permissions { get; set; }

public Role(int id, string name)
: base(id, name)
Expand Down
1 change: 1 addition & 0 deletions src/SearchBugs.Domain/Users/UserRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
RoleId = roleId;
}

private UserRole()

Check warning on line 11 in src/SearchBugs.Domain/Users/UserRole.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'UserId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
}
public UserId UserId { get; private set; }

public int RoleId { get; private set; }

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;

namespace SearchBugs.Infrastructure.Authentication;

public sealed class HasPermissionAttribute : AuthorizeAttribute
{
public HasPermissionAttribute(string permission) : base(permission)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace SearchBugs.Infrastructure.Authentication;

public interface IPermissionService
{
Task<HashSet<string>> GetPermissionsAsync(Guid userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using System.IdentityModel.Tokens.Jwt;

namespace SearchBugs.Infrastructure.Authentication;

public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{

private readonly IServiceScopeFactory _serviceScopeFactory;

public PermissionAuthorizationHandler(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
string? userId = context.User.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;

if (!Guid.TryParse(userId, out Guid userIdGuid))
{
return;
}

using IServiceScope scope = _serviceScopeFactory.CreateScope();

IPermissionService permissionService = scope.ServiceProvider.GetRequiredService<IPermissionService>();

HashSet<string> permissions = await permissionService.GetPermissionsAsync(userIdGuid);

if (permissions.Contains(requirement.Permission))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

namespace SearchBugs.Infrastructure.Authentication;

public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
{
}

public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
AuthorizationPolicy? policy = await base.GetPolicyAsync(policyName);

if (policy is not null)
{
return policy;
}

return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new PermissionRequirement(policyName))
.Build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

using Microsoft.AspNetCore.Authorization;

namespace SearchBugs.Infrastructure.Authentication;

public class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; }

public PermissionRequirement(string permission)
{
Permission = permission;
}
}
28 changes: 28 additions & 0 deletions src/SearchBugs.Infrastructure/Authentication/PermissionService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using SearchBugs.Domain.Roles;
using SearchBugs.Domain.Users;
using SearchBugs.Persistence;

namespace SearchBugs.Infrastructure.Authentication;

public class PermissionService : IPermissionService
{
private readonly ApplicationDbContext _dbContext;

public PermissionService(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<HashSet<string>> GetPermissionsAsync(Guid userId)
{
ICollection<Role>[] roles = (ICollection<Role>[])await _dbContext.Set<User>()
.Include(u => u.Roles)
.ThenInclude(ur => ur.Permissions)
.Where(u => u.Id == new UserId(userId))
.Select(u => u.Roles)
.ToArrayAsync();

return roles.SelectMany(r => r.SelectMany(r => r.Permissions.Select(p => p.Name))).ToHashSet();

}
}
8 changes: 7 additions & 1 deletion src/SearchBugs.Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using SearchBugs.Domain.Git;
using SearchBugs.Domain.Services;
Expand All @@ -22,6 +23,7 @@ public static void AddInfrastructure(this IServiceCollection services)
//{
// options.WaitForJobsToComplete = true;
//});
services.AddAuthorization();
services.AddHttpContextAccessor();
services.ConfigureOptions<LoggingBackgroundJobSetup>();

Expand All @@ -35,7 +37,6 @@ public static void AddInfrastructure(this IServiceCollection services)
services.AddScoped<IDataEncryptionService, DataEncryptionService>();
services.AddScoped<IGitHttpService, GitHttpService>();
services.AddScoped<IGitRepositoryService, GitRepositoryService>();

services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
Expand All @@ -45,5 +46,10 @@ public static void AddInfrastructure(this IServiceCollection services)
.AllowCredentials());
});

services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
services.AddScoped<IPermissionService, PermissionService>();


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SearchBugs.Domain\SearchBugs.Domain.csproj" />
<ProjectReference Include="..\SearchBugs.Persistence\SearchBugs.Persistence.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/SearchBugs.Persistence/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using SearchBugs.Domain.Bugs;
using SearchBugs.Domain.Git;
using SearchBugs.Domain.Projects;
using SearchBugs.Domain.Roles;
using SearchBugs.Domain.Users;
using SearchBugs.Persistence.Repositories;
using Shared.Data;
Expand Down Expand Up @@ -37,6 +38,7 @@ public static IServiceCollection AddPersistence(
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IProjectRepository, ProjectRepository>();
services.AddScoped<IGitRepository, GitRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();

services.AddMemoryCache()
.ConfigureOptions<ConnectionStringSetup>();
Expand Down
2 changes: 1 addition & 1 deletion src/SearchBugs.Persistence/Repositories/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public abstract class Repository<TEntity, TEntityId> : IRepository<TEntity, TEnt

public async Task<Result> Add(TEntity entity) => Result.Create(_context.Set<TEntity>().Add(entity));

public async Task<Result> Update(TEntity entity) => Result.Create(_context.Set<TEntity>().Update(entity));
public async Task<Result> Update(TEntity entity) => Result.Create( _context.Set<TEntity>().Update(entity));

public async Task<Result> Remove(TEntity entity) => Result.Create(_context.Set<TEntity>().Remove(entity));

Expand Down
Loading
Loading