feat: add policy based authorization where it was appropriate

This commit is contained in:
cuqmbr 2023-04-24 18:30:14 +03:00
parent 30629f9f71
commit 711846f347
26 changed files with 134 additions and 108 deletions

View File

@ -1,4 +1,3 @@
using System.Dynamic;
using AutoMapper;
using Server.Models;
using SharedModels.DataTransferObjects;
@ -86,4 +85,4 @@ public class MapperInitializer : Profile
CreateMap<RouteAddressDetails, RouteAddressDetailsInVehicleEnrollmentDto>().ReverseMap();
CreateMap<RouteAddressDetails, CreateRouteAddressDetailsInVehicleEnrollmentDto>().ReverseMap();
}
}
}

View File

@ -1,15 +0,0 @@
namespace Server.Constants;
public class Authorization
{
public enum Roles
{
Admin,
User
}
public const string DefaultUsername = "user";
public const string DefaultEmail = "user@email.com";
public const string DefaultPassword = "125ASgl^%@lsdgjk!@#%^12eas";
public const Roles DefaultRole = Roles.User;
}

View File

@ -0,0 +1,17 @@
namespace Server.Constants;
public class Identity
{
public enum Roles
{
User,
Driver,
Company,
Administrator
}
public const string DefaultUsername = "admin";
public const string DefaultEmail = "admin@subdomain.domain";
public const string DefaultPassword = "123qwe!@#QWE";
public const Roles DefaultRole = Roles.Administrator;
}

View File

@ -72,7 +72,7 @@ public class AuthenticationController : ControllerBase
return Ok(authResponse);
}
[Authorize(AuthenticationSchemes = "Bearer")]
[Authorize]
[HttpPost("revoke-session")]
public async Task<IActionResult> RevokeToken()
{

View File

@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Server.Services;
@ -6,6 +7,7 @@ using SharedModels.QueryParameters.Objects;
namespace Server.Controllers;
[Authorize]
[Route("api/cities")]
[ApiController]
public class CityController : ControllerBase
@ -17,6 +19,7 @@ public class CityController : ControllerBase
_cityManagementService = cityManagementService;
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPost]
public async Task<IActionResult> AddCity(CreateCityDto city)
{
@ -30,6 +33,7 @@ public class CityController : ControllerBase
return CreatedAtAction(nameof(GetCity), new {id = result.city.Id}, result.city);
}
[Authorize(Policy = "CompanyAccess")]
[HttpGet]
public async Task<IActionResult> GetCities([FromQuery] CityParameters parameters)
{
@ -45,6 +49,7 @@ public class CityController : ControllerBase
return Ok(result.cities);
}
[Authorize(Policy = "CompanyAccess")]
[HttpGet("{id}")]
public async Task<IActionResult> GetCity(int id, [FromQuery] string? fields)
{
@ -58,6 +63,7 @@ public class CityController : ControllerBase
return Ok(result.city);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPut("{id}")]
public async Task<IActionResult> UpdateCountry(int id, UpdateCityDto city)
{
@ -76,6 +82,7 @@ public class CityController : ControllerBase
return Ok(result.city);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCountry(int id)
{

View File

@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Server.Services;
@ -6,6 +7,7 @@ using SharedModels.QueryParameters.Objects;
namespace Server.Controllers;
[Authorize]
[Route("api/companies")]
[ApiController]
public class CompanyController : ControllerBase
@ -17,6 +19,7 @@ public class CompanyController : ControllerBase
_companyManagementService = companyManagementService;
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPost]
public async Task<IActionResult> AddCompany(CreateCompanyDto company)
{
@ -30,6 +33,7 @@ public class CompanyController : ControllerBase
return CreatedAtAction(nameof(GetCompany), new {id = result.company.Id}, result.company);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpGet]
public async Task<IActionResult> GetCompanies([FromQuery] CompanyParameters parameters)
{
@ -45,6 +49,7 @@ public class CompanyController : ControllerBase
return Ok(result.companies);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpGet("{id}")]
public async Task<IActionResult> GetCompany(int id, [FromQuery] string? fields)
{
@ -58,6 +63,7 @@ public class CompanyController : ControllerBase
return Ok(result.company);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPut("{id}")]
public async Task<IActionResult> UpdateCompany(int id, UpdateCompanyDto company)
{
@ -71,6 +77,7 @@ public class CompanyController : ControllerBase
return Ok(result.company);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCompany(int id)
{

View File

@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Server.Services;
@ -6,6 +7,7 @@ using SharedModels.QueryParameters.Objects;
namespace Server.Controllers;
[Authorize]
[Route("api/countries")]
[ApiController]
public class CountryController : ControllerBase
@ -17,6 +19,7 @@ public class CountryController : ControllerBase
_countryManagementService = countryManagementService;
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPost]
public async Task<IActionResult> AddCountry(CreateCountryDto country)
{
@ -30,6 +33,7 @@ public class CountryController : ControllerBase
return CreatedAtAction(nameof(GetCountry), new {id = result.country.Id}, result.country);
}
[Authorize(Policy = "CompanyAccess")]
[HttpGet]
public async Task<IActionResult> GetCountries([FromQuery] CountryParameters parameters)
{
@ -45,6 +49,7 @@ public class CountryController : ControllerBase
return Ok(result.countries);
}
[Authorize(Policy = "CompanyAccess")]
[HttpGet("{id}")]
public async Task<IActionResult> GetCountry(int id, [FromQuery] string? fields)
{
@ -58,6 +63,7 @@ public class CountryController : ControllerBase
return Ok(result.country);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPut("{id}")]
public async Task<IActionResult> UpdateCountry(int id, UpdateCountryDto country)
{
@ -71,6 +77,7 @@ public class CountryController : ControllerBase
return Ok(result.country);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCountry(int id)
{
@ -83,4 +90,4 @@ public class CountryController : ControllerBase
return NoContent();
}
}
}

View File

@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Server.Services;
@ -6,6 +7,7 @@ using SharedModels.QueryParameters.Objects;
namespace Server.Controllers;
[Authorize]
[Route("api/states")]
[ApiController]
public class StateController : ControllerBase
@ -17,6 +19,7 @@ public class StateController : ControllerBase
_stateManagementService = stateManagementService;
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPost]
public async Task<IActionResult> AddState(CreateStateDto state)
{
@ -30,6 +33,7 @@ public class StateController : ControllerBase
return CreatedAtAction(nameof(GetState), new {id = result.state.Id}, result.state);
}
[Authorize(Policy = "CompanyAccess")]
[HttpGet]
public async Task<IActionResult> GetStates([FromQuery] StateParameters parameters)
{
@ -45,6 +49,7 @@ public class StateController : ControllerBase
return Ok(result.states);
}
[Authorize(Policy = "CompanyAccess")]
[HttpGet("{id}")]
public async Task<IActionResult> GetState(int id, [FromQuery] string? fields)
{
@ -58,6 +63,7 @@ public class StateController : ControllerBase
return Ok(result.state);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpPut("{id}")]
public async Task<IActionResult> UpdateState(int id, UpdateStateDto state)
{
@ -76,6 +82,7 @@ public class StateController : ControllerBase
return Ok(result.state);
}
[Authorize(Policy = "AdministratorAccess")]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteState(int id)
{

View File

@ -10,6 +10,7 @@ public class ApplicationDbContext : IdentityDbContext<User>
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
Database.EnsureCreated();
}
public DbSet<Company> Companies { get; set; } = null!;
@ -26,4 +27,4 @@ public class ApplicationDbContext : IdentityDbContext<User>
public DbSet<TicketGroup> TicketGroups { get; set; } = null!;
public DbSet<Ticket> Tickets { get; set; } = null!;
public DbSet<Review> Reviews { get; set; } = null!;
}
}

View File

@ -1,31 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Server.Constants;
using Server.Models;
namespace Server.Data;
public class ApplicationDbContextSeed
{
public static async Task SeedEssentialsAsync(UserManager<User> userManager,
RoleManager<IdentityRole> roleManager)
{
//Seed Roles
await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.Admin.ToString()));
await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.User.ToString()));
//Seed Default User
var defaultUser = new User
{
UserName = Authorization.DefaultUsername,
Email = Authorization.DefaultEmail,
EmailConfirmed = true,
PhoneNumberConfirmed = true
};
if (userManager.Users.All(u => u.Id != defaultUser.Id))
{
await userManager.CreateAsync(defaultUser, Authorization.DefaultPassword);
await userManager.AddToRoleAsync(defaultUser, Authorization.DefaultRole.ToString());
}
}
}

33
Server/Data/SeedData.cs Normal file
View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Identity;
using Server.Models;
namespace Server.Data;
public class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider)
{
var userManager = (UserManager<User>)serviceProvider.GetService(typeof(UserManager<User>))!;
var roleManager = (RoleManager<IdentityRole>)serviceProvider.GetService(typeof(RoleManager<IdentityRole>))!;
//Seed Roles
foreach (var role in Enum.GetValues(typeof(Constants.Identity.Roles)))
{
await roleManager.CreateAsync(new IdentityRole(role.ToString()));
}
//Seed Default User
var defaultUser = new User
{
UserName = Constants.Identity.DefaultUsername,
Email = Constants.Identity.DefaultEmail,
EmailConfirmed = true
};
if (userManager.Users.All(u => u.Id != defaultUser.Id))
{
await userManager.CreateAsync(defaultUser, Constants.Identity.DefaultPassword);
await userManager.AddToRoleAsync(defaultUser, Constants.Identity.DefaultRole.ToString());
}
}
}

View File

@ -1,6 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using SharedModels.DataTransferObjects;
namespace Server.Models;
@ -31,4 +30,4 @@ public class Address
return $"{City.GetFullName()}, {Name}";
}
}
}

View File

@ -1,6 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using SharedModels.DataTransferObjects;
namespace Server.Models;
@ -29,4 +28,4 @@ public class City
return $"{State.GetFullName()}, {Name}";
}
}
}

View File

@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using SharedModels.DataTransferObjects;
namespace Server.Models;
@ -18,4 +16,4 @@ public class Country
{
return $"{Name}";
}
}
}

View File

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using SharedModels.DataTransferObjects;
namespace Server.Models;
@ -12,4 +11,4 @@ public class Route
public virtual IList<RouteAddress> RouteAddresses { get; set; } = null!;
public virtual IList<VehicleEnrollment> VehicleEnrollments { get; set; } = null!;
}
}

View File

@ -1,6 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using SharedModels.DataTransferObjects;
namespace Server.Models;
@ -26,4 +25,4 @@ public class State
return $"{Country.GetFullName()}, {Name}";
}
}
}

View File

@ -8,12 +8,12 @@ using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Server.Configurations;
using Server.Constants;
using Server.Data;
using Server.Helpers;
using Server.Models;
using Server.Services;
using SharedModels.DataTransferObjects;
using Route = Server.Models.Route;
var builder = WebApplication.CreateBuilder(args);
@ -52,21 +52,21 @@ builder.Services.AddSwaggerGen(options => {
});
});
var corsPolicyName = "defaultCorsPolicy";
builder.Services.AddCors(options => {
options.AddPolicy(corsPolicyName,
policy => policy.WithOrigins("http://localhost:4200").AllowCredentials()
.AllowAnyHeader().AllowAnyMethod());
options.AddDefaultPolicy(policy => policy.AllowAnyOrigin()
.AllowAnyHeader().AllowAnyMethod());
});
builder.Services.AddIdentityCore<User>(options => {
options.User.RequireUniqueEmail = true;
options.Password.RequiredLength = 7;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567889-_.";
}).AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>();
// Configuration from AppSettings
builder.Services.Configure<Jwt>(builder.Configuration.GetSection("Jwt"));
// Adding Authentication - JWT
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
// options.RequireHttpsMetadata = false;
// options.SaveToken = false;
@ -76,22 +76,30 @@ builder.Services.AddAuthentication(options => {
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
builder.Services.AddAuthorization();
builder.Services.AddAuthorization(options => {
// options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
// Policies for accessing endpoints on a top level based on user role
options.AddPolicy(Identity.Roles.User + "Access", policy =>
policy.RequireRole(Identity.Roles.User.ToString()));
options.AddPolicy(Identity.Roles.Driver + "Access", policy =>
policy.RequireRole(Identity.Roles.Driver.ToString(), Identity.Roles.Company.ToString(),
Identity.Roles.Administrator.ToString()));
options.AddPolicy(Identity.Roles.Company + "Access", policy =>
policy.RequireRole(Identity.Roles.Company.ToString(), Identity.Roles.Administrator.ToString()));
options.AddPolicy(Identity.Roles.Administrator + "Access", policy =>
policy.RequireRole(Identity.Roles.Administrator.ToString()));
});
builder.Services.AddAutoMapper(typeof(MapperInitializer));
builder.Services.AddIdentity<User, IdentityRole>(options => {
options.User.RequireUniqueEmail = true;
options.Password.RequiredLength = 8;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_.";
}).AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
builder.Services.AddScoped<ICountryManagementService, CountryManagementService>();
@ -141,6 +149,11 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
var app = builder.Build();
// Data seeding
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider;
await SeedData.Initialize(services);
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@ -148,19 +161,15 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
/*
app.UseHttpsRedirection();
// Data seeding
// using var scope = app.Services.CreateScope();
// var userManager = (UserManager<User>)scope.ServiceProvider.GetService(typeof(UserManager<User>))!;
// var roleManager = (RoleManager<IdentityRole>)scope.ServiceProvider.GetService(typeof(RoleManager<IdentityRole>))!;
// await ApplicationDbContextSeed.SeedEssentialsAsync(userManager, roleManager);
app.MapControllers();
app.UseCors(corsPolicyName);
*/
app.UseAuthentication();
app.UseAuthorization();
app.UseCors();
app.MapControllers();
app.Run();

View File

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />

View File

@ -7,7 +7,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Server.Configurations;
using Server.Constants;
using Server.Models;
using SharedModels.Requests;
using SharedModels.Responses;
@ -45,7 +44,7 @@ public class AuthenticationService : IAuthenticationService
return (false, $"{result.Errors?.First().Description}");
}
await _userManager.AddToRoleAsync(user, Authorization.DefaultRole.ToString());
await _userManager.AddToRoleAsync(user, Constants.Identity.DefaultRole.ToString());
return (true, $"User registered with email {user.Email}.");
}
@ -212,4 +211,4 @@ public class AuthenticationService : IAuthenticationService
ExpiryDateTime = DateTime.UtcNow.AddDays(_jwt.RefreshTokenValidityInDays)
};
}
}
}

View File

@ -1,4 +1,3 @@
using Server.Models;
using SharedModels.Requests;
using SharedModels.Responses;
@ -13,4 +12,4 @@ public interface IAuthenticationService
Task<(bool succeeded, AuthenticationResponse authResponse, string? refreshToken)> RenewRefreshTokenAsync(string? token);
Task<bool> RevokeRefreshToken(string? token);
}
}

View File

@ -1,6 +1,5 @@
using System.Dynamic;
using Microsoft.AspNetCore.Mvc;
using Server.Models;
using SharedModels.DataTransferObjects;
using SharedModels.QueryParameters;
using SharedModels.QueryParameters.Objects;
@ -16,4 +15,4 @@ public interface ICompanyManagementService
Task<(bool isSucceed, IActionResult? actionResult, CompanyDto company)> UpdateCompany(UpdateCompanyDto updateCompanyDto);
Task<(bool isSucceed, IActionResult? actionResult)> DeleteCompany(int id);
Task<bool> IsCompanyExists(int id);
}
}

View File

@ -1,6 +1,5 @@
using System.Dynamic;
using Microsoft.AspNetCore.Mvc;
using Server.Models;
using SharedModels.DataTransferObjects;
using SharedModels.QueryParameters;
using SharedModels.QueryParameters.Objects;
@ -16,4 +15,4 @@ public interface ICountryManagementService
Task<(bool isSucceed, IActionResult? actionResult, CountryDto country)> UpdateCountry(UpdateCountryDto updateCountryDto);
Task<(bool isSucceed, IActionResult? actionResult)> DeleteCountry(int id);
Task<bool> IsCountryExists(int id);
}
}

View File

@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc;
using SharedModels.DataTransferObjects;
using SharedModels.QueryParameters;
using SharedModels.QueryParameters.Objects;
using Route = Server.Models.Route;
namespace Server.Services;
@ -20,4 +19,4 @@ public interface IRouteManagementService
Task<(bool isSucceed, IActionResult? actionResult, UpdateRouteDto route)> UpdateRoute(UpdateRouteDto updateRouteDto);
Task<(bool isSucceed, IActionResult? actionResult)> DeleteRoute(int id);
Task<bool> IsRouteExists(int id);
}
}

View File

@ -1,6 +1,5 @@
using System.Dynamic;
using Microsoft.AspNetCore.Mvc;
using Server.Models;
using SharedModels.QueryParameters;
using SharedModels.QueryParameters.Statistics;
@ -23,4 +22,4 @@ public interface IStatisticsService
Task<(bool IsSucceed, IActionResult? actionResult, IEnumerable<ExpandoObject> stations,
PagingMetadata<ExpandoObject> pagingMetadata)>
GetPopularStations(PopularAddressesParameters parameters);
}
}

View File

@ -12,7 +12,7 @@
"Key": "Secret which will never be exposed",
"Audience": "Application URL",
"Issuer": "Application URL",
"ValidityInMinutes": 1,
"ValidityInMinutes": 60,
"RefreshTokenValidityInDays": 10
}
}

View File

@ -12,7 +12,4 @@ public class RegistrationRequest
[Required(ErrorMessage = "Password is required")]
[DataType(DataType.Password)]
public string Password { get; set; } = null!;
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = null!;
}
}