Feature/Auth: last one was authentication, this one is authorization #4

Merged
homeburger merged 33 commits from feature/auth into main 2026-04-23 00:18:39 -05:00
6 changed files with 27 additions and 12 deletions
Showing only changes of commit 152db3d99f - Show all commits

View File

@@ -1,5 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -249,6 +250,10 @@ namespace agologum_api.Migrations
b.Property<string>("PasswordHash") b.Property<string>("PasswordHash")
.HasColumnType("text"); .HasColumnType("text");
b.PrimitiveCollection<List<string>>("Permissions")
.IsRequired()
.HasColumnType("text[]");
b.Property<string>("PhoneNumber") b.Property<string>("PhoneNumber")
.HasColumnType("text"); .HasColumnType("text");

View File

@@ -49,11 +49,9 @@ builder.Services.AddAuthentication(options => {
builder.Services.AddAuthorization(options => { builder.Services.AddAuthorization(options => {
options.AddPolicy("SensitiveDataRead", policy => options.AddPolicy("SensitiveDataRead", policy =>
policy.RequireRole("admin", "superuser")); policy.RequireClaim("permission", Permission.SensitiveData_Read));
options.AddPolicy("SensitiveDataModify", policy => options.AddPolicy("SensitiveDataModify", policy =>
policy.RequireRole("superuser")); policy.RequireClaim("permission", Permission.SensitiveData_Modify));
// TODO: policies are read at runtime. define policy names in a central place and distribute the symbol
}); });

View File

@@ -20,7 +20,7 @@ public class UsersController : ControllerBase {
service_ = service; service_ = service;
} }
[Authorize(Policy = "SensitiveDataRead")] [Authorize(Policy = Permission.SensitiveData_Read)]
[HttpGet] [HttpGet]
public async Task<ActionResult<List<User>>> getUsers() { public async Task<ActionResult<List<User>>> getUsers() {
List<User> rawArray = await service_.GetAll(); List<User> rawArray = await service_.GetAll();
@@ -42,7 +42,7 @@ public class UsersController : ControllerBase {
return Ok(dtoArray); return Ok(dtoArray);
} }
[Authorize(Policy = "SensitiveDataRead")] [Authorize(Policy = Permission.SensitiveData_Read)]
[HttpGet("{id:int}")] [HttpGet("{id:int}")]
public async Task<ActionResult<User>> getUser(string id) { public async Task<ActionResult<User>> getUser(string id) {
@@ -60,7 +60,7 @@ public class UsersController : ControllerBase {
return Ok(newDto); return Ok(newDto);
} }
[Authorize(Policy = "SensitiveDataModify")] [Authorize(Policy = Permission.SensitiveData_Modify)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<ActionResult> deleteUser(string id) { public async Task<ActionResult> deleteUser(string id) {
@@ -68,6 +68,11 @@ public class UsersController : ControllerBase {
if (!success) return NotFound(); if (!success) return NotFound();
// TODO: set safeguard to no delete the current user
return NoContent(); return NoContent();
} }
// TODO: add controls on editing roles
} }

View File

@@ -7,6 +7,8 @@ public class User : IdentityUser {
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public List<string> Permissions { get; set; } = [ Permission.SensitiveData_Read, Permission.SensitiveData_Modify ]; // just seeding these here initially
// properties inherited from IdentityUser: // properties inherited from IdentityUser:
/* /*
AccessFailedCount: Gets or sets the number of failed login attempts for the current user. AccessFailedCount: Gets or sets the number of failed login attempts for the current user.
@@ -49,7 +51,8 @@ public class LoginDto {
public class UserDto { public class UserDto {
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // gets compressed to a string public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // gets compressed to a string'
public List<string> permissions { get; set; } = [];
public string? Email { get; set; } = ""; public string? Email { get; set; } = "";
public string Id { get; set; } = ""; public string Id { get; set; } = "";
public string? UserName { get; set; } = ""; public string? UserName { get; set; } = "";

View File

@@ -31,21 +31,22 @@ public class JwtService {
if(user.UserName == null) return null; if(user.UserName == null) return null;
var roles = await userManager_.GetRolesAsync(user);
// not too sure // not too sure
var claims = new List<Claim> { var claims = new List<Claim> {
new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
}; };
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); List<string> permissions = user.Permissions;
foreach(var perm in permissions) {
claims.Add(new Claim("permission", perm));
}
var token = new JwtSecurityToken( var token = new JwtSecurityToken(
issuer: "agologum", issuer: "agologum",
audience: "agologum", audience: "agologum",
claims: claims, claims: claims,
expires: DateTime.UtcNow.AddHours(2), // will add a refresher later expires: DateTime.UtcNow.AddHours(2),
signingCredentials: creds signingCredentials: creds
); );

View File

@@ -87,4 +87,7 @@ api.interceptors.response.use(response => response, async error => { // mainly f
return Promise.reject(error); return Promise.reject(error);
}) })
// TODO: if you get a 403 while navigating then redirect to the last authenticated page
// if you gert a 403 on a form submissio nthen do like an unauthorized popup
export default api; export default api;