Merge pull request #3357 from crobibero/api-authorization
Add Authorization handlers
This commit is contained in:
commit
522e44de59
|
@ -39,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
|
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
|
||||||
{
|
{
|
||||||
ValidateUser(request, authAttribtues);
|
ValidateUser(request, authAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
||||||
|
@ -51,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||||
|
{
|
||||||
|
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||||
|
if (auth?.User == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
||||||
|
{
|
||||||
|
throw new SecurityException("User account has been disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
|
||||||
{
|
{
|
||||||
// This code is executed before the service
|
// This code is executed before the service
|
||||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||||
|
|
||||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
if (!IsExemptFromAuthenticationToken(authAttributes, request))
|
||||||
{
|
{
|
||||||
ValidateSecurityToken(request, auth.Token);
|
ValidateSecurityToken(request, auth.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
|
if (authAttributes.AllowLocalOnly && !request.IsLocal)
|
||||||
{
|
{
|
||||||
throw new SecurityException("Operation not found.");
|
throw new SecurityException("Operation not found.");
|
||||||
}
|
}
|
||||||
|
@ -75,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
ValidateUserAccess(user, request, authAttribtues, auth);
|
ValidateUserAccess(user, request, authAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = GetTokenInfo(request);
|
var info = GetTokenInfo(request);
|
||||||
|
|
||||||
if (!IsExemptFromRoles(auth, authAttribtues, request, info))
|
if (!IsExemptFromRoles(auth, authAttributes, request, info))
|
||||||
{
|
{
|
||||||
var roles = authAttribtues.GetRoles();
|
var roles = authAttributes.GetRoles();
|
||||||
|
|
||||||
ValidateRoles(roles, user);
|
ValidateRoles(roles, user);
|
||||||
}
|
}
|
||||||
|
@ -106,8 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
private void ValidateUserAccess(
|
private void ValidateUserAccess(
|
||||||
User user,
|
User user,
|
||||||
IRequest request,
|
IRequest request,
|
||||||
IAuthenticationAttributes authAttributes,
|
IAuthenticationAttributes authAttributes)
|
||||||
AuthorizationInfo auth)
|
|
||||||
{
|
{
|
||||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||||
{
|
{
|
||||||
|
@ -230,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Access token is invalid or expired.");
|
throw new AuthenticationException("Access token is invalid or expired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (!string.IsNullOrEmpty(info.UserId))
|
|
||||||
//{
|
|
||||||
// var user = _userManager.GetUserById(info.UserId);
|
|
||||||
|
|
||||||
// if (user == null || user.Configuration.IsDisabled)
|
|
||||||
// {
|
|
||||||
// throw new SecurityException("User account has been disabled.");
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Security;
|
using MediaBrowser.Controller.Security;
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer.Security
|
namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
|
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
return GetAuthorization(requestContext);
|
return GetAuthorization(requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||||
|
{
|
||||||
|
var auth = GetAuthorizationDictionary(requestContext);
|
||||||
|
var (authInfo, _) =
|
||||||
|
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||||
|
return authInfo;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authorization.
|
/// Gets the authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
private AuthorizationInfo GetAuthorization(IRequest httpReq)
|
private AuthorizationInfo GetAuthorization(IRequest httpReq)
|
||||||
{
|
{
|
||||||
var auth = GetAuthorizationDictionary(httpReq);
|
var auth = GetAuthorizationDictionary(httpReq);
|
||||||
|
var (authInfo, originalAuthInfo) =
|
||||||
|
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
|
||||||
|
|
||||||
|
if (originalAuthInfo != null)
|
||||||
|
{
|
||||||
|
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq.Items["AuthorizationInfo"] = authInfo;
|
||||||
|
return authInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||||
|
in Dictionary<string, string> auth,
|
||||||
|
in IHeaderDictionary headers,
|
||||||
|
in IQueryCollection queryString)
|
||||||
|
{
|
||||||
string deviceId = null;
|
string deviceId = null;
|
||||||
string device = null;
|
string device = null;
|
||||||
string client = null;
|
string client = null;
|
||||||
|
@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
token = httpReq.Headers["X-Emby-Token"];
|
token = headers["X-Emby-Token"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(token))
|
if (string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
token = httpReq.Headers["X-MediaBrowser-Token"];
|
token = headers["X-MediaBrowser-Token"];
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
{
|
|
||||||
token = httpReq.QueryString["api_key"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = new AuthorizationInfo
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
token = queryString["ApiKey"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO deprecate this query parameter.
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
token = queryString["api_key"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var authInfo = new AuthorizationInfo
|
||||||
{
|
{
|
||||||
Client = client,
|
Client = client,
|
||||||
Device = device,
|
Device = device,
|
||||||
|
@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
Token = token
|
Token = token
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AuthenticationInfo originalAuthenticationInfo = null;
|
||||||
if (!string.IsNullOrWhiteSpace(token))
|
if (!string.IsNullOrWhiteSpace(token))
|
||||||
{
|
{
|
||||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||||
|
@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
AccessToken = token
|
AccessToken = token
|
||||||
});
|
});
|
||||||
|
|
||||||
var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||||
|
|
||||||
if (tokenInfo != null)
|
if (originalAuthenticationInfo != null)
|
||||||
{
|
{
|
||||||
var updateToken = false;
|
var updateToken = false;
|
||||||
|
|
||||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||||
if (string.IsNullOrWhiteSpace(info.Client))
|
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||||
{
|
{
|
||||||
info.Client = tokenInfo.AppName;
|
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.DeviceId))
|
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||||
{
|
{
|
||||||
info.DeviceId = tokenInfo.DeviceId;
|
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||||
var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.Device))
|
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||||
{
|
{
|
||||||
info.Device = tokenInfo.DeviceName;
|
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||||
}
|
}
|
||||||
|
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||||
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
if (allowTokenInfoUpdate)
|
if (allowTokenInfoUpdate)
|
||||||
{
|
{
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
tokenInfo.DeviceName = info.Device;
|
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(info.Version))
|
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||||
{
|
{
|
||||||
info.Version = tokenInfo.AppVersion;
|
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||||
}
|
}
|
||||||
else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (allowTokenInfoUpdate)
|
if (allowTokenInfoUpdate)
|
||||||
{
|
{
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
tokenInfo.AppVersion = info.Version;
|
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
|
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||||
{
|
{
|
||||||
tokenInfo.DateLastActivity = DateTime.UtcNow;
|
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tokenInfo.UserId.Equals(Guid.Empty))
|
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||||
{
|
{
|
||||||
info.User = _userManager.GetUserById(tokenInfo.UserId);
|
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||||
|
|
||||||
if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
tokenInfo.UserName = info.User.Username;
|
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateToken)
|
if (updateToken)
|
||||||
{
|
{
|
||||||
_authRepo.Update(tokenInfo);
|
_authRepo.Update(originalAuthenticationInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
httpReq.Items["AuthorizationInfo"] = info;
|
return (authInfo, originalAuthenticationInfo);
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
return GetAuthorization(auth);
|
return GetAuthorization(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the auth.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpReq">The HTTP req.</param>
|
||||||
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
|
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
||||||
|
{
|
||||||
|
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(auth))
|
||||||
|
{
|
||||||
|
auth = httpReq.Headers[HeaderNames.Authorization];
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetAuthorization(auth);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the authorization.
|
/// Gets the authorization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||||
|
|
||||||
private static string NormalizeValue(string value)
|
private static string NormalizeValue(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WebUtility.HtmlEncode(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
102
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
102
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Jellyfin.Api.Helpers;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base authorization handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
|
||||||
|
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
|
||||||
|
where T : IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
protected BaseAuthorizationHandler(
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate authenticated claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="claimsPrincipal">Request claims.</param>
|
||||||
|
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
|
||||||
|
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
||||||
|
/// <returns>Validated claim status.</returns>
|
||||||
|
protected bool ValidateClaims(
|
||||||
|
ClaimsPrincipal claimsPrincipal,
|
||||||
|
bool ignoreSchedule = false,
|
||||||
|
bool localAccessOnly = false)
|
||||||
|
{
|
||||||
|
// Ensure claim has userId.
|
||||||
|
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure userId links to a valid user.
|
||||||
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure user is not disabled.
|
||||||
|
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
|
||||||
|
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
|
||||||
|
// User cannot access remotely and user is remote
|
||||||
|
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localAccessOnly && !isInLocalNetwork)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User attempting to access out of parental control hours.
|
||||||
|
if (!ignoreSchedule
|
||||||
|
&& !user.HasPermission(PermissionKind.IsAdministrator)
|
||||||
|
&& !user.IsParentalScheduleAllowed())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPAddress NormalizeIp(IPAddress ip)
|
||||||
|
{
|
||||||
|
return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
@ -39,15 +42,10 @@ namespace Jellyfin.Api.Auth
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
{
|
{
|
||||||
var authenticatedAttribute = new AuthenticatedAttribute
|
|
||||||
{
|
|
||||||
IgnoreLegacyAuth = true
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var user = _authService.Authenticate(Request, authenticatedAttribute);
|
var authorizationInfo = _authService.Authenticate(Request);
|
||||||
if (user == null)
|
if (authorizationInfo == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(AuthenticateResult.NoResult());
|
return Task.FromResult(AuthenticateResult.NoResult());
|
||||||
// TODO return when legacy API is removed.
|
// TODO return when legacy API is removed.
|
||||||
|
@ -57,11 +55,16 @@ namespace Jellyfin.Api.Auth
|
||||||
|
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(ClaimTypes.Name, user.Username),
|
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
||||||
new Claim(
|
new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
|
||||||
ClaimTypes.Role,
|
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
||||||
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
|
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
||||||
|
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
||||||
|
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
||||||
|
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
||||||
|
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
||||||
};
|
};
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||||
var principal = new ClaimsPrincipal(identity);
|
var principal = new ClaimsPrincipal(identity);
|
||||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default authorization handler.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public DefaultAuthorizationHandler(
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
||||||
|
{
|
||||||
|
var validated = ValidateClaims(context.User);
|
||||||
|
if (!validated)
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The default authorization requirement.
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,33 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authorization handler for requiring first time setup or elevated privileges.
|
/// Authorization handler for requiring first time setup or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
||||||
{
|
{
|
||||||
private readonly IConfigurationManager _configurationManager;
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">The jellyfin configuration manager.</param>
|
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public FirstTimeSetupOrElevatedHandler(
|
||||||
|
IConfigurationManager configurationManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
{
|
{
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
}
|
}
|
||||||
|
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
else if (context.User.IsInRole(UserRoles.Administrator))
|
|
||||||
|
var validated = ValidateClaims(context.User);
|
||||||
|
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Escape schedule controls handler.
|
||||||
|
/// </summary>
|
||||||
|
public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public IgnoreScheduleHandler(
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
|
||||||
|
{
|
||||||
|
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||||
|
if (!validated)
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Escape schedule controls requirement.
|
||||||
|
/// </summary>
|
||||||
|
public class IgnoreScheduleRequirement : IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
44
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
Normal file
44
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Local access handler.
|
||||||
|
/// </summary>
|
||||||
|
public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public LocalAccessHandler(
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
|
||||||
|
{
|
||||||
|
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
||||||
|
if (!validated)
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The local access authorization requirement.
|
||||||
|
/// </summary>
|
||||||
|
public class LocalAccessRequirement : IAuthorizationRequirement
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,43 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authorization handler for requiring elevated privileges.
|
/// Authorization handler for requiring elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
|
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
|
public RequiresElevationHandler(
|
||||||
|
IUserManager userManager,
|
||||||
|
INetworkManager networkManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
|
||||||
{
|
{
|
||||||
if (context.User.IsInRole(UserRoles.Administrator))
|
var validated = ValidateClaims(context.User);
|
||||||
|
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
38
Jellyfin.Api/Constants/InternalClaimTypes.cs
Normal file
38
Jellyfin.Api/Constants/InternalClaimTypes.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
namespace Jellyfin.Api.Constants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Internal claim types for authorization.
|
||||||
|
/// </summary>
|
||||||
|
public static class InternalClaimTypes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User Id.
|
||||||
|
/// </summary>
|
||||||
|
public const string UserId = "Jellyfin-UserId";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device Id.
|
||||||
|
/// </summary>
|
||||||
|
public const string DeviceId = "Jellyfin-DeviceId";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device.
|
||||||
|
/// </summary>
|
||||||
|
public const string Device = "Jellyfin-Device";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client.
|
||||||
|
/// </summary>
|
||||||
|
public const string Client = "Jellyfin-Client";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Version.
|
||||||
|
/// </summary>
|
||||||
|
public const string Version = "Jellyfin-Version";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Token.
|
||||||
|
/// </summary>
|
||||||
|
public const string Token = "Jellyfin-Token";
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Policies
|
public static class Policies
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for default authorization.
|
||||||
|
/// </summary>
|
||||||
|
public const string DefaultAuthorization = "DefaultAuthorization";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring first time setup or elevated privileges.
|
/// Policy name for requiring first time setup or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
|
||||||
/// Policy name for requiring elevated privileges.
|
/// Policy name for requiring elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RequiresElevation = "RequiresElevation";
|
public const string RequiresElevation = "RequiresElevation";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for allowing local access only.
|
||||||
|
/// </summary>
|
||||||
|
public const string LocalAccessOnly = "LocalAccessOnly";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for escaping schedule controls.
|
||||||
|
/// </summary>
|
||||||
|
public const string IgnoreSchedule = "IgnoreSchedule";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// Configuration Controller.
|
/// Configuration Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("System")]
|
[Route("System")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class ConfigurationController : BaseJellyfinApiController
|
public class ConfigurationController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Devices Controller.
|
/// Devices Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class DevicesController : BaseJellyfinApiController
|
public class DevicesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IDeviceManager _deviceManager;
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// Package Controller.
|
/// Package Controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Packages")]
|
[Route("Packages")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class PackageController : BaseJellyfinApiController
|
public class PackageController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IInstallationManager _installationManager;
|
private readonly IInstallationManager _installationManager;
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// Search controller.
|
/// Search controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("/Search/Hints")]
|
[Route("/Search/Hints")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class SearchController : BaseJellyfinApiController
|
public class SearchController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ISearchEngine _searchEngine;
|
private readonly ISearchEngine _searchEngine;
|
||||||
|
|
|
@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="200">Subtitles retrieved.</response>
|
/// <response code="200">Subtitles retrieved.</response>
|
||||||
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
|
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
|
||||||
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
|
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
|
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
|
@ -129,7 +129,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="204">Subtitle downloaded.</response>
|
/// <response code="204">Subtitle downloaded.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
|
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> DownloadRemoteSubtitles(
|
public async Task<ActionResult> DownloadRemoteSubtitles(
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
|
@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="200">File returned.</response>
|
/// <response code="200">File returned.</response>
|
||||||
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
|
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
|
||||||
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
|
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Produces(MediaTypeNames.Application.Octet)]
|
[Produces(MediaTypeNames.Application.Octet)]
|
||||||
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
|
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
|
||||||
|
@ -249,7 +249,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="200">Subtitle playlist retrieved.</response>
|
/// <response code="200">Subtitle playlist retrieved.</response>
|
||||||
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
|
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
|
||||||
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
|
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult> GetSubtitlePlaylist(
|
public async Task<ActionResult> GetSubtitlePlaylist(
|
||||||
[FromRoute] Guid id,
|
[FromRoute] Guid id,
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
@ -15,7 +16,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// Attachments controller.
|
/// Attachments controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Videos")]
|
[Route("Videos")]
|
||||||
[Authorize]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class VideoAttachmentsController : BaseJellyfinApiController
|
public class VideoAttachmentsController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
77
Jellyfin.Api/Helpers/ClaimHelpers.cs
Normal file
77
Jellyfin.Api/Helpers/ClaimHelpers.cs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Claim Helpers.
|
||||||
|
/// </summary>
|
||||||
|
public static class ClaimHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get user id from claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>User id.</returns>
|
||||||
|
public static Guid? GetUserId(in ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var value = GetClaimValue(user, InternalClaimTypes.UserId);
|
||||||
|
return string.IsNullOrEmpty(value)
|
||||||
|
? null
|
||||||
|
: (Guid?)Guid.Parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get device id from claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>Device id.</returns>
|
||||||
|
public static string? GetDeviceId(in ClaimsPrincipal user)
|
||||||
|
=> GetClaimValue(user, InternalClaimTypes.DeviceId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get device from claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>Device.</returns>
|
||||||
|
public static string? GetDevice(in ClaimsPrincipal user)
|
||||||
|
=> GetClaimValue(user, InternalClaimTypes.Device);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get client from claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>Client.</returns>
|
||||||
|
public static string? GetClient(in ClaimsPrincipal user)
|
||||||
|
=> GetClaimValue(user, InternalClaimTypes.Client);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get version from claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>Version.</returns>
|
||||||
|
public static string? GetVersion(in ClaimsPrincipal user)
|
||||||
|
=> GetClaimValue(user, InternalClaimTypes.Version);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get token from claims.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">Current claims principal.</param>
|
||||||
|
/// <returns>Token.</returns>
|
||||||
|
public static string? GetToken(in ClaimsPrincipal user)
|
||||||
|
=> GetClaimValue(user, InternalClaimTypes.Token);
|
||||||
|
|
||||||
|
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
|
||||||
|
{
|
||||||
|
return user?.Identities
|
||||||
|
.SelectMany(c => c.Claims)
|
||||||
|
.Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(claim => claim.Value)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,10 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Jellyfin.Api;
|
using Jellyfin.Api;
|
||||||
using Jellyfin.Api.Auth;
|
using Jellyfin.Api.Auth;
|
||||||
|
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||||
|
using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
|
||||||
|
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Controllers;
|
using Jellyfin.Api.Controllers;
|
||||||
|
@ -15,6 +18,8 @@ using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
@ -33,16 +38,19 @@ namespace Jellyfin.Server.Extensions
|
||||||
/// <returns>The updated service collection.</returns>
|
/// <returns>The updated service collection.</returns>
|
||||||
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
|
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreScheduleHandler>();
|
||||||
|
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
|
||||||
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
|
||||||
return serviceCollection.AddAuthorizationCore(options =>
|
return serviceCollection.AddAuthorizationCore(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.RequiresElevation,
|
Policies.DefaultAuthorization,
|
||||||
policy =>
|
policy =>
|
||||||
{
|
{
|
||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new RequiresElevationRequirement());
|
policy.AddRequirements(new DefaultAuthorizationRequirement());
|
||||||
});
|
});
|
||||||
options.AddPolicy(
|
options.AddPolicy(
|
||||||
Policies.FirstTimeSetupOrElevated,
|
Policies.FirstTimeSetupOrElevated,
|
||||||
|
@ -51,6 +59,27 @@ namespace Jellyfin.Server.Extensions
|
||||||
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
|
policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
|
||||||
});
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.IgnoreSchedule,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new IgnoreScheduleRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.LocalAccessOnly,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new LocalAccessRequirement());
|
||||||
|
});
|
||||||
|
options.AddPolicy(
|
||||||
|
Policies.RequiresElevation,
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
|
||||||
|
policy.AddRequirements(new RequiresElevationRequirement());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +107,10 @@ namespace Jellyfin.Server.Extensions
|
||||||
{
|
{
|
||||||
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
|
options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy);
|
||||||
})
|
})
|
||||||
|
.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
})
|
||||||
.AddMvc(opts =>
|
.AddMvc(opts =>
|
||||||
{
|
{
|
||||||
opts.UseGeneralRoutePrefix(baseUrl);
|
opts.UseGeneralRoutePrefix(baseUrl);
|
||||||
|
|
|
@ -6,10 +6,31 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IAuthService.
|
||||||
|
/// </summary>
|
||||||
public interface IAuthService
|
public interface IAuthService
|
||||||
{
|
{
|
||||||
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues);
|
/// <summary>
|
||||||
|
/// Authenticate and authorize request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request.</param>
|
||||||
|
/// <param name="authAttribtutes">Authorization attributes.</param>
|
||||||
|
void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes);
|
||||||
|
|
||||||
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtues);
|
/// <summary>
|
||||||
|
/// Authenticate and authorize request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request.</param>
|
||||||
|
/// <param name="authAttribtutes">Authorization attributes.</param>
|
||||||
|
/// <returns>Authenticated user.</returns>
|
||||||
|
User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticate request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request.</param>
|
||||||
|
/// <returns>Authorization information. Null if unauthenticated.</returns>
|
||||||
|
AuthorizationInfo Authenticate(HttpRequest request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
using MediaBrowser.Model.Services;
|
using MediaBrowser.Model.Services;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IAuthorization context.
|
||||||
|
/// </summary>
|
||||||
public interface IAuthorizationContext
|
public interface IAuthorizationContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -17,5 +21,12 @@ namespace MediaBrowser.Controller.Net
|
||||||
/// <param name="requestContext">The request context.</param>
|
/// <param name="requestContext">The request context.</param>
|
||||||
/// <returns>AuthorizationInfo.</returns>
|
/// <returns>AuthorizationInfo.</returns>
|
||||||
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
|
AuthorizationInfo GetAuthorizationInfo(IRequest requestContext);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authorization information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestContext">The request context.</param>
|
||||||
|
/// <returns>AuthorizationInfo.</returns>
|
||||||
|
AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using AutoFixture.AutoMoq;
|
using AutoFixture.AutoMoq;
|
||||||
|
@ -9,7 +8,6 @@ using Jellyfin.Api.Auth;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
@ -26,12 +24,6 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
private readonly IFixture _fixture;
|
private readonly IFixture _fixture;
|
||||||
|
|
||||||
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
|
private readonly Mock<IAuthService> _jellyfinAuthServiceMock;
|
||||||
private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitorMock;
|
|
||||||
private readonly Mock<ISystemClock> _clockMock;
|
|
||||||
private readonly Mock<IServiceProvider> _serviceProviderMock;
|
|
||||||
private readonly Mock<IAuthenticationService> _authenticationServiceMock;
|
|
||||||
private readonly UrlEncoder _urlEncoder;
|
|
||||||
private readonly HttpContext _context;
|
|
||||||
|
|
||||||
private readonly CustomAuthenticationHandler _sut;
|
private readonly CustomAuthenticationHandler _sut;
|
||||||
private readonly AuthenticationScheme _scheme;
|
private readonly AuthenticationScheme _scheme;
|
||||||
|
@ -47,26 +39,23 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
AllowFixtureCircularDependencies();
|
AllowFixtureCircularDependencies();
|
||||||
|
|
||||||
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
|
_jellyfinAuthServiceMock = _fixture.Freeze<Mock<IAuthService>>();
|
||||||
_optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
|
var optionsMonitorMock = _fixture.Freeze<Mock<IOptionsMonitor<AuthenticationSchemeOptions>>>();
|
||||||
_clockMock = _fixture.Freeze<Mock<ISystemClock>>();
|
var serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
|
||||||
_serviceProviderMock = _fixture.Freeze<Mock<IServiceProvider>>();
|
var authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
|
||||||
_authenticationServiceMock = _fixture.Freeze<Mock<IAuthenticationService>>();
|
|
||||||
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
|
_fixture.Register<ILoggerFactory>(() => new NullLoggerFactory());
|
||||||
|
|
||||||
_urlEncoder = UrlEncoder.Default;
|
serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
|
||||||
|
.Returns(authenticationServiceMock.Object);
|
||||||
|
|
||||||
_serviceProviderMock.Setup(s => s.GetService(typeof(IAuthenticationService)))
|
optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
|
||||||
.Returns(_authenticationServiceMock.Object);
|
|
||||||
|
|
||||||
_optionsMonitorMock.Setup(o => o.Get(It.IsAny<string>()))
|
|
||||||
.Returns(new AuthenticationSchemeOptions
|
.Returns(new AuthenticationSchemeOptions
|
||||||
{
|
{
|
||||||
ForwardAuthenticate = null
|
ForwardAuthenticate = null
|
||||||
});
|
});
|
||||||
|
|
||||||
_context = new DefaultHttpContext
|
HttpContext context = new DefaultHttpContext
|
||||||
{
|
{
|
||||||
RequestServices = _serviceProviderMock.Object
|
RequestServices = serviceProviderMock.Object
|
||||||
};
|
};
|
||||||
|
|
||||||
_scheme = new AuthenticationScheme(
|
_scheme = new AuthenticationScheme(
|
||||||
|
@ -75,24 +64,7 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
typeof(CustomAuthenticationHandler));
|
typeof(CustomAuthenticationHandler));
|
||||||
|
|
||||||
_sut = _fixture.Create<CustomAuthenticationHandler>();
|
_sut = _fixture.Create<CustomAuthenticationHandler>();
|
||||||
_sut.InitializeAsync(_scheme, _context).Wait();
|
_sut.InitializeAsync(_scheme, context).Wait();
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task HandleAuthenticateAsyncShouldFailWithNullUser()
|
|
||||||
{
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
|
||||||
a => a.Authenticate(
|
|
||||||
It.IsAny<HttpRequest>(),
|
|
||||||
It.IsAny<AuthenticatedAttribute>()))
|
|
||||||
.Returns((User?)null);
|
|
||||||
|
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
|
||||||
|
|
||||||
Assert.False(authenticateResult.Succeeded);
|
|
||||||
Assert.True(authenticateResult.None);
|
|
||||||
// TODO return when legacy API is removed.
|
|
||||||
// Assert.Equal("Invalid user", authenticateResult.Failure.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -102,8 +74,7 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
_jellyfinAuthServiceMock.Setup(
|
||||||
a => a.Authenticate(
|
a => a.Authenticate(
|
||||||
It.IsAny<HttpRequest>(),
|
It.IsAny<HttpRequest>()))
|
||||||
It.IsAny<AuthenticatedAttribute>()))
|
|
||||||
.Throws(new SecurityException(errorMessage));
|
.Throws(new SecurityException(errorMessage));
|
||||||
|
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
var authenticateResult = await _sut.AuthenticateAsync();
|
||||||
|
@ -125,10 +96,10 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
|
public async Task HandleAuthenticateAsyncShouldAssignNameClaim()
|
||||||
{
|
{
|
||||||
var user = SetupUser();
|
var authorizationInfo = SetupUser();
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
var authenticateResult = await _sut.AuthenticateAsync();
|
||||||
|
|
||||||
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, user.Username));
|
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
@ -136,10 +107,10 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
[InlineData(false)]
|
[InlineData(false)]
|
||||||
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
|
public async Task HandleAuthenticateAsyncShouldAssignRoleClaim(bool isAdmin)
|
||||||
{
|
{
|
||||||
var user = SetupUser(isAdmin);
|
var authorizationInfo = SetupUser(isAdmin);
|
||||||
var authenticateResult = await _sut.AuthenticateAsync();
|
var authenticateResult = await _sut.AuthenticateAsync();
|
||||||
|
|
||||||
var expectedRole = user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
|
var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
|
||||||
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
|
Assert.True(authenticateResult.Principal.HasClaim(ClaimTypes.Role, expectedRole));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,18 +123,18 @@ namespace Jellyfin.Api.Tests.Auth
|
||||||
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
|
Assert.Equal(_scheme.Name, authenticatedResult.Ticket.AuthenticationScheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
private User SetupUser(bool isAdmin = false)
|
private AuthorizationInfo SetupUser(bool isAdmin = false)
|
||||||
{
|
{
|
||||||
var user = _fixture.Create<User>();
|
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
|
||||||
user.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
authorizationInfo.User = _fixture.Create<User>();
|
||||||
|
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
||||||
|
|
||||||
_jellyfinAuthServiceMock.Setup(
|
_jellyfinAuthServiceMock.Setup(
|
||||||
a => a.Authenticate(
|
a => a.Authenticate(
|
||||||
It.IsAny<HttpRequest>(),
|
It.IsAny<HttpRequest>()))
|
||||||
It.IsAny<AuthenticatedAttribute>()))
|
.Returns(authorizationInfo);
|
||||||
.Returns(user);
|
|
||||||
|
|
||||||
return user;
|
return authorizationInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AllowFixtureCircularDependencies()
|
private void AllowFixtureCircularDependencies()
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using AutoFixture.AutoMoq;
|
||||||
|
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
|
||||||
|
{
|
||||||
|
public class DefaultAuthorizationHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
|
private readonly DefaultAuthorizationHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
|
||||||
|
public DefaultAuthorizationHandlerTests()
|
||||||
|
{
|
||||||
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
|
_requirements = new List<IAuthorizationRequirement> { new DefaultAuthorizationRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
|
||||||
|
_sut = fixture.Create<DefaultAuthorizationHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(UserRoles.Administrator)]
|
||||||
|
[InlineData(UserRoles.Guest)]
|
||||||
|
[InlineData(UserRoles.User)]
|
||||||
|
public async Task ShouldSucceedOnUser(string userRole)
|
||||||
|
{
|
||||||
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
|
var claims = TestHelpers.SetupUser(
|
||||||
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
userRole);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
|
||||||
|
await _sut.HandleAsync(context);
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using AutoFixture.AutoMoq;
|
using AutoFixture.AutoMoq;
|
||||||
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
@ -18,12 +18,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
|
||||||
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
private readonly List<IAuthorizationRequirement> _requirements;
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
private readonly FirstTimeSetupOrElevatedHandler _sut;
|
private readonly FirstTimeSetupOrElevatedHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
|
||||||
public FirstTimeSetupOrElevatedHandlerTests()
|
public FirstTimeSetupOrElevatedHandlerTests()
|
||||||
{
|
{
|
||||||
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
|
_requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
|
||||||
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
|
_sut = fixture.Create<FirstTimeSetupOrElevatedHandler>();
|
||||||
}
|
}
|
||||||
|
@ -34,9 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
|
||||||
[InlineData(UserRoles.User)]
|
[InlineData(UserRoles.User)]
|
||||||
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
|
public async Task ShouldSucceedIfStartupWizardIncomplete(string userRole)
|
||||||
{
|
{
|
||||||
SetupConfigurationManager(false);
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, false);
|
||||||
var user = SetupUser(userRole);
|
var claims = TestHelpers.SetupUser(
|
||||||
var context = new AuthorizationHandlerContext(_requirements, user, null);
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
userRole);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
|
||||||
await _sut.HandleAsync(context);
|
await _sut.HandleAsync(context);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
|
@ -48,30 +56,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy
|
||||||
[InlineData(UserRoles.User, false)]
|
[InlineData(UserRoles.User, false)]
|
||||||
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
|
public async Task ShouldRequireAdministratorIfStartupWizardComplete(string userRole, bool shouldSucceed)
|
||||||
{
|
{
|
||||||
SetupConfigurationManager(true);
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
var user = SetupUser(userRole);
|
var claims = TestHelpers.SetupUser(
|
||||||
var context = new AuthorizationHandlerContext(_requirements, user, null);
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
userRole);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
|
||||||
await _sut.HandleAsync(context);
|
await _sut.HandleAsync(context);
|
||||||
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClaimsPrincipal SetupUser(string role)
|
|
||||||
{
|
|
||||||
var claims = new[] { new Claim(ClaimTypes.Role, role) };
|
|
||||||
var identity = new ClaimsIdentity(claims);
|
|
||||||
return new ClaimsPrincipal(identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupConfigurationManager(bool startupWizardCompleted)
|
|
||||||
{
|
|
||||||
var commonConfiguration = new BaseApplicationConfiguration
|
|
||||||
{
|
|
||||||
IsStartupWizardCompleted = startupWizardCompleted
|
|
||||||
};
|
|
||||||
|
|
||||||
_configurationManagerMock.Setup(c => c.CommonConfiguration)
|
|
||||||
.Returns(commonConfiguration);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using AutoFixture.AutoMoq;
|
||||||
|
using Jellyfin.Api.Auth.IgnoreSchedulePolicy;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy
|
||||||
|
{
|
||||||
|
public class IgnoreScheduleHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
|
private readonly IgnoreScheduleHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Globally disallow access.
|
||||||
|
/// </summary>
|
||||||
|
private readonly AccessSchedule[] _accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
|
||||||
|
|
||||||
|
public IgnoreScheduleHandlerTests()
|
||||||
|
{
|
||||||
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
|
_requirements = new List<IAuthorizationRequirement> { new IgnoreScheduleRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
|
||||||
|
_sut = fixture.Create<IgnoreScheduleHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(UserRoles.Administrator, true)]
|
||||||
|
[InlineData(UserRoles.User, true)]
|
||||||
|
[InlineData(UserRoles.Guest, true)]
|
||||||
|
public async Task ShouldAllowScheduleCorrectly(string role, bool shouldSucceed)
|
||||||
|
{
|
||||||
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
|
var claims = TestHelpers.SetupUser(
|
||||||
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
role,
|
||||||
|
_accessSchedules);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
|
||||||
|
await _sut.HandleAsync(context);
|
||||||
|
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using AutoFixture.AutoMoq;
|
||||||
|
using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Tests.Auth.LocalAccessPolicy
|
||||||
|
{
|
||||||
|
public class LocalAccessHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
|
private readonly LocalAccessHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
private readonly Mock<INetworkManager> _networkManagerMock;
|
||||||
|
|
||||||
|
public LocalAccessHandlerTests()
|
||||||
|
{
|
||||||
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
|
_requirements = new List<IAuthorizationRequirement> { new LocalAccessRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
_networkManagerMock = fixture.Freeze<Mock<INetworkManager>>();
|
||||||
|
|
||||||
|
_sut = fixture.Create<LocalAccessHandler>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, true)]
|
||||||
|
[InlineData(false, false)]
|
||||||
|
public async Task LocalAccessOnly(bool isInLocalNetwork, bool shouldSucceed)
|
||||||
|
{
|
||||||
|
_networkManagerMock
|
||||||
|
.Setup(n => n.IsInLocalNetwork(It.IsAny<string>()))
|
||||||
|
.Returns(isInLocalNetwork);
|
||||||
|
|
||||||
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
|
var claims = TestHelpers.SetupUser(
|
||||||
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
UserRoles.User);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
|
await _sut.HandleAsync(context);
|
||||||
|
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,35 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AutoFixture;
|
||||||
|
using AutoFixture.AutoMoq;
|
||||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
|
namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
|
||||||
{
|
{
|
||||||
public class RequiresElevationHandlerTests
|
public class RequiresElevationHandlerTests
|
||||||
{
|
{
|
||||||
|
private readonly Mock<IConfigurationManager> _configurationManagerMock;
|
||||||
|
private readonly List<IAuthorizationRequirement> _requirements;
|
||||||
private readonly RequiresElevationHandler _sut;
|
private readonly RequiresElevationHandler _sut;
|
||||||
|
private readonly Mock<IUserManager> _userManagerMock;
|
||||||
|
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
|
||||||
|
|
||||||
public RequiresElevationHandlerTests()
|
public RequiresElevationHandlerTests()
|
||||||
{
|
{
|
||||||
_sut = new RequiresElevationHandler();
|
var fixture = new Fixture().Customize(new AutoMoqCustomization());
|
||||||
|
_configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>();
|
||||||
|
_requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
|
||||||
|
_userManagerMock = fixture.Freeze<Mock<IUserManager>>();
|
||||||
|
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
|
||||||
|
|
||||||
|
_sut = fixture.Create<RequiresElevationHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
@ -23,13 +38,13 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy
|
||||||
[InlineData(UserRoles.Guest, false)]
|
[InlineData(UserRoles.Guest, false)]
|
||||||
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
|
public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed)
|
||||||
{
|
{
|
||||||
var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() };
|
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
|
||||||
|
var claims = TestHelpers.SetupUser(
|
||||||
|
_userManagerMock,
|
||||||
|
_httpContextAccessor,
|
||||||
|
role);
|
||||||
|
|
||||||
var claims = new[] { new Claim(ClaimTypes.Role, role) };
|
var context = new AuthorizationHandlerContext(_requirements, claims, null);
|
||||||
var identity = new ClaimsIdentity(claims);
|
|
||||||
var user = new ClaimsPrincipal(identity);
|
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(requirements, user, null);
|
|
||||||
|
|
||||||
await _sut.HandleAsync(context);
|
await _sut.HandleAsync(context);
|
||||||
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
Assert.Equal(shouldSucceed, context.HasSucceeded);
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
|
<ProjectReference Include="../../MediaBrowser.Api/MediaBrowser.Api.csproj" />
|
||||||
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
|
<ProjectReference Include="../../Jellyfin.Api/Jellyfin.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\..\Jellyfin.Server\Jellyfin.Server.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
|
81
tests/Jellyfin.Api.Tests/TestHelpers.cs
Normal file
81
tests/Jellyfin.Api.Tests/TestHelpers.cs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Server.Implementations.Users;
|
||||||
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Moq;
|
||||||
|
using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Tests
|
||||||
|
{
|
||||||
|
public static class TestHelpers
|
||||||
|
{
|
||||||
|
public static ClaimsPrincipal SetupUser(
|
||||||
|
Mock<IUserManager> userManagerMock,
|
||||||
|
Mock<IHttpContextAccessor> httpContextAccessorMock,
|
||||||
|
string role,
|
||||||
|
IEnumerable<AccessSchedule>? accessSchedules = null)
|
||||||
|
{
|
||||||
|
var user = new User(
|
||||||
|
"jellyfin",
|
||||||
|
typeof(DefaultAuthenticationProvider).FullName,
|
||||||
|
typeof(DefaultPasswordResetProvider).FullName);
|
||||||
|
|
||||||
|
// Set administrator flag.
|
||||||
|
user.SetPermission(PermissionKind.IsAdministrator, role.Equals(UserRoles.Administrator, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// Add access schedules if set.
|
||||||
|
if (accessSchedules != null)
|
||||||
|
{
|
||||||
|
foreach (var accessSchedule in accessSchedules)
|
||||||
|
{
|
||||||
|
user.AccessSchedules.Add(accessSchedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Role, role),
|
||||||
|
new Claim(ClaimTypes.Name, "jellyfin"),
|
||||||
|
new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
||||||
|
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
||||||
|
new Claim(InternalClaimTypes.Device, "test"),
|
||||||
|
new Claim(InternalClaimTypes.Client, "test"),
|
||||||
|
new Claim(InternalClaimTypes.Version, "test"),
|
||||||
|
new Claim(InternalClaimTypes.Token, "test"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims);
|
||||||
|
|
||||||
|
userManagerMock
|
||||||
|
.Setup(u => u.GetUserById(It.IsAny<Guid>()))
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
httpContextAccessorMock
|
||||||
|
.Setup(h => h.HttpContext.Connection.RemoteIpAddress)
|
||||||
|
.Returns(new IPAddress(0));
|
||||||
|
|
||||||
|
return new ClaimsPrincipal(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetupConfigurationManager(in Mock<IConfigurationManager> configurationManagerMock, bool startupWizardCompleted)
|
||||||
|
{
|
||||||
|
var commonConfiguration = new BaseApplicationConfiguration
|
||||||
|
{
|
||||||
|
IsStartupWizardCompleted = startupWizardCompleted
|
||||||
|
};
|
||||||
|
|
||||||
|
configurationManagerMock
|
||||||
|
.Setup(c => c.CommonConfiguration)
|
||||||
|
.Returns(commonConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user