Grant shared accounts the intersection of member library access
Previously the shared account's libraries were chosen independently of its members, so a group could see a library that one of its members was blocked from - joining a group became a way to gain access. That was especially sharp with auto-created groups, where no admin is in the loop. A shared account is now granted exactly the libraries every member can already reach. If one member is blocked from a library, no group containing them can see it. The account is therefore always a subset of what each member could reach alone, which is what makes creating groups at the login screen safe to leave on by default. Details: - "Enable all folders" is expanded to concrete library ids before intersecting, since it cannot otherwise be compared with an explicit list. Shared accounts are always given an explicit list, never the all-folders permission, so newly added libraries do not silently widen an existing group. - Explicitly blocked folders are subtracted even for members who otherwise have access to everything. - Fails closed: an unresolvable member contributes no access rather than being treated as unrestricted. - Recomputed when membership changes, and re-applied to every group at startup so narrowing a member's own access narrows their groups. Drops the now-meaningless EnableAllFolders/EnabledFolders provisioning inputs and the DynamicGroupsEnableAllFolders setting. Adds 8 tests covering the intersection rules.
This commit is contained in:
@@ -31,25 +31,27 @@ public class ProvisioningService : IProvisioningService
|
||||
typeof(Auth.SharedAccountAuthenticationProvider).FullName!;
|
||||
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryAccessService _libraryAccessService;
|
||||
private readonly ILogger<ProvisioningService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProvisioningService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="libraryAccessService">The library access service.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ProvisioningService(IUserManager userManager, ILogger<ProvisioningService> logger)
|
||||
public ProvisioningService(
|
||||
IUserManager userManager,
|
||||
ILibraryAccessService libraryAccessService,
|
||||
ILogger<ProvisioningService> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_libraryAccessService = libraryAccessService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SharedGroup> CreateGroupAsync(
|
||||
IReadOnlyList<Guid> memberIds,
|
||||
string? name,
|
||||
bool enableAllFolders,
|
||||
IReadOnlyList<Guid>? enabledFolders)
|
||||
public async Task<SharedGroup> CreateGroupAsync(IReadOnlyList<Guid> memberIds, string? name)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(memberIds);
|
||||
|
||||
@@ -103,7 +105,7 @@ public class ProvisioningService : IProvisioningService
|
||||
await _userManager.ChangePassword(sharedUser, GenerateUnusedPassword()).ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(sharedUser).ConfigureAwait(false);
|
||||
|
||||
await ApplyLibraryAccessAsync(sharedUser.Id, enableAllFolders, enabledFolders).ConfigureAwait(false);
|
||||
await ApplyLibraryAccessAsync(sharedUser.Id, distinctIds).ConfigureAwait(false);
|
||||
|
||||
var group = new SharedGroup
|
||||
{
|
||||
@@ -124,7 +126,7 @@ public class ProvisioningService : IProvisioningService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SharedGroup> UpdateGroupAsync(
|
||||
public async Task<SharedGroup> UpdateGroupAsync(
|
||||
Guid sharedUserId,
|
||||
IReadOnlyList<Guid> memberIds,
|
||||
bool syncUnwatched,
|
||||
@@ -170,13 +172,17 @@ public class ProvisioningService : IProvisioningService
|
||||
|
||||
plugin.UpdateConfiguration(config);
|
||||
|
||||
// Membership drives library access, so recompute it: adding a member can only narrow the
|
||||
// intersection, and removing one may widen it.
|
||||
await ApplyLibraryAccessAsync(sharedUserId, distinctIds).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Updated group {SharedUserId}: {MemberCount} members, disabled={IsDisabled}",
|
||||
sharedUserId,
|
||||
distinctIds.Count,
|
||||
isDisabled);
|
||||
|
||||
return Task.FromResult(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -239,13 +245,9 @@ public class ProvisioningService : IProvisioningService
|
||||
/// Sets library access on the shared account.
|
||||
/// </summary>
|
||||
/// <param name="sharedUserId">The shared account.</param>
|
||||
/// <param name="enableAllFolders">Whether to grant access to every library.</param>
|
||||
/// <param name="enabledFolders">The explicit library list when not granting all.</param>
|
||||
/// <param name="memberIds">The members whose access is intersected.</param>
|
||||
/// <returns>A task representing the update.</returns>
|
||||
private async Task ApplyLibraryAccessAsync(
|
||||
Guid sharedUserId,
|
||||
bool enableAllFolders,
|
||||
IReadOnlyList<Guid>? enabledFolders)
|
||||
private async Task ApplyLibraryAccessAsync(Guid sharedUserId, IReadOnlyList<Guid> memberIds)
|
||||
{
|
||||
var user = _userManager.GetUserById(sharedUserId);
|
||||
if (user is null)
|
||||
@@ -253,24 +255,11 @@ public class ProvisioningService : IProvisioningService
|
||||
return;
|
||||
}
|
||||
|
||||
// Library access on the shared account is deliberate and independent of what each member
|
||||
// can reach individually - any member's password opens whatever this account can see.
|
||||
user.SetPermission(PermissionKind.EnableAllFolders, enableAllFolders);
|
||||
// Never "all folders": the shared account gets an explicit list of the libraries every
|
||||
// member can already reach, so joining a group can never grant access to anything.
|
||||
user.SetPermission(PermissionKind.EnableAllFolders, false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
if (enableAllFolders)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var policy = _userManager.GetUserDto(user).Policy;
|
||||
if (policy is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
policy.EnableAllFolders = false;
|
||||
policy.EnabledFolders = enabledFolders?.ToArray() ?? [];
|
||||
await _userManager.UpdatePolicyAsync(sharedUserId, policy).ConfigureAwait(false);
|
||||
await _libraryAccessService.ApplyIntersectionAsync(sharedUserId, memberIds).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user