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> /// </summary>
private readonly ILibraryManager _libraryManager; 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> /// <summary>
/// The map between sessions and groups. /// The map between sessions and groups.
/// </summary> /// </summary>
@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!"); throw new InvalidOperationException("Could not add session to group!");
} }
UpdateSessionsCounter(session.UserId, 1);
group.CreateGroup(session, request, cancellationToken); group.CreateGroup(session, request, cancellationToken);
} }
} }
@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (existingGroup.GroupId.Equals(request.GroupId)) if (existingGroup.GroupId.Equals(request.GroupId))
{ {
// Restore session. // Restore session.
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken); group.SessionJoin(session, request, cancellationToken);
return; return;
} }
@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!"); throw new InvalidOperationException("Could not add session to group!");
} }
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken); group.SessionJoin(session, request, cancellationToken);
} }
} }
@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not remove session from group!"); throw new InvalidOperationException("Could not remove session from group!");
} }
UpdateSessionsCounter(session.UserId, -1);
group.SessionLeave(session, request, cancellationToken); group.SessionLeave(session, request, cancellationToken);
if (group.IsGroupEmpty()) 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> /// <summary>
/// Releases unmanaged and optionally managed resources. /// Releases unmanaged and optionally managed resources.
/// </summary> /// </summary>
@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
JoinGroup(session, request, CancellationToken.None); 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 Jellyfin.Data.Enums;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary> /// </summary>
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement> public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
{ {
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class. /// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
/// </summary> /// </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="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler( public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
IUserManager userManager, IUserManager userManager,
INetworkManager networkManager, INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor) IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor) : base(userManager, networkManager, httpContextAccessor)
{ {
_syncPlayManager = syncPlayManager;
_userManager = userManager; _userManager = userManager;
} }
@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
var userId = ClaimHelpers.GetUserId(context.User); var userId = ClaimHelpers.GetUserId(context.User);
var user = _userManager.GetUserById(userId!.Value); var user = _userManager.GetUserById(userId!.Value);
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess) if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
{ {
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 else
{ {

View File

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

View File

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

View File

@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
EnableAutoLogin = false; EnableAutoLogin = false;
PlayDefaultAudioTrack = true; PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default; SubtitleMode = SubtitlePlaybackMode.Default;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
AddDefaultPermissions(); AddDefaultPermissions();
AddDefaultPreferences(); AddDefaultPreferences();
@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
/// <summary> /// <summary>
/// Gets or sets the level of sync play permissions this user has. /// Gets or sets the level of sync play permissions this user has.
/// </summary> /// </summary>
public SyncPlayAccess SyncPlayAccess { get; set; } public SyncPlayUserAccessType SyncPlayAccess { get; set; }
/// <summary> /// <summary>
/// Gets or sets the row version. /// 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 namespace Jellyfin.Data.Enums
{ {
/// <summary> /// <summary>
/// Enum SyncPlayAccess. /// Enum SyncPlayUserAccessType.
/// </summary> /// </summary>
public enum SyncPlayAccess public enum SyncPlayUserAccessType
{ {
/// <summary> /// <summary>
/// User can create groups and join them. /// User can create groups and join them.

View File

@ -127,18 +127,32 @@ namespace Jellyfin.Server.Extensions
policy.AddRequirements(new RequiresElevationRequirement()); policy.AddRequirements(new RequiresElevationRequirement());
}); });
options.AddPolicy( options.AddPolicy(
Policies.SyncPlayAccess, Policies.SyncPlayHasAccess,
policy => policy =>
{ {
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups)); policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
}); });
options.AddPolicy( options.AddPolicy(
Policies.SyncPlayCreateGroupAccess, Policies.SyncPlayCreateGroup,
policy => policy =>
{ {
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); 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="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken); 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. /// Gets or sets a value indicating what SyncPlay features the user can access.
/// </summary> /// </summary>
/// <value>Access level to SyncPlay features.</value> /// <value>Access level to SyncPlay features.</value>
public SyncPlayAccess SyncPlayAccess { get; set; } public SyncPlayUserAccessType SyncPlayAccess { get; set; }
public UserPolicy() public UserPolicy()
{ {
@ -160,7 +160,7 @@ namespace MediaBrowser.Model.Users
EnableContentDownloading = true; EnableContentDownloading = true;
EnablePublicSharing = true; EnablePublicSharing = true;
EnableRemoteAccess = true; EnableRemoteAccess = true;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups; SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
} }
} }
} }