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

Authorization #1187

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
71 changes: 71 additions & 0 deletions src/Application/Common/Behaviours/AuthorizationBehaviour.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using CleanArchitecture.Application.Common.Interfaces;
using CleanArchitecture.Application.Common.Security;
using MediatR;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace CleanArchitecture.Application.Common.Behaviours
{
public class AuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<TRequest> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly IIdentityService _identityService;

public AuthorizationBehaviour(
ILogger<TRequest> logger,
ICurrentUserService currentUserService,
IIdentityService identityService)
{
_logger = logger;
_currentUserService = currentUserService;
_identityService = identityService;
}

public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var authorizeAttributes = request.GetType().GetCustomAttributes<AuthorizeAttribute>();

if (!authorizeAttributes.Any())
{
// Must be authenticated user
if (_currentUserService.UserId == null)
{
throw new UnauthorizedAccessException();
}

var authorizeAttributesWithRoles = authorizeAttributes.Where(a => !string.IsNullOrWhiteSpace(a.Roles));

if (authorizeAttributesWithRoles.Any())
{
foreach (var roles in authorizeAttributesWithRoles.Select(a => a.Roles.Split(',')))
{
var authorized = false;
foreach (var role in roles)
{
var isInRole = await _identityService.UserIsInRole(_currentUserService.UserId, role.Trim());
if (isInRole)
{
authorized = true;
continue;
}
}

// Must be a member of at least one role in roles
if (!authorized)
{
throw new UnauthorizedAccessException();
}
}
}
}

// User is authorized / authorization not required
return await next();
}
}
}
2 changes: 2 additions & 0 deletions src/Application/Common/Interfaces/IIdentityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public interface IIdentityService
{
Task<string> GetUserNameAsync(string userId);

Task<bool> UserIsInRole(string userId, string role);

Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password);

Task<Result> DeleteUserAsync(string userId);
Expand Down
21 changes: 21 additions & 0 deletions src/Application/Common/Security/AuthorizeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace CleanArchitecture.Application.Common.Security
{
/// <summary>
/// Specifies the class this attribute is applied to requires authorization.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class.
/// </summary>
public AuthorizeAttribute() { }

/// <summary>
/// Gets or sets a comma delimited list of roles that are allowed to access the resource.
/// </summary>
public string Roles { get; set; }
}
}
1 change: 1 addition & 0 deletions src/Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehaviour<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
Expand Down
2 changes: 2 additions & 0 deletions src/Application/TodoLists/Queries/GetTodos/GetTodosQuery.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using CleanArchitecture.Application.Common.Interfaces;
using CleanArchitecture.Application.Common.Security;
using CleanArchitecture.Domain.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
Expand All @@ -11,6 +12,7 @@

namespace CleanArchitecture.Application.TodoLists.Queries.GetTodos
{
[Authorize]
public class GetTodosQuery : IRequest<TodosVm>
{
}
Expand Down
2 changes: 2 additions & 0 deletions src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.Infrastructure.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -30,6 +31,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());

services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

services.AddIdentityServer()
Expand Down
7 changes: 7 additions & 0 deletions src/Infrastructure/Identity/IdentityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ public async Task<string> GetUserNameAsync(string userId)
return (result.ToApplicationResult(), user.Id);
}

public async Task<bool> UserIsInRole(string userId, string role)
{
var user = _userManager.Users.SingleOrDefault(u => u.Id == userId);

return await _userManager.IsInRoleAsync(user, role);
}

public async Task<Result> DeleteUserAsync(string userId)
{
var user = _userManager.Users.SingleOrDefault(u => u.Id == userId);
Expand Down
6 changes: 6 additions & 0 deletions tests/Applicaton.IntegrationTests/Testing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,16 @@ public static async Task<string> RunAsUserAsync(string userName, string password

var userManager = scope.ServiceProvider.GetService<UserManager<ApplicationUser>>();

var roleManager = scope.ServiceProvider.GetService<RoleManager<IdentityRole>>();

await roleManager.CreateAsync(new IdentityRole("Admin"));

var user = new ApplicationUser { UserName = userName, Email = userName };

var result = await userManager.CreateAsync(user, password);

await userManager.AddToRoleAsync(user, "Admin");

_currentUserId = user.Id;

return _currentUserId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using CleanArchitecture.Application.TodoLists.Queries.GetTodos;
using CleanArchitecture.Application.Common.Security;
using CleanArchitecture.Application.TodoLists.Queries.GetTodos;
using CleanArchitecture.Domain.Entities;
using FluentAssertions;
using NUnit.Framework;
using System;
using System.Linq;
using System.Threading.Tasks;

Expand All @@ -14,6 +16,8 @@ public class GetTodosTests : TestBase
[Test]
public async Task ShouldReturnPriorityLevels()
{
await RunAsDefaultUserAsync();

var query = new GetTodosQuery();

var result = await SendAsync(query);
Expand All @@ -24,6 +28,8 @@ public async Task ShouldReturnPriorityLevels()
[Test]
public async Task ShouldReturnAllListsAndItems()
{
await RunAsDefaultUserAsync();

await AddAsync(new TodoList
{
Title = "Shopping",
Expand All @@ -46,5 +52,16 @@ await AddAsync(new TodoList
result.Lists.Should().HaveCount(1);
result.Lists.First().Items.Should().HaveCount(7);
}

[Test]
public void ShouldRequireAuthorization()
{
var query = new GetTodosQuery();

query.GetType().Should().BeDecoratedWith<AuthorizeAttribute>();

FluentActions.Invoking(() =>
SendAsync(query)).Should().Throw<UnauthorizedAccessException>();
}
}
}