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);
}
}