Skip to content

Commit

Permalink
Merge pull request #22 from iayti/feature/CAS-18_Authorization
Browse files Browse the repository at this point in the history
Feature/cas 18 authorization
  • Loading branch information
iayti committed Nov 23, 2020
2 parents 4cbc117 + 55066f2 commit c76707b
Show file tree
Hide file tree
Showing 29 changed files with 576 additions and 263 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -49,7 +49,9 @@ To use `dotnet-ef` for your migrations please add the following flags to your co

For example, to add a new migration from the root folder:

`dotnet ef migrations add "SampleMigration" --project src\Common\Infrastructure --startup-project src\Apps\WebApi --output-dir Persistence\Migrations`
`dotnet ef migrations add "CreateDb" --project src\Common\Infrastructure --startup-project src\Apps\WebApi --output-dir Persistence\Migrations`

`dotnet ef database update --project src\Common\Infrastructure --startup-project src\Apps\WebApi`

## Overview

Expand Down
94 changes: 50 additions & 44 deletions src/Apps/Client.WorkerService/Client/WebApiClient.cs

Large diffs are not rendered by default.

Expand Up @@ -8,18 +8,19 @@

namespace WebApi.Filters
{
public class ApiExceptionFilter : ExceptionFilterAttribute
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;

public ApiExceptionFilter()
public ApiExceptionFilterAttribute()
{
// Register known exception types and handlers.
_exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
{
{ typeof(ValidationException), HandleValidationException },
{ typeof(NotFoundException), HandleNotFoundException },
{ typeof(UnauthorizeException), HandleNotAuthorizeException },
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
{ typeof(ForbiddenAccessException), HandleForbiddenAccessException },
};
}

Expand Down Expand Up @@ -90,7 +91,19 @@ private void HandleNotFoundException(ExceptionContext context)
context.ExceptionHandled = true;
}

private void HandleNotAuthorizeException(ExceptionContext context)
private void HandleForbiddenAccessException(ExceptionContext context)
{
var details = ServiceResult.Failed(ServiceError.ForbiddenError);

context.Result = new ObjectResult(details)
{
StatusCode = StatusCodes.Status403Forbidden
};

context.ExceptionHandled = true;
}

private void HandleUnauthorizedAccessException(ExceptionContext context)
{
var details = ServiceResult.Failed(ServiceError.ForbiddenError);

Expand Down
3 changes: 2 additions & 1 deletion src/Apps/WebApi/Program.cs
Expand Up @@ -33,8 +33,9 @@ public static async Task Main(string[] args)
}

var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();

await ApplicationDbContextSeed.SeedDefaultUserAsync(userManager);
await ApplicationDbContextSeed.SeedDefaultUserAsync(userManager, roleManager);
await ApplicationDbContextSeed.SeedSampleCityDataAsync(context);
}
catch (Exception ex)
Expand Down
9 changes: 6 additions & 3 deletions src/Apps/WebApi/Startup.cs
Expand Up @@ -11,6 +11,7 @@
using NSwag;
using NSwag.Generation.Processors.Security;
using System.Linq;
using FluentValidation.AspNetCore;
using WebApi.Filters;
using WebApi.Services;

Expand All @@ -32,14 +33,16 @@ public void ConfigureServices(IServiceCollection services)
services.AddApplication();
services.AddInfrastructure(Configuration);//, Environment);

services.AddScoped<ICurrentUserService, CurrentUserService>();
services.AddSingleton<ICurrentUserService, CurrentUserService>();

services.AddHttpContextAccessor();

services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>();

services.AddControllers(options => options.Filters.Add(new ApiExceptionFilter()));
services.AddControllers(options =>
options.Filters.Add<ApiExceptionFilterAttribute>())
.AddFluentValidation();

// Customise default API behaviour
services.Configure<ApiBehaviorOptions>(options =>
Expand Down Expand Up @@ -91,8 +94,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});

app.UseRouting();

app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
Expand Down
5 changes: 3 additions & 2 deletions src/Apps/WebApi/WebApi.csproj
Expand Up @@ -12,6 +12,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="9.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -20,8 +21,8 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="5.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="13.8.2" />
<PackageReference Include="NSwag.MSBuild" Version="13.8.2">
<PackageReference Include="NSwag.AspNetCore" Version="13.9.4" />
<PackageReference Include="NSwag.MSBuild" Version="13.9.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 1 addition & 1 deletion src/Apps/WebApi/wwwroot/api/specification.json
@@ -1,5 +1,5 @@
{
"x-generator": "NSwag v13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0))",
"x-generator": "NSwag v13.9.4.0 (NJsonSchema v10.3.1.0 (Newtonsoft.Json v12.0.0.0))",
"openapi": "3.0.0",
"info": {
"title": "CleanArchitecture API",
Expand Down
2 changes: 1 addition & 1 deletion src/Common/Application/Application.csproj
Expand Up @@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="FluentValidation" Version="9.3.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="9.3.0" />
<PackageReference Include="Mapster" Version="6.5.1" />
<PackageReference Include="Mapster" Version="7.0.0" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
Expand Down
72 changes: 72 additions & 0 deletions src/Common/Application/Common/Behaviours/AuthorizationBehaviour.cs
@@ -0,0 +1,72 @@
using Application.Common.Exceptions;
using Application.Common.Interfaces;
using MediatR;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Application.Common.Security;

namespace 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 ForbiddenAccessException();
}
}
}
}

// User is authorized / authorization not required
return await next();
}
}
}
@@ -0,0 +1,9 @@
using System;

namespace Application.Common.Exceptions
{
public class ForbiddenAccessException : Exception
{
public ForbiddenAccessException() : base() { }
}
}
2 changes: 2 additions & 0 deletions src/Common/Application/Common/Interfaces/IIdentityService.cs
Expand Up @@ -12,6 +12,8 @@ public interface IIdentityService

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

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

Task<Result> DeleteUserAsync(string userId);
}
}
18 changes: 18 additions & 0 deletions src/Common/Application/Common/Security/AuthorizeAttribute.cs
@@ -0,0 +1,18 @@
using System;

namespace Application.Common.Security
{
[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/Common/Application/DependencyInjection.cs
Expand Up @@ -18,6 +18,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services

services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(AuthorizationBehaviour<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>));
Expand Down
@@ -1,4 +1,5 @@
using Application.Common.Interfaces;
using Application.Common.Security;
using Application.Dto;
using Mapster;
using MapsterMapper;
Expand All @@ -10,7 +11,8 @@

namespace Application.Districts.Queries
{
public class ExportDistrictsQuery :IRequest<ExportDto>
[Authorize(Roles = "Administrator")]
public class ExportDistrictsQuery : IRequest<ExportDto>
{
public int CityId { get; set; }
}
Expand Down
Expand Up @@ -2,7 +2,7 @@

namespace Domain.Common
{
public abstract class BaseEntity
public abstract class AuditableEntity
{
public string Creator { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion src/Common/Domain/Entities/City.cs
Expand Up @@ -4,7 +4,7 @@

namespace Domain.Entities
{
public class City : BaseEntity, IHasDomainEvent
public class City : AuditableEntity, IHasDomainEvent
{
public City()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Common/Domain/Entities/District.cs
Expand Up @@ -3,7 +3,7 @@

namespace Domain.Entities
{
public class District : BaseEntity
public class District : AuditableEntity
{
public District()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Common/Domain/Entities/Village.cs
Expand Up @@ -2,7 +2,7 @@

namespace Domain.Entities
{
public class Village : BaseEntity
public class Village : AuditableEntity
{
public int Id { get; set; }

Expand Down
13 changes: 10 additions & 3 deletions src/Common/Infrastructure/DependencyInjection.cs
@@ -1,14 +1,16 @@
using Application.Common.Interfaces;
using System.Text;
using Application.Common.Interfaces;
using Infrastructure.Files;
using Infrastructure.Identity;
using Infrastructure.Persistence;
using Infrastructure.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace Infrastructure
{
Expand Down Expand Up @@ -40,8 +42,12 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped<IDomainEventService, DomainEventService>();

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

services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

services.AddTransient<IDateTime, DateTimeService>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<IEmailService, EmailService>();
Expand All @@ -67,7 +73,8 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
ValidIssuer = configuration["JWT:ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Secret"]))
};
});
})
.AddIdentityServerJwt();

return services;
}
Expand Down
7 changes: 7 additions & 0 deletions src/Common/Infrastructure/Identity/IdentityService.cs
Expand Up @@ -58,6 +58,13 @@ public async Task<(Result Result, string UserId)> CreateUserAsync(string userNam
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
4 changes: 2 additions & 2 deletions src/Common/Infrastructure/Infrastructure.csproj
Expand Up @@ -5,13 +5,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CsvHelper" Version="15.0.10" />
<PackageReference Include="CsvHelper" Version="16.1.0" />
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Expand Up @@ -37,7 +37,7 @@ public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>,

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
foreach (Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<BaseEntity> entry in ChangeTracker.Entries<BaseEntity>())
foreach (Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<AuditableEntity> entry in ChangeTracker.Entries<AuditableEntity>())
{
switch (entry.State)
{
Expand Down

0 comments on commit c76707b

Please sign in to comment.