Update authorization policies for SyncPlay

This commit is contained in:
Ionut Andrei Oanca 2020-12-07 10:33:15 +01:00
parent a7b461adb4
commit 499f3ee950
11 changed files with 191 additions and 32 deletions

View File

@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The map between users and counter of active sessions.
/// </summary>
private readonly ConcurrentDictionary<Guid, int> _activeUsers =
new ConcurrentDictionary<Guid, int>();
/// <summary>
/// The map between sessions and groups.
/// </summary>
@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!");
}
UpdateSessionsCounter(session.UserId, 1);
group.CreateGroup(session, request, cancellationToken);
}
}
@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (existingGroup.GroupId.Equals(request.GroupId))
{
// Restore session.
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken);
return;
}
@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!");
}
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken);
}
}
@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not remove session from group!");
}
UpdateSessionsCounter(session.UserId, -1);
group.SessionLeave(session, request, cancellationToken);
if (group.IsGroupEmpty())
@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay
}
}
/// <inheritdoc />
public bool IsUserActive(Guid userId)
{
if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
{
return sessionsCounter > 0;
}
else
{
return false;
}
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
JoinGroup(session, request, CancellationToken.None);
}
}
private void UpdateSessionsCounter(Guid userId, int toAdd)
{
// Update sessions counter.
var newSessionsCounter = _activeUsers.AddOrUpdate(
userId,
1,
(key, sessionsCounter) => sessionsCounter + toAdd);
// Should never happen.
if (newSessionsCounter < 0)
{
throw new InvalidOperationException("Sessions counter is negative!");
}
// Clean record if user has no more active sessions.
if (newSessionsCounter == 0)
{
_activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
}
}
}
}

View File

@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary>
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
{
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
/// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> 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 SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
_syncPlayManager = syncPlayManager;
_userManager = userManager;
}
@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
var userId = ClaimHelpers.GetUserId(context.User);
var user = _userManager.GetUserById(userId!.Value);
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess)
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{
context.Succeed(requirement);
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups ||
user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups ||
_syncPlayManager.IsUserActive(userId!.Value))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups ||
user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
if (_syncPlayManager.IsUserActive(userId!.Value))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else
{

View File

@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary>
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param>
public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess)
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
{
RequiredAccess = requiredAccess;
}
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary>
public SyncPlayAccessRequirement()
{
RequiredAccess = null;
}
/// <summary>
/// Gets the required SyncPlay access.
/// </summary>
public SyncPlayAccess? RequiredAccess { get; }
public SyncPlayAccessRequirementType RequiredAccess { get; }
}
}

View File

@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
/// <summary>
/// Policy name for requiring access to SyncPlay.
/// Policy name for accessing SyncPlay.
/// </summary>
public const string SyncPlayAccess = "SyncPlayAccess";
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
/// <summary>
/// Policy name for requiring group creation access to SyncPlay.
/// Policy name for creating a SyncPlay group.
/// </summary>
public const string SyncPlayCreateGroupAccess = "SyncPlayCreateGroupAccess";
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
/// <summary>
/// Policy name for joining a SyncPlay group.
/// </summary>
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
/// <summary>
/// Policy name for accessing a SyncPlay group.
/// </summary>
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
}
}

View File

@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// The sync play controller.
/// </summary>
[Authorize(Policy = Policies.SyncPlayAccess)]
[Authorize(Policy = Policies.SyncPlayHasAccess)]
public class SyncPlayController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
public ActionResult SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData)
{
@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayAccess)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData)
{
@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Leave")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayLeaveGroup()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.SyncPlayAccess)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData)
{
@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData)
{
@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData)
{
@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData)
{
@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayUnpause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayStop()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData)
{
@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData)
{
@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData)
{
@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData)
{
@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData)
{
@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData)
{
@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData)
{
@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData)
{

View File

@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
EnableAutoLogin = false;
PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
AddDefaultPermissions();
AddDefaultPreferences();
@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
/// <summary>
/// Gets or sets the level of sync play permissions this user has.
/// </summary>
public SyncPlayAccess SyncPlayAccess { get; set; }
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
/// <summary>
/// Gets or sets the row version.

View File

@ -0,0 +1,28 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccessRequirementType.
/// </summary>
public enum SyncPlayAccessRequirementType
{
/// <summary>
/// User must have access to SyncPlay, in some form.
/// </summary>
HasAccess = 0,
/// <summary>
/// User must be able to create groups.
/// </summary>
CreateGroup = 1,
/// <summary>
/// User must be able to join groups.
/// </summary>
JoinGroup = 2,
/// <summary>
/// User must be in a group.
/// </summary>
IsInGroup = 3
}
}

View File

@ -1,9 +1,9 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccess.
/// Enum SyncPlayUserAccessType.
/// </summary>
public enum SyncPlayAccess
public enum SyncPlayUserAccessType
{
/// <summary>
/// User can create groups and join them.

View File

@ -127,18 +127,32 @@ namespace Jellyfin.Server.Extensions
policy.AddRequirements(new RequiresElevationRequirement());
});
options.AddPolicy(
Policies.SyncPlayAccess,
Policies.SyncPlayHasAccess,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups));
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
});
options.AddPolicy(
Policies.SyncPlayCreateGroupAccess,
Policies.SyncPlayCreateGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.CreateAndJoinGroups));
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
});
options.AddPolicy(
Policies.SyncPlayJoinGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
});
options.AddPolicy(
Policies.SyncPlayIsInGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
});
});
}

View File

@ -51,5 +51,12 @@ namespace MediaBrowser.Controller.SyncPlay
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken);
/// <summary>
/// Checks whether a user has an active session using SyncPlay.
/// </summary>
/// <param name="userId">The user identifier to check.</param>
/// <returns><c>true</c> if the user is using SyncPlay; <c>false</c> otherwise.</returns>
bool IsUserActive(Guid userId);
}
}

View File

@ -111,7 +111,7 @@ namespace MediaBrowser.Model.Users
/// Gets or sets a value indicating what SyncPlay features the user can access.
/// </summary>
/// <value>Access level to SyncPlay features.</value>
public SyncPlayAccess SyncPlayAccess { get; set; }
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
public UserPolicy()
{
@ -160,7 +160,7 @@ namespace MediaBrowser.Model.Users
EnableContentDownloading = true;
EnablePublicSharing = true;
EnableRemoteAccess = true;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
}
}
}