using System.Text.RegularExpressions; namespace SharepointToolbox.Core.Helpers; /// /// Classifies a SharePoint group name into one of the known system-group shapes. /// public enum SystemGroupKind { /// Not a system group — a normal SharePoint group, user, or external user. None, /// Bare "Limited Access System Group" pseudo-principal (no embedded target). LimitedAccessBare, /// "Limited Access System Group For Web {webId}". LimitedAccessWeb, /// "Limited Access System Group For List {listId}". LimitedAccessList, /// "SharingLinks.{itemUniqueId}.{linkType}.{shareId}". SharingLink } /// /// Result of classifying a SharePoint group name. /// Carries the embedded GUIDs / link type so a resolver can look up the target. /// public readonly record struct SystemGroupClassification( SystemGroupKind Kind, Guid? WebId, Guid? ListId, Guid? ItemUniqueId, string? LinkType, Guid? ShareId); /// /// Pure static helpers for classifying SharePoint permission entries. /// public static class PermissionEntryHelper { // "Limited Access System Group For Web {guid}" private static readonly Regex LimitedAccessWebRegex = new( @"^Limited Access System Group For Web\s+(?[0-9a-fA-F-]{36})\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); // "Limited Access System Group For List {guid}" private static readonly Regex LimitedAccessListRegex = new( @"^Limited Access System Group For List\s+(?[0-9a-fA-F-]{36})\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); // "SharingLinks.{itemUniqueId}.{linkType}.{shareId}" private static readonly Regex SharingLinkRegex = new( @"^SharingLinks\.(?[0-9a-fA-F-]{36})\.(?[^.]+)\.(?[0-9a-fA-F-]{36})\s*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// /// Returns true when the login name is a B2B guest (contains #EXT#). /// public static bool IsExternalUser(string loginName) => loginName.Contains("#EXT#", StringComparison.OrdinalIgnoreCase); /// /// Removes "Limited Access" from the supplied permission levels. /// Returns the remaining levels; returns an empty list when all are removed. /// public static IReadOnlyList FilterPermissionLevels(IEnumerable levels) => levels .Where(l => !string.Equals(l, "Limited Access", StringComparison.OrdinalIgnoreCase)) .ToList(); /// /// Returns true when the supplied name/login is the bare "Limited Access System Group" /// pseudo-principal — the noise entry with no embedded GUID. The /// per-Web/List and SharingLinks variants are NOT filtered: they are enriched. /// public static bool IsBareLimitedAccessSystemGroup(string name) => name.Equals("Limited Access System Group", StringComparison.OrdinalIgnoreCase); /// /// Classifies a SharePoint group title (the value used in Granted Through). When the /// title matches a known system pattern, the embedded GUIDs / link type are parsed /// out so a resolver can look up the actual targeted Web/List/Item. /// public static SystemGroupClassification Classify(string groupTitle) { if (string.IsNullOrWhiteSpace(groupTitle)) return new SystemGroupClassification(SystemGroupKind.None, null, null, null, null, null); var trimmed = groupTitle.Trim(); if (IsBareLimitedAccessSystemGroup(trimmed)) return new SystemGroupClassification(SystemGroupKind.LimitedAccessBare, null, null, null, null, null); var mWeb = LimitedAccessWebRegex.Match(trimmed); if (mWeb.Success && Guid.TryParse(mWeb.Groups["id"].Value, out var webId)) return new SystemGroupClassification(SystemGroupKind.LimitedAccessWeb, webId, null, null, null, null); var mList = LimitedAccessListRegex.Match(trimmed); if (mList.Success && Guid.TryParse(mList.Groups["id"].Value, out var listId)) return new SystemGroupClassification(SystemGroupKind.LimitedAccessList, null, listId, null, null, null); var mShare = SharingLinkRegex.Match(trimmed); if (mShare.Success && Guid.TryParse(mShare.Groups["item"].Value, out var itemId) && Guid.TryParse(mShare.Groups["share"].Value, out var shareId)) { return new SystemGroupClassification( SystemGroupKind.SharingLink, null, null, itemId, mShare.Groups["type"].Value, shareId); } return new SystemGroupClassification(SystemGroupKind.None, null, null, null, null, null); } }