From b5821b0d02ca2bca18121b22ad56214d8bd54511 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 9 May 2022 22:50:28 +0200 Subject: [PATCH] Add authentication documentation to the swagger --- .../Permission/UserOnlyAttribute.cs | 31 +++++++++++++++++++ .../AuthenticationModule.cs | 22 ------------- .../Controllers/ITokenController.cs | 5 ++- .../Controllers/TokenController.cs | 2 +- .../Kyoo.Authentication.csproj | 7 ----- src/Kyoo.Authentication/Models/JwtToken.cs | 2 +- src/Kyoo.Authentication/Views/AuthApi.cs | 16 ++++++---- src/Kyoo.Core/Views/Watch/SubtitleApi.cs | 2 +- .../OperationPermissionProcessor.cs | 16 +++++++--- src/Kyoo.Swagger/SwaggerModule.cs | 31 +++---------------- 10 files changed, 63 insertions(+), 71 deletions(-) create mode 100644 src/Kyoo.Abstractions/Models/Attributes/Permission/UserOnlyAttribute.cs diff --git a/src/Kyoo.Abstractions/Models/Attributes/Permission/UserOnlyAttribute.cs b/src/Kyoo.Abstractions/Models/Attributes/Permission/UserOnlyAttribute.cs new file mode 100644 index 00000000..884187a0 --- /dev/null +++ b/src/Kyoo.Abstractions/Models/Attributes/Permission/UserOnlyAttribute.cs @@ -0,0 +1,31 @@ +// 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; + +namespace Kyoo.Abstractions.Models.Permissions +{ + /// + /// The annotated route can only be accessed by a logged in user. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] + public class UserOnlyAttribute : Attribute + { + // TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation. + } +} diff --git a/src/Kyoo.Authentication/AuthenticationModule.cs b/src/Kyoo.Authentication/AuthenticationModule.cs index b2dcfcdb..726e22fe 100644 --- a/src/Kyoo.Authentication/AuthenticationModule.cs +++ b/src/Kyoo.Authentication/AuthenticationModule.cs @@ -18,8 +18,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Reflection; using System.Text; using Autofac; using Kyoo.Abstractions; @@ -27,10 +25,8 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Authentication.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; using Microsoft.IdentityModel.Tokens; namespace Kyoo.Authentication @@ -105,25 +101,7 @@ namespace Kyoo.Authentication /// public IEnumerable ConfigureSteps => new IStartupAction[] { - SA.New(app => - { - PhysicalFileProvider provider = new(Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, - "login")); - app.UseDefaultFiles(new DefaultFilesOptions - { - RequestPath = new PathString("/login"), - FileProvider = provider, - RedirectToAppendTrailingSlash = true - }); - app.UseStaticFiles(new StaticFileOptions - { - RequestPath = new PathString("/login"), - FileProvider = provider - }); - }, SA.StaticFiles), SA.New(app => app.UseAuthentication(), SA.Authentication), - // SA.New(app => app.UseAuthorization(), SA.Authorization) }; } } diff --git a/src/Kyoo.Authentication/Controllers/ITokenController.cs b/src/Kyoo.Authentication/Controllers/ITokenController.cs index f0ae7670..a6d488d9 100644 --- a/src/Kyoo.Authentication/Controllers/ITokenController.cs +++ b/src/Kyoo.Authentication/Controllers/ITokenController.cs @@ -18,7 +18,6 @@ using System; using System.Threading.Tasks; -using JetBrains.Annotations; using Kyoo.Abstractions.Models; using Microsoft.IdentityModel.Tokens; @@ -35,14 +34,14 @@ namespace Kyoo.Authentication /// The user to create a token for. /// When this token will expire. /// A new, valid access token. - string CreateAccessToken([NotNull] User user, out TimeSpan expireIn); + string CreateAccessToken(User user, out TimeSpan expireIn); /// /// Create a new refresh token for the given user. /// /// The user to create a token for. /// A new, valid refresh token. - Task CreateRefreshToken([NotNull] User user); + Task CreateRefreshToken(User user); /// /// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to. diff --git a/src/Kyoo.Authentication/Controllers/TokenController.cs b/src/Kyoo.Authentication/Controllers/TokenController.cs index c7962cd4..d006cf4e 100644 --- a/src/Kyoo.Authentication/Controllers/TokenController.cs +++ b/src/Kyoo.Authentication/Controllers/TokenController.cs @@ -105,7 +105,7 @@ namespace Kyoo.Authentication }, expires: DateTime.UtcNow.AddYears(1) ); - // TODO refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user. + // TODO: refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user. return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token)); } diff --git a/src/Kyoo.Authentication/Kyoo.Authentication.csproj b/src/Kyoo.Authentication/Kyoo.Authentication.csproj index b34a7b42..e17c9615 100644 --- a/src/Kyoo.Authentication/Kyoo.Authentication.csproj +++ b/src/Kyoo.Authentication/Kyoo.Authentication.csproj @@ -13,11 +13,4 @@ - - - - login/%(RecursiveDir)%(Filename)%(Extension) - Always - - diff --git a/src/Kyoo.Authentication/Models/JwtToken.cs b/src/Kyoo.Authentication/Models/JwtToken.cs index c0dedc70..bb4c325e 100644 --- a/src/Kyoo.Authentication/Models/JwtToken.cs +++ b/src/Kyoo.Authentication/Models/JwtToken.cs @@ -49,7 +49,7 @@ namespace Kyoo.Authentication public string RefreshToken { get; set; } /// - /// When the access token will expire. After this tume, the refresh token should be used to retrieve. + /// When the access token will expire. After this time, the refresh token should be used to retrieve. /// a new token.cs /// [JsonProperty("expire_in")] diff --git a/src/Kyoo.Authentication/Views/AuthApi.cs b/src/Kyoo.Authentication/Views/AuthApi.cs index b237975b..e3899eba 100644 --- a/src/Kyoo.Authentication/Views/AuthApi.cs +++ b/src/Kyoo.Authentication/Views/AuthApi.cs @@ -23,9 +23,9 @@ using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; +using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Authentication.Models.DTO; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; @@ -84,7 +84,7 @@ namespace Kyoo.Authentication.Views /// The user and password does not match. [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> Login([FromBody] LoginRequest request) { User user = await _users.GetOrDefault(x => x.Username == request.Username); @@ -139,10 +139,11 @@ namespace Kyoo.Authentication.Views /// /// A valid refresh token. /// A new access and refresh token. - /// The given refresh token is invalid. + /// The given refresh token is invalid. [HttpGet("refresh")] + [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] + [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] public async Task> Refresh([FromQuery] string token) { try @@ -157,11 +158,11 @@ namespace Kyoo.Authentication.Views } catch (ItemNotFoundException) { - return BadRequest(new RequestError("Invalid refresh token.")); + return Forbid(new RequestError("Invalid refresh token.")); } catch (SecurityTokenException ex) { - return BadRequest(new RequestError(ex.Message)); + return Forbid(new RequestError(ex.Message)); } } @@ -175,6 +176,7 @@ namespace Kyoo.Authentication.Views /// The currently authenticated user. /// The given access token is invalid. [HttpGet("me")] + [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> GetMe() @@ -204,6 +206,7 @@ namespace Kyoo.Authentication.Views /// The currently authenticated user after modifications. /// The given access token is invalid. [HttpPut("me")] + [UserOnly] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> EditMe(User user, [FromQuery] bool resetOld = true) @@ -230,6 +233,7 @@ namespace Kyoo.Authentication.Views /// The currently authenticated user after modifications. /// The given access token is invalid. [HttpDelete("me")] + [UserOnly] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> DeleteMe() diff --git a/src/Kyoo.Core/Views/Watch/SubtitleApi.cs b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs index 3266af8d..7e89bef6 100644 --- a/src/Kyoo.Core/Views/Watch/SubtitleApi.cs +++ b/src/Kyoo.Core/Views/Watch/SubtitleApi.cs @@ -37,7 +37,7 @@ namespace Kyoo.Core.Api /// [Route("subtitles")] [Route("subtitle", Order = AlternativeRoute)] - [PartialPermission(nameof(SubtitleApi))] + [PartialPermission("subtitle")] [ApiController] [ApiDefinition("Subtitles", Group = WatchGroup)] public class SubtitleApi : ControllerBase diff --git a/src/Kyoo.Swagger/OperationPermissionProcessor.cs b/src/Kyoo.Swagger/OperationPermissionProcessor.cs index b0511b24..e2d14bf9 100644 --- a/src/Kyoo.Swagger/OperationPermissionProcessor.cs +++ b/src/Kyoo.Swagger/OperationPermissionProcessor.cs @@ -16,6 +16,7 @@ // 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.Reflection; @@ -36,12 +37,19 @@ namespace Kyoo.Swagger public bool Process(OperationProcessorContext context) { context.OperationDescription.Operation.Security ??= new List(); - OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes() + OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes() .Aggregate(new OpenApiSecurityRequirement(), (agg, cur) => + { + agg[nameof(Kyoo)] = Array.Empty(); + return agg; + }); + + perms = context.MethodInfo.GetCustomAttributes() + .Aggregate(perms, (agg, cur) => { ICollection permissions = _GetPermissionsList(agg, cur.Group); permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}"); - agg[cur.Group.ToString()] = permissions; + agg[nameof(Kyoo)] = permissions; return agg; }); @@ -61,7 +69,7 @@ namespace Kyoo.Swagger : cur.Kind; ICollection permissions = _GetPermissionsList(agg, group); permissions.Add($"{type}.{kind.ToString().ToLower()}"); - agg[group.ToString()] = permissions; + agg[nameof(Kyoo)] = permissions; return agg; }); } @@ -70,7 +78,7 @@ namespace Kyoo.Swagger return true; } - private ICollection _GetPermissionsList(OpenApiSecurityRequirement security, Group group) + private static ICollection _GetPermissionsList(OpenApiSecurityRequirement security, Group group) { return security.TryGetValue(group.ToString(), out IEnumerable perms) ? perms.ToList() diff --git a/src/Kyoo.Swagger/SwaggerModule.cs b/src/Kyoo.Swagger/SwaggerModule.cs index fafb1281..5025fa7c 100644 --- a/src/Kyoo.Swagger/SwaggerModule.cs +++ b/src/Kyoo.Swagger/SwaggerModule.cs @@ -120,35 +120,14 @@ namespace Kyoo.Swagger })); document.SchemaProcessors.Add(new ThumbnailProcessor()); - document.AddSecurity("Kyoo", new OpenApiSecurityScheme + document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme { - Type = OpenApiSecuritySchemeType.OpenIdConnect, - OpenIdConnectUrl = "/.well-known/openid-configuration", - Description = "You can login via an OIDC client, clients must be first registered in kyoo. " + - "Documentation coming soon." + Type = OpenApiSecuritySchemeType.Http, + Scheme = "Bearer", + BearerFormat = "JWT", + Description = "The user's bearer" }); document.OperationProcessors.Add(new OperationPermissionProcessor()); - // This does not respect the swagger's specification but it works for swaggerUi and ReDoc so honestly this will do. - document.AddSecurity(Group.Overall.ToString(), new OpenApiSecurityScheme - { - ExtensionData = new Dictionary - { - ["type"] = "OpenID Connect or Api Key" - }, - Description = "Kyoo's permissions work by groups. Permissions are attributed to " + - "a specific group and if a user has a group permission, it will be the same as having every " + - "permission in the group. For example, having overall.read gives you collections.read, " + - "shows.read and so on." - }); - document.AddSecurity(Group.Admin.ToString(), new OpenApiSecurityScheme - { - ExtensionData = new Dictionary - { - ["type"] = "OpenID Connect or Api Key" - }, - Description = "The permission group used for administrative items like tasks, account management " + - "and library creation." - }); }); }