Merge pull request #118 from AnonymusRaccoon/fix/unauthorized

Set first user account admin
This commit is contained in:
Zoe Roux 2022-05-28 11:33:21 +02:00 committed by GitHub
commit 9dd783fd2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 143 additions and 34 deletions

View File

@ -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 };
}
}
}

View File

@ -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.");

View File

@ -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>();
}
}

View 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";
}
}

View File

@ -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" };
}
}

View File

@ -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"));
}
}
}

View File

@ -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