SharePoint Permission Roles Quick Overview

Worth flagging directly before anything else: neither of the two role-assignment examples that usually accompany this topic will actually run against a fresh list or library — both skip breaking permission inheritance first, which SharePoint requires before it accepts a role assignment at all. This post covers the built-in roles, the real numeric IDs behind them, the missing inheritance step, and a current fix for a legacy PnPjs import pattern still showing up in examples of this exact scenario.

In this post: The built-in roles and their real IDs · The step that’s usually missing: breaking inheritance · Assigning a role via REST · The current PnPjs pattern · Reverting to inherited permissions · Creating a custom role · Best practices · Related reading


The built-in roles and their real IDs

REST and CSOM both take a role assignment as a numeric ID, not a name — worth having the real mapping rather than guessing or copying a number from an unrelated example:

RoleRole Definition ID
Limited Access1073741825
Read1073741826
Contribute1073741827
Design1073741828
Full Control1073741829
Edit1073741830

These IDs are stable across the standard built-in roles on a normal team site, and stay the same regardless of the site’s display language. Worth a real caveat before hardcoding one, though: a site with custom role definitions — or one built from a heavily customized template — can have different IDs behind roles with the same names. Confirming against `_api/web/roledefinitions` directly is worth the extra call before relying on a hardcoded number in anything that runs unattended.


A role assignment call fails against any list, library, or item that’s still inheriting permissions from its parent — breaking inheritance has to happen first, and it’s the step most examples of this pattern skip entirely.

The step that’s usually missing: breaking inheritance

Worth knowing directly: a role assignment call against a securable object that’s still inheriting permissions from its parent doesn’t fail with a helpful message — it typically comes back as a generic “value does not fall within the expected range” error, which points nowhere near the actual cause. The object needs unique permissions first:

fetch("https://yourtenant.sharepoint.com/sites/yoursite/_api/web/lists/getbytitle('YourList')/breakroleinheritance(copyRoleAssignments=false,clearSubscopes=true)", {
    method: "POST",
    headers: {
        "Accept": "application/json;odata=nometadata",
        "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value
    }
});

`copyRoleAssignments=false` starts the object with no permissions at all rather than a copy of the parent’s — worth setting `true` instead if the intent is to keep everything currently inherited and only add to it, since `false` otherwise removes access for everyone not explicitly re-granted immediately after.


Assigning a role via REST

Once the object has unique permissions, the role assignment call itself works as expected — `principalid` is the user or group’s ID, `roledefid` is one of the values from the table above:

fetch("https://yourtenant.sharepoint.com/sites/yoursite/_api/web/lists/getbytitle('YourList')/roleassignments/addroleassignment(principalid=5,roledefid=1073741827)", {
    method: "POST",
    headers: {
        "Accept": "application/json;odata=nometadata",
        "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value
    }
})
.then(response => {
    if (!response.ok) {
        throw new Error(`Assignment failed: ${response.status} ${response.statusText}`);
    }
    console.log("Permission granted");
})
.catch(error => console.error(error));

Worth checking `response.ok` explicitly, as shown above, rather than assuming success from the call simply not throwing — a role assignment against a still-inheriting object returns a normal HTTP error response, not a rejected promise, so a script that only wraps the call in `.catch()` without checking status can silently continue as if the assignment worked.


The current PnPjs pattern

`import { sp } from “@pnp/sp/presets/all”` is the legacy PnPjs v2 singleton import — current PnPjs initializes an `spfi()` instance instead, including the same break-inheritance-then-assign order:

import { spfi } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/security";

const sp = spfi().using(/* your auth setup */);

async function grantPermission() {
    const list = sp.web.lists.getByTitle("YourList");
    await list.breakRoleInheritance(false, true);
    await list.roleAssignments.add(5, 1073741827);
    console.log("Permission granted successfully");
}

Retrieving the current role assignments needs no special handling beyond the standard `spfi()` init:

async function getPermissions() {
    const roles = await sp.web.lists.getByTitle("YourList").roleAssignments();
    console.log(roles);
}

Reverting to inherited permissions

Worth knowing this exists, since it’s the genuinely common cleanup step neither the original pattern nor most examples of it mention: `resetroleinheritance` removes the unique permissions entirely and hands control back to the parent object:

await sp.web.lists.getByTitle("YourList").resetRoleInheritance();

Worth reaching for this directly when a unique-permission scope is no longer needed, rather than leaving it in place indefinitely — unnecessary unique permissions are one of the concrete, verified drivers behind the list-throttling and performance issues covered in the SharePoint list-limits content on this site, not just a tidiness concern.


Creating a custom role

Worth knowing when none of the six built-in roles fit — a common case is wanting Contribute’s add/edit rights without delete, for a submissions-style list where removing an entry should never be a normal user action. `Add-PnPRoleDefinition` clones an existing role and adds or removes specific permission flags rather than building one from scratch:

Add-PnPRoleDefinition -RoleName "Add Only" -Clone "Contribute" -Exclude DeleteListItems, EditListItems

The new role appears in `_api/web/roledefinitions` immediately with its own ID, ready to reference in the REST or PnPjs assignment calls above like any built-in role. `Add-PnPRoleDefinition` only adds — it never removes or overwrites an existing role definition, so cloning from a role that’s since been customized on the target site picks up whatever that role currently grants, not necessarily its out-of-the-box defaults.


Best practices
  1. Assign roles to SharePoint groups, not individual users directly — managing one group’s membership is simpler than tracking direct assignments scattered across many objects.
  2. Grant the minimum role that lets the job get done — Contribute rather than Edit or Full Control when deletion and structural changes genuinely aren’t needed.
  3. Review unique-permission scopes periodically and reset the ones no longer serving a real purpose, using `resetRoleInheritance` above.
  4. Confirm role definition IDs against `_api/web/roledefinitions` on any site using custom role definitions, rather than assuming the standard table above applies universally.


The role IDs and the API calls are the easy part once written down correctly. The step actually worth remembering is the one that’s easy to skip: break inheritance before assigning, and reset it later once a unique scope has served its purpose and stops being worth the ongoing overhead of tracking it separately.

App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

Leave a Comment

Your email address will not be published. Required fields are marked *