diff --git a/src/Umbraco.Core/Services/ContentPermissionManager.cs b/src/Umbraco.Core/Services/ContentPermissionManager.cs new file mode 100644 index 0000000000..ad6fe0ee5c --- /dev/null +++ b/src/Umbraco.Core/Services/ContentPermissionManager.cs @@ -0,0 +1,117 @@ +// src/Umbraco.Core/Services/ContentPermissionManager.cs +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services; + +/// +/// Internal manager for content permission operations. +/// +/// +/// +/// This is an internal class that encapsulates permission operations extracted from ContentService +/// as part of the ContentService refactoring initiative (Phase 6). +/// +/// +/// Design Decision: This class is internal (not public interface) because: +/// +/// Permission operations are tightly coupled to content entities +/// They don't require independent testability beyond ContentService tests +/// The public API remains through IContentService for backward compatibility +/// +/// +/// +/// Note: GetPermissionsForEntity returns EntityPermissionCollection which is a +/// materialized collection (not deferred), so scope disposal before enumeration is safe. +/// +/// +internal sealed class ContentPermissionManager +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDocumentRepository _documentRepository; + private readonly ILogger _logger; + + public ContentPermissionManager( + ICoreScopeProvider scopeProvider, + IDocumentRepository documentRepository, + ILoggerFactory loggerFactory) + { + // v1.1: Use ArgumentNullException.ThrowIfNull for consistency with codebase patterns + ArgumentNullException.ThrowIfNull(scopeProvider); + ArgumentNullException.ThrowIfNull(documentRepository); + ArgumentNullException.ThrowIfNull(loggerFactory); + + _scopeProvider = scopeProvider; + _documentRepository = documentRepository; + _logger = loggerFactory.CreateLogger(); + } + + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// The permission set to assign. + public void SetPermissions(EntityPermissionSet permissionSet) + { + // v1.1: Add input validation + ArgumentNullException.ThrowIfNull(permissionSet); + + // v1.1: Add logging for security-relevant operations + _logger.LogDebug("Replacing all permissions for entity {EntityId}", permissionSet.EntityId); + + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + scope.WriteLock(Constants.Locks.ContentTree); + _documentRepository.ReplaceContentPermissions(permissionSet); + scope.Complete(); + } + + /// + /// Assigns a single permission to the current content item for the specified group ids. + /// + /// The content entity. + /// The permission character (e.g., "F" for Browse, "U" for Update). + /// The user group IDs to assign the permission to. + public void SetPermission(IContent entity, string permission, IEnumerable groupIds) + { + // v1.1: Add input validation + ArgumentNullException.ThrowIfNull(entity); + ArgumentException.ThrowIfNullOrWhiteSpace(permission); + ArgumentNullException.ThrowIfNull(groupIds); + + // v1.2: Add warning for non-standard permission codes (Umbraco uses single characters) + if (permission.Length != 1) + { + _logger.LogWarning( + "Permission code {Permission} has length {Length}; expected single character for entity {EntityId}", + permission, permission.Length, entity.Id); + } + + // v1.1: Add logging for security-relevant operations + _logger.LogDebug("Assigning permission {Permission} to groups for entity {EntityId}", + permission, entity.Id); + + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + scope.WriteLock(Constants.Locks.ContentTree); + _documentRepository.AssignEntityPermission(entity, permission, groupIds); + scope.Complete(); + } + + /// + /// Returns implicit/inherited permissions assigned to the content item for all user groups. + /// + /// The content item to get permissions for. + /// Collection of entity permissions (materialized, not deferred). + public EntityPermissionCollection GetPermissions(IContent content) + { + // v1.1: Add input validation + ArgumentNullException.ThrowIfNull(content); + + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.GetPermissionsForEntity(content.Id); + } +}