Block plugin routes for share guests (#16)

* block plugin routes for share guests

Adds a global MVC filter that refuses share-guest accounts on any plugin
controller. Jellyfin's own API stays open, the share tag policy already
bounds it and playback needs it.

Guests are identified by the auth provider marker on the account, so this
covers a leaked token used from curl or a native client, not just the web
client where the CSS lockdown runs.

* add plugin exception list and honest wording

Config page lists installed plugins with a checkbox each, for the ones that
need to answer guests during playback. Default is unticked.

Renames the hidden selectors setting to say it is cosmetic, and stops the
readme implying the web client lockdown confines anything.

* bump to 1.0.4.0
Cette révision appartient à :
Francois CB
2026-07-27 20:39:21 +02:00
révisé par GitHub
Parent 4c48861506
révision d1ee74677b
8 fichiers modifiés avec 373 ajouts et 23 suppressions
+49
Voir le fichier
@@ -11,6 +11,7 @@ using Jellyfin.Plugin.ShareLinks.Configuration;
using Jellyfin.Plugin.ShareLinks.Models;
using Jellyfin.Plugin.ShareLinks.Services;
using Jellyfin.Plugin.ShareLinks.Storage;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
@@ -99,6 +100,19 @@ public sealed class ShareLinkGuestStateDto
public string? HiddenSelectors { get; set; }
}
/// <summary>An installed plugin, as offered in the guard's exception list.</summary>
public sealed class ShareLinkPluginDto
{
/// <summary>Gets or sets the plugin id.</summary>
public Guid Id { get; set; }
/// <summary>Gets or sets the plugin's display name.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Gets or sets a value indicating whether guests may currently reach it.</summary>
public bool AllowedForGuests { get; set; }
}
/// <summary>ShareLinks API surface.</summary>
[ApiController]
[Route("ShareLinks")]
@@ -109,6 +123,7 @@ public sealed class ShareLinksController : ControllerBase
private readonly ShareLinkCleanupService _cleanupService;
private readonly ShareLinkRedemptionService _redemptionService;
private readonly ShareLinkStore _store;
private readonly IPluginManager _pluginManager;
private readonly ILogger<ShareLinksController> _logger;
/// <summary>Initializes a new instance of the <see cref="ShareLinksController"/> class.</summary>
@@ -118,6 +133,7 @@ public sealed class ShareLinksController : ControllerBase
ShareLinkCleanupService cleanupService,
ShareLinkRedemptionService redemptionService,
ShareLinkStore store,
IPluginManager pluginManager,
ILogger<ShareLinksController> logger)
{
_libraryManager = libraryManager;
@@ -125,6 +141,7 @@ public sealed class ShareLinksController : ControllerBase
_cleanupService = cleanupService;
_redemptionService = redemptionService;
_store = store;
_pluginManager = pluginManager;
_logger = logger;
}
@@ -284,6 +301,38 @@ public sealed class ShareLinksController : ControllerBase
return Ok(new { removed });
}
/// <summary>
/// Lists the installed plugins so the config page can offer them as guard
/// exceptions. ShareLinks itself is left out: it is always reachable, since the
/// guest's browser fetches the lockdown script and guest state from it.
/// </summary>
[HttpGet("Admin/Plugins")]
[Authorize(AuthenticationSchemes = "CustomAuthentication")]
public ActionResult<IEnumerable<ShareLinkPluginDto>> Plugins()
{
SetNoStoreHeaders();
if (!User.IsInRole("Administrator"))
{
return Forbid();
}
var allowed = Config.GuestAllowedPluginIds ?? Array.Empty<string>();
var ownId = Plugin.Instance?.Id;
var plugins = _pluginManager.Plugins
.Where(plugin => !ownId.HasValue || plugin.Id != ownId.Value)
.Select(plugin => new ShareLinkPluginDto
{
Id = plugin.Id,
Name = plugin.Name,
AllowedForGuests = allowed.Any(value => Guid.TryParse(value, out var parsed) && parsed == plugin.Id)
})
.OrderBy(plugin => plugin.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
return Ok(plugins);
}
/// <summary>Returns the guest session state for the current authenticated user.</summary>
[HttpGet("GuestState")]
[Authorize(AuthenticationSchemes = "CustomAuthentication")]