From 503fac7dffaac940fcbc5bad85a8756dc8cb558d Mon Sep 17 00:00:00 2001 From: Dragos DOBRE Date: Thu, 24 Jan 2019 17:36:33 +0100 Subject: [PATCH] Add custom authorization based on user roles --- .../Logic/FamilyLogic.cs | 2 +- .../Logic/IFamilyLogic.cs | 2 +- .../Logic/IInsureeLogic.cs | 1 + .../Logic/InsureeLogic.cs | 23 ++++++++++- .../OpenImis.RestApi.IntegrationTests.csproj | 5 +++ .../appsettings.json | 18 +++++++++ .../Controllers/FamilyControllerV1.cs | 5 ++- .../Controllers/LoginControllerV1.cs | 9 +++-- .../Controllers/ValuesControllerV1.cs | 6 ++- OpenImis.RestApi/Docs/SwaggerHelper.cs | 2 +- OpenImis.RestApi/OpenImis.RestApi.csproj | 3 ++ .../Security/AuthorizationPolicyProvider.cs | 40 +++++++++++++++++++ .../Security/HasAuthorityHandler.cs | 38 ++++++++++++++++++ .../Security/HasAuthorityRequirement.cs | 20 ++++++++++ OpenImis.RestApi/Startup.cs | 16 +++++++- 15 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 OpenImis.RestApi.IntegrationTests/appsettings.json create mode 100644 OpenImis.RestApi/Security/AuthorizationPolicyProvider.cs create mode 100644 OpenImis.RestApi/Security/HasAuthorityHandler.cs create mode 100644 OpenImis.RestApi/Security/HasAuthorityRequirement.cs diff --git a/OpenImis.Modules/InsureeManagementModule/Logic/FamilyLogic.cs b/OpenImis.Modules/InsureeManagementModule/Logic/FamilyLogic.cs index be1e6cca..b3df94d8 100644 --- a/OpenImis.Modules/InsureeManagementModule/Logic/FamilyLogic.cs +++ b/OpenImis.Modules/InsureeManagementModule/Logic/FamilyLogic.cs @@ -104,7 +104,7 @@ public async Task GetFamilies(int page = 1, int resultsPerP return getFamiliesResponse; } - public async Task AddFamily(FamilyModel family) + public async Task AddFamilyAsync(FamilyModel family) { // Authorize user diff --git a/OpenImis.Modules/InsureeManagementModule/Logic/IFamilyLogic.cs b/OpenImis.Modules/InsureeManagementModule/Logic/IFamilyLogic.cs index 31d167e9..8332d3d5 100644 --- a/OpenImis.Modules/InsureeManagementModule/Logic/IFamilyLogic.cs +++ b/OpenImis.Modules/InsureeManagementModule/Logic/IFamilyLogic.cs @@ -16,7 +16,7 @@ public interface IFamilyLogic Task GetFamilies(int page = 1, int resultsPerPage = 20); - Task AddFamily(FamilyModel family); + Task AddFamilyAsync(FamilyModel family); Task UpdateFamilyAsync(int familyId, FamilyModel family); diff --git a/OpenImis.Modules/InsureeManagementModule/Logic/IInsureeLogic.cs b/OpenImis.Modules/InsureeManagementModule/Logic/IInsureeLogic.cs index 15b574a9..f3e3be8d 100644 --- a/OpenImis.Modules/InsureeManagementModule/Logic/IInsureeLogic.cs +++ b/OpenImis.Modules/InsureeManagementModule/Logic/IInsureeLogic.cs @@ -20,5 +20,6 @@ public interface IInsureeLogic /// InsureeModel Task GetInsureeByInsureeIdAsync(string insureeId); + } } diff --git a/OpenImis.Modules/InsureeManagementModule/Logic/InsureeLogic.cs b/OpenImis.Modules/InsureeManagementModule/Logic/InsureeLogic.cs index 0b65f47d..b8466826 100644 --- a/OpenImis.Modules/InsureeManagementModule/Logic/InsureeLogic.cs +++ b/OpenImis.Modules/InsureeManagementModule/Logic/InsureeLogic.cs @@ -1,6 +1,8 @@ using System.Threading.Tasks; using OpenImis.Modules.InsureeManagementModule.Repositories; using OpenImis.Modules.InsureeManagementModule.Models; +using OpenImis.Modules.InsureeManagementModule.Validators; +using System.ComponentModel.DataAnnotations; namespace OpenImis.Modules.InsureeManagementModule.Logic { @@ -12,10 +14,12 @@ public class InsureeLogic: IInsureeLogic protected readonly IInsureeRepository insureeRepository; protected readonly IImisModules imisModules; + protected IValidator insureeNumberValidator; - public InsureeLogic(IImisModules imisModules) + public InsureeLogic(IImisModules imisModules) { insureeRepository = new InsureeRepository(); + this.insureeNumberValidator = new InsureeNumberValidator(null); this.imisModules = imisModules; } @@ -40,5 +44,22 @@ public async Task GetInsureeByInsureeIdAsync(string insureeId) return insuree; } + public async Task IsUniqueInsureeAsync(string insureeId) + { + bool validInsuree = false; + + UniqueInsureeNumberValidator uniqueInsureeNumberValidator = new UniqueInsureeNumberValidator(this, insureeNumberValidator); + + try + { + await uniqueInsureeNumberValidator.ValidateAsync(insureeId); + } + catch (ValidationException e) + { + return false; + } + return validInsuree; + } + } } diff --git a/OpenImis.RestApi.IntegrationTests/OpenImis.RestApi.IntegrationTests.csproj b/OpenImis.RestApi.IntegrationTests/OpenImis.RestApi.IntegrationTests.csproj index 9921ba69..3582498a 100644 --- a/OpenImis.RestApi.IntegrationTests/OpenImis.RestApi.IntegrationTests.csproj +++ b/OpenImis.RestApi.IntegrationTests/OpenImis.RestApi.IntegrationTests.csproj @@ -13,11 +13,16 @@ + + + Always + PreserveNewest + Always PreserveNewest diff --git a/OpenImis.RestApi.IntegrationTests/appsettings.json b/OpenImis.RestApi.IntegrationTests/appsettings.json new file mode 100644 index 00000000..6509d20a --- /dev/null +++ b/OpenImis.RestApi.IntegrationTests/appsettings.json @@ -0,0 +1,18 @@ +{ + "JwtIssuer": "http://openimis.org", + "JwtAudience": "http://openimis.org", + "JwtExpireDays": 5, + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/OpenImis.RestApi/Controllers/FamilyControllerV1.cs b/OpenImis.RestApi/Controllers/FamilyControllerV1.cs index dd5d4741..d81529df 100644 --- a/OpenImis.RestApi/Controllers/FamilyControllerV1.cs +++ b/OpenImis.RestApi/Controllers/FamilyControllerV1.cs @@ -14,7 +14,6 @@ namespace OpenImis.RestApi.Controllers { [ApiVersion("1")] - [Authorize(Roles = "IMISAdmin, EnrollmentOfficer")] [Route("api/family")] [ApiController] [EnableCors("AllowSpecificOrigin")] @@ -44,6 +43,7 @@ public FamilyControllerV1(IImisModules imisModules) /// Returns the list of families /// If the request is incomplete /// If the token is missing, is wrong or expired + [Authorize("EnrollmentOfficer")] [HttpGet] [ProducesResponseType(typeof(GetFamiliesResponse), 200)] [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] @@ -57,6 +57,7 @@ public async Task GetFamilies([FromQuery]int page = 1, [FromQuery } // GET api/ws/family/00001 + [Authorize("IMISAdmin")] [HttpGet("insuree/{insureeId}", Name = "GetFamilyByInsureeId")] public async Task GetFamilyByInsureeId(string insureeId) { @@ -110,7 +111,7 @@ public async Task AddNewFamily([FromBody]FamilyModel family) FamilyModel newFamily; try { - newFamily = await _imisModules.GetInsureeManagementModule().GetFamilyLogic().AddFamily(family); + newFamily = await _imisModules.GetInsureeManagementModule().GetFamilyLogic().AddFamilyAsync(family); } catch (ValidationException e) { diff --git a/OpenImis.RestApi/Controllers/LoginControllerV1.cs b/OpenImis.RestApi/Controllers/LoginControllerV1.cs index 0a0a7e31..7aab1356 100644 --- a/OpenImis.RestApi/Controllers/LoginControllerV1.cs +++ b/OpenImis.RestApi/Controllers/LoginControllerV1.cs @@ -77,14 +77,17 @@ public async Task Login([FromBody]LoginRequestModel request) new Claim(ClaimTypes.Name, request.Username) }; - var roles = user.GetRolesStringArray(); + /*var roles = user.GetRolesStringArray(); foreach (var role in roles) { claims = claims.Append(new Claim(ClaimTypes.Role, role)); - } + }*/ - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(user.PrivateKey)); + //claims = claims.Append(new Claim("scope", "read:messages")); + + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(user.PrivateKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( diff --git a/OpenImis.RestApi/Controllers/ValuesControllerV1.cs b/OpenImis.RestApi/Controllers/ValuesControllerV1.cs index f06e4278..b04d168b 100644 --- a/OpenImis.RestApi/Controllers/ValuesControllerV1.cs +++ b/OpenImis.RestApi/Controllers/ValuesControllerV1.cs @@ -23,8 +23,10 @@ public async Task Get() return Ok(result); } - // GET api/values/5 - [HttpGet("{id}")] + // GET api/values/5 + //[Authorize("read:messages")] + [Authorize("MedicalOfficer")] + [HttpGet("{id}")] public async Task Get(int id) { return Ok(id); diff --git a/OpenImis.RestApi/Docs/SwaggerHelper.cs b/OpenImis.RestApi/Docs/SwaggerHelper.cs index e0b8a6de..05288058 100644 --- a/OpenImis.RestApi/Docs/SwaggerHelper.cs +++ b/OpenImis.RestApi/Docs/SwaggerHelper.cs @@ -91,7 +91,7 @@ public static void ConfigureSwaggerUI(SwaggerUIOptions swaggerUIOptions) var apiVersions = GetApiVersions(webApiAssembly); foreach (var apiVersion in apiVersions) { - swaggerUIOptions.SwaggerEndpoint($"/RestApi/api-docs/v{apiVersion}/swagger.json", $"V{apiVersion} Docs"); + swaggerUIOptions.SwaggerEndpoint($"/api-docs/v{apiVersion}/swagger.json", $"V{apiVersion} Docs"); } swaggerUIOptions.RoutePrefix = "api-docs"; swaggerUIOptions.InjectStylesheet("theme-feeling-blue-v2.css"); diff --git a/OpenImis.RestApi/OpenImis.RestApi.csproj b/OpenImis.RestApi/OpenImis.RestApi.csproj index f166d970..873989b4 100644 --- a/OpenImis.RestApi/OpenImis.RestApi.csproj +++ b/OpenImis.RestApi/OpenImis.RestApi.csproj @@ -84,6 +84,9 @@ Always + + Always + diff --git a/OpenImis.RestApi/Security/AuthorizationPolicyProvider.cs b/OpenImis.RestApi/Security/AuthorizationPolicyProvider.cs new file mode 100644 index 00000000..abf1cb03 --- /dev/null +++ b/OpenImis.RestApi/Security/AuthorizationPolicyProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace OpenImis.RestApi.Security +{ + public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider + { + private readonly AuthorizationOptions _options; + private readonly IConfiguration _configuration; + + public AuthorizationPolicyProvider(IOptions options, IConfiguration configuration) : base(options) + { + _options = options.Value; + _configuration = configuration; + } + + public override async Task GetPolicyAsync(string policyName) + { + // Check static policies first + var policy = await base.GetPolicyAsync(policyName); + + if (policy == null) + { + policy = new AuthorizationPolicyBuilder() + .AddRequirements(new HasAuthorityRequirement(policyName, _configuration["JwtIssuer"])) + .Build(); + + // Add policy to the AuthorizationOptions, so we don't have to re-create it each time + _options.AddPolicy(policyName, policy); + } + + return policy; + } + } +} diff --git a/OpenImis.RestApi/Security/HasAuthorityHandler.cs b/OpenImis.RestApi/Security/HasAuthorityHandler.cs new file mode 100644 index 00000000..ab505a99 --- /dev/null +++ b/OpenImis.RestApi/Security/HasAuthorityHandler.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Authorization; +using OpenImis.Modules; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace OpenImis.RestApi.Security +{ + public class HasAuthorityHandler : AuthorizationHandler + { + IImisModules _imisModules; + + public HasAuthorityHandler(IImisModules imisModules) + { + _imisModules = imisModules; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasAuthorityRequirement requirement) + { + // If user does not have the scope claim, get out of here + if (!context.User.HasClaim(c => c.Type == ClaimTypes.Name && c.Issuer == requirement.Issuer)) + return Task.CompletedTask; + + // Split the scopes string into an array + //var scopes = context.User.FindFirst(c => c.Type == ClaimTypes.Name && c.Issuer == requirement.Issuer).Value.Split(' '); + var username = context.User.FindFirst(claim => claim.Type == ClaimTypes.Name).Value; + var scopes = _imisModules.GetUserModule().GetUserController().GetByUsername(username).GetRolesStringArray(); + + // Succeed if the scope array contains the required scope + if (scopes.Any(s => s == requirement.Authority)) + context.Succeed(requirement); + + return Task.CompletedTask; + } + } +} diff --git a/OpenImis.RestApi/Security/HasAuthorityRequirement.cs b/OpenImis.RestApi/Security/HasAuthorityRequirement.cs new file mode 100644 index 00000000..b9afb92d --- /dev/null +++ b/OpenImis.RestApi/Security/HasAuthorityRequirement.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace OpenImis.RestApi.Security +{ + public class HasAuthorityRequirement : IAuthorizationRequirement + { + public string Issuer { get; } + public string Authority { get; } + + public HasAuthorityRequirement(string authority, string issuer) + { + Authority = authority ?? throw new ArgumentNullException(nameof(authority)); + Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer)); + } + } +} diff --git a/OpenImis.RestApi/Startup.cs b/OpenImis.RestApi/Startup.cs index 0e7e038d..6116f420 100644 --- a/OpenImis.RestApi/Startup.cs +++ b/OpenImis.RestApi/Startup.cs @@ -17,6 +17,7 @@ using OpenImis.RestApi.Docs; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Authorization; namespace OpenImis.RestApi { @@ -54,7 +55,18 @@ public void ConfigureServices(IServiceCollection services) options.SecurityTokenValidators.Add(new IMISJwtSecurityTokenHandler(services.BuildServiceProvider().GetService())); }); - services.AddMvc() + services.AddAuthorization(); + //(options => + //{ + // options.AddPolicy("MedicalOfficer", policy => policy.Requirements.Add(new HasAuthorityRequirement("MedicalOfficer", Configuration["JwtIssuer"]))); + // options.AddPolicy("EnrollmentOfficer", policy => policy.Requirements.Add(new HasAuthorityRequirement("EnrollmentOfficer", Configuration["JwtIssuer"]))); + //}); + + // register the scope authorization handler + services.AddSingleton(); + services.AddSingleton(); + + services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddApiVersioning(o => { @@ -89,7 +101,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseSwaggerUI(SwaggerHelper.ConfigureSwaggerUI); } - app.UseAuthentication(); + app.UseAuthentication(); app.UseMvc(); app.UseCors("AllowSpecificOrigin");