Implemented simple authorization, with users, groups & sessions.

This commit is contained in:
Max 2023-10-09 21:05:38 +02:00
parent 9338d8c106
commit 25ca94ae3a
5 changed files with 72 additions and 30 deletions

View File

@ -3,9 +3,9 @@ using System.Linq;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Primitives;
using SharpRSS.API.Contracts.Models; using SharpRSS.API.Contracts.Models;
using SharpRSS.API.Data; using SharpRSS.API.Data;
using SharpRSS.API.Models;
using ToolQit; using ToolQit;
using ToolQit.Extensions; using ToolQit.Extensions;
using ToolQit.Logging; using ToolQit.Logging;
@ -23,8 +23,7 @@ namespace SharpRSS.API.Auth
private readonly ILog _log; private readonly ILog _log;
private readonly bool _admin; private readonly bool _admin;
public void OnAuthorization(AuthorizationFilterContext context)
public async void OnAuthorization(AuthorizationFilterContext context)
{ {
if (context.ActionDescriptor.EndpointMetadata.Any(obj => obj.GetType() == typeof(AllowAnonymousAttribute))) if (context.ActionDescriptor.EndpointMetadata.Any(obj => obj.GetType() == typeof(AllowAnonymousAttribute)))
return; return;
@ -43,19 +42,26 @@ namespace SharpRSS.API.Auth
context.Result = new UnauthorizedObjectResult(new Result("Invalid session ID")); context.Result = new UnauthorizedObjectResult(new Result("Invalid session ID"));
return; return;
} }
var authSet = await authService.ValidateSession(headerVal); var authSetResult = authService.ValidateSession(headerVal).Result;
if (!authSet.Success || authSet.Value == null) if (!authSetResult.Success || authSetResult.Value == null)
{ {
context.Result = new UnauthorizedResult(); context.Result = new UnauthorizedResult();
return; return;
} }
if (authSet.Value.Session is { Expired: true }) if (authSetResult.Value.Session is { Expired: true })
{ {
context.Result = new UnauthorizedObjectResult(new Result("Session expired", ResultStatus.Failed)); context.Result = new UnauthorizedObjectResult(new Result("Session expired", ResultStatus.Failed));
return; return;
} }
authSet.Value.AdminRequired = _admin; if (!authSetResult.Value.User.Active)
context.HttpContext.Items["auth"] = authSet.Value; {
context.Result = new UnauthorizedObjectResult(new Result(
"User is not active, contact your administrator to enable this account!", ResultStatus.Failed));
return;
}
authSetResult.Value.AdminRequired = _admin;
context.RouteData.Values.Add(nameof(AuthorizationSet), authSetResult.Value);
return; return;
} }
context.Result = new UnauthorizedResult(); context.Result = new UnauthorizedResult();

View File

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -15,7 +16,6 @@ using ToolQit.Logging;
namespace SharpRSS.API.Controllers namespace SharpRSS.API.Controllers
{ {
[ApiController] [ApiController]
[SessionAuthorize(false)]
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
@ -44,13 +44,29 @@ namespace SharpRSS.API.Controllers
// To update only fill the values that need to be updated. // To update only fill the values that need to be updated.
[HttpPost("user")] [HttpPost("user")]
[SessionAuthorize] [SessionAuthorize(true)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<ResultOr<User>>> InsertUser(InsertUser payload) public async Task<ActionResult<ResultOr<User>>> InsertUser(InsertUser payload)
{ {
object? authSetObj = RouteData.Values[nameof(AuthorizationSet)];
if (authSetObj is AuthorizationSet authSet)
{
if (!authSet.Group.Administrator)
{
if (payload.Uid == authSet.User.Uid) // User can self change own information, but not the group property!
payload.GroupId = string.Empty;
else
return new UnauthorizedObjectResult(new Result(
$"User '{authSet.User.Uid}' in group '{authSet.Group.DisplayName}' does not has the right permission to change user '{payload.Uid}'!"));
}
}
else
return StatusCode(StatusCodes.Status500InternalServerError, new Result("Failed to get the authorization data!", ResultStatus.InternalFail));
var createdUserResult = await _authService.InsertUserAsync(payload); var createdUserResult = await _authService.InsertUserAsync(payload);
return createdUserResult.Success ? Created("", createdUserResult) : return createdUserResult.Success ? Created("", createdUserResult) :
createdUserResult.Status == ResultStatus.Failed ? BadRequest(createdUserResult) : createdUserResult.Status == ResultStatus.Failed ? BadRequest(createdUserResult) :
@ -58,7 +74,7 @@ namespace SharpRSS.API.Controllers
} }
[HttpDelete("user")] [HttpDelete("user")]
[SessionAuthorize] [SessionAuthorize(true)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -72,7 +88,7 @@ namespace SharpRSS.API.Controllers
} }
[HttpGet("user")] [HttpGet("user")]
[SessionAuthorize] [SessionAuthorize(true)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -83,7 +99,7 @@ namespace SharpRSS.API.Controllers
} }
[HttpGet("users")] [HttpGet("users")]
[SessionAuthorize] [SessionAuthorize(true)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -98,7 +114,7 @@ namespace SharpRSS.API.Controllers
} }
[HttpPost("group")] [HttpPost("group")]
[SessionAuthorize] [SessionAuthorize(true)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -112,7 +128,7 @@ namespace SharpRSS.API.Controllers
} }
[HttpDelete("group")] [HttpDelete("group")]
[SessionAuthorize] [SessionAuthorize(true)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
@ -126,12 +142,13 @@ namespace SharpRSS.API.Controllers
} }
[HttpGet("groups")] [HttpGet("groups")]
[SessionAuthorize(false)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<ListResult<Group>>> GetGroups(string search = "", int results = 20, int skip = 0) public async Task<ActionResult<ListResult<GroupItem>>> GetGroups(string search = "", int results = 20, int skip = 0)
{ //TODO: Change DTO to GroupItem! {
var groupsResult = await _authService.GetGroupsAsync(results, skip, search); var groupsResult = await _authService.GetGroupsAsync(results, skip, search);
return groupsResult.Success ? Ok(groupsResult) : return groupsResult.Success ? Ok(groupsResult) :
groupsResult.Status == ResultStatus.Failed ? BadRequest(groupsResult) : groupsResult.Status == ResultStatus.Failed ? BadRequest(groupsResult) :
@ -139,6 +156,7 @@ namespace SharpRSS.API.Controllers
} }
[HttpGet("group")] [HttpGet("group")]
[SessionAuthorize(false)]
[Produces("application/json")] [Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]

View File

@ -61,9 +61,23 @@ namespace SharpRSS.API.Data
DbAccess access = new DbAccess(_configuration); DbAccess access = new DbAccess(_configuration);
var dbSession = await access.Sessions.Where(s => s.Sid == sessionId).FirstOrDefaultAsync(); var dbSession = await access.Sessions.Where(s => s.Sid == sessionId).FirstOrDefaultAsync();
if (dbSession == null) return ResultOr<AuthorizationSet>.Failed($"Could not get session: '{sessionId}'"); if (dbSession == null) return ResultOr<AuthorizationSet>.Failed($"Could not get session: '{sessionId}'");
if (!dbSession.Expired && dbSession.Expires - DateTime.Now <= TimeSpan.FromDays(1))
{
DbSession extended = dbSession.Extend();
access.Sessions.Update(extended);
bool sessionExtended = await access.SaveChangesAsync() > 0;
if (sessionExtended)
{
dbSession = extended;
_log.Information("Extended session: '{SessionId}' to '{SessionExpirationDate}'", dbSession.Sid, dbSession.Expires);
}
else
_log.Warning("Failed to extend session: '{SessionId}'", dbSession.Sid);
}
var dbUser = await access.Users.Where(u => u.Uid == dbSession.Uid).FirstOrDefaultAsync(); var dbUser = await access.Users.Where(u => u.Uid == dbSession.Uid).FirstOrDefaultAsync();
if (dbUser == null) return ResultOr<AuthorizationSet>.Failed($"Could not get user: '{dbSession.Uid}'"); if (dbUser == null) return ResultOr<AuthorizationSet>.Failed($"Could not get user: '{dbSession.Uid}'");
var dbGroup = await access.Groups.Where(g => g.Gid == dbUser.Gid).FirstOrDefaultAsync(); var dbGroup = await access.Groups.Where(g => g.Gid == dbUser.Gid).FirstOrDefaultAsync();
if (dbGroup == null) return ResultOr<AuthorizationSet>.Failed($"Could not get group: '{dbUser.Gid}'");
var set = new AuthorizationSet() var set = new AuthorizationSet()
{ {
Session = dbSession, Session = dbSession,

View File

@ -2,9 +2,9 @@ namespace SharpRSS.API.Models
{ {
internal class AuthorizationSet internal class AuthorizationSet
{ {
public DbSession? Session { get; set; } public DbSession Session { get; set; }
public DbUser? User { get; set; } public DbUser User { get; set; }
public DbGroup? Group { get; set; } public DbGroup Group { get; set; }
public bool AdminRequired { get; set; } public bool AdminRequired { get; set; }
} }
} }

View File

@ -1,9 +1,8 @@
using System; using System;
using System.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Serilog; using Serilog;
using SharpRSS.API.Contracts.DTOs;
using SharpRSS.API.Contracts.DTOs.Sessions;
namespace SharpRSS.API.Models namespace SharpRSS.API.Models
{ {
@ -20,21 +19,26 @@ namespace SharpRSS.API.Models
Uid = uid; Uid = uid;
Created = DateTime.Now; Created = DateTime.Now;
Expires = Created.AddMinutes(expiresMinutes); Expires = Created.AddMinutes(expiresMinutes);
using SHA1 sha1 = SHA1.Create(); string uidHash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(Uid)));
string uidHash = Convert.ToHexString(sha1.ComputeHash(Encoding.UTF8.GetBytes(Uid))); string createdHash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(Created.ToString(CultureInfo.CurrentCulture))));
Sid = $"{new DateTimeOffset(Created).ToUnixTimeMilliseconds()}.{uidHash}.{new DateTimeOffset(Expires).ToUnixTimeMilliseconds()}"; Sid = Convert.ToHexString(SHA512.HashData(Encoding.UTF8.GetBytes($"{createdHash}.{uidHash}")));
} }
public string Uid { get; set; }
public string Uid { get; set; } = string.Empty;
public string Sid { get; set; } = string.Empty; public string Sid { get; set; } = string.Empty;
public DateTime Created { get; set; } public DateTime Created { get; set; }
public DateTime Expires { get; set; } public DateTime Expires { get; set; }
public bool Expired => Expires < DateTime.Now; public bool Expired => Expires < DateTime.Now;
public static DbSession CreateSession(string uid, double expiresMinutes = 10080) public DbSession Extend(double extendMinutes = 10080) => new DbSession()
{ {
DbSession newSession = new DbSession(uid, expiresMinutes); Uid = Uid,
return newSession; Sid = Sid,
} Created = Created,
Expires = DateTime.Now.AddMinutes(extendMinutes)
};
public static DbSession CreateSession(string uid, double expiresMinutes = 10080) => new DbSession(uid, expiresMinutes);
} }
} }