From 712ce4f4dae171ed3e587b54cf16531e5ece79f8 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Fri, 14 Jul 2023 22:23:45 -0700 Subject: [PATCH 01/11] Added notes, db migrations on startup, and injecting admin and user roles --- Newsbot.Collector.Api/Program.cs | 55 ++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/Newsbot.Collector.Api/Program.cs b/Newsbot.Collector.Api/Program.cs index e82802f..843e449 100644 --- a/Newsbot.Collector.Api/Program.cs +++ b/Newsbot.Collector.Api/Program.cs @@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Newsbot.Collector.Api; using Newsbot.Collector.Api.Authentication; +using Newsbot.Collector.Api.Domain; using Newsbot.Collector.Api.Services; using Newsbot.Collector.Database; using Newsbot.Collector.Database.Repositories; @@ -39,10 +40,12 @@ Log.Information("Starting up"); var dbconn = config.GetConnectionString("Database"); builder.Services.AddDbContext(o => o.UseNpgsql(dbconn ?? "")); +// Configure how Identity will be managed builder.Services.AddIdentity() .AddRoles() .AddEntityFrameworkStores(); +// Allow the controllers to access all the table repositories based on the interface builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -60,7 +63,7 @@ builder.Services.AddHangfire(f => f.UseMemoryStorage()); builder.Services.AddHangfireServer(); GlobalConfiguration.Configuration.UseSerilogLogProvider(); -// Add Health Checks +// Build Health Checks builder.Services.AddHealthChecks() .AddNpgSql(config.GetValue(ConfigConnectionStringConst.Database) ?? ""); @@ -75,13 +78,13 @@ builder.Services.Configure(config.GetSection("ConnectionStrin builder.Services.Configure(config.GetSection(ConfigSectionsConst.ConnectionStrings)); builder.Services.Configure(config.GetSection(ConfigSectionsConst.Rss)); builder.Services.Configure(config.GetSection(ConfigSectionsConst.Youtube)); -//builder.Services.Configure< -// Configure JWT for auth +// Configure JWT for auth and load it into DI so we can use it in the controllers var jwtSettings = new JwtSettings(); config.Bind(nameof(jwtSettings), jwtSettings); builder.Services.AddSingleton(jwtSettings); +// Configure how the Token Validation will be handled var tokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, @@ -93,6 +96,7 @@ var tokenValidationParameters = new TokenValidationParameters }; builder.Services.AddSingleton(tokenValidationParameters); +// Build the Authentication that will be used builder.Services.AddAuthentication(x => { x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; @@ -104,9 +108,13 @@ builder.Services.AddAuthentication(x => x.TokenValidationParameters = tokenValidationParameters; }); +// Build the Authorization Policy that the users will conform to. +builder.Services.AddAuthorization(options => + options.AddPolicy(Authorization.AdministratorPolicy, b => b.RequireClaim( Authorization.AdministratorClaim, "true") )); + +// Configure swagger authentication builder.Services.AddSwaggerGen(cfg => { - cfg.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme { Description = "The API key to access the API", @@ -159,7 +167,9 @@ builder.Services.AddSwaggerGen(cfg => var app = builder.Build(); // Configure the HTTP request pipeline. -if (config.GetValue("EnableSwagger")) + +// Enable Swagger if requested based on config +if (config.GetValue(ConfigConst.EnableSwagger)) { app.UseSwagger(); app.UseSwaggerUI(); @@ -167,14 +177,17 @@ if (config.GetValue("EnableSwagger")) app.UseHttpsRedirection(); +// Enable Hangfire background jobs app.UseHangfireDashboard(); BackgroundJobs.SetupRecurringJobs(config); -//app.UseAuthorization(); +app.UseAuthorization(); app.UseAuthentication(); +// Add middleware //app.UseMiddleware(); +// Add HealthChecks app.MapHealthChecks("/health", new HealthCheckOptions { Predicate = _ => true, @@ -182,6 +195,34 @@ app.MapHealthChecks("/health", new HealthCheckOptions }); app.MapControllers(); +// Run Database Migrations if requested +using var serviceScope = app.Services.CreateScope(); +if (config.GetValue(ConfigConst.RunDatabaseMigrationsOnStartup)) +{ + var dbContext = serviceScope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); + +} +else +{ + Log.Warning("Database Migrations have been skipped. Make sure you run them on your own"); +} + +// Inject the roles +var roleManager = serviceScope.ServiceProvider.GetRequiredService>(); +if (!await roleManager.RoleExistsAsync("Administrators")) +{ + var adminRole = new IdentityRole("Administrators"); + await roleManager.CreateAsync(adminRole); +} + +if (!await roleManager.RoleExistsAsync("Users")) +{ + var userRole = new IdentityRole("Users"); + await roleManager.CreateAsync(userRole); +} + +// Start the application app.Run(); @@ -215,4 +256,4 @@ static ILogger GetLogger(IConfiguration configuration) { "service.name", "newsbot-collector-api" } }) .CreateLogger(); -} \ No newline at end of file +} From 0aa6c1489def3c80f5e209f2bb81333360818a3b Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Fri, 14 Jul 2023 22:24:32 -0700 Subject: [PATCH 02/11] Adding roles into the Identity side --- .../Controllers/AccountController.cs | 20 ++++++++++++++++--- .../Domain/AuthorizationRoles.cs | 6 ++++++ .../Domain/Requests/NewRoleRequest.cs | 7 +++++++ .../Services/IdentityService.cs | 14 +++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 Newsbot.Collector.Api/Domain/AuthorizationRoles.cs create mode 100644 Newsbot.Collector.Api/Domain/Requests/NewRoleRequest.cs diff --git a/Newsbot.Collector.Api/Controllers/AccountController.cs b/Newsbot.Collector.Api/Controllers/AccountController.cs index c0b2b36..a0c7d85 100644 --- a/Newsbot.Collector.Api/Controllers/AccountController.cs +++ b/Newsbot.Collector.Api/Controllers/AccountController.cs @@ -1,11 +1,10 @@ -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Newsbot.Collector.Api.Domain; using Newsbot.Collector.Api.Domain.Requests; using Newsbot.Collector.Api.Domain.Response; using Newsbot.Collector.Api.Domain.Results; using Newsbot.Collector.Api.Services; -using Newsbot.Collector.Domain.Dto; -using Newsbot.Collector.Domain.Entities; namespace Newsbot.Collector.Api.Controllers; @@ -71,6 +70,21 @@ public class AccountController : ControllerBase return CheckIfSuccessful(response); } + [HttpPost("addRole")] + [Authorize(Roles = AuthorizationRoles.Administrators)] + public ActionResult AddRole([FromBody] AddRoleRequest request) + { + try + { + _identityService.AddRole(request.RoleName ?? "", request.UserId ?? ""); + return new OkResult(); + } + catch (Exception ex) + { + return new BadRequestResult(); + } + } + private ActionResult CheckIfSuccessful(AuthenticationResult result) { if (!result.IsSuccessful) diff --git a/Newsbot.Collector.Api/Domain/AuthorizationRoles.cs b/Newsbot.Collector.Api/Domain/AuthorizationRoles.cs new file mode 100644 index 0000000..079d6b8 --- /dev/null +++ b/Newsbot.Collector.Api/Domain/AuthorizationRoles.cs @@ -0,0 +1,6 @@ +namespace Newsbot.Collector.Api.Domain; + +public class AuthorizationRoles +{ + public const string Administrators = "Administrators"; +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Domain/Requests/NewRoleRequest.cs b/Newsbot.Collector.Api/Domain/Requests/NewRoleRequest.cs new file mode 100644 index 0000000..706ebef --- /dev/null +++ b/Newsbot.Collector.Api/Domain/Requests/NewRoleRequest.cs @@ -0,0 +1,7 @@ +namespace Newsbot.Collector.Api.Domain.Requests; + +public class AddRoleRequest +{ + public string? RoleName { get; set; } + public string? UserId { get; set; } +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Services/IdentityService.cs b/Newsbot.Collector.Api/Services/IdentityService.cs index 4a48d0e..bd84b33 100644 --- a/Newsbot.Collector.Api/Services/IdentityService.cs +++ b/Newsbot.Collector.Api/Services/IdentityService.cs @@ -16,6 +16,7 @@ public interface IIdentityService AuthenticationResult Register(string email, string password); AuthenticationResult Login(string email, string password); AuthenticationResult RefreshToken(string token, string refreshToken); + void AddRole(string roleName, string userId); } public class IdentityService : IIdentityService @@ -178,6 +179,19 @@ public class IdentityService : IIdentityService return GenerateJwtToken(user.Result); } + public void AddRole(string roleName, string userId) + { + var user = _userManager.FindByIdAsync(userId); + user.Wait(); + + if (user.Result is null) + { + throw new Exception("User was not found"); + } + + _userManager.AddToRoleAsync(user.Result, roleName); + } + private ClaimsPrincipal? CheckTokenSigner(string token) { var tokenHandler = new JwtSecurityTokenHandler(); From d5278a8be98d53013c6b40c4ea38c4380427a747 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Fri, 14 Jul 2023 22:24:53 -0700 Subject: [PATCH 03/11] Added consts to lookup things in config easier --- Newsbot.Collector.Domain/Consts/ConfigConst.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Newsbot.Collector.Domain/Consts/ConfigConst.cs b/Newsbot.Collector.Domain/Consts/ConfigConst.cs index 7aa8227..da62405 100644 --- a/Newsbot.Collector.Domain/Consts/ConfigConst.cs +++ b/Newsbot.Collector.Domain/Consts/ConfigConst.cs @@ -2,6 +2,9 @@ namespace Newsbot.Collector.Domain.Consts; public class ConfigConst { + public const string RunDatabaseMigrationsOnStartup = "RunDatabaseMigrationsOnStartup"; + public const string ApiKeys = "ApiKeys"; + public const string LoggingDefault = "Logging:LogLevel:Default"; public const string SectionConnectionStrings = "ConnectionStrings"; From bc79b507ac455651f578751454cb20bb29533de2 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Fri, 14 Jul 2023 22:25:11 -0700 Subject: [PATCH 04/11] Added a API Key Attribute but not used yet --- .../Filters/ApiKeyAuthAttribute.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Newsbot.Collector.Api/Filters/ApiKeyAuthAttribute.cs diff --git a/Newsbot.Collector.Api/Filters/ApiKeyAuthAttribute.cs b/Newsbot.Collector.Api/Filters/ApiKeyAuthAttribute.cs new file mode 100644 index 0000000..e30f4b2 --- /dev/null +++ b/Newsbot.Collector.Api/Filters/ApiKeyAuthAttribute.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Newsbot.Collector.Domain.Consts; + +namespace Newsbot.Collector.Api.Filters; + +[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method)] +public class ApiKeyAuthAttribute : Attribute, IAsyncActionFilter +{ + private const string ApiKeyHeaderName = "X-API-KEY"; + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var foundKey)) + { + context.Result = new BadRequestResult(); + return; + } + + var config = context.HttpContext.RequestServices.GetRequiredService(); + var apiKeys = config.GetValue(ConfigConst.ApiKeys); + + foreach (var key in apiKeys ?? Array.Empty()) + { + if (key != foundKey) continue; + await next(); + return; + } + + context.Result = new BadRequestResult(); + } +} \ No newline at end of file From 71319c05ef145e5e1950b87e144961e32a3bbdca Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Fri, 14 Jul 2023 22:25:44 -0700 Subject: [PATCH 05/11] Controllers have been updated to support Authorize --- .../Controllers/CodeProjectController.cs | 2 ++ Newsbot.Collector.Api/Controllers/RssController.cs | 2 ++ Newsbot.Collector.Api/Controllers/SourcesController.cs | 4 ++++ Newsbot.Collector.Api/Controllers/YoutubeController.cs | 2 ++ Newsbot.Collector.Api/Controllers/v1/UserController.cs | 9 ++++++--- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Newsbot.Collector.Api/Controllers/CodeProjectController.cs b/Newsbot.Collector.Api/Controllers/CodeProjectController.cs index 11033db..e4ec6d9 100644 --- a/Newsbot.Collector.Api/Controllers/CodeProjectController.cs +++ b/Newsbot.Collector.Api/Controllers/CodeProjectController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using Newsbot.Collector.Api.Domain; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Services.Jobs; @@ -24,6 +25,7 @@ public class CodeProjectController } [HttpPost("check")] + [Authorize(Roles = AuthorizationRoles.Administrators)] public void PullNow() { BackgroundJob.Enqueue(x => x.InitAndExecute(new CodeProjectWatcherJobOptions diff --git a/Newsbot.Collector.Api/Controllers/RssController.cs b/Newsbot.Collector.Api/Controllers/RssController.cs index c97acad..8b13143 100644 --- a/Newsbot.Collector.Api/Controllers/RssController.cs +++ b/Newsbot.Collector.Api/Controllers/RssController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using Newsbot.Collector.Api.Domain; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Domain.Models.Config.Sources; using Newsbot.Collector.Services.Jobs; @@ -27,6 +28,7 @@ public class RssController } [HttpPost("check")] + [Authorize(Roles = AuthorizationRoles.Administrators)] public void CheckReddit() { BackgroundJob.Enqueue(x => x.InitAndExecute(new RssWatcherJobOptions diff --git a/Newsbot.Collector.Api/Controllers/SourcesController.cs b/Newsbot.Collector.Api/Controllers/SourcesController.cs index e6cd0f2..1afda4f 100644 --- a/Newsbot.Collector.Api/Controllers/SourcesController.cs +++ b/Newsbot.Collector.Api/Controllers/SourcesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using Newsbot.Collector.Api.Domain; using Newsbot.Collector.Database.Repositories; using Newsbot.Collector.Domain.Consts; using Newsbot.Collector.Domain.Dto; @@ -199,12 +200,14 @@ public class SourcesController : ControllerBase return SourceDto.Convert(item); } + [Authorize(Roles = AuthorizationRoles.Administrators)] [HttpPost("{id}/disable")] public void Disable(Guid id) { _sources.Disable(id); } + [Authorize(Roles = AuthorizationRoles.Administrators)] [HttpPost("{id}/enable")] public void Enable(Guid id) { @@ -212,6 +215,7 @@ public class SourcesController : ControllerBase } [HttpDelete("{id}")] + [Authorize(Roles = AuthorizationRoles.Administrators)] public void Delete(Guid id, bool purgeOrphanedRecords) { _sources.Delete(id); diff --git a/Newsbot.Collector.Api/Controllers/YoutubeController.cs b/Newsbot.Collector.Api/Controllers/YoutubeController.cs index 6767c79..91c909f 100644 --- a/Newsbot.Collector.Api/Controllers/YoutubeController.cs +++ b/Newsbot.Collector.Api/Controllers/YoutubeController.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using Newsbot.Collector.Api.Domain; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Domain.Models.Config.Sources; using Newsbot.Collector.Services.Jobs; @@ -27,6 +28,7 @@ public class YoutubeController } [HttpPost("check")] + [Authorize(Policy = AuthorizationRoles.Administrators)] public void CheckYoutube() { BackgroundJob.Enqueue(x => x.InitAndExecute(new YoutubeWatcherJobOptions diff --git a/Newsbot.Collector.Api/Controllers/v1/UserController.cs b/Newsbot.Collector.Api/Controllers/v1/UserController.cs index 624525c..9125463 100644 --- a/Newsbot.Collector.Api/Controllers/v1/UserController.cs +++ b/Newsbot.Collector.Api/Controllers/v1/UserController.cs @@ -1,16 +1,19 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Newsbot.Collector.Api.Authentication; using Newsbot.Collector.Domain.Entities; using Newsbot.Collector.Domain.Interfaces; -namespace Newsbot.Collector.Api.Controllers; +namespace Newsbot.Collector.Api.Controllers.v1; +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [ApiController] [Route("api/v1/user")] public class UserController : Controller { - private ILogger _logger; - private IUserSourceSubscription _subscription; + private readonly ILogger _logger; + private readonly IUserSourceSubscription _subscription; public UserController(ILogger logger, IUserSourceSubscription subscription) { From 2bc20fccc82f9998ec2994c396d3c71518c5a88d Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Fri, 14 Jul 2023 22:25:53 -0700 Subject: [PATCH 06/11] docker-compose.example.yaml was updated --- docker-compose.example.yaml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docker-compose.example.yaml b/docker-compose.example.yaml index 0ae311c..65bc1ed 100644 --- a/docker-compose.example.yaml +++ b/docker-compose.example.yaml @@ -28,12 +28,14 @@ services: api: image: newsbot.collector:latest environment: - # Used for database migrations - GOOSE_DRIVER: "postgres" - GOOSE_DBSTRING: "host=localhost user=${PostgresUser} password=${PostgresPassword} dbname=${PostgresDatabaseName} sslmode=disable" - - SERVER_ADDRESS: "localhost" - + # This will run the migrations for you on API Startup. + "RunDatabaseMigrationsOnStartup": true + + # If this is false then /swagger/index.html will not be active on the API + EnableSwagger: true + + JwtSettings__Secret: "ThisNeedsToBeSecretAnd32CharactersLong" + Logging__LogLevel__Default: "Information" Logging__LogLevel__Microsoft.AspNetCore: "Warning" Logging__LogLevel__Hangfire: "Information" @@ -56,6 +58,8 @@ services: # If you want to collect news on Final Fantasy XIV, set this to true FFXIV__IsEnabled: false + + CodeProjects__IsEnabled: true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: "1m" From b9c07eda7db587491c4233b031ad709ece6d7790 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 23 Jul 2023 16:20:16 -0700 Subject: [PATCH 07/11] Refactored how the start up works... everything in one file was getting very messy to read --- Newsbot.Collector.Api/Program.cs | 144 +----------------- .../{ => Startup}/BackgroundJobs.cs | 2 +- .../Startup/DatabaseStartup.cs | 61 ++++++++ .../Startup/IdentityStartup.cs | 50 ++++++ .../Startup/SwaggerStartup.cs | 67 ++++++++ 5 files changed, 187 insertions(+), 137 deletions(-) rename Newsbot.Collector.Api/{ => Startup}/BackgroundJobs.cs (98%) create mode 100644 Newsbot.Collector.Api/Startup/DatabaseStartup.cs create mode 100644 Newsbot.Collector.Api/Startup/IdentityStartup.cs create mode 100644 Newsbot.Collector.Api/Startup/SwaggerStartup.cs diff --git a/Newsbot.Collector.Api/Program.cs b/Newsbot.Collector.Api/Program.cs index 843e449..e3ae5fc 100644 --- a/Newsbot.Collector.Api/Program.cs +++ b/Newsbot.Collector.Api/Program.cs @@ -1,27 +1,13 @@ -using System.Text; using Hangfire; using Hangfire.MemoryStorage; using HealthChecks.UI.Client; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; -using Newsbot.Collector.Api; -using Newsbot.Collector.Api.Authentication; -using Newsbot.Collector.Api.Domain; -using Newsbot.Collector.Api.Services; -using Newsbot.Collector.Database; -using Newsbot.Collector.Database.Repositories; +using Newsbot.Collector.Api.Startup; using Newsbot.Collector.Domain.Consts; -using Newsbot.Collector.Domain.Entities; -using Newsbot.Collector.Domain.Interfaces; using Newsbot.Collector.Domain.Models; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Domain.Models.Config.Sources; using Serilog; -using Serilog.Events; using ILogger = Serilog.ILogger; var builder = WebApplication.CreateBuilder(args); @@ -37,26 +23,10 @@ Log.Logger = GetLogger(config); Log.Information("Starting up"); // configure Entity Framework -var dbconn = config.GetConnectionString("Database"); -builder.Services.AddDbContext(o => o.UseNpgsql(dbconn ?? "")); - -// Configure how Identity will be managed -builder.Services.AddIdentity() - .AddRoles() - .AddEntityFrameworkStores(); - // Allow the controllers to access all the table repositories based on the interface -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Configure Identity -builder.Services.AddScoped(); +DatabaseStartup.BuildDatabase(builder.Services, config); +DatabaseStartup.InjectTableClasses(builder.Services); +DatabaseStartup.InjectIdentityService(builder.Services); // Configure Hangfire builder.Services.AddHangfire(f => f.UseMemoryStorage()); @@ -70,99 +40,14 @@ builder.Services.AddHealthChecks() builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +SwaggerStartup.ConfigureSwagger(builder.Services); builder.Services.Configure(config.GetSection("ConnectionStrings")); builder.Services.Configure(config.GetSection(ConfigSectionsConst.ConnectionStrings)); builder.Services.Configure(config.GetSection(ConfigSectionsConst.Rss)); builder.Services.Configure(config.GetSection(ConfigSectionsConst.Youtube)); -// Configure JWT for auth and load it into DI so we can use it in the controllers -var jwtSettings = new JwtSettings(); -config.Bind(nameof(jwtSettings), jwtSettings); -builder.Services.AddSingleton(jwtSettings); - -// Configure how the Token Validation will be handled -var tokenValidationParameters = new TokenValidationParameters -{ - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret ?? "")), - ValidateIssuer = false, - ValidateAudience = false, - RequireExpirationTime = false, - ValidateLifetime = true -}; -builder.Services.AddSingleton(tokenValidationParameters); - -// Build the Authentication that will be used -builder.Services.AddAuthentication(x => -{ - x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}).AddJwtBearer(x => -{ - x.SaveToken = true; - x.TokenValidationParameters = tokenValidationParameters; -}); - -// Build the Authorization Policy that the users will conform to. -builder.Services.AddAuthorization(options => - options.AddPolicy(Authorization.AdministratorPolicy, b => b.RequireClaim( Authorization.AdministratorClaim, "true") )); - -// Configure swagger authentication -builder.Services.AddSwaggerGen(cfg => -{ - cfg.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme - { - Description = "The API key to access the API", - Type = SecuritySchemeType.ApiKey, - Name = "x-api-key", - In = ParameterLocation.Header, - Scheme = "ApiKeyScheme" - }); - - cfg.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Description = "JWT Authorization Header using the bearer scheme", - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey - }); - - cfg.AddSecurityRequirement(new OpenApiSecurityRequirement - { - //{ - // new OpenApiSecurityScheme - // { - // Reference = new OpenApiReference - // { - // Type = ReferenceType.SecurityScheme, - // Id = "ApiKey" - // }, - // In = ParameterLocation.Header - // }, - // new List() - //}, - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer" - }, - Scheme = "oauth2", - Name = "Bearer", - In = ParameterLocation.Header - }, - new List() - } - }); -}); - +IdentityStartup.DefineJwtRequirements(builder.Services, config); var app = builder.Build(); @@ -199,9 +84,7 @@ app.MapControllers(); using var serviceScope = app.Services.CreateScope(); if (config.GetValue(ConfigConst.RunDatabaseMigrationsOnStartup)) { - var dbContext = serviceScope.ServiceProvider.GetRequiredService(); - dbContext.Database.Migrate(); - + await DatabaseStartup.RunDatabaseMigrationsAsync(serviceScope); } else { @@ -209,18 +92,7 @@ else } // Inject the roles -var roleManager = serviceScope.ServiceProvider.GetRequiredService>(); -if (!await roleManager.RoleExistsAsync("Administrators")) -{ - var adminRole = new IdentityRole("Administrators"); - await roleManager.CreateAsync(adminRole); -} - -if (!await roleManager.RoleExistsAsync("Users")) -{ - var userRole = new IdentityRole("Users"); - await roleManager.CreateAsync(userRole); -} +await DatabaseStartup.InjectIdentityRolesAsync(serviceScope); // Start the application app.Run(); diff --git a/Newsbot.Collector.Api/BackgroundJobs.cs b/Newsbot.Collector.Api/Startup/BackgroundJobs.cs similarity index 98% rename from Newsbot.Collector.Api/BackgroundJobs.cs rename to Newsbot.Collector.Api/Startup/BackgroundJobs.cs index e1e6892..8f6fc02 100644 --- a/Newsbot.Collector.Api/BackgroundJobs.cs +++ b/Newsbot.Collector.Api/Startup/BackgroundJobs.cs @@ -3,7 +3,7 @@ using Newsbot.Collector.Domain.Consts; using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Services.Jobs; -namespace Newsbot.Collector.Api; +namespace Newsbot.Collector.Api.Startup; public static class BackgroundJobs { diff --git a/Newsbot.Collector.Api/Startup/DatabaseStartup.cs b/Newsbot.Collector.Api/Startup/DatabaseStartup.cs new file mode 100644 index 0000000..04495ae --- /dev/null +++ b/Newsbot.Collector.Api/Startup/DatabaseStartup.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Newsbot.Collector.Api.Domain; +using Newsbot.Collector.Api.Services; +using Newsbot.Collector.Database; +using Newsbot.Collector.Database.Repositories; +using Newsbot.Collector.Domain.Interfaces; + +namespace Newsbot.Collector.Api.Startup; + +public class DatabaseStartup +{ + public static void BuildDatabase(IServiceCollection services, IConfiguration config) + { + var dbconn = config.GetConnectionString("Database"); + services.AddDbContext(o => o.UseNpgsql(dbconn ?? "")); + + // Add identity to our ef connection + services.AddIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + } + + public static void InjectTableClasses(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + public static void InjectIdentityService(IServiceCollection services) + { + // Configure Identity + services.AddScoped(); + } + + public static async Task RunDatabaseMigrationsAsync(IServiceScope serviceScope) + { + var dbContext = serviceScope.ServiceProvider.GetRequiredService(); + await dbContext.Database.MigrateAsync(); + } + + public static async Task InjectIdentityRolesAsync(IServiceScope serviceScope) + { + var roleManager = serviceScope.ServiceProvider.GetRequiredService>(); + if (!await roleManager.RoleExistsAsync(Authorization.AdministratorsRole)) + { + await roleManager.CreateAsync(new IdentityRole(Authorization.AdministratorsRole)); + } + + if (!await roleManager.RoleExistsAsync(Authorization.UsersRole)) + { + await roleManager.CreateAsync(new IdentityRole(Authorization.UsersRole)); + } + } +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Startup/IdentityStartup.cs b/Newsbot.Collector.Api/Startup/IdentityStartup.cs new file mode 100644 index 0000000..f233b6d --- /dev/null +++ b/Newsbot.Collector.Api/Startup/IdentityStartup.cs @@ -0,0 +1,50 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Newsbot.Collector.Api.Domain; +using Newsbot.Collector.Domain.Models.Config; + +namespace Newsbot.Collector.Api.Startup; + +public static class IdentityStartup +{ + public static void DefineJwtRequirements(IServiceCollection services, IConfiguration config) + { + // Configure JWT for auth and load it into DI so we can use it in the controllers + + var jwtSettings = new JwtSettings(); + config.Bind(nameof(jwtSettings), jwtSettings); + services.AddSingleton(jwtSettings); + + // Configure how the Token Validation will be handled + var tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret ?? "")), + ValidateIssuer = false, + ValidateAudience = false, + RequireExpirationTime = false, + ValidateLifetime = true + }; + services.AddSingleton(tokenValidationParameters); + + // Build the Authentication that will be used + services.AddAuthentication(x => + { + x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.SaveToken = true; + x.TokenValidationParameters = tokenValidationParameters; + }); + + // Build the Authorization Policy that the users will conform to. + services.AddAuthorization(options => + { + options.AddPolicy(Authorization.AdministratorPolicy, + b => b.RequireClaim(Authorization.AdministratorClaim, "true")); + }); + } +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Startup/SwaggerStartup.cs b/Newsbot.Collector.Api/Startup/SwaggerStartup.cs new file mode 100644 index 0000000..3854979 --- /dev/null +++ b/Newsbot.Collector.Api/Startup/SwaggerStartup.cs @@ -0,0 +1,67 @@ +using Microsoft.OpenApi.Models; + +namespace Newsbot.Collector.Api.Startup; + +public static class SwaggerStartup +{ + public static void ConfigureSwagger(IServiceCollection services) + { + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + + // Configure swagger authentication + services.AddSwaggerGen(cfg => + { + cfg.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme + { + Description = "The API key to access the API", + Type = SecuritySchemeType.ApiKey, + Name = "x-api-key", + In = ParameterLocation.Header, + Scheme = "ApiKeyScheme" + }); + + cfg.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization Header using the bearer scheme", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + + cfg.AddSecurityRequirement(new OpenApiSecurityRequirement + { + //{ + // new OpenApiSecurityScheme + // { + // Reference = new OpenApiReference + // { + // Type = ReferenceType.SecurityScheme, + // Id = "ApiKey" + // }, + // In = ParameterLocation.Header + // }, + // new List() + //}, + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header + }, + new List() + } + }); + }); + } +} \ No newline at end of file From 16e63aa4a12389bb6594257bba50a7222576374a Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 23 Jul 2023 16:20:47 -0700 Subject: [PATCH 08/11] Added the Authorization.cs file to contain policy, claims and roles --- Newsbot.Collector.Api/Domain/Authorization.cs | 12 ++++++++++++ Newsbot.Collector.Api/Domain/AuthorizationRoles.cs | 6 ------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 Newsbot.Collector.Api/Domain/Authorization.cs delete mode 100644 Newsbot.Collector.Api/Domain/AuthorizationRoles.cs diff --git a/Newsbot.Collector.Api/Domain/Authorization.cs b/Newsbot.Collector.Api/Domain/Authorization.cs new file mode 100644 index 0000000..89136a1 --- /dev/null +++ b/Newsbot.Collector.Api/Domain/Authorization.cs @@ -0,0 +1,12 @@ +namespace Newsbot.Collector.Api.Domain; + +public static class Authorization +{ + public const string AdministratorPolicy = "Administrator"; + public const string AdministratorClaim = "administrator"; + + public const string AdministratorsRole = AdministratorPolicy; + + public const string UserPolicy = "User"; + public const string UsersRole = UserPolicy; +} \ No newline at end of file diff --git a/Newsbot.Collector.Api/Domain/AuthorizationRoles.cs b/Newsbot.Collector.Api/Domain/AuthorizationRoles.cs deleted file mode 100644 index 079d6b8..0000000 --- a/Newsbot.Collector.Api/Domain/AuthorizationRoles.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Newsbot.Collector.Api.Domain; - -public class AuthorizationRoles -{ - public const string Administrators = "Administrators"; -} \ No newline at end of file From 64f167c0c5b0efa79ab721a9e05abf2337fe3839 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 23 Jul 2023 16:21:00 -0700 Subject: [PATCH 09/11] added global.json to pin dotnet version --- global.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000..08585a2 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "7.0.100" + } +} \ No newline at end of file From c4a84e9adccb95eb8bcf562525b8426a1941db42 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 23 Jul 2023 16:22:11 -0700 Subject: [PATCH 10/11] moved some rest actions over to roles --- Newsbot.Collector.Api/Controllers/CodeProjectController.cs | 2 +- Newsbot.Collector.Api/Controllers/RssController.cs | 2 +- Newsbot.Collector.Api/Controllers/SourcesController.cs | 6 +++--- Newsbot.Collector.Api/Controllers/YoutubeController.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Newsbot.Collector.Api/Controllers/CodeProjectController.cs b/Newsbot.Collector.Api/Controllers/CodeProjectController.cs index e4ec6d9..4929d39 100644 --- a/Newsbot.Collector.Api/Controllers/CodeProjectController.cs +++ b/Newsbot.Collector.Api/Controllers/CodeProjectController.cs @@ -25,7 +25,7 @@ public class CodeProjectController } [HttpPost("check")] - [Authorize(Roles = AuthorizationRoles.Administrators)] + [Authorize(Roles = Authorization.AdministratorsRole)] public void PullNow() { BackgroundJob.Enqueue(x => x.InitAndExecute(new CodeProjectWatcherJobOptions diff --git a/Newsbot.Collector.Api/Controllers/RssController.cs b/Newsbot.Collector.Api/Controllers/RssController.cs index 8b13143..bfaaa7d 100644 --- a/Newsbot.Collector.Api/Controllers/RssController.cs +++ b/Newsbot.Collector.Api/Controllers/RssController.cs @@ -28,7 +28,7 @@ public class RssController } [HttpPost("check")] - [Authorize(Roles = AuthorizationRoles.Administrators)] + [Authorize(Roles = Authorization.AdministratorsRole)] public void CheckReddit() { BackgroundJob.Enqueue(x => x.InitAndExecute(new RssWatcherJobOptions diff --git a/Newsbot.Collector.Api/Controllers/SourcesController.cs b/Newsbot.Collector.Api/Controllers/SourcesController.cs index 1afda4f..8bd4b27 100644 --- a/Newsbot.Collector.Api/Controllers/SourcesController.cs +++ b/Newsbot.Collector.Api/Controllers/SourcesController.cs @@ -200,14 +200,14 @@ public class SourcesController : ControllerBase return SourceDto.Convert(item); } - [Authorize(Roles = AuthorizationRoles.Administrators)] + [Authorize(Roles = Authorization.AdministratorsRole)] [HttpPost("{id}/disable")] public void Disable(Guid id) { _sources.Disable(id); } - [Authorize(Roles = AuthorizationRoles.Administrators)] + [Authorize(Roles = Authorization.AdministratorsRole)] [HttpPost("{id}/enable")] public void Enable(Guid id) { @@ -215,7 +215,7 @@ public class SourcesController : ControllerBase } [HttpDelete("{id}")] - [Authorize(Roles = AuthorizationRoles.Administrators)] + [Authorize(Roles = Authorization.AdministratorsRole)] public void Delete(Guid id, bool purgeOrphanedRecords) { _sources.Delete(id); diff --git a/Newsbot.Collector.Api/Controllers/YoutubeController.cs b/Newsbot.Collector.Api/Controllers/YoutubeController.cs index 91c909f..b3f34e1 100644 --- a/Newsbot.Collector.Api/Controllers/YoutubeController.cs +++ b/Newsbot.Collector.Api/Controllers/YoutubeController.cs @@ -28,7 +28,7 @@ public class YoutubeController } [HttpPost("check")] - [Authorize(Policy = AuthorizationRoles.Administrators)] + [Authorize(Policy = Authorization.AdministratorsRole)] public void CheckYoutube() { BackgroundJob.Enqueue(x => x.InitAndExecute(new YoutubeWatcherJobOptions From 3697093df1b111ce71909df89646b1105d39fedb Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 23 Jul 2023 16:22:49 -0700 Subject: [PATCH 11/11] Moved AccountController.cs to IdentityController.cs and defined roles on a route --- .../{AccountController.cs => v1/IdentityController.cs} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename Newsbot.Collector.Api/Controllers/{AccountController.cs => v1/IdentityController.cs} (91%) diff --git a/Newsbot.Collector.Api/Controllers/AccountController.cs b/Newsbot.Collector.Api/Controllers/v1/IdentityController.cs similarity index 91% rename from Newsbot.Collector.Api/Controllers/AccountController.cs rename to Newsbot.Collector.Api/Controllers/v1/IdentityController.cs index a0c7d85..ddfd6e7 100644 --- a/Newsbot.Collector.Api/Controllers/AccountController.cs +++ b/Newsbot.Collector.Api/Controllers/v1/IdentityController.cs @@ -6,15 +6,15 @@ using Newsbot.Collector.Api.Domain.Response; using Newsbot.Collector.Api.Domain.Results; using Newsbot.Collector.Api.Services; -namespace Newsbot.Collector.Api.Controllers; +namespace Newsbot.Collector.Api.Controllers.v1; [ApiController] -[Route("/api/account")] -public class AccountController : ControllerBase +[Route("/api/v1/account")] +public class IdentityController : ControllerBase { private IIdentityService _identityService; - public AccountController(IIdentityService identityService) + public IdentityController(IIdentityService identityService) { _identityService = identityService; } @@ -71,7 +71,7 @@ public class AccountController : ControllerBase } [HttpPost("addRole")] - [Authorize(Roles = AuthorizationRoles.Administrators)] + [Authorize(Roles = Authorization.AdministratorsRole)] public ActionResult AddRole([FromBody] AddRoleRequest request) { try