mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-06-03 05:34:16 -04: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;
|
_networkManager = networkManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
|
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
|
||||||
{
|
{
|
||||||
ValidateUser(request, authAttribtues);
|
ValidateUser(request, authAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
||||||
@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||||
|
{
|
||||||
|
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||||
|
if (auth?.User == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
||||||
|
{
|
||||||
|
throw new SecurityException("User account has been disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
|
||||||
{
|
{
|
||||||
// This code is executed before the service
|
// This code is executed before the service
|
||||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||||
|
|
||||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
if (!IsExemptFromAuthenticationToken(authAttributes, request))
|
||||||
{
|
{
|
||||||
ValidateSecurityToken(request, auth.Token);
|
ValidateSecurityToken(request, auth.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
|
if (authAttributes.AllowLocalOnly && !request.IsLocal)
|
||||||
{
|
{
|
||||||
throw new SecurityException("Operation not found.");
|
throw new SecurityException("Operation not found.");
|
||||||
}
|
}
|
||||||
@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
ValidateUserAccess(user, request, authAttribtues, auth);
|
ValidateUserAccess(user, request, authAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = GetTokenInfo(request);
|
var info = GetTokenInfo(request);
|
||||||
|
|
||||||
if (!IsExemptFromRoles(auth, authAttribtues, request, info))
|
if (!IsExemptFromRoles(auth, authAttributes, request, info))
|
||||||
{
|
{
|
||||||
var roles = authAttribtues.GetRoles();
|
var roles = authAttributes.GetRoles();
|
||||||
|
|
||||||
ValidateRoles(roles, user);
|
ValidateRoles(roles, user);
|
||||||
}
|
}
|
||||||
@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
private void ValidateUserAccess(
|
private void ValidateUserAccess(
|
||||||
User user,
|
User user,
|
||||||
IRequest request,
|
IRequest request,
|
||||||
IAuthenticationAttributes authAttributes,
|
IAuthenticationAttributes authAttributes)
|
||||||
AuthorizationInfo auth)
|
|
||||||
{
|
{
|
||||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||||
{
|
{
|
||||||
@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
{
|
{
|
||||||
throw new AuthenticationException("Access token is invalid or expired.");
|
throw new AuthenticationException("Access token is invalid or expired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (!string.IsNullOrEmpty(info.UserId))
|
|
||||||
//{
|
|
||||||
// var user = _userManager.GetUserById(info.UserId);
|
|
||||||
|
|
||||||
// if (user == null || user.Configuration.IsDisabled)
|
|
||||||
// {
|
|
||||||
// throw new SecurityException("User account has been disabled.");
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
|
|||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Security;
|
using MediaBrowser.Controller.Security;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer.Security
|
namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
return GetAuthorization(requestContext);
|
return GetAuthorization(requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||||
|
{
|
||||||
|
var auth = GetAuthorizationDictionary(requestContext);
|
||||||
|
var (authInfo, _) =
|
||||||
|
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||||
|
return authInfo;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authorization.
|
/// Gets the authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
private AuthorizationInfo GetAuthorization(IRequest httpReq)
|
private AuthorizationInfo GetAuthorization(IRequest httpReq)
|
||||||
{
|
{
|
||||||
var auth = GetAuthorizationDictionary(httpReq);
|
var auth = GetAuthorizationDictionary(httpReq);
|
||||||
|
var (authInfo, originalAuthInfo) =
|
||||||
|
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
|
||||||
|
|
||||||
|
if (originalAuthInfo != null)
|
||||||
|
{
|
||||||
|
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq.Items["AuthorizationInfo"] = authInfo;
|
||||||
|
return authInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||||
|
in Dictionary<string, string> auth,
|
||||||
|
in IHeaderDictionary headers,
|
||||||
|
in IQueryCollection queryString)
|
||||||
|
{
|
||||||
string deviceId = null;
|
string deviceId = null;
|
||||||
string device = null;
|
string device = null;
|
||||||
string client = null;
|
string client = null;
|
||||||
@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
token = httpReq.Headers["X-Emby-Token"];
|
token = headers["X-Emby-Token"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
token = httpReq.Headers["X-MediaBrowser-Token"];
|
token = headers["X-MediaBrowser-Token"];
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
token = httpReq.QueryString["api_key"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = new AuthorizationInfo
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
token = queryString["ApiKey"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO deprecate this query parameter.
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
token = queryString["api_key"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var authInfo = new AuthorizationInfo
|
||||||
{
|
{
|
||||||
Client = client,
|
Client = client,
|
||||||
Device = device,
|
Device = device,
|
||||||
@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
Token = token
|
Token = token
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AuthenticationInfo originalAuthenticationInfo = null;
|
||||||
if (!string.IsNullOrWhiteSpace(token))
|
if (!string.IsNullOrWhiteSpace(token))
|
||||||
{
|
{
|
||||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||||
@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
AccessToken = token
|
AccessToken = token
|
||||||
});
|
});
|
||||||
|
|
||||||
var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||||
|
|
||||||
if (tokenInfo != null)
|
if (originalAuthenticationInfo != null)
|
||||||
{
|
{
|
||||||
var updateToken = false;
|
var updateToken = false;
|
||||||
|
|
||||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||||
if (string.IsNullOrWhiteSpace(info.Client))
|
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||||
{
|
{
|
||||||
info.Client = tokenInfo.AppName;
|
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.DeviceId))
|
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||||
{
|
{
|
||||||
info.DeviceId = tokenInfo.DeviceId;
|
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||||
var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.Device))
|
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||||
{
|
{
|
||||||
info.Device = tokenInfo.DeviceName;
|
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||||
}
|
}
|
||||||
|
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||||
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
if (allowTokenInfoUpdate)
|
if (allowTokenInfoUpdate)
|
||||||
{
|
{
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
tokenInfo.DeviceName = info.Device;
|
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.Version))
|
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||||
{
|
{
|
||||||
info.Version = tokenInfo.AppVersion;
|
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||||
}
|
}
|
||||||
else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (allowTokenInfoUpdate)
|
if (allowTokenInfoUpdate)
|
||||||
{
|
{
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
tokenInfo.AppVersion = info.Version;
|
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
|
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||||
{
|
{
|
||||||
tokenInfo.DateLastActivity = DateTime.UtcNow;
|
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tokenInfo.UserId.Equals(Guid.Empty))
|
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||||
{
|
{
|
||||||
info.User = _userManager.GetUserById(tokenInfo.UserId);
|
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||||
|
|
||||||
if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
tokenInfo.UserName = info.User.Username;
|
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateToken)
|
if (updateToken)
|
||||||
{
|
{
|
||||||
_authRepo.Update(tokenInfo);
|
_authRepo.Update(originalAuthenticationInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
httpReq.Items["AuthorizationInfo"] = info;
|
return (authInfo, originalAuthenticationInfo);
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
return GetAuthorization(auth);
|
return GetAuthorization(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the auth.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpReq">The HTTP req.</param>
|
||||||
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
|
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
||||||
|
{
|
||||||
|
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(auth))
|
||||||
|
{
|
||||||
|
auth = httpReq.Headers[HeaderNames.Authorization];
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetAuthorization(auth);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authorization.
|
/// Gets the authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
|
|
||||||
private static string NormalizeValue(string value)
|
private static string NormalizeValue(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WebUtility.HtmlEncode(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.Authentication;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
var authenticatedAttribute = new AuthenticatedAttribute
|
|
||||||
{
|
|
||||||
IgnoreLegacyAuth = true
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var user = _authService.Authenticate(Request, authenticatedAttribute);
|
var authorizationInfo = _authService.Authenticate(Request);
|
||||||
if (user == null)
|
if (authorizationInfo == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(AuthenticateResult.NoResult());
|
return Task.FromResult(AuthenticateResult.NoResult());
|
||||||
// TODO return when legacy API is removed.
|
// TODO return when legacy API is removed.
|
||||||
@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
|
|||||||
|
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, user.Username),
|
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
||||||
new Claim(
|
new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
|
||||||
ClaimTypes.Role,
|
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
||||||
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
|
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
||||||
|
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
||||||
|
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
||||||
|
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
||||||
|
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
||||||
};
|
};
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||||
var principal = new ClaimsPrincipal(identity);
|
var principal = new ClaimsPrincipal(identity);
|
||||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||||
|
@ -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 System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authorization handler for requiring first time setup or elevated privileges.
|
/// Authorization handler for requiring first time setup or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
||||||
{
|
{
|
||||||
private readonly IConfigurationManager _configurationManager;
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">The jellyfin configuration manager.</param>
|
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public FirstTimeSetupOrElevatedHandler(
|
||||||
|
IConfigurationManager configurationManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
{
|
{
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
}
|
}
|
||||||
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
|||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
else if (context.User.IsInRole(UserRoles.Administrator))
|
|
||||||
|
var validated = ValidateClaims(context.User);
|
||||||
|
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||||
}
|
}
|
||||||
|
@ -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 System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authorization handler for requiring elevated privileges.
|
/// Authorization handler for requiring elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
|
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public RequiresElevationHandler(
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
|
||||||
{
|
{
|
||||||
if (context.User.IsInRole(UserRoles.Administrator))
|
var validated = ValidateClaims(context.User);
|
||||||
|
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
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>
|
/// </summary>
|
||||||
public static class Policies
|
public static class Policies
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for default authorization.
|
||||||
|
/// </summary>
|
||||||
|
public const string DefaultAuthorization = "DefaultAuthorization";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring first time setup or elevated privileges.
|
/// Policy name for requiring first time setup or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
|
|||||||
/// Policy name for requiring elevated privileges.
|
/// Policy name for requiring elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RequiresElevation = "RequiresElevation";
|
public const string RequiresElevation = "RequiresElevation";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for allowing local access only.
|
||||||
|
/// </summary>
|
||||||
|
public const string LocalAccessOnly = "LocalAccessOnly";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for escaping schedule controls.
|
||||||
|
/// </summary>
|
||||||
|
public const string IgnoreSchedule = "IgnoreSchedule";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
#pragma warning disable CA1801
|
#pragma warning disable CA1801
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Configuration Controller.
|
/// Configuration Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("System")]
|
[Route("System")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class ConfigurationController : BaseJellyfinApiController
|
public class ConfigurationController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
@ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Devices Controller.
|
/// Devices Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class DevicesController : BaseJellyfinApiController
|
public class DevicesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IDeviceManager _deviceManager;
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
#pragma warning disable CA1801
|
||||||
#pragma warning disable CA1801
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
#pragma warning disable CA1801
|
#pragma warning disable CA1801
|
||||||
|
|
||||||
using System.ComponentModel;
|
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
|
#pragma warning disable CA1801
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Package Controller.
|
/// Package Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Packages")]
|
[Route("Packages")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class PackageController : BaseJellyfinApiController
|
public class PackageController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IInstallationManager _installationManager;
|
private readonly IInstallationManager _installationManager;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
#pragma warning disable CA1801
|
||||||
#pragma warning disable CA1801
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -21,7 +19,7 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers.Images
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Remote Images Controller.
|
/// Remote Images Controller.
|
@ -3,6 +3,7 @@ using System.ComponentModel;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Search controller.
|
/// Search controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("/Search/Hints")]
|
[Route("/Search/Hints")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class SearchController : BaseJellyfinApiController
|
public class SearchController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ISearchEngine _searchEngine;
|
private readonly ISearchEngine _searchEngine;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
#pragma warning disable CA1801
|
#pragma warning disable CA1801
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -110,7 +109,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Subtitles retrieved.</response>
|
/// <response code="200">Subtitles retrieved.</response>
|
||||||
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
|
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
|
||||||
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
|
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
|
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
@ -130,7 +129,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Subtitle downloaded.</response>
|
/// <response code="204">Subtitle downloaded.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
|
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> DownloadRemoteSubtitles(
|
public async Task<ActionResult> DownloadRemoteSubtitles(
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
@ -160,7 +159,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">File returned.</response>
|
/// <response code="200">File returned.</response>
|
||||||
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
|
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
|
||||||
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
|
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Produces(MediaTypeNames.Application.Octet)]
|
[Produces(MediaTypeNames.Application.Octet)]
|
||||||
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
|
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
|
||||||
@ -250,7 +249,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="200">Subtitle playlist retrieved.</response>
|
/// <response code="200">Subtitle playlist retrieved.</response>
|
||||||
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
|
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
|
||||||
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
|
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetSubtitlePlaylist(
|
public async Task<ActionResult> GetSubtitlePlaylist(
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
@ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Attachments controller.
|
/// Attachments controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Videos")]
|
[Route("Videos")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class VideoAttachmentsController : BaseJellyfinApiController
|
public class VideoAttachmentsController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
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
|
namespace Jellyfin.Api.Models.ConfigurationDtos
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using MediaBrowser.Model.Notifications;
|
using MediaBrowser.Model.Notifications;
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using MediaBrowser.Model.Notifications;
|
using MediaBrowser.Model.Notifications;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Models.NotificationDtos
|
namespace Jellyfin.Api.Models.NotificationDtos
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
#nullable enable
|
using System;
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Models.PluginDtos
|
namespace Jellyfin.Api.Models.PluginDtos
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
#nullable enable
|
namespace Jellyfin.Api.Models.PluginDtos
|
||||||
|
|
||||||
namespace Jellyfin.Api.Models.PluginDtos
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plugin security info.
|
/// Plugin security info.
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Models.StartupDtos
|
namespace Jellyfin.Api.Models.StartupDtos
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets UI language culture.
|
/// Gets or sets UI language culture.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string UICulture { get; set; }
|
public string? UICulture { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the metadata country code.
|
/// Gets or sets the metadata country code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MetadataCountryCode { get; set; }
|
public string? MetadataCountryCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the preferred language for the metadata.
|
/// Gets or sets the preferred language for the metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PreferredMetadataLanguage { get; set; }
|
public string? PreferredMetadataLanguage { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Models.StartupDtos
|
namespace Jellyfin.Api.Models.StartupDtos
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the username.
|
/// Gets or sets the username.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user's password.
|
/// Gets or sets the user's password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Password { get; set; }
|
public string? Password { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,10 @@ using System.Linq;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Jellyfin.Api;
|
using Jellyfin.Api;
|
||||||
using Jellyfin.Api.Auth;
|
using Jellyfin.Api.Auth;
|
||||||
|
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||||
|
using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
|
||||||
|
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Controllers;
|
using Jellyfin.Api.Controllers;
|
||||||
@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
|
|||||||
/// <returns>The updated service collection.</returns>
|
/// <returns>The updated service collection.</returns>
|
||||||
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
|
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
||||||
return serviceCollection.AddAuthorizationCore(options =>
|
return serviceCollection.AddAuthorizationCore(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.RequiresElevation,
|
Policies.DefaultAuthorization,
|
||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new RequiresElevationRequirement());
|
policy.AddRequirements(new DefaultAuthorizationRequirement());
|
||||||
});
|
});
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.FirstTimeSetupOrElevated,
|
Policies.FirstTimeSetupOrElevated,
|
||||||
@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
|
|||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
|
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
|
||||||
});
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.IgnoreSchedule,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new IgnoreScheduleRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.LocalAccessOnly,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new LocalAccessRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.RequiresElevation,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new RequiresElevationRequirement());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +107,10 @@ namespace Jellyfin.Server.Extensions
|
|||||||
{
|
{
|
||||||
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
|
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
|
||||||
})
|
})
|
||||||
|
.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
})
|
||||||
.AddMvc(opts =>
|
.AddMvc(opts =>
|
||||||
{
|
{
|
||||||
opts.UseGeneralRoutePrefix(baseUrl);
|
opts.UseGeneralRoutePrefix(baseUrl);
|
||||||
|
@ -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
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IAuthService.
|
||||||
|
/// </summary>
|
||||||
public interface IAuthService
|
public interface IAuthService
|
||||||
{
|
{
|
||||||
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
|
/// <summary>
|
||||||
|
/// Authenticate and authorize request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request.</param>
|
||||||
|
/// <param name="authAttribtutes">Authorization attributes.</param>
|
||||||
|
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
|
||||||
|
|
||||||
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
|
/// <summary>
|
||||||
|
/// Authenticate and authorize request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request.</param>
|
||||||
|
/// <param name="authAttribtutes">Authorization attributes.</param>
|
||||||
|
/// <returns>Authenticated user.</returns>
|
||||||
|
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticate request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request.</param>
|
||||||
|
/// <returns>Authorization information. Null if unauthenticated.</returns>
|
||||||
|
AuthorizationInfo Authenticate(HttpRequest request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IAuthorization context.
|
||||||
|
/// </summary>
|
||||||
public interface IAuthorizationContext
|
public interface IAuthorizationContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
|
|||||||
/// <param name="requestContext">The request context.</param>
|
/// <param name="requestContext">The request context.</param>
|
||||||
/// <returns>AuthorizationInfo.</returns>
|
/// <returns>AuthorizationInfo.</returns>
|
||||||
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
|
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authorization information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestContext">The request context.</param>
|
||||||
|
/// <returns>AuthorizationInfo.</returns>
|
||||||
|
AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using AutoFixture.AutoMoq;
|
using AutoFixture.AutoMoq;
|
||||||
@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
|
|||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
private readonly IFixture _fixture;
|
private readonly IFixture _fixture;
|
||||||
|
|
||||||
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
|
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
|
||||||
private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
|
|
||||||
private readonly Mock<ISystemClock> _clockMock;
|
|
||||||
private readonly Mock<IServiceProvider> _serviceProviderMock;
|
|
||||||
private readonly Mock<IAuthenticationService> _authenticationServiceMock;
|
|
||||||
private readonly UrlEncoder _urlEncoder;
|
|
||||||
private readonly HttpContext _context;
|
|
||||||
|
|
||||||
private readonly CustomAuthenticationHandler _sut;
|
private readonly CustomAuthenticationHandler _sut;
|
||||||
private readonly AuthenticationScheme _scheme;
|
private readonly AuthenticationScheme _scheme;
|
||||||
@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
AllowFixtureCircularDependencies();
|
AllowFixtureCircularDependencies();
|
||||||
|
|
||||||
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
|
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
|
||||||
_optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
|
var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
|
||||||
_clockMock = _fixture.Freeze<Mock<ISystemClock>>();
|
var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
|
||||||
_serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
|
var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
|
||||||
_authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
|
|
||||||
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
|
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
|
||||||
|
|
||||||
_urlEncoder = UrlEncoder.Default;
|
serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
|
||||||
|
.Returns(authenticationServiceMock.Object);
|
||||||
|
|
||||||
_serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
|
optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
|
||||||
.Returns(_authenticationServiceMock.Object);
|
|
||||||
|
|
||||||
_optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
|
|
||||||
.Returns(new AuthenticationSchemeOptions
|
.Returns(new AuthenticationSchemeOptions
|
||||||
{
|
{
|
||||||
ForwardAuthenticate = null
|
ForwardAuthenticate = null
|
||||||
});
|
});
|
||||||
|
|
||||||
_context = new DefaultHttpContext
|
HttpContext context = new DefaultHttpContext
|
||||||
{
|
{
|
||||||
RequestServices = _serviceProviderMock.Object
|
RequestServices = serviceProviderMock.Object
|
||||||
};
|
};
|
||||||
|
|
||||||
_scheme = new AuthenticationScheme(
|
_scheme = new AuthenticationScheme(
|
||||||
@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
typeof(CustomAuthenticationHandler));
|
typeof(CustomAuthenticationHandler));
|
||||||
|
|
||||||
_sut = _fixture.Create<CustomAuthenticationHandler>();
|
_sut = _fixture.Create<CustomAuthenticationHandler>();
|
||||||
_sut.InitializeAsync(_scheme, _context).Wait();
|
_sut.InitializeAsync(_scheme, context).Wait();
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
|
|
||||||
{
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
|
||||||
a => a.Authenticate(
|
|
||||||
It.IsAny<HttpRequest>(),
|
|
||||||
It.IsAny<AuthenticatedAttribute>()))
|
|
||||||
.Returns((User?)null);
|
|
||||||
|
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
|
||||||
|
|
||||||
Assert.False(authenticateResult.Succeeded);
|
|
||||||
Assert.True(authenticateResult.None);
|
|
||||||
// TODO return when legacy API is removed.
|
|
||||||
// Assert.Equal("Invalid user", authenticateResult.Failure.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
_jellyfinAuthServiceMock.Setup(
|
||||||
a => a.Authenticate(
|
a => a.Authenticate(
|
||||||
It.IsAny<HttpRequest>(),
|
It.IsAny<HttpRequest>()))
|
||||||
It.IsAny<AuthenticatedAttribute>()))
|
|
||||||
.Throws(new SecurityException(errorMessage));
|
.Throws(new SecurityException(errorMessage));
|
||||||
|
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
var authenticateResult = await _sut.AuthenticateAsync();
|
||||||
@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
|
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
|
||||||
{
|
{
|
||||||
var user = SetupUser();
|
var authorizationInfo = SetupUser();
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
var authenticateResult = await _sut.AuthenticateAsync();
|
||||||
|
|
||||||
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username));
|
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
[InlineData(false)]
|
[InlineData(false)]
|
||||||
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
|
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
|
||||||
{
|
{
|
||||||
var user = SetupUser(isAdmin);
|
var authorizationInfo = SetupUser(isAdmin);
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
var authenticateResult = await _sut.AuthenticateAsync();
|
||||||
|
|
||||||
var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
|
var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
|
||||||
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
|
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
|
|||||||
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
|
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
private User SetupUser(bool isAdmin = false)
|
private AuthorizationInfo SetupUser(bool isAdmin = false)
|
||||||
{
|
{
|
||||||
var user = _fixture.Create<User>();
|
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
|
||||||
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
authorizationInfo.User = _fixture.Create<User>();
|
||||||
|
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
||||||
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
_jellyfinAuthServiceMock.Setup(
|
||||||
a => a.Authenticate(
|
a => a.Authenticate(
|
||||||
It.IsAny<HttpRequest>(),
|
It.IsAny<HttpRequest>()))
|
||||||
It.IsAny<AuthenticatedAttribute>()))
|
.Returns(authorizationInfo);
|
||||||
.Returns(user);
|
|
||||||
|
|
||||||
return user;
|
return authorizationInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AllowFixtureCircularDependencies()
|
private void AllowFixtureCircularDependencies()
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using AutoFixture.AutoMoq;
|
using AutoFixture.AutoMoq;
|
||||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
|
|||||||
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
private readonly List<IAuthorizationRequirement> _requirements;
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
private readonly FirstTimeSetupOrElevatedHandler _sut;
|
private readonly FirstTimeSetupOrElevatedHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
|
||||||
public FirstTimeSetupOrElevatedHandlerTests()
|
public FirstTimeSetupOrElevatedHandlerTests()
|
||||||
{
|
{
|
||||||
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
|
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
|
||||||
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
|
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
|
||||||
}
|
}
|
||||||
@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
|
|||||||
[InlineData(UserRoles.User)]
|
[InlineData(UserRoles.User)]
|
||||||
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
|
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
|
||||||
{
|
{
|
||||||
SetupConfigurationManager(false);
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
|
||||||
var user = SetupUser(userRole);
|
var claims = TestHelpers.SetupUser(
|
||||||
var context = new AuthorizationHandlerContext(_requirements, user, null);
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
userRole);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
|
||||||
await _sut.HandleAsync(context);
|
await _sut.HandleAsync(context);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
|
|||||||
[InlineData(UserRoles.User, false)]
|
[InlineData(UserRoles.User, false)]
|
||||||
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
|
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
|
||||||
{
|
{
|
||||||
SetupConfigurationManager(true);
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
var user = SetupUser(userRole);
|
var claims = TestHelpers.SetupUser(
|
||||||
var context = new AuthorizationHandlerContext(_requirements, user, null);
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
userRole);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
|
||||||
await _sut.HandleAsync(context);
|
await _sut.HandleAsync(context);
|
||||||
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClaimsPrincipal SetupUser(string role)
|
|
||||||
{
|
|
||||||
var claims = new[] { new Claim(ClaimTypes.Role, role) };
|
|
||||||
var identity = new ClaimsIdentity(claims);
|
|
||||||
return new ClaimsPrincipal(identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupConfigurationManager(bool startupWizardCompleted)
|
|
||||||
{
|
|
||||||
var commonConfiguration = new BaseApplicationConfiguration
|
|
||||||
{
|
|
||||||
IsStartupWizardCompleted = startupWizardCompleted
|
|
||||||
};
|
|
||||||
|
|
||||||
_configurationManagerMock.Setup(c => c.CommonConfiguration)
|
|
||||||
.Returns(commonConfiguration);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using AutoFixture.AutoMoq;
|
||||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
|
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
|
||||||
{
|
{
|
||||||
public class RequiresElevationHandlerTests
|
public class RequiresElevationHandlerTests
|
||||||
{
|
{
|
||||||
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
private readonly RequiresElevationHandler _sut;
|
private readonly RequiresElevationHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
|
||||||
public RequiresElevationHandlerTests()
|
public RequiresElevationHandlerTests()
|
||||||
{
|
{
|
||||||
_sut = new RequiresElevationHandler();
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
|
_requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
|
||||||
|
_sut = fixture.Create<RequiresElevationHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
|
|||||||
[InlineData(UserRoles.Guest, false)]
|
[InlineData(UserRoles.Guest, false)]
|
||||||
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
|
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
|
||||||
{
|
{
|
||||||
var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
|
var claims = TestHelpers.SetupUser(
|
||||||
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
role);
|
||||||
|
|
||||||
var claims = new[] { new Claim(ClaimTypes.Role, role) };
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
var identity = new ClaimsIdentity(claims);
|
|
||||||
var user = new ClaimsPrincipal(identity);
|
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(requirements, user, null);
|
|
||||||
|
|
||||||
await _sut.HandleAsync(context);
|
await _sut.HandleAsync(context);
|
||||||
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
|
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
|
||||||
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
|
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
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…
x
Reference in New Issue
Block a user