mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Merge pull request #118 from AnonymusRaccoon/fix/unauthorized
Set first user account admin
This commit is contained in:
commit
9dd783fd2f
@ -22,12 +22,14 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
@ -173,15 +175,26 @@ namespace Kyoo.Authentication
|
||||
{
|
||||
ICollection<string> permissions = res.Principal.GetPermissions();
|
||||
if (permissions.All(x => x != permStr && x != overallStr))
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
context.Result = _ErrorResult($"Missing permission {permStr} or {overallStr}", StatusCodes.Status403Forbidden);
|
||||
}
|
||||
else
|
||||
{
|
||||
ICollection<string> permissions = _options.CurrentValue.Default ?? Array.Empty<string>();
|
||||
if (res.Failure != null || permissions.All(x => x != permStr && x != overallStr))
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status401Unauthorized);
|
||||
context.Result = _ErrorResult($"Unlogged user does not have permission {permStr} or {overallStr}", StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new action result with the given error message and error code.
|
||||
/// </summary>
|
||||
/// <param name="error">The error message.</param>
|
||||
/// <param name="code">The status code of the error.</param>
|
||||
/// <returns>The resulting error action.</returns>
|
||||
private static IActionResult _ErrorResult(string error, int code)
|
||||
{
|
||||
return new ObjectResult(new RequestError(error)) { StatusCode = code };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,13 +71,13 @@ namespace Kyoo.Authentication
|
||||
: string.Empty;
|
||||
List<Claim> claims = new()
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.ID.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim(ClaimTypes.Name, user.Username),
|
||||
new Claim(ClaimTypes.Role, permissions),
|
||||
new Claim("type", "access")
|
||||
new Claim(Claims.Id, user.ID.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim(Claims.Name, user.Username),
|
||||
new Claim(Claims.Permissions, permissions),
|
||||
new Claim(Claims.Type, "access")
|
||||
};
|
||||
if (user.Email != null)
|
||||
claims.Add(new Claim(ClaimTypes.Email, user.Email));
|
||||
claims.Add(new Claim(Claims.Email, user.Email));
|
||||
JwtSecurityToken token = new(
|
||||
signingCredentials: credential,
|
||||
issuer: _configuration.GetPublicUrl().ToString(),
|
||||
@ -99,9 +99,9 @@ namespace Kyoo.Authentication
|
||||
audience: _configuration.GetPublicUrl().ToString(),
|
||||
claims: new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.ID.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim("guid", Guid.NewGuid().ToString()),
|
||||
new Claim("type", "refresh")
|
||||
new Claim(Claims.Id, user.ID.ToString(CultureInfo.InvariantCulture)),
|
||||
new Claim(Claims.Guid, Guid.NewGuid().ToString()),
|
||||
new Claim(Claims.Type, "refresh")
|
||||
},
|
||||
expires: DateTime.UtcNow.AddYears(1)
|
||||
);
|
||||
@ -133,9 +133,9 @@ namespace Kyoo.Authentication
|
||||
throw new SecurityTokenException(ex.Message);
|
||||
}
|
||||
|
||||
if (principal.Claims.First(x => x.Type == "type").Value != "refresh")
|
||||
if (principal.Claims.First(x => x.Type == Claims.Type).Value != "refresh")
|
||||
throw new SecurityTokenException("Invalid token type. The token should be a refresh token.");
|
||||
Claim identifier = principal.Claims.First(x => x.Type == ClaimTypes.NameIdentifier);
|
||||
Claim identifier = principal.Claims.First(x => x.Type == Claims.Id);
|
||||
if (int.TryParse(identifier.Value, out int id))
|
||||
return id;
|
||||
throw new SecurityTokenException("Token not associated to any user.");
|
||||
|
@ -20,6 +20,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Kyoo.Authentication.Models;
|
||||
|
||||
namespace Kyoo.Authentication
|
||||
{
|
||||
@ -35,7 +36,7 @@ namespace Kyoo.Authentication
|
||||
/// <returns>The list of permissions</returns>
|
||||
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
|
||||
{
|
||||
return user.Claims.FirstOrDefault(x => x.Type == "permissions")?.Value.Split(',')
|
||||
return user.Claims.FirstOrDefault(x => x.Type == Claims.Permissions)?.Value.Split(',')
|
||||
?? Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
56
src/Kyoo.Authentication/Models/Claims.cs
Normal file
56
src/Kyoo.Authentication/Models/Claims.cs
Normal file
@ -0,0 +1,56 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Kyoo.Authentication.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// List of well known claims of kyoo
|
||||
/// </summary>
|
||||
public static class Claims
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the user
|
||||
/// </summary>
|
||||
public static string Id => "id";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the user
|
||||
/// </summary>
|
||||
public static string Name => "name";
|
||||
|
||||
/// <summary>
|
||||
/// The email of the user.
|
||||
/// </summary>
|
||||
public static string Email => "email";
|
||||
|
||||
/// <summary>
|
||||
/// The list of permissions that the user has.
|
||||
/// </summary>
|
||||
public static string Permissions => "permissions";
|
||||
|
||||
/// <summary>
|
||||
/// The type of the token (either "access" or "refresh").
|
||||
/// </summary>
|
||||
public static string Type => "type";
|
||||
|
||||
/// <summary>
|
||||
/// A guid used to identify a specific refresh token. This is only useful for the server to revokate tokens.
|
||||
/// </summary>
|
||||
public static string Guid => "guid";
|
||||
}
|
||||
}
|
@ -16,6 +16,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
|
||||
namespace Kyoo.Authentication.Models
|
||||
{
|
||||
/// <summary>
|
||||
@ -28,14 +32,28 @@ namespace Kyoo.Authentication.Models
|
||||
/// </summary>
|
||||
public const string Path = "authentication:permissions";
|
||||
|
||||
/// <summary>
|
||||
/// All permissions possibles, this is used to create an admin group.
|
||||
/// </summary>
|
||||
public static string[] Admin
|
||||
{
|
||||
get
|
||||
{
|
||||
return Enum.GetNames<Group>()
|
||||
.SelectMany(group => Enum.GetNames<Kind>()
|
||||
.Select(kind => $"{group}.{kind}".ToLowerInvariant())
|
||||
).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default permissions that will be given to a non-connected user.
|
||||
/// </summary>
|
||||
public string[] Default { get; set; } = new[] { "overall.read", "overall.write" };
|
||||
public string[] Default { get; set; } = new[] { "overall.read" };
|
||||
|
||||
/// <summary>
|
||||
/// Permissions applied to a new user.
|
||||
/// </summary>
|
||||
public string[] NewUser { get; set; } = new[] { "overall.read", "overall.write" };
|
||||
public string[] NewUser { get; set; } = new[] { "overall.read" };
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,11 @@ using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Kyoo.Authentication.Models;
|
||||
using Kyoo.Authentication.Models.DTO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||
using BCryptNet = BCrypt.Net.BCrypt;
|
||||
@ -52,15 +54,22 @@ namespace Kyoo.Authentication.Views
|
||||
/// </summary>
|
||||
private readonly ITokenController _token;
|
||||
|
||||
/// <summary>
|
||||
/// The permisson options.
|
||||
/// </summary>
|
||||
private readonly IOptionsMonitor<PermissionOption> _permissions;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="AuthApi"/>.
|
||||
/// </summary>
|
||||
/// <param name="users">The repository used to check if the user exists.</param>
|
||||
/// <param name="token">The token generator.</param>
|
||||
public AuthApi(IUserRepository users, ITokenController token)
|
||||
/// <param name="permissions">The permission opitons.</param>
|
||||
public AuthApi(IUserRepository users, ITokenController token, IOptionsMonitor<PermissionOption> permissions)
|
||||
{
|
||||
_users = users;
|
||||
_token = token;
|
||||
_permissions = permissions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -115,6 +124,10 @@ namespace Kyoo.Authentication.Views
|
||||
public async Task<ActionResult<JwtToken>> Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
User user = request.ToUser();
|
||||
user.Permissions = _permissions.CurrentValue.NewUser;
|
||||
// If no users exists, the new one will be an admin. Give it every permissions.
|
||||
if (await _users.GetOrDefault(where: x => true) == null)
|
||||
user.Permissions = PermissionOption.Admin;
|
||||
try
|
||||
{
|
||||
await _users.Create(user);
|
||||
@ -174,22 +187,24 @@ namespace Kyoo.Authentication.Views
|
||||
/// logged in.
|
||||
/// </remarks>
|
||||
/// <returns>The currently authenticated user.</returns>
|
||||
/// <response code="401">The user is not authenticated.</response>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpGet("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<User>> GetMe()
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID))
|
||||
return Unauthorized(new RequestError("User not authenticated"));
|
||||
try
|
||||
{
|
||||
return await _users.Get(userID);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
return Forbid(new RequestError("Invalid token"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,15 +216,17 @@ namespace Kyoo.Authentication.Views
|
||||
/// </remarks>
|
||||
/// <param name="user">The new data for the current user.</param>
|
||||
/// <returns>The currently authenticated user after modifications.</returns>
|
||||
/// <response code="401">The user is not authenticated.</response>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpPut("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<User>> EditMe(User user)
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID))
|
||||
return Unauthorized(new RequestError("User not authenticated"));
|
||||
try
|
||||
{
|
||||
user.ID = userID;
|
||||
@ -217,7 +234,7 @@ namespace Kyoo.Authentication.Views
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
return Forbid(new RequestError("Invalid token"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,15 +246,17 @@ namespace Kyoo.Authentication.Views
|
||||
/// </remarks>
|
||||
/// <param name="user">The new data for the current user.</param>
|
||||
/// <returns>The currently authenticated user after modifications.</returns>
|
||||
/// <response code="401">The user is not authenticated.</response>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpPut("me")]
|
||||
[HttpPatch("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<User>> PatchMe(User user)
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID))
|
||||
return Unauthorized(new RequestError("User not authenticated"));
|
||||
try
|
||||
{
|
||||
user.ID = userID;
|
||||
@ -245,7 +264,7 @@ namespace Kyoo.Authentication.Views
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
return Forbid(new RequestError("Invalid token"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,15 +275,17 @@ namespace Kyoo.Authentication.Views
|
||||
/// Delete the current account.
|
||||
/// </remarks>
|
||||
/// <returns>The currently authenticated user after modifications.</returns>
|
||||
/// <response code="401">The user is not authenticated.</response>
|
||||
/// <response code="403">The given access token is invalid.</response>
|
||||
[HttpDelete("me")]
|
||||
[UserOnly]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<User>> DeleteMe()
|
||||
{
|
||||
if (!int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out int userID))
|
||||
return Forbid();
|
||||
if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID))
|
||||
return Unauthorized(new RequestError("User not authenticated"));
|
||||
try
|
||||
{
|
||||
await _users.Delete(userID);
|
||||
@ -272,7 +293,7 @@ namespace Kyoo.Authentication.Views
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return Forbid();
|
||||
return Forbid(new RequestError("Invalid token"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ Logout
|
||||
Me cant be accessed without an account
|
||||
Get /auth/me
|
||||
Output
|
||||
Integer response status 403
|
||||
Integer response status 401
|
||||
|
||||
Bad Account
|
||||
[Documentation] Login fails if user does not exist
|
||||
|
Loading…
x
Reference in New Issue
Block a user