Added new feature : display the file/folder and link of a SharingLink object in the permissions reports.

This commit is contained in:
Dev
2026-05-12 15:20:51 +02:00
parent ecc7b329d4
commit 1312dcdb1e
26 changed files with 937 additions and 82 deletions
@@ -1,4 +1,5 @@
using System.Text;
using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
@@ -137,7 +138,10 @@ document.addEventListener('click', function(ev) {
string? principalType,
IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>? groupMembers,
int colSpan,
ref int grpMemIdx)
ref int grpMemIdx,
string? targetLabel = null,
string? sharingLinkType = null,
bool hideSystemGroupRaw = false)
{
var T = TranslationSource.Instance;
var logins = userLogins.Split(';', StringSplitOptions.RemoveEmptyEntries);
@@ -151,14 +155,40 @@ document.addEventListener('click', function(ev) {
var name = i < names.Length ? names[i].Trim() : login;
var isExt = login.Contains("#EXT#", StringComparison.OrdinalIgnoreCase);
bool isExpandable = principalType == "SharePointGroup"
// When the principal is a resolved system group and the user wants the raw
// name hidden, replace the pill's visible text with the link-type badge
// (sharing links) and/or the target label. Falls back to the raw name when
// resolution failed (no targetLabel).
var classification = principalType == "SharePointGroup"
? PermissionEntryHelper.Classify(name)
: new SystemGroupClassification(SystemGroupKind.None, null, null, null, null, null);
bool isResolvedSystemGroup = hideSystemGroupRaw
&& classification.Kind != SystemGroupKind.None
&& classification.Kind != SystemGroupKind.LimitedAccessBare
&& !string.IsNullOrEmpty(targetLabel);
bool hasResolvedMembers = principalType == "SharePointGroup"
&& groupMembers != null
&& groupMembers.TryGetValue(name, out _);
if (isExpandable && groupMembers != null && groupMembers.TryGetValue(name, out var resolved))
if (hasResolvedMembers && groupMembers!.TryGetValue(name, out var resolved))
{
var grpId = $"grpmem{grpMemIdx}";
pills.Append($"<span class=\"user-pill group-expandable\" data-group-target=\"{HtmlEncode(grpId)}\" data-email=\"{HtmlEncode(login)}\">{HtmlEncode(name)} &#9660;</span>");
pills.Append("<span class=\"user-pill group-expandable\"");
if (isResolvedSystemGroup)
pills.Append(" data-system-group=\"1\"");
pills.Append($" data-group-target=\"{HtmlEncode(grpId)}\" data-email=\"{HtmlEncode(login)}\">");
if (isResolvedSystemGroup)
{
if (!string.IsNullOrEmpty(sharingLinkType))
pills.Append(BuildSharingLinkBadge(sharingLinkType!));
pills.Append(HtmlEncode(targetLabel!));
}
else
{
pills.Append(HtmlEncode(name));
}
pills.Append(" &#9660;</span>");
string memberContent;
if (resolved.Count > 0)
@@ -173,6 +203,14 @@ document.addEventListener('click', function(ev) {
subRows.AppendLine($"<tr data-group=\"{HtmlEncode(grpId)}\" style=\"display:none\"><td colspan=\"{colSpan}\" style=\"padding-left:2em;font-size:.8rem;color:#555\">{memberContent}</td></tr>");
grpMemIdx++;
}
else if (isResolvedSystemGroup)
{
pills.Append("<span class=\"user-pill\" data-system-group=\"1\">");
if (!string.IsNullOrEmpty(sharingLinkType))
pills.Append(BuildSharingLinkBadge(sharingLinkType!));
pills.Append(HtmlEncode(targetLabel!));
pills.Append("</span>");
}
else
{
var cls = isExt ? "user-pill external-user" : "user-pill";
@@ -183,6 +221,80 @@ document.addEventListener('click', function(ev) {
return (pills.ToString(), subRows.ToString());
}
/// <summary>
/// Renders the Granted Through cell. When the entry carries a resolved system-group
/// target (Limited Access For Web/List or SharingLinks), a clickable link to the
/// targeted resource is appended on a second line. For sharing links the link type
/// (OrganizationEdit / AnonymousView / …) is surfaced alongside the target.
///
/// When <paramref name="hideSystemGroupRaw"/> is true and a target was resolved, the
/// raw "SharePoint Group: SharingLinks.{guid}…" / "Limited Access System Group For
/// Web|List {guid}" prefix is suppressed and only the link-type badge + clickable
/// target are shown — keeps the report readable without losing information.
/// </summary>
internal static string BuildGrantedThroughCell(
string grantedThrough,
string? targetUrl,
string? targetLabel,
string? sharingLinkType,
bool hideSystemGroupRaw = false)
{
var hasTarget = !string.IsNullOrEmpty(targetUrl) && !string.IsNullOrEmpty(targetLabel);
var hasLinkType = !string.IsNullOrEmpty(sharingLinkType);
var suppressRaw = hideSystemGroupRaw && hasTarget;
var sb = new StringBuilder();
if (!suppressRaw)
sb.Append(HtmlEncode(grantedThrough));
if (!hasTarget && !hasLinkType)
return sb.ToString();
if (suppressRaw)
{
// Inline layout — no leading raw text to wrap under.
if (hasLinkType)
sb.Append(BuildSharingLinkBadge(sharingLinkType!));
if (hasTarget)
{
sb.Append("<a href=\"");
sb.Append(HtmlEncode(targetUrl!));
sb.Append("\" target=\"_blank\">");
sb.Append(HtmlEncode(targetLabel!));
sb.Append("</a>");
}
return sb.ToString();
}
sb.Append("<div style=\"margin-top:4px;font-size:.75rem;color:#555\">");
if (hasLinkType)
sb.Append(BuildSharingLinkBadge(sharingLinkType!));
if (hasTarget)
{
sb.Append("&rarr; <a href=\"");
sb.Append(HtmlEncode(targetUrl!));
sb.Append("\" target=\"_blank\">");
sb.Append(HtmlEncode(targetLabel!));
sb.Append("</a>");
}
sb.Append("</div>");
return sb.ToString();
}
/// <summary>
/// Builds the colored badge for a SharePoint sharing-link type. Translates the
/// raw <c>linkType</c> code (e.g. <c>OrganizationEdit</c>) into a human label
/// (e.g. <c>Org link · Edit</c>) and tints by risk tier; raw code surfaces as a
/// <c>title</c> tooltip so operators can still trace it back to the source.
/// </summary>
internal static string BuildSharingLinkBadge(string rawLinkType)
{
var (label, risk) = SharingLinkLabels.Describe(rawLinkType);
var (bg, fg) = SharingLinkLabels.Colors(risk);
return $"<span class=\"badge\" style=\"background:{bg};color:{fg};margin-right:6px\" " +
$"title=\"{HtmlEncode(rawLinkType)}\">{HtmlEncode(label)}</span>";
}
/// <summary>Returns the CSS class for the object-type badge.</summary>
internal static string ObjectTypeCss(string t) => t switch
{