mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-16 02:18:54 -07:00
Merge remote-tracking branch 'remotes/upstream/api-migration' into api-user
This commit is contained in:
commit
762eeb51e6
@ -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.");
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
102
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The default authorization requirement.
|
||||
/// </summary>
|
||||
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Escape schedule controls requirement.
|
||||
/// </summary>
|
||||
public class IgnoreScheduleRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
44
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
Normal file
44
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The local access authorization requirement.
|
||||
/// </summary>
|
||||
public class LocalAccessRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
38
Jellyfin.Api/Constants/InternalClaimTypes.cs
Normal file
38
Jellyfin.Api/Constants/InternalClaimTypes.cs
Normal 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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System;
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
@ -18,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;
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
@ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Devices Controller.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class DevicesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CA1801
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
@ -1,4 +1,3 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System.ComponentModel;
|
||||
|
341
Jellyfin.Api/Controllers/LibraryStructureController.cs
Normal file
341
Jellyfin.Api/Controllers/LibraryStructureController.cs
Normal file
@ -0,0 +1,341 @@
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The library structure controller.
|
||||
/// </summary>
|
||||
[Route("/Library/VirtualFolders")]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||
public class LibraryStructureController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILibraryMonitor _libraryMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
|
||||
public LibraryStructureController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILibraryManager libraryManager,
|
||||
ILibraryMonitor libraryMonitor)
|
||||
{
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_libraryManager = libraryManager;
|
||||
_libraryMonitor = libraryMonitor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all virtual folders.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <response code="200">Virtual folders retrieved.</response>
|
||||
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
|
||||
{
|
||||
return _libraryManager.GetVirtualFolders(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a virtual folder.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the virtual folder.</param>
|
||||
/// <param name="collectionType">The type of the collection.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <param name="paths">The paths of the virtual folder.</param>
|
||||
/// <param name="libraryOptions">The library options.</param>
|
||||
/// <response code="204">Folder added.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddVirtualFolder(
|
||||
[FromQuery] string name,
|
||||
[FromQuery] string collectionType,
|
||||
[FromQuery] bool refreshLibrary,
|
||||
[FromQuery] string[] paths,
|
||||
[FromQuery] LibraryOptions libraryOptions)
|
||||
{
|
||||
libraryOptions ??= new LibraryOptions();
|
||||
|
||||
if (paths != null && paths.Length > 0)
|
||||
{
|
||||
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
|
||||
}
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a virtual folder.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the folder.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <response code="204">Folder removed.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> RemoveVirtualFolder(
|
||||
[FromQuery] string name,
|
||||
[FromQuery] bool refreshLibrary)
|
||||
{
|
||||
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames a virtual folder.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the virtual folder.</param>
|
||||
/// <param name="newName">The new name.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <response code="204">Folder renamed.</response>
|
||||
/// <response code="404">Library doesn't exist.</response>
|
||||
/// <response code="409">Library already exists.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
|
||||
/// <exception cref="ArgumentNullException">The new name may not be null.</exception>
|
||||
[HttpPost("Name")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public ActionResult RenameVirtualFolder(
|
||||
[FromQuery] string name,
|
||||
[FromQuery] string newName,
|
||||
[FromQuery] bool refreshLibrary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newName));
|
||||
}
|
||||
|
||||
var rootFolderPath = _appPaths.DefaultUserViewsPath;
|
||||
|
||||
var currentPath = Path.Combine(rootFolderPath, name);
|
||||
var newPath = Path.Combine(rootFolderPath, newName);
|
||||
|
||||
if (!Directory.Exists(currentPath))
|
||||
{
|
||||
return NotFound("The media collection does not exist.");
|
||||
}
|
||||
|
||||
if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
|
||||
{
|
||||
return Conflict($"The media library already exists at {newPath}.");
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
// Changing capitalization. Handle windows case insensitivity
|
||||
if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var tempPath = Path.Combine(
|
||||
rootFolderPath,
|
||||
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
|
||||
Directory.Move(currentPath, tempPath);
|
||||
currentPath = tempPath;
|
||||
}
|
||||
|
||||
Directory.Move(currentPath, newPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CollectionFolder.OnCollectionFolderChange();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
// Have to block here to allow exceptions to bubble
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a media path to a library.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the library.</param>
|
||||
/// <param name="path">The path to add.</param>
|
||||
/// <param name="pathInfo">The path info.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
/// <response code="204">Media path added.</response>
|
||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||
[HttpPost("Paths")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult AddMediaPath(
|
||||
[FromQuery] string name,
|
||||
[FromQuery] string path,
|
||||
[FromQuery] MediaPathInfo pathInfo,
|
||||
[FromQuery] bool refreshLibrary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
var mediaPath = pathInfo ?? new MediaPathInfo { Path = path };
|
||||
|
||||
_libraryManager.AddMediaPath(name, mediaPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
// Have to block here to allow exceptions to bubble
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a media path.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the library.</param>
|
||||
/// <param name="pathInfo">The path info.</param>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
/// <response code="204">Media path updated.</response>
|
||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||
[HttpPost("Paths/Update")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateMediaPath(
|
||||
[FromQuery] string name,
|
||||
[FromQuery] MediaPathInfo pathInfo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_libraryManager.UpdateMediaPath(name, pathInfo);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a media path.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the library.</param>
|
||||
/// <param name="path">The path to remove.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
/// <response code="204">Media path removed.</response>
|
||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||
[HttpDelete("Paths")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult RemoveMediaPath(
|
||||
[FromQuery] string name,
|
||||
[FromQuery] string path,
|
||||
[FromQuery] bool refreshLibrary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
_libraryManager.RemoveMediaPath(name, path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
// Have to block here to allow exceptions to bubble
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update library options.
|
||||
/// </summary>
|
||||
/// <param name="id">The library name.</param>
|
||||
/// <param name="libraryOptions">The library options.</param>
|
||||
/// <response code="204">Library updated.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("LibraryOptions")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateLibraryOptions(
|
||||
[FromQuery] string id,
|
||||
[FromQuery] LibraryOptions libraryOptions)
|
||||
{
|
||||
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
|
||||
|
||||
collectionFolder.UpdateLibraryOptions(libraryOptions);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System;
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -18,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;
|
||||
|
@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CA1801
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -21,7 +19,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Jellyfin.Api.Controllers.Images
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Remote Images Controller.
|
@ -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;
|
||||
|
@ -1,4 +1,3 @@
|
||||
#nullable enable
|
||||
#pragma warning disable CA1801
|
||||
|
||||
using System;
|
||||
@ -110,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,
|
||||
@ -130,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,
|
||||
@ -160,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)
|
||||
@ -250,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,
|
||||
|
@ -1,9 +1,8 @@
|
||||
#nullable enable
|
||||
|
||||
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;
|
||||
@ -17,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;
|
||||
|
77
Jellyfin.Api/Helpers/ClaimHelpers.cs
Normal file
77
Jellyfin.Api/Helpers/ClaimHelpers.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Jellyfin.Api.Models.ConfigurationDtos
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using MediaBrowser.Model.Notifications;
|
||||
|
||||
namespace Jellyfin.Api.Models.NotificationDtos
|
||||
|
@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace Jellyfin.Api.Models.PluginDtos
|
||||
{
|
||||
|
@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Jellyfin.Api.Models.PluginDtos
|
||||
namespace Jellyfin.Api.Models.PluginDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugin security info.
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Api.Models.StartupDtos
|
||||
{
|
||||
/// <summary>
|
||||
@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos
|
||||
/// <summary>
|
||||
/// Gets or sets UI language culture.
|
||||
/// </summary>
|
||||
public string UICulture { get; set; }
|
||||
public string? UICulture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata country code.
|
||||
/// </summary>
|
||||
public string MetadataCountryCode { get; set; }
|
||||
public string? MetadataCountryCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the preferred language for the metadata.
|
||||
/// </summary>
|
||||
public string PreferredMetadataLanguage { get; set; }
|
||||
public string? PreferredMetadataLanguage { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Api.Models.StartupDtos
|
||||
{
|
||||
/// <summary>
|
||||
@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos
|
||||
/// <summary>
|
||||
/// Gets or sets the username.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user's password.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -1,412 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Api.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetDefaultVirtualFolders
|
||||
/// </summary>
|
||||
[Route("/Library/VirtualFolders", "GET")]
|
||||
public class GetVirtualFolders : IReturn<List<VirtualFolderInfo>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders", "POST")]
|
||||
public class AddVirtualFolder : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the collection.
|
||||
/// </summary>
|
||||
/// <value>The type of the collection.</value>
|
||||
public string CollectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [refresh library].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
|
||||
public bool RefreshLibrary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string[] Paths { get; set; }
|
||||
|
||||
public LibraryOptions LibraryOptions { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders", "DELETE")]
|
||||
public class RemoveVirtualFolder : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [refresh library].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
|
||||
public bool RefreshLibrary { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders/Name", "POST")]
|
||||
public class RenameVirtualFolder : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string NewName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [refresh library].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
|
||||
public bool RefreshLibrary { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders/Paths", "POST")]
|
||||
public class AddMediaPath : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
public MediaPathInfo PathInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [refresh library].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
|
||||
public bool RefreshLibrary { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders/Paths/Update", "POST")]
|
||||
public class UpdateMediaPath : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
public MediaPathInfo PathInfo { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders/Paths", "DELETE")]
|
||||
public class RemoveMediaPath : IReturnVoid
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [refresh library].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [refresh library]; otherwise, <c>false</c>.</value>
|
||||
public bool RefreshLibrary { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Library/VirtualFolders/LibraryOptions", "POST")]
|
||||
public class UpdateLibraryOptions : IReturnVoid
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public LibraryOptions LibraryOptions { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class LibraryStructureService
|
||||
/// </summary>
|
||||
[Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
|
||||
public class LibraryStructureService : BaseApiService
|
||||
{
|
||||
/// <summary>
|
||||
/// The _app paths
|
||||
/// </summary>
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
|
||||
/// <summary>
|
||||
/// The _library manager
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILibraryMonitor _libraryMonitor;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryStructureService" /> class.
|
||||
/// </summary>
|
||||
public LibraryStructureService(
|
||||
ILogger<LibraryStructureService> logger,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
ILibraryManager libraryManager,
|
||||
ILibraryMonitor libraryMonitor)
|
||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
||||
{
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_libraryManager = libraryManager;
|
||||
_libraryMonitor = libraryMonitor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetVirtualFolders request)
|
||||
{
|
||||
var result = _libraryManager.GetVirtualFolders(true);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Post(UpdateLibraryOptions request)
|
||||
{
|
||||
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id);
|
||||
|
||||
collectionFolder.UpdateLibraryOptions(request.LibraryOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public Task Post(AddVirtualFolder request)
|
||||
{
|
||||
var libraryOptions = request.LibraryOptions ?? new LibraryOptions();
|
||||
|
||||
if (request.Paths != null && request.Paths.Length > 0)
|
||||
{
|
||||
libraryOptions.PathInfos = request.Paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
|
||||
}
|
||||
|
||||
return _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, libraryOptions, request.RefreshLibrary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(RenameVirtualFolder request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.NewName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
var rootFolderPath = _appPaths.DefaultUserViewsPath;
|
||||
|
||||
var currentPath = Path.Combine(rootFolderPath, request.Name);
|
||||
var newPath = Path.Combine(rootFolderPath, request.NewName);
|
||||
|
||||
if (!Directory.Exists(currentPath))
|
||||
{
|
||||
throw new FileNotFoundException("The media collection does not exist");
|
||||
}
|
||||
|
||||
if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
|
||||
{
|
||||
throw new ArgumentException("Media library already exists at " + newPath + ".");
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
// Changing capitalization. Handle windows case insensitivity
|
||||
if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
|
||||
Directory.Move(currentPath, tempPath);
|
||||
currentPath = tempPath;
|
||||
}
|
||||
|
||||
Directory.Move(currentPath, newPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CollectionFolder.OnCollectionFolderChange();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public Task Delete(RemoveVirtualFolder request)
|
||||
{
|
||||
return _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(AddMediaPath request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
var mediaPath = request.PathInfo ?? new MediaPathInfo
|
||||
{
|
||||
Path = request.Path
|
||||
};
|
||||
|
||||
_libraryManager.AddMediaPath(request.Name, mediaPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Post(UpdateMediaPath request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
_libraryManager.UpdateMediaPath(request.Name, request.PathInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
public void Delete(RemoveMediaPath request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
_libraryManager.RemoveMediaPath(request.Name, request.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (request.RefreshLibrary)
|
||||
{
|
||||
_libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
var task = Task.Delay(1000);
|
||||
// Have to block here to allow exceptions to bubble
|
||||
Task.WaitAll(task);
|
||||
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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' ">
|
||||
|
81
tests/Jellyfin.Api.Tests/TestHelpers.cs
Normal file
81
tests/Jellyfin.Api.Tests/TestHelpers.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user