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; _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) public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
return user; 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 // This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request); var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(authAttribtues, request)) if (!IsExemptFromAuthenticationToken(authAttributes, request))
{ {
ValidateSecurityToken(request, auth.Token); ValidateSecurityToken(request, auth.Token);
} }
if (authAttribtues.AllowLocalOnly && !request.IsLocal) if (authAttributes.AllowLocalOnly && !request.IsLocal)
{ {
throw new SecurityException("Operation not found."); throw new SecurityException("Operation not found.");
} }
@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null) if (user != null)
{ {
ValidateUserAccess(user, request, authAttribtues, auth); ValidateUserAccess(user, request, authAttributes);
} }
var info = GetTokenInfo(request); 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); ValidateRoles(roles, user);
} }
@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess( private void ValidateUserAccess(
User user, User user,
IRequest request, IRequest request,
IAuthenticationAttributes authAttributes, IAuthenticationAttributes authAttributes)
AuthorizationInfo auth)
{ {
if (user.HasPermission(PermissionKind.IsDisabled)) if (user.HasPermission(PermissionKind.IsDisabled))
{ {
@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{ {
throw new AuthenticationException("Access token is invalid or expired."); 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.Net;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services; using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security namespace Emby.Server.Implementations.HttpServer.Security
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext); return GetAuthorization(requestContext);
} }
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var (authInfo, _) =
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
/// <summary> /// <summary>
/// Gets the authorization. /// Gets the authorization.
/// </summary> /// </summary>
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(IRequest httpReq) private AuthorizationInfo GetAuthorization(IRequest httpReq)
{ {
var auth = GetAuthorizationDictionary(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 deviceId = null;
string device = null; string device = null;
string client = null; string client = null;
@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = httpReq.Headers["X-Emby-Token"]; token = headers["X-Emby-Token"];
} }
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = httpReq.Headers["X-MediaBrowser-Token"]; token = headers["X-MediaBrowser-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = httpReq.QueryString["api_key"];
} }
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, Client = client,
Device = device, Device = device,
@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token Token = token
}; };
AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token)) if (!string.IsNullOrWhiteSpace(token))
{ {
var result = _authRepo.Get(new AuthenticationInfoQuery var result = _authRepo.Get(new AuthenticationInfoQuery
@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token 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; var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace // 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 // 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(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{ {
if (allowTokenInfoUpdate) if (allowTokenInfoUpdate)
{ {
updateToken = true; 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) if (allowTokenInfoUpdate)
{ {
updateToken = true; 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; 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; updateToken = true;
} }
} }
if (updateToken) if (updateToken)
{ {
_authRepo.Update(tokenInfo); _authRepo.Update(originalAuthenticationInfo);
} }
} }
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
} }
httpReq.Items["AuthorizationInfo"] = info; return (authInfo, originalAuthenticationInfo);
return info;
} }
/// <summary> /// <summary>
@ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(auth); 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> /// <summary>
/// Gets the authorization. /// Gets the authorization.
/// </summary> /// </summary>
@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value) private static string NormalizeValue(string value)
{ {
if (string.IsNullOrEmpty(value)) return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
{
return value;
}
return 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.Authentication;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
/// <inheritdoc /> /// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync() protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{ {
var authenticatedAttribute = new AuthenticatedAttribute
{
IgnoreLegacyAuth = true
};
try try
{ {
var user = _authService.Authenticate(Request, authenticatedAttribute); var authorizationInfo = _authService.Authenticate(Request);
if (user == null) if (authorizationInfo == null)
{ {
return Task.FromResult(AuthenticateResult.NoResult()); return Task.FromResult(AuthenticateResult.NoResult());
// TODO return when legacy API is removed. // TODO return when legacy API is removed.
@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
var claims = new[] var claims = new[]
{ {
new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
new Claim( new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
ClaimTypes.Role, new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User) 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 identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity); var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name); 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 System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
{ {
/// <summary> /// <summary>
/// Authorization handler for requiring first time setup or elevated privileges. /// Authorization handler for requiring first time setup or elevated privileges.
/// </summary> /// </summary>
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement> public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
{ {
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class. /// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
/// </summary> /// </summary>
/// <param name="configurationManager">The jellyfin configuration manager.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager) /// <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; _configurationManager = configurationManager;
} }
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{ {
context.Succeed(firstTimeSetupOrElevatedRequirement); 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); 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 System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.RequiresElevationPolicy namespace Jellyfin.Api.Auth.RequiresElevationPolicy
{ {
/// <summary> /// <summary>
/// Authorization handler for requiring elevated privileges. /// Authorization handler for requiring elevated privileges.
/// </summary> /// </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 /> /// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement) 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); context.Succeed(requirement);
} }
else
{
context.Fail();
}
return Task.CompletedTask; 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> /// </summary>
public static class Policies public static class Policies
{ {
/// <summary>
/// Policy name for default authorization.
/// </summary>
public const string DefaultAuthorization = "DefaultAuthorization";
/// <summary> /// <summary>
/// Policy name for requiring first time setup or elevated privileges. /// Policy name for requiring first time setup or elevated privileges.
/// </summary> /// </summary>
@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
/// Policy name for requiring elevated privileges. /// Policy name for requiring elevated privileges.
/// </summary> /// </summary>
public const string RequiresElevation = "RequiresElevation"; 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. /// Configuration Controller.
/// </summary> /// </summary>
[Route("System")] [Route("System")]
[Authorize] [Authorize(Policy = Policies.DefaultAuthorization)]
public class ConfigurationController : BaseJellyfinApiController public class ConfigurationController : BaseJellyfinApiController
{ {
private readonly IServerConfigurationManager _configurationManager; private readonly IServerConfigurationManager _configurationManager;

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ using System;
using System.Net.Mime; using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@ -15,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Attachments controller. /// Attachments controller.
/// </summary> /// </summary>
[Route("Videos")] [Route("Videos")]
[Authorize] [Authorize(Policy = Policies.DefaultAuthorization)]
public class VideoAttachmentsController : BaseJellyfinApiController public class VideoAttachmentsController : BaseJellyfinApiController
{ {
private readonly ILibraryManager _libraryManager; 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 System.Reflection;
using Jellyfin.Api; using Jellyfin.Api;
using Jellyfin.Api.Auth; using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers; using Jellyfin.Api.Controllers;
@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns> /// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{ {
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options => return serviceCollection.AddAuthorizationCore(options =>
{ {
options.AddPolicy( options.AddPolicy(
Policies.RequiresElevation, Policies.DefaultAuthorization,
policy => policy =>
{ {
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new RequiresElevationRequirement()); policy.AddRequirements(new DefaultAuthorizationRequirement());
}); });
options.AddPolicy( options.AddPolicy(
Policies.FirstTimeSetupOrElevated, Policies.FirstTimeSetupOrElevated,
@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); 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); options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
}) })
.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
})
.AddMvc(opts => .AddMvc(opts =>
{ {
opts.UseGeneralRoutePrefix(baseUrl); opts.UseGeneralRoutePrefix(baseUrl);

View file

@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net namespace MediaBrowser.Controller.Net
{ {
/// <summary>
/// IAuthService.
/// </summary>
public interface IAuthService 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 MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net namespace MediaBrowser.Controller.Net
{ {
/// <summary>
/// IAuthorization context.
/// </summary>
public interface IAuthorizationContext public interface IAuthorizationContext
{ {
/// <summary> /// <summary>
@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
/// <param name="requestContext">The request context.</param> /// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns> /// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); 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;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoFixture; using AutoFixture;
using AutoFixture.AutoMoq; using AutoFixture.AutoMoq;
@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
private readonly IFixture _fixture; private readonly IFixture _fixture;
private readonly Mock<IAuthService> _jellyfinAuthServiceMock; 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 CustomAuthenticationHandler _sut;
private readonly AuthenticationScheme _scheme; private readonly AuthenticationScheme _scheme;
@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
AllowFixtureCircularDependencies(); AllowFixtureCircularDependencies();
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>(); _jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
_optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>(); var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
_clockMock = _fixture.Freeze<Mock<ISystemClock>>(); var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
_serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>(); var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
_authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory()); _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))) optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
.Returns(_authenticationServiceMock.Object);
_optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
.Returns(new AuthenticationSchemeOptions .Returns(new AuthenticationSchemeOptions
{ {
ForwardAuthenticate = null ForwardAuthenticate = null
}); });
_context = new DefaultHttpContext HttpContext context = new DefaultHttpContext
{ {
RequestServices = _serviceProviderMock.Object RequestServices = serviceProviderMock.Object
}; };
_scheme = new AuthenticationScheme( _scheme = new AuthenticationScheme(
@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
typeof(CustomAuthenticationHandler)); typeof(CustomAuthenticationHandler));
_sut = _fixture.Create<CustomAuthenticationHandler>(); _sut = _fixture.Create<CustomAuthenticationHandler>();
_sut.InitializeAsync(_scheme, _context).Wait(); _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);
} }
[Fact] [Fact]
@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup( _jellyfinAuthServiceMock.Setup(
a => a.Authenticate( a => a.Authenticate(
It.IsAny<HttpRequest>(), It.IsAny<HttpRequest>()))
It.IsAny<AuthenticatedAttribute>()))
.Throws(new SecurityException(errorMessage)); .Throws(new SecurityException(errorMessage));
var authenticateResult = await _sut.AuthenticateAsync(); var authenticateResult = await _sut.AuthenticateAsync();
@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
[Fact] [Fact]
public async Task HandleAuthenticateAsyncShouldAssignNameClaim() public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
{ {
var user = SetupUser(); var authorizationInfo = SetupUser();
var authenticateResult = await _sut.AuthenticateAsync(); 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] [Theory]
@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
[InlineData(false)] [InlineData(false)]
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin) public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
{ {
var user = SetupUser(isAdmin); var authorizationInfo = SetupUser(isAdmin);
var authenticateResult = await _sut.AuthenticateAsync(); 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)); Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
} }
@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme); Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
} }
private User SetupUser(bool isAdmin = false) private AuthorizationInfo SetupUser(bool isAdmin = false)
{ {
var user = _fixture.Create<User>(); var authorizationInfo = _fixture.Create<AuthorizationInfo>();
user.SetPermission(PermissionKind.IsAdministrator, isAdmin); authorizationInfo.User = _fixture.Create<User>();
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
_jellyfinAuthServiceMock.Setup( _jellyfinAuthServiceMock.Setup(
a => a.Authenticate( a => a.Authenticate(
It.IsAny<HttpRequest>(), It.IsAny<HttpRequest>()))
It.IsAny<AuthenticatedAttribute>())) .Returns(authorizationInfo);
.Returns(user);
return user; return authorizationInfo;
} }
private void AllowFixtureCircularDependencies() 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.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoFixture; using AutoFixture;
using AutoFixture.AutoMoq; using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Configuration; using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq; using Moq;
using Xunit; using Xunit;
@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
private readonly Mock<IConfigurationManager> _configurationManagerMock; private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements; private readonly List<IAuthorizationRequirement> _requirements;
private readonly FirstTimeSetupOrElevatedHandler _sut; private readonly FirstTimeSetupOrElevatedHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public FirstTimeSetupOrElevatedHandlerTests() public FirstTimeSetupOrElevatedHandlerTests()
{ {
var fixture = new Fixture().Customize(new AutoMoqCustomization()); var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() }; _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>(); _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
} }
@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User)] [InlineData(UserRoles.User)]
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole) public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
{ {
SetupConfigurationManager(false); TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
var user = SetupUser(userRole); var claims = TestHelpers.SetupUser(
var context = new AuthorizationHandlerContext(_requirements, user, null); _userManagerMock,
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context); await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User, false)] [InlineData(UserRoles.User, false)]
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed) public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
{ {
SetupConfigurationManager(true); TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var user = SetupUser(userRole); var claims = TestHelpers.SetupUser(
var context = new AuthorizationHandlerContext(_requirements, user, null); _userManagerMock,
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context); await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded); 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.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Moq;
using Xunit; using Xunit;
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
{ {
public class RequiresElevationHandlerTests public class RequiresElevationHandlerTests
{ {
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly RequiresElevationHandler _sut; private readonly RequiresElevationHandler _sut;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public RequiresElevationHandlerTests() 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] [Theory]
@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
[InlineData(UserRoles.Guest, false)] [InlineData(UserRoles.Guest, false)]
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) 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 context = new AuthorizationHandlerContext(_requirements, claims, null);
var identity = new ClaimsIdentity(claims);
var user = new ClaimsPrincipal(identity);
var context = new AuthorizationHandlerContext(requirements, user, null);
await _sut.HandleAsync(context); await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded); Assert.Equal(shouldSucceed, context.HasSucceeded);

View file

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