// 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 . using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; 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.Primitives; namespace Kyoo.Authentication { /// /// A permission validator to validate permission with user Permission array /// or the default array from the configurations if the user is not logged. /// public class PermissionValidator : IPermissionValidator { /// /// The permissions options to retrieve default permissions. /// private readonly PermissionOption _options; /// /// Create a new factory with the given options. /// /// The option containing default values. public PermissionValidator(PermissionOption options) { _options = options; } /// public IFilterMetadata Create(PermissionAttribute attribute) { return new PermissionValidatorFilter( attribute.Type, attribute.Kind, attribute.Group, _options ); } /// public IFilterMetadata Create(PartialPermissionAttribute attribute) { return new PermissionValidatorFilter( ((object?)attribute.Type ?? attribute.Kind)!, attribute.Group, _options ); } /// /// The authorization filter used by . /// private class PermissionValidatorFilter : IAsyncAuthorizationFilter { /// /// The permission to validate. /// private readonly string? _permission; /// /// The kind of permission needed. /// private readonly Kind? _kind; /// /// The group of he permission. /// private readonly Group _group = Group.Overall; /// /// The permissions options to retrieve default permissions. /// private readonly PermissionOption _options; /// /// Create a new permission validator with the given options. /// /// The permission to validate. /// The kind of permission needed. /// The group of the permission. /// The option containing default values. public PermissionValidatorFilter( string permission, Kind kind, Group group, PermissionOption options ) { _permission = permission; _kind = kind; _group = group; _options = options; } /// /// Create a new permission validator with the given options. /// /// The partial permission to validate. /// The group of the permission. /// The option containing default values. public PermissionValidatorFilter( object partialInfo, Group? group, PermissionOption options ) { switch (partialInfo) { case Kind kind: _kind = kind; break; case string perm: _permission = perm; break; default: throw new ArgumentException( $"{nameof(partialInfo)} can only be a permission string or a kind." ); } if (group != null) _group = group.Value; _options = options; } /// public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { string? permission = _permission; Kind? kind = _kind; if (permission == null || kind == null) { switch (context.HttpContext.Items["PermissionType"]) { case string perm: permission = perm; break; case Kind kin: kind = kin; break; case null when kind != null: context.HttpContext.Items["PermissionType"] = kind; return; case null when permission != null: context.HttpContext.Items["PermissionType"] = permission; return; default: throw new ArgumentException( "Multiple non-matching partial permission attribute " + "are not supported." ); } if (permission == null || kind == null) { throw new ArgumentException( "The permission type or kind is still missing after two partial " + "permission attributes, this is unsupported." ); } } string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}"; string overallStr = $"{_group.ToString().ToLower()}.{kind.ToString()!.ToLower()}"; AuthenticateResult res = _ApiKeyCheck(context); if (res.None) res = await _JwtCheck(context); if (res.Succeeded) { ICollection permissions = res.Principal.GetPermissions(); if (permissions.All(x => x != permStr && x != overallStr)) context.Result = _ErrorResult( $"Missing permission {permStr} or {overallStr}", StatusCodes.Status403Forbidden ); } else if (res.None) { ICollection permissions = _options.Default ?? Array.Empty(); if (permissions.All(x => x != permStr && x != overallStr)) { context.Result = _ErrorResult( $"Unlogged user does not have permission {permStr} or {overallStr}", StatusCodes.Status401Unauthorized ); } } else if (res.Failure != null) context.Result = _ErrorResult( res.Failure.Message, StatusCodes.Status403Forbidden ); else context.Result = _ErrorResult( "Authentication panic", StatusCodes.Status500InternalServerError ); } private AuthenticateResult _ApiKeyCheck(ActionContext context) { if ( !context.HttpContext.Request.Headers.TryGetValue( "X-API-Key", out StringValues apiKey ) ) return AuthenticateResult.NoResult(); if (!_options.ApiKeys.Contains(apiKey!)) return AuthenticateResult.Fail("Invalid API-Key."); return AuthenticateResult.Success( new AuthenticationTicket( new ClaimsPrincipal( new[] { new ClaimsIdentity( new[] { // TODO: Make permission configurable, for now every APIKEY as all permissions. new Claim( Claims.Permissions, string.Join(',', PermissionOption.Admin) ) } ) } ), "apikey" ) ); } private async Task _JwtCheck(ActionContext context) { AuthenticateResult ret = await context.HttpContext.AuthenticateAsync( JwtBearerDefaults.AuthenticationScheme ); // Change the failure message to make the API nice to use. if (ret.Failure != null) return AuthenticateResult.Fail( "Invalid JWT token. The token may have expired." ); return ret; } } /// /// Create a new action result with the given error message and error code. /// /// The error message. /// The status code of the error. /// The resulting error action. private static IActionResult _ErrorResult(string error, int code) { return new ObjectResult(new RequestError(error)) { StatusCode = code }; } } }