Showing posts with label Sites.Selected. Show all posts
Showing posts with label Sites.Selected. Show all posts

Saturday, 24 January 2026

Configuring Permission in SharePoint with .NET 8 and Microsoft Graph (Azure-first)

If you need to automate permission in SharePoint reliably, use Microsoft Graph with .NET 8 and Azure-managed identities. The goal: grant site-scoped access (least privilege via Sites.Selected), verify effective roles, and perform read/write operations without client secrets.

The Problem

SharePoint permissions are often over-provisioned or managed manually. That leads to audit gaps, break-glass patterns, and production drift. You need a repeatable, least-privilege approach that grants only the required access to specific sites, automates verification, and avoids client secrets.

Prerequisites

Required tools and permissions:

  • .NET 8 SDK
  • Azure CLI v2.58+ (logged in as a tenant admin for one-time grants)
  • Microsoft Graph application permissions consent capability (tenant admin)
  • Azure subscription access to create a user-assigned managed identity (Contributor on resource group)

The Solution (Step-by-Step)

Step 1. Choose the authentication model

Use managed identity for workloads in Azure (Functions, Container Apps). This removes client secrets entirely. For CI/CD, use workload identity federation instead of secrets.

  • Runtime principal: user-assigned/system-assigned managed identity
  • Graph permission model: application permission Sites.Selected for the runtime principal
  • Grant site-scoped roles: read or write at the specific SharePoint site level

Why Sites.Selected: it blocks blanket access (e.g., Sites.Read.All) and forces explicit grants per site.

Step 2. Infrastructure as Code (Bicep): create a user-assigned managed identity

// main.bicep
targetScope = 'resourceGroup'

// User-assigned managed identity that will call Microsoft Graph
resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'sp-sites-selected-uami'
  location: resourceGroup().location
}

output uamiClientId string = uami.properties.clientId
output uamiPrincipalId string = uami.properties.principalId

After deployment, assign Microsoft Graph application permission Sites.Selected to the managed identity’s service principal. This is a one-time admin action.

# Assign Graph app role (Sites.Selected) to the managed identity service principal
# 1) Get Graph service principal (well-known)
GRAPH_SP_ID=$(az ad sp list --filter "appId eq '00000003-0000-0000-c000-000000000000'" --query "[0].id" -o tsv)

# 2) Get the Sites.Selected app role ID
SITES_SELECTED_ROLE_ID=$(az ad sp show --id $GRAPH_SP_ID --query "appRoles[?value=='Sites.Selected' && allowedMemberTypes[@]=='Application'].id" -o tsv)

# 3) Get your managed identity's service principal object id
UAMI_PRINCIPAL_ID=<uamiPrincipalId from bicep output>

# 4) Assign the app role to the managed identity
az ad sp add-approle-assignment \
  --id $UAMI_PRINCIPAL_ID \
  --principal-object-id $UAMI_PRINCIPAL_ID \
  --resource-id $GRAPH_SP_ID \
  --app-role-id $SITES_SELECTED_ROLE_ID

Admin consent is implicit when you add an app role assignment to Graph for an enterprise application (service principal). Validate in Entra ID under Enterprise applications > Your Managed Identity > Permissions.

Step 3. One-time site-scoped grant to the managed identity

Sites.Selected requires a per-site grant. Use an admin-only tool to grant read/write on a single site to the managed identity. Below is a .NET 8 minimal API you can run locally as a tenant admin via Azure CLI authentication to grant permissions. It uses DI, file-scoped namespaces, and the Microsoft Graph SDK.

using Azure.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions.Authentication;

namespace SharePointPermAdmin;

var builder = WebApplication.CreateBuilder(args);

// Admin credential for local, one-time operations only.
// Uses Azure CLI token of a tenant admin. No client secrets.
builder.Services.AddSingleton((sp) =>
{
    // Authenticate as the signed-in Azure CLI account
    var credential = new AzureCliCredential();

    // Adapter for Microsoft Graph SDK
    var authProvider = new TokenCredentialAuthenticationProvider(
        credential,
        // Microsoft Graph default scope for app-permission endpoints
        new[] { "https://graph.microsoft.com/.default" });

    return new GraphServiceClient(authProvider);
});

var app = builder.Build();

// Code Overview: Grants site-level permission (read or write) to a target application or managed identity
// Endpoint parameters:
//  - hostname: e.g., "contoso.sharepoint.com"
//  - sitePath: e.g., "/sites/Engineering"
//  - targetAppId: the clientId of the target app or managed identity (UAMI clientId)
//  - role: "read" or "write" (least privilege)
app.MapPost("/grant", async (GraphServiceClient graph,
    string hostname, string sitePath, string targetAppId, string role) =>
{
    // 1) Resolve the site by hostname and site path
    var site = await graph.Sites["{hostname}:{sitePath}"].GetAsync();
    if (site is null) return Results.NotFound("Site not found.");

    // 2) Prepare the permission grant
    var requestedRole = role.Equals("write", StringComparison.OrdinalIgnoreCase) ? "write" : "read";

    var permission = new Permission
    {
        // Roles supported for Sites.Selected are "read" and "write"
        Roles = new List<string> { requestedRole },
        GrantedToIdentities = new List<IdentitySet>
        {
            new IdentitySet
            {
                Application = new Identity
                {
                    // targetAppId can be the clientId of a user-assigned managed identity
                    // or an app registration clientId
                    Id = targetAppId,
                    DisplayName = "Sites.Selected App"
                }
            }
        }
    };

    // 3) Grant the permission at the site-level
    var created = await graph.Sites[site.Id].Permissions.PostAsync(permission);

    // 4) Return what was granted for audit
    return Results.Ok(new
    {
        SiteId = site.Id,
        GrantedRoles = created?.Roles,
        GrantedTo = created?.GrantedToIdentities?.Select(x => x.Application?.Id)
    });
});

app.Run();

Run this locally with an Azure CLI context that has tenant admin privileges. This API grants the site-scoped role to your managed identity (identified by its clientId). Keep this tool restricted and audit its use.

Step 4. Runtime workload: access the site using the managed identity

Your production service (Azure Functions or Container Apps) uses DefaultAzureCredential to get a token for Graph and perform read actions (if granted read) or write actions (if granted write).

using Azure.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;

namespace SharePointRuntime;

var builder = WebApplication.CreateBuilder(args);

// Graph client using managed identity in Azure (no secrets)
// In local dev, DefaultAzureCredential falls back to Azure CLI login.
builder.Services.AddSingleton((sp) =>
{
    var credential = new DefaultAzureCredential();
    var authProvider = new TokenCredentialAuthenticationProvider(
        credential,
        new[] { "https://graph.microsoft.com/.default" });

    return new GraphServiceClient(authProvider);
});

var app = builder.Build();

// Code Overview: Example read-only endpoint listing drive root items for a given site.
// Requires the managed identity to have Sites.Selected + "read" on that site.
app.MapGet("/sites/{hostname}/{*sitePath}/drive-items", async (GraphServiceClient graph, string hostname, string sitePath) =>
{
    // 1) Resolve site by hostname and site path
    var site = await graph.Sites[$"{hostname}:{sitePath}"].GetAsync();
    if (site is null) return Results.NotFound("Site not found.");

    // 2) Read drive root items (read permission is sufficient)
    var items = await graph.Sites[site.Id].Drive.Root.Children.GetAsync();

    return Results.Ok(items?.Value?.Select(i => new { i.Id, i.Name, i.Size, i.LastModifiedDateTime }));
});

app.Run();

Step 5. Optional: strict front-end validation for an internal admin form

If you build an admin UI to call the grant API, validate inputs with Zod and strict TypeScript types.

import { z } from 'zod';

// Discriminated union for role
const RoleSchema = z.union([z.literal('read'), z.literal('write')]);

// Strict payload schema
export const GrantPayloadSchema = z.object({
  hostname: z.string().min(1).regex(/^([a-zA-Z0-9-]+)\.sharepoint\.com$/),
  sitePath: z.string().min(1).startsWith('/'),
  targetAppId: z.string().uuid(),
  role: RoleSchema
});

export type GrantPayload = z.infer<typeof GrantPayloadSchema>;

// Example safe submit
export async function submitGrant(payload: GrantPayload) {
  const parsed = GrantPayloadSchema.parse(payload); // throws on invalid input
  const res = await fetch('/grant?'
    + new URLSearchParams({
      hostname: parsed.hostname,
      sitePath: parsed.sitePath,
      targetAppId: parsed.targetAppId,
      role: parsed.role
    }), { method: 'POST' });

  if (!res.ok) throw new Error('Grant failed');
  return res.json();
}

Best Practices & Security

  • Best Practice: Use Sites.Selected and grant per-site roles (read/write) instead of tenant-wide Sites.Read.All or Sites.FullControl.All.
  • Best Practice: Prefer managed identity (Azure) or workload identity federation (CI/CD) over client secrets.
  • Best Practice: Separate duties. Keep the grant tool under tenant admin control; the runtime app should never be able to self-elevate.
  • Best Practice: Log every permission grant with who, what, when, and siteId. Store in an immutable audit store.
  • Best Practice: Implement retries with exponential backoff for Graph calls and handle 429/5xx responses gracefully.
  • Best Practice: Validate all inputs server-side. Reject unknown hostnames and unexpected site paths.
  • Best Practice: Monitor with Microsoft 365 audit logs and alert on unexpected grants or role changes.

Minimum permissions required (explicit)

  • Runtime principal (managed identity): Microsoft Graph application permission Sites.Selected.
  • Admin/grant principal: Microsoft Graph application permission Sites.FullControl.All or delegated SharePoint admin role sufficient to create site-level grants via Graph.
  • SharePoint site roles used for Sites.Selected: read (equivalent to site Read), write (equivalent to Edit). Grant the lowest role that satisfies requirements.

Error handling notes

  • Catch GraphServiceException and map common statuses: 403 (insufficient role), 404 (site not found), 429 (throttle) with Retry-After.
  • Add circuit breakers/timeouts. Do not leak raw exception messages in responses.
  • Return correlation IDs in responses to aid troubleshooting.

Summary

  • Grant permission in SharePoint using Sites.Selected for least privilege and auditability.
  • Use managed identities or workload identity federation to remove secrets and simplify compliance.
  • Automate grants and access with .NET 8, Microsoft Graph SDK, and IaC for consistent, repeatable operations.

Friday, 23 January 2026

SharePoint Indexing: A Step-by-Step Guide for Faster Search and Precise Results

SharePoint indexing often fails silently, causing slow search and missing results. This guide shows exactly how to configure SharePoint indexing end-to-end—site settings, list-level indexing, search schema, and safe automation—so your users get fast, accurate results. Primary keyword: SharePoint indexing.

The Problem

Content updates are not discoverable, queries are slow, and filters return inconsistent items because lists aren’t indexed, sites aren’t reindexed, or managed properties are not mapped. You need a repeatable approach to fix indexing across sites and automate reindexing safely.

Prerequisites

  • Microsoft 365 tenant with SharePoint Online
  • PowerShell 7+ and PnP.PowerShell module (Install-Module PnP.PowerShell -Scope CurrentUser)
  • Role-based access (least privilege):
  • Site Owner or above for list/site indexing and reindex
  • Search Schema changes typically require SharePoint Administrator
  • Automation with app-only: Entra ID App with Sites.Selected + site-scoped permissions
  • Optional for automation: Azure Automation or Azure Functions with a System-Assigned Managed Identity

The Solution (Step-by-Step)

1) Confirm site-level indexing is allowed

Site Owners can check if content is visible to search crawlers.

  • Go to Site Settings → Search → Search and offline availability
  • Set Allow this site to appear in search results to Yes

Pro-Tip: If you changed this from No to Yes, trigger a site reindex to speed up propagation.

2) Index columns at the list/library level

Index columns used in filters, sorts, and query rules to avoid throttling and improve query performance.

  • Open your list/library → Settings → Indexed columns → Create a new index
  • Index only frequently queried columns (e.g., Status, Department, Created)

Pro-Tip: Avoid indexing lookup or multi-line rich text columns unless absolutely necessary; they can increase index size and crawl churn.

3) Reindex a list or library (targeted)

Use targeted reindex after column or schema changes on a specific list.

  • List Settings → Advanced settings → Reindex List

This flags the list for the next crawl so updated properties and new mappings are applied faster.

4) Reindex a site (broad fix)

Use this when you changed site-level search settings, content types, or many lists.

  • Site Settings → Search → Search and offline availability → Reindex site

Pro-Tip: Reindexing a site is heavier than a list reindex. Prefer list reindex when possible to reduce crawl load.

5) Map crawled properties to managed properties (search schema)

To make custom columns queryable/filterable/sortable, map crawled properties to managed properties with the right search attributes (Searchable, Queryable, Retrievable, Refinable, Sortable).

  • SharePoint Admin Center → More features → Search → Open
  • Manage Search Schema → Managed Properties
  • Create or edit a managed property (e.g., RefinableStringXX)
  • Map the relevant crawled property (e.g., ows_Status)

Pro-Tip: Use reserved RefinableStringXX or RefinableDateXX for facets and filters to avoid schema conflicts.

6) Automate reindex with PnP.PowerShell (interactive)

# Install PnP.PowerShell if needed
# Install-Module PnP.PowerShell -Scope CurrentUser

# 1) Interactive login for an admin or site owner
Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/ProjectA" -Interactive

# 2) Reindex a single list by title
# This sets the reindex flag so the next crawl refreshes properties
$web = Get-PnPWeb
$list = Get-PnPList -Identity "Documents"
Set-PnPList -Identity $list -NoCrawl:$false   # Ensure list is crawlable
Request-PnPReIndexList -Identity $list        # Mark list for reindex

# 3) Reindex the entire site (use sparingly)
Request-PnPReIndexWeb

# 4) Verify a column is indexed
# Returns indexed column definitions for the list
(Get-PnPField -List $list) | Where-Object { $_.Indexed -eq $true } | Select-Object InternalName, Title

Comments:

  • Request-PnPReIndexList and Request-PnPReIndexWeb mark content for recrawl.
  • Set-PnPList -NoCrawl:$false ensures the list is included in search.
  • Use least privilege: Site Owner is sufficient for list-level operations.

7) Secure automation with Managed Identity or app-only (no secrets)

For production jobs, avoid interactive auth and stored passwords. Use a Managed Identity (Azure Automation/Functions) or an Entra ID app with Sites.Selected and scoped permissions to specific sites.

# Option A: Managed Identity (runs inside Azure with System-Assigned MI)
# Prereqs:
# - Assign the Managed Identity Sites.Selected permissions and grant site-level rights
# - Use Grant-PnPAzureADAppSitePermission to scope access to the target site

# Connect using Managed Identity
Connect-PnPOnline -Url "https://contoso.sharepoint.com/sites/ProjectA" -ManagedIdentity

# Reindex specific list
Request-PnPReIndexList -Identity "Documents"
# Option B: Entra ID App with certificate (app-only, least privilege)
# Prereqs:
# - App registration with Microsoft Graph Sites.Selected (Application) permission
# - Admin consent granted
# - Grant site-level permission:
#   Grant-PnPAzureADAppSitePermission -AppId <CLIENT_ID> -DisplayName "Indexer" -Site "https://contoso.sharepoint.com/sites/ProjectA" -Permissions Read
# - Upload certificate and use its thumbprint

$tenant = "contoso.onmicrosoft.com"
$siteUrl = "https://contoso.sharepoint.com/sites/ProjectA"
$clientId = "00000000-0000-0000-0000-000000000000"
$thumb = "THUMBPRINT_HERE"

Connect-PnPOnline -Url $siteUrl -ClientId $clientId -Tenant $tenant -Thumbprint $thumb

# Safely trigger reindex
Request-PnPReIndexList -Identity "Documents"

Comments:

  • Sites.Selected lets you grant per-site permissions, enforcing least privilege.
  • Avoid legacy ACS app-only. Prefer Entra ID + Sites.Selected or Managed Identity.
  • No secrets in code; use certificates or Managed Identity.

8) Validate search consistency

  • Use Microsoft Search to query values you expect to find (e.g., Status:Active)
  • Validate refiners show the expected values (requires RefinableStringXX mapping)
  • Check list view performance with indexed columns applied as filters

Best Practices & Security

  • Principle of Least Privilege:
  • Day-to-day indexing: Site Owner
  • Search Schema: SharePoint Administrator (only when needed)
  • Automation: Managed Identity or app with Sites.Selected scoped to the target sites
  • Authentication:
  • Prefer Managed Identity in Azure Automation/Functions
  • Else use certificate-based app-only; avoid passwords and client secrets
  • Search Schema Hygiene:
  • Reuse RefinableStringXX/DateXX for facets and filters
  • Document managed property usage to prevent collisions across teams
  • Performance:
  • Index only columns that improve key queries
  • Prefer list reindex over site reindex to reduce crawl load

Pro-Tip: Create a small pilot list to test new mappings and reindex timings before applying to large libraries.

Troubleshooting

  • My column values don’t appear in search:
  • Confirm list is crawlable (NoCrawl = false)
  • Ensure a managed property is mapped and set to Queryable/Retrievable
  • Trigger Request-PnPReIndexList and wait for the next crawl cycle
  • Refiners don’t show my custom metadata:
  • Use a RefinableStringXX or RefinableDateXX managed property
  • Map the correct crawled property (often ows_ColumnInternalName)
  • App-only connection fails with 401/403:
  • Verify Sites.Selected consent and site-level grant (Grant-PnPAzureADAppSitePermission)
  • Confirm certificate thumbprint and validity
  • Reindex seems to do nothing:
  • Allow time for the next crawl; reindex sets a flag, it doesn’t force immediate recrawl
  • Check Service Health and Message Center for crawl incidents

Summary

  • Index the right columns and map to managed properties to make content queryable, refinable, and fast.
  • Use targeted list reindex first; reserve site reindex for broad changes.
  • Automate safely with Managed Identity or Sites.Selected app-only to enforce least privilege and avoid secrets.

References