aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid <daullmer@gmail.com>2020-06-18 18:52:01 +0200
committerDavid <daullmer@gmail.com>2020-06-18 18:52:01 +0200
commit762eeb51e66368e5f6fdede7b6fab84ea5859107 (patch)
treec9d18360013b149f204e0d6e4e60292ce45d54f4
parent713ae7ae363cafd95bd93bfd69b4ac7ab5b9b32b (diff)
parent522e44de59a8661a859f6a373e495a9e0e8d13ff (diff)
Merge remote-tracking branch 'remotes/upstream/api-migration' into api-user
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs45
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs115
-rw-r--r--Jellyfin.Api/Auth/BaseAuthorizationHandler.cs102
-rw-r--r--Jellyfin.Api/Auth/CustomAuthenticationHandler.cs25
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs22
-rw-r--r--Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs42
-rw-r--r--Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs44
-rw-r--r--Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs11
-rw-r--r--Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs26
-rw-r--r--Jellyfin.Api/Constants/InternalClaimTypes.cs38
-rw-r--r--Jellyfin.Api/Constants/Policies.cs15
-rw-r--r--Jellyfin.Api/Controllers/ActivityLogController.cs1
-rw-r--r--Jellyfin.Api/Controllers/ConfigurationController.cs4
-rw-r--r--Jellyfin.Api/Controllers/DevicesController.cs4
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ImageByNameController.cs2
-rw-r--r--Jellyfin.Api/Controllers/ItemRefreshController.cs1
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs341
-rw-r--r--Jellyfin.Api/Controllers/NotificationsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/PackageController.cs4
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs3
-rw-r--r--Jellyfin.Api/Controllers/RemoteImageController.cs (renamed from Jellyfin.Api/Controllers/Images/RemoteImageController.cs)4
-rw-r--r--Jellyfin.Api/Controllers/SearchController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs9
-rw-r--r--Jellyfin.Api/Controllers/VideoAttachmentsController.cs5
-rw-r--r--Jellyfin.Api/Helpers/ClaimHelpers.cs77
-rw-r--r--Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs2
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs2
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs2
-rw-r--r--Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs2
-rw-r--r--Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs4
-rw-r--r--Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs4
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs8
-rw-r--r--Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs6
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs37
-rw-r--r--MediaBrowser.Api/Library/LibraryStructureService.cs412
-rw-r--r--MediaBrowser.Controller/Net/IAuthService.cs25
-rw-r--r--MediaBrowser.Controller/Net/IAuthorizationContext.cs11
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs71
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs53
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs46
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs62
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs58
-rw-r--r--tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs31
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj1
-rw-r--r--tests/Jellyfin.Api.Tests/TestHelpers.cs81
49 files changed, 1302 insertions, 627 deletions
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index c65d4694a..6a2d8fdbb 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_networkManager = networkManager;
}
- public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
+ public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
- ValidateUser(request, authAttribtues);
+ ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
return user;
}
- private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
+ public AuthorizationInfo Authenticate(HttpRequest request)
+ {
+ var auth = _authorizationContext.GetAuthorizationInfo(request);
+ if (auth?.User == null)
+ {
+ return null;
+ }
+
+ if (auth.User.HasPermission(PermissionKind.IsDisabled))
+ {
+ throw new SecurityException("User account has been disabled.");
+ }
+
+ return auth;
+ }
+
+ private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
- if (!IsExemptFromAuthenticationToken(authAttribtues, request))
+ if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
- if (authAttribtues.AllowLocalOnly && !request.IsLocal)
+ if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
@@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null)
{
- ValidateUserAccess(user, request, authAttribtues, auth);
+ ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
- if (!IsExemptFromRoles(auth, authAttribtues, request, info))
+ if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
- var roles = authAttribtues.GetRoles();
+ var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
@@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess(
User user,
IRequest request,
- IAuthenticationAttributes authAttributes,
- AuthorizationInfo auth)
+ IAuthenticationAttributes authAttributes)
{
if (user.HasPermission(PermissionKind.IsDisabled))
{
@@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
throw new AuthenticationException("Access token is invalid or expired.");
}
-
- //if (!string.IsNullOrEmpty(info.UserId))
- //{
- // var user = _userManager.GetUserById(info.UserId);
-
- // if (user == null || user.Configuration.IsDisabled)
- // {
- // throw new SecurityException("User account has been disabled.");
- // }
- //}
}
}
}
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
index 9558cb4c6..fb93fae3e 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
@@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
@@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext);
}
+ public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
+ {
+ var auth = GetAuthorizationDictionary(requestContext);
+ var (authInfo, _) =
+ GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
+ return authInfo;
+ }
+
/// <summary>
/// Gets the authorization.
/// </summary>
@@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(IRequest httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
+ var (authInfo, originalAuthInfo) =
+ GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
+ if (originalAuthInfo != null)
+ {
+ httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
+ }
+
+ httpReq.Items["AuthorizationInfo"] = authInfo;
+ return authInfo;
+ }
+
+ private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
+ in Dictionary<string, string> auth,
+ in IHeaderDictionary headers,
+ in IQueryCollection queryString)
+ {
string deviceId = null;
string device = null;
string client = null;
@@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token))
{
- token = httpReq.Headers["X-Emby-Token"];
+ token = headers["X-Emby-Token"];
}
if (string.IsNullOrEmpty(token))
{
- token = httpReq.Headers["X-MediaBrowser-Token"];
+ token = headers["X-MediaBrowser-Token"];
}
+
+ if (string.IsNullOrEmpty(token))
+ {
+ token = queryString["ApiKey"];
+ }
+
+ // TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token))
{
- token = httpReq.QueryString["api_key"];
+ token = queryString["api_key"];
}
- var info = new AuthorizationInfo
+ var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
@@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token
};
+ AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token))
{
var result = _authRepo.Get(new AuthenticationInfoQuery
@@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token
});
- var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
+ originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
- if (tokenInfo != null)
+ if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
- if (string.IsNullOrWhiteSpace(info.Client))
+ if (string.IsNullOrWhiteSpace(authInfo.Client))
{
- info.Client = tokenInfo.AppName;
+ authInfo.Client = originalAuthenticationInfo.AppName;
}
- if (string.IsNullOrWhiteSpace(info.DeviceId))
+ if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
- info.DeviceId = tokenInfo.DeviceId;
+ authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
- var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
+ var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
- if (string.IsNullOrWhiteSpace(info.Device))
+ if (string.IsNullOrWhiteSpace(authInfo.Device))
{
- info.Device = tokenInfo.DeviceName;
+ authInfo.Device = originalAuthenticationInfo.DeviceName;
}
-
- else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- tokenInfo.DeviceName = info.Device;
+ originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
- if (string.IsNullOrWhiteSpace(info.Version))
+ if (string.IsNullOrWhiteSpace(authInfo.Version))
{
- info.Version = tokenInfo.AppVersion;
+ authInfo.Version = originalAuthenticationInfo.AppVersion;
}
- else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
+ else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
- tokenInfo.AppVersion = info.Version;
+ originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
- if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
+ if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
- tokenInfo.DateLastActivity = DateTime.UtcNow;
+ originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
- if (!tokenInfo.UserId.Equals(Guid.Empty))
+ if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
- info.User = _userManager.GetUserById(tokenInfo.UserId);
+ authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
- if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
+ if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
- tokenInfo.UserName = info.User.Username;
+ originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
}
if (updateToken)
{
- _authRepo.Update(tokenInfo);
+ _authRepo.Update(originalAuthenticationInfo);
}
}
- httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
}
- httpReq.Items["AuthorizationInfo"] = info;
-
- return info;
+ return (authInfo, originalAuthenticationInfo);
}
/// <summary>
@@ -187,6 +216,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
}
/// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
+ {
+ var auth = httpReq.Headers["X-Emby-Authorization"];
+
+ if (string.IsNullOrEmpty(auth))
+ {
+ auth = httpReq.Headers[HeaderNames.Authorization];
+ }
+
+ return GetAuthorization(auth);
+ }
+
+ /// <summary>
/// Gets the authorization.
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
@@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value)
{
- if (string.IsNullOrEmpty(value))
- {
- return value;
- }
-
- return WebUtility.HtmlEncode(value);
+ return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}
diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
new file mode 100644
index 000000000..b5b9d8904
--- /dev/null
+++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
@@ -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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
index a5c4e9974..5e5e25e84 100644
--- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
+++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
@@ -1,3 +1,6 @@
+#nullable enable
+
+using System.Globalization;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
@@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
- var authenticatedAttribute = new AuthenticatedAttribute
- {
- IgnoreLegacyAuth = true
- };
-
try
{
- var user = _authService.Authenticate(Request, authenticatedAttribute);
- if (user == null)
+ var authorizationInfo = _authService.Authenticate(Request);
+ if (authorizationInfo == null)
{
return Task.FromResult(AuthenticateResult.NoResult());
// TODO return when legacy API is removed.
@@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
var claims = new[]
{
- new Claim(ClaimTypes.Name, user.Username),
- new Claim(
- ClaimTypes.Role,
- value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
+ new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
+ new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
+ new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
+ new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
+ new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
+ new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
+ new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+ new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
};
+
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
new file mode 100644
index 000000000..b5913daab
--- /dev/null
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
new file mode 100644
index 000000000..7cea00b69
--- /dev/null
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
+{
+ /// <summary>
+ /// The default authorization requirement.
+ /// </summary>
+ public class DefaultAuthorizationRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
index 34aa5d12c..decbe0c03 100644
--- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
+++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs
@@ -1,22 +1,33 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
{
/// <summary>
/// Authorization handler for requiring first time setup or elevated privileges.
/// </summary>
- public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
+ public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
{
private readonly IConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
/// </summary>
- /// <param name="configurationManager">The jellyfin configuration manager.</param>
- public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
+ /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ public FirstTimeSetupOrElevatedHandler(
+ IConfigurationManager configurationManager,
+ IUserManager userManager,
+ INetworkManager networkManager,
+ IHttpContextAccessor httpContextAccessor)
+ : base(userManager, networkManager, httpContextAccessor)
{
_configurationManager = configurationManager;
}
@@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
+ return Task.CompletedTask;
}
- else if (context.User.IsInRole(UserRoles.Administrator))
+
+ var validated = ValidateClaims(context.User);
+ if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
}
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
new file mode 100644
index 000000000..9afa0b28f
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs
@@ -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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
new file mode 100644
index 000000000..d5bb61ce6
--- /dev/null
+++ b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
+{
+ /// <summary>
+ /// Escape schedule controls requirement.
+ /// </summary>
+ public class IgnoreScheduleRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
new file mode 100644
index 000000000..af73352bc
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
@@ -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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
new file mode 100644
index 000000000..761127fa4
--- /dev/null
+++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessRequirement.cs
@@ -0,0 +1,11 @@
+using Microsoft.AspNetCore.Authorization;
+
+namespace Jellyfin.Api.Auth.LocalAccessPolicy
+{
+ /// <summary>
+ /// The local access authorization requirement.
+ /// </summary>
+ public class LocalAccessRequirement : IAuthorizationRequirement
+ {
+ }
+}
diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
index 2d3bb1aa4..b235c4b63 100644
--- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs
@@ -1,21 +1,43 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
{
/// <summary>
/// Authorization handler for requiring elevated privileges.
/// </summary>
- public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
+ public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
+ /// </summary>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
+ /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
+ public RequiresElevationHandler(
+ IUserManager userManager,
+ INetworkManager networkManager,
+ IHttpContextAccessor httpContextAccessor)
+ : base(userManager, networkManager, httpContextAccessor)
+ {
+ }
+
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
{
- if (context.User.IsInRole(UserRoles.Administrator))
+ var validated = ValidateClaims(context.User);
+ if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
+ else
+ {
+ context.Fail();
+ }
return Task.CompletedTask;
}
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
new file mode 100644
index 000000000..4d7c7135d
--- /dev/null
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -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";
+ }
+}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index e2b383f75..cf574e43d 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -6,6 +6,11 @@ namespace Jellyfin.Api.Constants
public static class Policies
{
/// <summary>
+ /// Policy name for default authorization.
+ /// </summary>
+ public const string DefaultAuthorization = "DefaultAuthorization";
+
+ /// <summary>
/// Policy name for requiring first time setup or elevated privileges.
/// </summary>
public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated";
@@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
/// Policy name for requiring elevated privileges.
/// </summary>
public const string RequiresElevation = "RequiresElevation";
+
+ /// <summary>
+ /// Policy name for allowing local access only.
+ /// </summary>
+ public const string LocalAccessOnly = "LocalAccessOnly";
+
+ /// <summary>
+ /// Policy name for escaping schedule controls.
+ /// </summary>
+ public const string IgnoreSchedule = "IgnoreSchedule";
}
}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index 895d9f719..4ae7cf506 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CA1801
using System;
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 780a38aa8..74f1677bd 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Configuration Controller.
/// </summary>
[Route("System")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class ConfigurationController : BaseJellyfinApiController
{
private readonly IServerConfigurationManager _configurationManager;
diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs
index 1754b0cbd..78368eed6 100644
--- a/Jellyfin.Api/Controllers/DevicesController.cs
+++ b/Jellyfin.Api/Controllers/DevicesController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Devices;
@@ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index 46911ce93..6a6e6a64a 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -1,5 +1,4 @@
-#nullable enable
-#pragma warning disable CA1801
+#pragma warning disable CA1801
using System;
using System.Linq;
diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs
index fa46b6dd1..70f46ffa4 100644
--- a/Jellyfin.Api/Controllers/ImageByNameController.cs
+++ b/Jellyfin.Api/Controllers/ImageByNameController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs
index d9b8357d2..a1df22e41 100644
--- a/Jellyfin.Api/Controllers/ItemRefreshController.cs
+++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CA1801
using System.ComponentModel;
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
new file mode 100644
index 000000000..ca2905b11
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -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();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs
index a76675d5a..a1f9b9e8f 100644
--- a/Jellyfin.Api/Controllers/NotificationsController.cs
+++ b/Jellyfin.Api/Controllers/NotificationsController.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CA1801
using System;
diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs
index 8200f891c..943c23f8e 100644
--- a/Jellyfin.Api/Controllers/PackageController.cs
+++ b/Jellyfin.Api/Controllers/PackageController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Package Controller.
/// </summary>
[Route("Packages")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 59196a41a..fdb2f4c35 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -1,5 +1,4 @@
-#nullable enable
-#pragma warning disable CA1801
+#pragma warning disable CA1801
using System;
using System.Collections.Generic;
diff --git a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs
index 7c5f17e9e..80983ee64 100644
--- a/Jellyfin.Api/Controllers/Images/RemoteImageController.cs
+++ b/Jellyfin.Api/Controllers/RemoteImageController.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -21,7 +19,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
-namespace Jellyfin.Api.Controllers.Images
+namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Remote Images Controller.
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index ec05e4fb4..d971889db 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
+using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
/// Search controller.
/// </summary>
[Route("/Search/Hints")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class SearchController : BaseJellyfinApiController
{
private readonly ISearchEngine _searchEngine;
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 97df8c60d..69b83379d 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -1,4 +1,3 @@
-#nullable enable
#pragma warning disable CA1801
using System;
@@ -110,7 +109,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute] Guid id,
@@ -130,7 +129,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute] Guid id,
@@ -160,7 +159,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
@@ -250,7 +249,7 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Subtitle playlist retrieved.</response>
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetSubtitlePlaylist(
[FromRoute] Guid id,
diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
index 86d9322fe..2528fd75d 100644
--- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
+++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs
@@ -1,9 +1,8 @@
-#nullable enable
-
using System;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Attachments controller.
/// </summary>
[Route("Videos")]
- [Authorize]
+ [Authorize(Policy = Policies.DefaultAuthorization)]
public class VideoAttachmentsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
diff --git a/Jellyfin.Api/Helpers/ClaimHelpers.cs b/Jellyfin.Api/Helpers/ClaimHelpers.cs
new file mode 100644
index 000000000..a07d4ed82
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ClaimHelpers.cs
@@ -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();
+ }
+ }
+}
diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
index 3706a11e3..3b827ec12 100644
--- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
+++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
namespace Jellyfin.Api.Models.ConfigurationDtos
{
/// <summary>
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
index 502b22623..af5239ec2 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using MediaBrowser.Model.Notifications;
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
index e34e176cb..64e92bd83 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
index b3746ee2d..0568dea66 100644
--- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
+++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using MediaBrowser.Model.Notifications;
namespace Jellyfin.Api.Models.NotificationDtos
diff --git a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
index aaaf54267..7f1255f4b 100644
--- a/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
+++ b/Jellyfin.Api/Models/PluginDtos/MBRegistrationRecord.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System;
+using System;
namespace Jellyfin.Api.Models.PluginDtos
{
diff --git a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
index 793002a6c..a90398425 100644
--- a/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
+++ b/Jellyfin.Api/Models/PluginDtos/PluginSecurityInfo.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-namespace Jellyfin.Api.Models.PluginDtos
+namespace Jellyfin.Api.Models.PluginDtos
{
/// <summary>
/// Plugin security info.
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
index 5a83a030d..a5f012245 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace Jellyfin.Api.Models.StartupDtos
{
/// <summary>
@@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos
/// <summary>
/// Gets or sets UI language culture.
/// </summary>
- public string UICulture { get; set; }
+ public string? UICulture { get; set; }
/// <summary>
/// Gets or sets the metadata country code.
/// </summary>
- public string MetadataCountryCode { get; set; }
+ public string? MetadataCountryCode { get; set; }
/// <summary>
/// Gets or sets the preferred language for the metadata.
/// </summary>
- public string PreferredMetadataLanguage { get; set; }
+ public string? PreferredMetadataLanguage { get; set; }
}
}
diff --git a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
index 0dbb245ec..e4c973548 100644
--- a/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
+++ b/Jellyfin.Api/Models/StartupDtos/StartupUserDto.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace Jellyfin.Api.Models.StartupDtos
{
/// <summary>
@@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos
/// <summary>
/// Gets or sets the username.
/// </summary>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>
/// Gets or sets the user's password.
/// </summary>
- public string Password { get; set; }
+ public string? Password { get; set; }
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 9cdaa0eb1..dbd5ba416 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -5,7 +5,10 @@ using System.Linq;
using System.Reflection;
using Jellyfin.Api;
using Jellyfin.Api.Auth;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
+using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
+using Jellyfin.Api.Auth.LocalAccessPolicy;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Controllers;
@@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
+ serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
options.AddPolicy(
- Policies.RequiresElevation,
+ Policies.DefaultAuthorization,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
- policy.AddRequirements(new RequiresElevationRequirement());
+ policy.AddRequirements(new DefaultAuthorizationRequirement());
});
options.AddPolicy(
Policies.FirstTimeSetupOrElevated,
@@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
});
+ options.AddPolicy(
+ Policies.IgnoreSchedule,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new IgnoreScheduleRequirement());
+ });
+ options.AddPolicy(
+ Policies.LocalAccessOnly,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new LocalAccessRequirement());
+ });
+ options.AddPolicy(
+ Policies.RequiresElevation,
+ policy =>
+ {
+ policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
+ policy.AddRequirements(new RequiresElevationRequirement());
+ });
});
}
@@ -78,6 +107,10 @@ namespace Jellyfin.Server.Extensions
{
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
})
+ .Configure<ForwardedHeadersOptions>(options =>
+ {
+ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ })
.AddMvc(opts =>
{
opts.UseGeneralRoutePrefix(baseUrl);
diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs
deleted file mode 100644
index 1e300814f..000000000
--- a/MediaBrowser.Api/Library/LibraryStructureService.cs
+++ /dev/null
@@ -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();
- }
- });
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs
index d8f6d19da..2055a656a 100644
--- a/MediaBrowser.Controller/Net/IAuthService.cs
+++ b/MediaBrowser.Controller/Net/IAuthService.cs
@@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// IAuthService.
+ /// </summary>
public interface IAuthService
{
- void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
+ /// <summary>
+ /// Authenticate and authorize request.
+ /// </summary>
+ /// <param name="request">Request.</param>
+ /// <param name="authAttribtutes">Authorization attributes.</param>
+ void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
- User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
+ /// <summary>
+ /// Authenticate and authorize request.
+ /// </summary>
+ /// <param name="request">Request.</param>
+ /// <param name="authAttribtutes">Authorization attributes.</param>
+ /// <returns>Authenticated user.</returns>
+ User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
+
+ /// <summary>
+ /// Authenticate request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>Authorization information. Null if unauthenticated.</returns>
+ AuthorizationInfo Authenticate(HttpRequest request);
}
}
diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
index 61598391f..37a7425b9 100644
--- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs
+++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs
@@ -1,7 +1,11 @@
using MediaBrowser.Model.Services;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
{
+ /// <summary>
+ /// IAuthorization context.
+ /// </summary>
public interface IAuthorizationContext
{
/// <summary>
@@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
+
+ /// <summary>
+ /// Gets the authorization information.
+ /// </summary>
+ /// <param name="requestContext">The request context.</param>
+ /// <returns>AuthorizationInfo.</returns>
+ AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
}
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
index 362d41b01..4ea5094b6 100644
--- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Security.Claims;
-using System.Text.Encodings.Web;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
@@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
@@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
private readonly IFixture _fixture;
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
- private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
- private readonly Mock<ISystemClock> _clockMock;
- private readonly Mock<IServiceProvider> _serviceProviderMock;
- private readonly Mock<IAuthenticationService> _authenticationServiceMock;
- private readonly UrlEncoder _urlEncoder;
- private readonly HttpContext _context;
private readonly CustomAuthenticationHandler _sut;
private readonly AuthenticationScheme _scheme;
@@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
AllowFixtureCircularDependencies();
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
- _optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
- _clockMock = _fixture.Freeze<Mock<ISystemClock>>();
- _serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
- _authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
+ var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
+ var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
+ var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
- _urlEncoder = UrlEncoder.Default;
+ serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
+ .Returns(authenticationServiceMock.Object);
- _serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
- .Returns(_authenticationServiceMock.Object);
-
- _optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
+ optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
.Returns(new AuthenticationSchemeOptions
{
ForwardAuthenticate = null
});
- _context = new DefaultHttpContext
+ HttpContext context = new DefaultHttpContext
{
- RequestServices = _serviceProviderMock.Object
+ RequestServices = serviceProviderMock.Object
};
_scheme = new AuthenticationScheme(
@@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
typeof(CustomAuthenticationHandler));
_sut = _fixture.Create<CustomAuthenticationHandler>();
- _sut.InitializeAsync(_scheme, _context).Wait();
- }
-
- [Fact]
- public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
- {
- _jellyfinAuthServiceMock.Setup(
- a => a.Authenticate(
- It.IsAny<HttpRequest>(),
- It.IsAny<AuthenticatedAttribute>()))
- .Returns((User?)null);
-
- var authenticateResult = await _sut.AuthenticateAsync();
-
- Assert.False(authenticateResult.Succeeded);
- Assert.True(authenticateResult.None);
- // TODO return when legacy API is removed.
- // Assert.Equal("Invalid user", authenticateResult.Failure.Message);
+ _sut.InitializeAsync(_scheme, context).Wait();
}
[Fact]
@@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
- It.IsAny<HttpRequest>(),
- It.IsAny<AuthenticatedAttribute>()))
+ It.IsAny<HttpRequest>()))
.Throws(new SecurityException(errorMessage));
var authenticateResult = await _sut.AuthenticateAsync();
@@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
[Fact]
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
{
- var user = SetupUser();
+ var authorizationInfo = SetupUser();
var authenticateResult = await _sut.AuthenticateAsync();
- Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username));
+ Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
}
[Theory]
@@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
[InlineData(false)]
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
{
- var user = SetupUser(isAdmin);
+ var authorizationInfo = SetupUser(isAdmin);
var authenticateResult = await _sut.AuthenticateAsync();
- var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
+ var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
}
@@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
}
- private User SetupUser(bool isAdmin = false)
+ private AuthorizationInfo SetupUser(bool isAdmin = false)
{
- var user = _fixture.Create<User>();
- user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
+ var authorizationInfo = _fixture.Create<AuthorizationInfo>();
+ authorizationInfo.User = _fixture.Create<User>();
+ authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
- It.IsAny<HttpRequest>(),
- It.IsAny<AuthenticatedAttribute>()))
- .Returns(user);
+ It.IsAny<HttpRequest>()))
+ .Returns(authorizationInfo);
- return user;
+ return authorizationInfo;
}
private void AllowFixtureCircularDependencies()
diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
new file mode 100644
index 000000000..a62fd8d5a
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -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);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
index e40af703f..ee42216e4 100644
--- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs
@@ -1,13 +1,13 @@
using System.Collections.Generic;
-using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
-using MediaBrowser.Model.Configuration;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Moq;
using Xunit;
@@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly FirstTimeSetupOrElevatedHandler _sut;
+ private readonly Mock<IUserManager> _userManagerMock;
+ private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public FirstTimeSetupOrElevatedHandlerTests()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
+ _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+ _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
}
@@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User)]
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
{
- SetupConfigurationManager(false);
- var user = SetupUser(userRole);
- var context = new AuthorizationHandlerContext(_requirements, user, null);
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ userRole);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
@@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
[InlineData(UserRoles.User, false)]
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
{
- SetupConfigurationManager(true);
- var user = SetupUser(userRole);
- var context = new AuthorizationHandlerContext(_requirements, user, null);
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ userRole);
+
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
}
-
- private static ClaimsPrincipal SetupUser(string role)
- {
- var claims = new[] { new Claim(ClaimTypes.Role, role) };
- var identity = new ClaimsIdentity(claims);
- return new ClaimsPrincipal(identity);
- }
-
- private void SetupConfigurationManager(bool startupWizardCompleted)
- {
- var commonConfiguration = new BaseApplicationConfiguration
- {
- IsStartupWizardCompleted = startupWizardCompleted
- };
-
- _configurationManagerMock.Setup(c => c.CommonConfiguration)
- .Returns(commonConfiguration);
- }
}
}
diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
new file mode 100644
index 000000000..b65d45aa0
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs
@@ -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);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
new file mode 100644
index 000000000..09ffa8468
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs
@@ -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);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
index cd05a8328..ffe88fcde 100644
--- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs
@@ -1,20 +1,35 @@
using System.Collections.Generic;
-using System.Security.Claims;
using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.RequiresElevationPolicy;
using Jellyfin.Api.Constants;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Moq;
using Xunit;
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
{
public class RequiresElevationHandlerTests
{
+ private readonly Mock<IConfigurationManager> _configurationManagerMock;
+ private readonly List<IAuthorizationRequirement> _requirements;
private readonly RequiresElevationHandler _sut;
+ private readonly Mock<IUserManager> _userManagerMock;
+ private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
public RequiresElevationHandlerTests()
{
- _sut = new RequiresElevationHandler();
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
+ _requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
+ _userManagerMock = fixture.Freeze<Mock<IUserManager>>();
+ _httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
+
+ _sut = fixture.Create<RequiresElevationHandler>();
}
[Theory]
@@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
[InlineData(UserRoles.Guest, false)]
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
{
- var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
-
- var claims = new[] { new Claim(ClaimTypes.Role, role) };
- var identity = new ClaimsIdentity(claims);
- var user = new ClaimsPrincipal(identity);
+ TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
+ var claims = TestHelpers.SetupUser(
+ _userManagerMock,
+ _httpContextAccessor,
+ role);
- var context = new AuthorizationHandlerContext(requirements, user, null);
+ var context = new AuthorizationHandlerContext(_requirements, claims, null);
await _sut.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index aedcc7c42..010fad520 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -35,6 +35,7 @@
<ItemGroup>
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
+ <ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs
new file mode 100644
index 000000000..a4dd4e409
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs
@@ -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);
+ }
+ }
+}