Merge pull request #3357 from crobibero/api-authorization

Add Authorization handlers
This commit is contained in:
David 2020-06-18 18:37:08 +02:00 committed by GitHub
commit 522e44de59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 954 additions and 174 deletions

View File

@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_networkManager = networkManager;
}
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
ValidateUser(request, authAttribtues);
ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
return user;
}
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
public AuthorizationInfo Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (auth?.User == null)
{
return null;
}
if (auth.User.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
return auth;
}
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null)
{
ValidateUserAccess(user, request, authAttribtues, auth);
ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
if (!IsExemptFromRoles(auth, authAttribtues, request, info))
if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
var roles = authAttribtues.GetRoles();
var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess(
User user,
IRequest request,
IAuthenticationAttributes authAttributes,
AuthorizationInfo auth)
IAuthenticationAttributes authAttributes)
{
if (user.HasPermission(PermissionKind.IsDisabled))
{
@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
throw new AuthenticationException("Access token is invalid or expired.");
}
//if (!string.IsNullOrEmpty(info.UserId))
//{
// var user = _userManager.GetUserById(info.UserId);
// if (user == null || user.Configuration.IsDisabled)
// {
// throw new SecurityException("User account has been disabled.");
// }
//}
}
}
}

View File

@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var (authInfo, _) =
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
/// <summary>
/// Gets the authorization.
/// </summary>
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(IRequest httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) =
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
if (originalAuthInfo != null)
{
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
httpReq.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
string deviceId = null;
string device = null;
string client = null;
@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token))
{
token = httpReq.Headers["X-Emby-Token"];
token = headers["X-Emby-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = httpReq.Headers["X-MediaBrowser-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = httpReq.QueryString["api_key"];
token = headers["X-MediaBrowser-Token"];
}
var info = new AuthorizationInfo
if (string.IsNullOrEmpty(token))
{
token = queryString["ApiKey"];
}
// TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token))
{
token = queryString["api_key"];
}
var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token
};
AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token))
{
var result = _authRepo.Get(new AuthenticationInfoQuery
@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token
});
var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (tokenInfo != null)
if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(info.Client))
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
info.Client = tokenInfo.AppName;
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(info.DeviceId))
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
info.DeviceId = tokenInfo.DeviceId;
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
if (string.IsNullOrWhiteSpace(info.Device))
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
info.Device = tokenInfo.DeviceName;
authInfo.Device = originalAuthenticationInfo.DeviceName;
}
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
tokenInfo.DeviceName = info.Device;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(info.Version))
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
info.Version = tokenInfo.AppVersion;
authInfo.Version = originalAuthenticationInfo.AppVersion;
}
else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
tokenInfo.AppVersion = info.Version;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
tokenInfo.DateLastActivity = DateTime.UtcNow;
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (!tokenInfo.UserId.Equals(Guid.Empty))
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
info.User = _userManager.GetUserById(tokenInfo.UserId);
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
tokenInfo.UserName = info.User.Username;
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
}
if (updateToken)
{
_authRepo.Update(tokenInfo);
_authRepo.Update(originalAuthenticationInfo);
}
}
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
}
httpReq.Items["AuthorizationInfo"] = info;
return info;
return (authInfo, originalAuthenticationInfo);
}
/// <summary>
@ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(auth);
}
/// <summary>
/// Gets the auth.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);
}
/// <summary>
/// Gets the authorization.
/// </summary>
@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
return WebUtility.HtmlEncode(value);
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}

View File

@ -0,0 +1,102 @@
#nullable enable
using System.Net;
using System.Security.Claims;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth
{
/// <summary>
/// Base authorization handler.
/// </summary>
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
where T : IAuthorizationRequirement
{
private readonly IUserManager _userManager;
private readonly INetworkManager _networkManager;
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
protected BaseAuthorizationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_networkManager = networkManager;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// Validate authenticated claims.
/// </summary>
/// <param name="claimsPrincipal">Request claims.</param>
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
/// <returns>Validated claim status.</returns>
protected bool ValidateClaims(
ClaimsPrincipal claimsPrincipal,
bool ignoreSchedule = false,
bool localAccessOnly = false)
{
// Ensure claim has userId.
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
if (userId == null)
{
return false;
}
// Ensure userId links to a valid user.
var user = _userManager.GetUserById(userId.Value);
if (user == null)
{
return false;
}
// Ensure user is not disabled.
if (user.HasPermission(PermissionKind.IsDisabled))
{
return false;
}
var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
// User cannot access remotely and user is remote
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
{
return false;
}
if (localAccessOnly && !isInLocalNetwork)
{
return false;
}
// User attempting to access out of parental control hours.
if (!ignoreSchedule
&& !user.HasPermission(PermissionKind.IsAdministrator)
&& !user.IsParentalScheduleAllowed())
{
return false;
}
return true;
}
private static IPAddress NormalizeIp(IPAddress ip)
{
return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
}
}
}

View File

@ -1,3 +1,6 @@
#nullable enable
using System.Globalization;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authenticatedAttribute = new AuthenticatedAttribute
{
IgnoreLegacyAuth = true
};
try
{
var user = _authService.Authenticate(Request, authenticatedAttribute);
if (user == null)
var authorizationInfo = _authService.Authenticate(Request);
if (authorizationInfo == null)
{
return Task.FromResult(AuthenticateResult.NoResult());
// TODO return when legacy API is removed.
@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(
ClaimTypes.Role,
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
{
/// <summary>
/// Default authorization handler.
/// </summary>
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public DefaultAuthorizationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{
var validated = ValidateClaims(context.User);
if (!validated)
{
context.Fail();
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
{
/// <summary>
/// The default authorization requirement.
/// </summary>
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
}
}

View File

@ -1,22 +1,33 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
{
/// <summary>
/// Authorization handler for requiring first time setup or elevated privileges.
/// </summary>
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
{
private readonly IConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
/// </summary>
/// <param name="configurationManager">The jellyfin configuration manager.</param>
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public FirstTimeSetupOrElevatedHandler(
IConfigurationManager configurationManager,
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
_configurationManager = configurationManager;
}
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
return Task.CompletedTask;
}
else if (context.User.IsInRole(UserRoles.Administrator))
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
}

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
{
/// <summary>
/// Escape schedule controls handler.
/// </summary>
public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public IgnoreScheduleHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
{
var validated = ValidateClaims(context.User, ignoreSchedule: true);
if (!validated)
{
context.Fail();
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
{
/// <summary>
/// Escape schedule controls requirement.
/// </summary>
public class IgnoreScheduleRequirement : IAuthorizationRequirement
{
}
}

View File

@ -0,0 +1,44 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.LocalAccessPolicy
{
/// <summary>
/// Local access handler.
/// </summary>
public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public LocalAccessHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
{
var validated = ValidateClaims(context.User, localAccessOnly: true);
if (!validated)
{
context.Fail();
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.LocalAccessPolicy
{
/// <summary>
/// The local access authorization requirement.
/// </summary>
public class LocalAccessRequirement : IAuthorizationRequirement
{
}
}

View File

@ -1,21 +1,43 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
{
/// <summary>
/// Authorization handler for requiring elevated privileges.
/// </summary>
public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public RequiresElevationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
{
if (context.User.IsInRole(UserRoles.Administrator))
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}

View File

@ -0,0 +1,38 @@
namespace Jellyfin.Api.Constants
{
/// <summary>
/// Internal claim types for authorization.
/// </summary>
public static class InternalClaimTypes
{
/// <summary>
/// User Id.
/// </summary>
public const string UserId = "Jellyfin-UserId";
/// <summary>
/// Device Id.
/// </summary>
public const string DeviceId = "Jellyfin-DeviceId";
/// <summary>
/// Device.
/// </summary>
public const string Device = "Jellyfin-Device";
/// <summary>
/// Client.
/// </summary>
public const string Client = "Jellyfin-Client";
/// <summary>
/// Version.
/// </summary>
public const string Version = "Jellyfin-Version";
/// <summary>
/// Token.
/// </summary>
public const string Token = "Jellyfin-Token";
}
}

View File

@ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants
/// </summary>
public static class Policies
{
/// <summary>
/// Policy name for default authorization.
/// </summary>
public const string DefaultAuthorization = "DefaultAuthorization";
/// <summary>
/// Policy name for requiring first time setup or elevated privileges.
/// </summary>
@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
/// Policy name for requiring elevated privileges.
/// </summary>
public const string RequiresElevation = "RequiresElevation";
/// <summary>
/// Policy name for allowing local access only.
/// </summary>
public const string LocalAccessOnly = "LocalAccessOnly";
/// <summary>
/// Policy name for escaping schedule controls.
/// </summary>
public const string IgnoreSchedule = "IgnoreSchedule";
}
}

View File

@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Configuration Controller.
/// </summary>
[Route("System")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class ConfigurationController : BaseJellyfinApiController
{
private readonly IServerConfigurationManager _configurationManager;

View File

@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;

View File

@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Package Controller.
/// </summary>
[Route("Packages")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;

View File

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
/// Search controller.
/// </summary>
[Route("/Search/Hints")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class SearchController : BaseJellyfinApiController
{
private readonly ISearchEngine _searchEngine;

View File

@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute] Guid id,
@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute] Guid id,
@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Subtitle playlist retrieved.</response>
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetSubtitlePlaylist(
[FromRoute] Guid id,

View File

@ -2,6 +2,7 @@ using System;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@ -15,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Attachments controller.
/// </summary>
[Route("Videos")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class VideoAttachmentsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;

View File

@ -0,0 +1,77 @@
#nullable enable
using System;
using System.Linq;
using System.Security.Claims;
using Jellyfin.Api.Constants;
namespace Jellyfin.Api.Helpers
{
/// <summary>
/// Claim Helpers.
/// </summary>
public static class ClaimHelpers
{
/// <summary>
/// Get user id from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>User id.</returns>
public static Guid? GetUserId(in ClaimsPrincipal user)
{
var value = GetClaimValue(user, InternalClaimTypes.UserId);
return string.IsNullOrEmpty(value)
? null
: (Guid?)Guid.Parse(value);
}
/// <summary>
/// Get device id from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Device id.</returns>
public static string? GetDeviceId(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.DeviceId);
/// <summary>
/// Get device from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Device.</returns>
public static string? GetDevice(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Device);
/// <summary>
/// Get client from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Client.</returns>
public static string? GetClient(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Client);
/// <summary>
/// Get version from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Version.</returns>
public static string? GetVersion(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Version);
/// <summary>
/// Get token from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Token.</returns>
public static string? GetToken(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Token);
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
{
return user?.Identities
.SelectMany(c => c.Claims)
.Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))
.Select(claim => claim.Value)
.FirstOrDefault();
}
}
}

View File

@ -5,7 +5,10 @@ using System.Linq;
using System.Reflection;
using Jellyfin.Api;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
options.AddPolicy(
Policies.RequiresElevation,
Policies.DefaultAuthorization,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new RequiresElevationRequirement());
policy.AddRequirements(new DefaultAuthorizationRequirement());
});
options.AddPolicy(
Policies.FirstTimeSetupOrElevated,
@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
});
options.AddPolicy(
Policies.IgnoreSchedule,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new IgnoreScheduleRequirement());
});
options.AddPolicy(
Policies.LocalAccessOnly,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new LocalAccessRequirement());
});
options.AddPolicy(
Policies.RequiresElevation,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new RequiresElevationRequirement());
});
});
}
@ -78,6 +107,10 @@ namespace Jellyfin.Server.Extensions
{
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
})
.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
})
.AddMvc(opts =>
{
opts.UseGeneralRoutePrefix(baseUrl);

View File

@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
/// <summary>
/// IAuthService.
/// </summary>
public interface IAuthService
{
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
/// <summary>
/// Authenticate and authorize request.
/// </summary>
/// <param name="request">Request.</param>
/// <param name="authAttribtutes">Authorization attributes.</param>
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
/// <summary>
/// Authenticate and authorize request.
/// </summary>
/// <param name="request">Request.</param>
/// <param name="authAttribtutes">Authorization attributes.</param>
/// <returns>Authenticated user.</returns>
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
/// <summary>
/// Authenticate request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Authorization information. Null if unauthenticated.</returns>
AuthorizationInfo Authenticate(HttpRequest request);
}
}

View File

@ -1,7 +1,11 @@
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
/// <summary>
/// IAuthorization context.
/// </summary>
public interface IAuthorizationContext
{
/// <summary>
@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
/// <summary>
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
private readonly IFixture _fixture;
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
private readonly Mock<ISystemClock> _clockMock;
private readonly Mock<IServiceProvider> _serviceProviderMock;
private readonly Mock<IAuthenticationService> _authenticationServiceMock;
private readonly UrlEncoder _urlEncoder;
private readonly HttpContext _context;
private readonly CustomAuthenticationHandler _sut;
private readonly AuthenticationScheme _scheme;
@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
AllowFixtureCircularDependencies();
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
_optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
_clockMock = _fixture.Freeze<Mock<ISystemClock>>();
_serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
_authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
_urlEncoder = UrlEncoder.Default;
serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
.Returns(authenticationServiceMock.Object);
_serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
.Returns(_authenticationServiceMock.Object);
_optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
.Returns(new AuthenticationSchemeOptions
{
ForwardAuthenticate = null
});
_context = new DefaultHttpContext
HttpContext context = new DefaultHttpContext
{
RequestServices = _serviceProviderMock.Object
RequestServices = serviceProviderMock.Object
};
_scheme = new AuthenticationScheme(
@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
typeof(CustomAuthenticationHandler));
_sut = _fixture.Create<CustomAuthenticationHandler>();
_sut.InitializeAsync(_scheme, _context).Wait();
}
[Fact]
public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
{
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>(),
It.IsAny<AuthenticatedAttribute>()))
.Returns((User?)null);
var authenticateResult = await _sut.AuthenticateAsync();
Assert.False(authenticateResult.Succeeded);
Assert.True(authenticateResult.None);
// TODO return when legacy API is removed.
// Assert.Equal("Invalid user", authenticateResult.Failure.Message);
_sut.InitializeAsync(_scheme, context).Wait();
}
[Fact]
@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>(),
It.IsAny<AuthenticatedAttribute>()))
It.IsAny<HttpRequest>()))
.Throws(new SecurityException(errorMessage));
var authenticateResult = await _sut.AuthenticateAsync();
@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
[Fact]
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
{
var user = SetupUser();
var authorizationInfo = SetupUser();
var authenticateResult = await _sut.AuthenticateAsync();
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username));
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
}
[Theory]
@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
[InlineData(false)]
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
{
var user = SetupUser(isAdmin);
var authorizationInfo = SetupUser(isAdmin);
var authenticateResult = await _sut.AuthenticateAsync();
var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
}
@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
}
private User SetupUser(bool isAdmin = false)
private AuthorizationInfo SetupUser(bool isAdmin = false)
{
var user = _fixture.Create<User>();
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
authorizationInfo.User = _fixture.Create<User>();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>(),
It.IsAny<AuthenticatedAttribute>()))
.Returns(user);
It.IsAny<HttpRequest>()))
.Returns(authorizationInfo);
return user;
return authorizationInfo;
}
private void AllowFixtureCircularDependencies()

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
{
public class DefaultAuthorizationHandlerTests
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly DefaultAuthorizationHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public DefaultAuthorizationHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<DefaultAuthorizationHandler>();
}
[Theory]
[InlineData(UserRoles.Administrator)]
[InlineData(UserRoles.Guest)]
[InlineData(UserRoles.User)]
public async Task ShouldSucceedOnUser(string userRole)
{
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
}
}

View File

@ -1,13 +1,13 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly FirstTimeSetupOrElevatedHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public FirstTimeSetupOrElevatedHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
}
@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User)]
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
{
SetupConfigurationManager(false);
var user = SetupUser(userRole);
var context = new AuthorizationHandlerContext(_requirements, user, null);
TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User, false)]
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
{
SetupConfigurationManager(true);
var user = SetupUser(userRole);
var context = new AuthorizationHandlerContext(_requirements, user, null);
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
private static ClaimsPrincipal SetupUser(string role)
{
var claims = new[] { new Claim(ClaimTypes.Role, role) };
var identity = new ClaimsIdentity(claims);
return new ClaimsPrincipal(identity);
}
private void SetupConfigurationManager(bool startupWizardCompleted)
{
var commonConfiguration = new BaseApplicationConfiguration
{
IsStartupWizardCompleted = startupWizardCompleted
};
_configurationManagerMock.Setup(c => c.CommonConfiguration)
.Returns(commonConfiguration);
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
{
public class IgnoreScheduleHandlerTests
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly IgnoreScheduleHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
/// <summary>
/// Globally disallow access.
/// </summary>
private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
public IgnoreScheduleHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new IgnoreScheduleRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<IgnoreScheduleHandler>();
}
[Theory]
[InlineData(UserRoles.Administrator, true)]
[InlineData(UserRoles.User, true)]
[InlineData(UserRoles.Guest, true)]
public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed)
{
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
role,
_accessSchedules);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
}
}

View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
{
public class LocalAccessHandlerTests
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly LocalAccessHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
private readonly Mock<INetworkManager> _networkManagerMock;
public LocalAccessHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_networkManagerMock = fixture.Freeze<Mock<INetworkManager>>();
_sut = fixture.Create<LocalAccessHandler>();
}
[Theory]
[InlineData(true, true)]
[InlineData(false, false)]
public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
{
_networkManagerMock
.Setup(n => n.IsInLocalNetwork(It.IsAny<string>()))
.Returns(isInLocalNetwork);
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
UserRoles.User);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
}
}

View File

@ -1,20 +1,35 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
{
public class RequiresElevationHandlerTests
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly RequiresElevationHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public RequiresElevationHandlerTests()
{
_sut = new RequiresElevationHandler();
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<RequiresElevationHandler>();
}
[Theory]
@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
[InlineData(UserRoles.Guest, false)]
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
{
var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
role);
var claims = new[] { new Claim(ClaimTypes.Role, role) };
var identity = new ClaimsIdentity(claims);
var user = new ClaimsPrincipal(identity);
var context = new AuthorizationHandlerContext(requirements, user, null);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);

View File

@ -35,6 +35,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
<ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Security.Claims;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using Microsoft.AspNetCore.Http;
using Moq;
using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
namespace Jellyfin.Api.Tests
{
public static class TestHelpers
{
public static ClaimsPrincipal SetupUser(
Mock<IUserManager> userManagerMock,
Mock<IHttpContextAccessor> httpContextAccessorMock,
string role,
IEnumerable<AccessSchedule>? accessSchedules = null)
{
var user = new User(
"jellyfin",
typeof(DefaultAuthenticationProvider).FullName,
typeof(DefaultPasswordResetProvider).FullName);
// Set administrator flag.
user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
// Add access schedules if set.
if (accessSchedules != null)
{
foreach (var accessSchedule in accessSchedules)
{
user.AccessSchedules.Add(accessSchedule);
}
}
var claims = new[]
{
new Claim(ClaimTypes.Role, role),
new Claim(ClaimTypes.Name, "jellyfin"),
new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.Device, "test"),
new Claim(InternalClaimTypes.Client, "test"),
new Claim(InternalClaimTypes.Version, "test"),
new Claim(InternalClaimTypes.Token, "test"),
};
var identity = new ClaimsIdentity(claims);
userManagerMock
.Setup(u => u.GetUserById(It.IsAny<Guid>()))
.Returns(user);
httpContextAccessorMock
.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
.Returns(new IPAddress(0));
return new ClaimsPrincipal(identity);
}
public static void SetupConfigurationManager(in Mock<IConfigurationManager> configurationManagerMock, bool startupWizardCompleted)
{
var commonConfiguration = new BaseApplicationConfiguration
{
IsStartupWizardCompleted = startupWizardCompleted
};
configurationManagerMock
.Setup(c => c.CommonConfiguration)
.Returns(commonConfiguration);
}
}
}