Handling overall permissions

This commit is contained in:
Zoe Roux 2021-05-11 01:20:25 +02:00
parent 71c18092e5
commit 21e354bf00
7 changed files with 85 additions and 24 deletions

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Authentication.Models; using Kyoo.Authentication.Models;
@ -35,7 +36,7 @@ namespace Kyoo.Authentication
/// <inheritdoc /> /// <inheritdoc />
public IFilterMetadata Create(PermissionAttribute attribute) public IFilterMetadata Create(PermissionAttribute attribute)
{ {
return new PermissionValidator(attribute.AsPermissionString(), _options); return new PermissionValidator(attribute.Type, attribute.Kind, _options);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -54,9 +55,9 @@ namespace Kyoo.Authentication
/// </summary> /// </summary>
private readonly string _permission; private readonly string _permission;
/// <summary> /// <summary>
/// Information about partial items. /// The kind of permission needed
/// </summary> /// </summary>
private readonly object _partialInfo; private readonly Kind? _kind;
/// <summary> /// <summary>
/// The permissions options to retrieve default permissions. /// The permissions options to retrieve default permissions.
/// </summary> /// </summary>
@ -66,10 +67,12 @@ namespace Kyoo.Authentication
/// Create a new permission validator with the given options /// Create a new permission validator with the given options
/// </summary> /// </summary>
/// <param name="permission">The permission to validate</param> /// <param name="permission">The permission to validate</param>
/// <param name="kind">The kind of permission needed</param>
/// <param name="options">The option containing default values.</param> /// <param name="options">The option containing default values.</param>
public PermissionValidator(string permission, IOptionsMonitor<PermissionOption> options) public PermissionValidator(string permission, Kind kind, IOptionsMonitor<PermissionOption> options)
{ {
_permission = permission; _permission = permission;
_kind = kind;
_options = options; _options = options;
} }
@ -80,7 +83,12 @@ namespace Kyoo.Authentication
/// <param name="options">The option containing default values.</param> /// <param name="options">The option containing default values.</param>
public PermissionValidator(object partialInfo, IOptionsMonitor<PermissionOption> options) public PermissionValidator(object partialInfo, IOptionsMonitor<PermissionOption> options)
{ {
_partialInfo = partialInfo; if (partialInfo is Kind kind)
_kind = kind;
else if (partialInfo is string perm)
_permission = perm;
else
throw new ArgumentException($"{nameof(partialInfo)} can only be a permission string or a kind.");
_options = options; _options = options;
} }
@ -89,35 +97,46 @@ namespace Kyoo.Authentication
public async Task OnAuthorizationAsync(AuthorizationFilterContext context) public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{ {
string permission = _permission; string permission = _permission;
Kind? kind = _kind;
if (_partialInfo != null) if (permission == null || kind == null)
{ {
switch (context.HttpContext.Items["PermissionType"]) switch (context.HttpContext.Items["PermissionType"])
{ {
case string perm when _partialInfo is Kind kind: case string perm:
permission = $"{perm}.{kind.ToString().ToLower()}"; permission = perm;
break; break;
case Kind kind when _partialInfo is string partial: case Kind kin:
permission = $"{partial}.{kind.ToString().ToLower()}"; kind = kin;
break; break;
case null: case null when kind != null:
context.HttpContext.Items["PermissionType"] = _partialInfo; context.HttpContext.Items["PermissionType"] = kind;
return;
case null when permission != null:
context.HttpContext.Items["PermissionType"] = permission;
return; return;
default: default:
throw new ArgumentException("Multiple non-matching partial permission attribute " + throw new ArgumentException("Multiple non-matching partial permission attribute " +
"are not supported."); "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 = $"overall.{kind.ToString()!.ToLower()}";
AuthenticateResult res = await context.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); AuthenticateResult res = await context.HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);
if (res.Succeeded) if (res.Succeeded)
{ {
if (res.Principal.GetPermissions().All(x => x != permission)) ICollection<string> permissions = res.Principal.GetPermissions();
if (permissions.All(x => x != permStr && x != overallStr))
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
} }
else else
{ {
if (res.Failure != null || _options.CurrentValue.Default.All(x => x != permission)) 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 = new StatusCodeResult(StatusCodes.Status401Unauthorized);
} }
} }

View File

@ -24,7 +24,11 @@ namespace Kyoo.Models.Permissions
/// <summary> /// <summary>
/// The needed permission as string. /// The needed permission as string.
/// </summary> /// </summary>
private readonly string _permission; public string Type { get; }
/// <summary>
/// The needed permission kind.
/// </summary>
public Kind Kind { get; }
/// <summary> /// <summary>
/// Ask a permission to run an action. /// Ask a permission to run an action.
@ -38,7 +42,8 @@ namespace Kyoo.Models.Permissions
{ {
if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase)) if (type.EndsWith("API", StringComparison.OrdinalIgnoreCase))
type = type[..^3]; type = type[..^3];
_permission = $"{type.ToLower()}.{permission.ToString().ToLower()}"; Type = type.ToLower();
Kind = permission;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -56,7 +61,7 @@ namespace Kyoo.Models.Permissions
/// <returns>The string representation.</returns> /// <returns>The string representation.</returns>
public string AsPermissionString() public string AsPermissionString()
{ {
return _permission; return Type;
} }
} }

View File

@ -0,0 +1,35 @@
using Kyoo.Models.Permissions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace Kyoo.Controllers
{
/// <summary>
/// A permission validator that always validate permissions. This effectively disable the permission system.
/// </summary>
public class PassthroughPermissionValidator : IPermissionValidator
{
// ReSharper disable once SuggestBaseTypeForParameter
public PassthroughPermissionValidator(ILogger<PassthroughPermissionValidator> logger)
{
logger.LogWarning("No permission validator has been enabled, all users will have all permissions");
}
/// <inheritdoc />
public IFilterMetadata Create(PermissionAttribute attribute)
{
return new PassthroughValidator();
}
/// <inheritdoc />
public IFilterMetadata Create(PartialPermissionAttribute attribute)
{
return new PassthroughValidator();
}
/// <summary>
/// An useless filter that does nothing.
/// </summary>
private class PassthroughValidator : IFilterMetadata { }
}
}

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models.Permissions;
using Kyoo.Tasks; using Kyoo.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -93,6 +95,9 @@ namespace Kyoo
} }
services.AddTask<Crawler>(); services.AddTask<Crawler>();
if (services.All(x => x.ServiceType != typeof(IPermissionValidator)))
services.AddSingleton<IPermissionValidator, PassthroughPermissionValidator>();
} }
} }
} }

View File

@ -45,7 +45,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj"> <ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj">
<!-- <ExcludeAssets>all</ExcludeAssets>--> <ExcludeAssets>all</ExcludeAssets>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>

View File

@ -1,13 +1,11 @@
using System; using System;
using System.IO; using System.IO;
using Kyoo.Authentication;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Postgresql; using Kyoo.Postgresql;
using Kyoo.Tasks; using Kyoo.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -48,8 +46,7 @@ namespace Kyoo
_configuration = configuration; _configuration = configuration;
_plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger<PluginManager>()); _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger<PluginManager>());
_plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)});
new AuthenticationModule(configuration, loggerFactory, host)});
} }
/// <summary> /// <summary>

View File

@ -32,8 +32,8 @@
"password": "passphrase" "password": "passphrase"
}, },
"permissions": { "permissions": {
"default": ["read", "play", "write", "admin"], "default": ["overall.read", "overall.write", "overall.create", "overall.delete"],
"newUser": ["read", "play", "write", "admin", "task.read"] "newUser": ["overall.read", "overall.write", "overall.create", "overall.delete"]
}, },
"profilePicturePath": "users/", "profilePicturePath": "users/",
"clients": [] "clients": []