Dealing with SharePoint site and list permissions inside SharePoint Framework (SPFx)

Dealing with SharePoint site and list permissions inside SharePoint Framework (SPFx)

Permissions are an essential and sensitive topic (not only!) in SharePoint environments. Transparency is a key here, also required by several regulatory acts. How to improve such transparency this post will show based on an existing sample: A SharePoint Framework application extension acting as a top placeholder. Nevertheless, the shown ways of implementation are applicable to any kind of SharePoint (Framework) implementation, so for webparts for instance as well.

Series on sample

Content

UI context: CommandBar in application extensions

As part of my previous blog post’s project there is still “space” on the right side in the top CommandBar where current site’s permissions and more can be flexibly displayed. This can look like this:

Permissions displayed in right side panel invoked from top CommandBar
Permissions displayed in right side panel invoked from top CommandBar

As seen in the previous post the CommandBar component is pretty simple. In this case the farItems now play the essential role:

<CommandBar          
        className={styles.top}    
        items={ commandItems }
        farItems={ farItems }
      />

As seen in the screenshot the farItems consist of two icon buttons. The one concerning external sharing is treated in a different post. Here the Permissions icon button is treated which consists of a sub menu item named “List permissions”.

const permissionItem: ICommandBarItemProps = {
    key: 'permission',
    name: 'Permissions',
    iconProps: {
      iconName: 'Repair'
    },
    iconOnly: true,
    subMenuProps: {
      items: []
    }    
  };
  const permissionPanelItem: IContextualMenuItem = {    
    key: 'ListPermissions',
    name: 'List Permissions',
  };
export const evaluateFarItems = (externalSharingEnabled: boolean, showPermissions: () => void): ICommandBarItemProps[] => {
    const farItems: ICommandBarItemProps[] = [];
    if (externalSharingEnabled !== null) {
      ...
      farItems.push(externalSharingItem);      
    }
    permissionItem.subMenuProps!.items = [];
    permissionPanelItem.onClick = () => { showPermissions(); };
    permissionItem.subMenuProps?.items.push(permissionPanelItem);
    farItems.push(permissionItem);
    return farItems;
  }

When “List permissions” is clicked the function showPermissions opens a panel which displays the various permission lists including manipulation buttons. Inside a Panel a Pivot (tabbed navigation) displays components for SitePermissions and ListPermissions.

<Panel
          headerText="Permissions"
          isOpen={permissionPanelOpen}
          onDismiss={togglePermissions}
          closeButtonAriaLabel="Close">
        <Pivot aria-label="Basic Pivot Example">
          <PivotItem headerText="Site">
            <SitePermissions serviceScope={props.serviceScope} currentSiteUrl={props.currentSiteUrl} isSiteOwner={props.isSiteOwner} />
          </PivotItem>
          <PivotItem headerText="Lists">
            <ListPermissions serviceScope={props.serviceScope} currentSiteUrl={props.currentSiteUrl} isSiteOwner={props.isSiteOwner} />
          </PivotItem>
          <PivotItem headerText="Sharing Links">
            <SharingLinks serviceScope={props.serviceScope} currentSiteUrl={props.currentSiteUrl} siteId={props.siteId} isSiteOwner={props.isSiteOwner} />
          </PivotItem>      
        </Pivot>        
      </Panel>

But before covering the individual kinds of permission operations, it’s essential to note that proper permissions to execute those operations itself are needed. Therefore, each component above gets an isSiteOwner attribute. This is evaluated upfront the following way:

const isSiteOwner = this.context.pageContext.web.permissions.hasAllPermissions(SPPermission.manageWeb, SPPermission.managePermissions);

As this is luckily something which does not need any additional API call but can be derived from current context, it’s done in the extension’s parent component. It simply checks for two essential permissions (manageWeb and managePermissions) which in fact are available for any Owner but potentially others, too. So isSiteOwner is not 100% accurate but the permissions are to execute following manipulation operations (otherwise Button is not shown).

Site permissions

Getting site permissions in fact means getting roleassignments. Those are a combination of RoleDefinition (permission, such as “Contribute”) and Principals they are assigned to. Everything is retrievable with the following Rest Api service call

public async getSitePermissions(currentSiteUrl: string): Promise<IPermissionItem[]> {
    const requestUrl = currentSiteUrl + '/_api/web/roleassignments?$expand=Member/users,RoleDefinitionBindings';
    ...
    return this._spHttpClient.get(requestUrl, SPHttpClient.configurations.v1)
      .then((response: SPHttpClientResponse) => {
        return response.json();
      })
      .then((jsonResponse: any) => {
        const permissionItems: IPermissionItem[] = [];        
        jsonResponse.value.forEach((l: any) => {
          ...
          permissionItems.push(....);
        });        
        return permissionItems;
      });
}

Some details are omitted and explained in the next section, but the key points here is the endpoint which includes a detailed $expand that enables to get more detailed information to display. Most of the attribues can be pushed to the custom permissionItem this way:

permissionItems.push({ key: l.PrincipalId, name: l.Member.Title, permission: l.RoleDefinitionBindings[0].Name, isDefault: isDefault, description: l.RoleDefinitionBindings[0].Description, url: ... });

So some information about user/group is retrieved from the Member expand while the information about the role/permission comes from the RoleDefinitionBindings expand.

Additionally some of the roleassignments are standard groups (Owners, Members, Visitors). Assuming they shouldn’t be removed, it’s easy to detect them, mark them as “default” and in that case prevent displaying the “Remove” button. First the standard groups can be detected from the /web endpoint.

private async getassociatedStdGroups(currentSiteUrl: string): Promise<string[]> {
    const requestUrl = currentSiteUrl + '/_api/web?$expand=associatedOwnerGroup,associatedMemberGroup,associatedVisitorGroup';
    const response = await this._spHttpClient.get(requestUrl, SPHttpClient.configurations.v1);
    const principlaIds: string[] = [];
    if (response.ok) {
      const jsonResponse = await response.json();
      principlaIds.push(jsonResponse.AssociatedOwnerGroup.Id);
      principlaIds.push(jsonResponse.AssociatedMemberGroup.Id);
      principlaIds.push(jsonResponse.AssociatedVisitorGroup.Id);
    }
    return principlaIds;
}

This returns an array of group ids. Those can be used to detect the standard groups inside the result of roleassignments to mark them.

.then((jsonResponse: any) => {
        const permissionItems: IPermissionItem[] = [];        
        jsonResponse.value.forEach((l: any) => {
          let isDefault: boolean = false;
          defaultGroups.forEach((g) => {
            if (g === l.PrincipalId) {
              isDefault = true;
            }
          })
          permissionItems.push({ key: l.PrincipalId, name: l.Member.Title, permission: l.RoleDefinitionBindings[0].Name, isDefault: isDefault, description: l.RoleDefinitionBindings[0].Description, url: this.currentSiteUrl + `/_layouts/15/people.aspx?MembershipGroupId=${l.PrincipalId}` });
        });        
        return permissionItems;

While in the UI the remove button is neither displayed when the user has not owner permissions (see above) nor when it is a default group:

{!item.isDefault && props.isSiteOwner &&
            <span>
              <IconButton iconProps={ cancelBtn } title='Remove permission' onClick={ () => confirmDeletePermission(item.key) } />
            </span>}

If the “Remove permission” button is clicked, the following Rest Api service call is executed. Pay attention how a http DELETE operation is executed with spHttpClient:

public async removeSitePermission(currentSiteUrl: string, principalId: string): Promise<boolean> {
    const requestUrl = currentSiteUrl + `/_api/web/roleassignments/getbyprincipalid(${principalId})`;
    const response = await this._spHttpClient.post(requestUrl, 
      SPHttpClient.configurations.v1,
      {  
        headers: {  
          'Accept': 'application/json;odata=nometadata',  
          'Content-type': 'application/json;odata=verbose',  
          'odata-version': '',  
          'IF-MATCH': '*',  
          'X-HTTP-Method': 'DELETE'  
        }  
      });
    if (response.ok) {
      return true;
    }
    else {
      return false;
    }    
}

This time the endpoint is no magic, simply identifying the roleassignment by id. But it’s essential to note that a POST request is executed and only in the body with 'X-HTTP-Method': 'DELETE' it’s made clear that a delete is requested. For simplicity reasons an ETag is omitted by using 'IF-MATCH': '*' instead. Think about that in production scenarios.

List permissions

Lists and libraries can have the same roleassignments than sites or they can simply inherit permissions from site level. The sample does not repeat the same functionality on roleassignments as seen above although a full solution would clearly do. The sample focusses on another effect: Break or return to permission inheritance. Therefore, at first all lists and libraries of the current site need to be evaluated together with the information if permissions are inherited or unique.

public async evalSiteListsPermInheritance(currentSiteUrl: string): Promise<IPermissionItem[]> {
    const requestUrl = currentSiteUrl + '/_api/web/lists?$select=HasUniqueRoleAssignments,Title,Id,BaseTemplate,RootFolder/ServerRelativeUrl&$expand=RootFolder&$filter=BaseTemplate eq 101 or BaseTemplate eq 100';
    return this._spHttpClient.get(requestUrl, SPHttpClient.configurations.v1)
      .then((response: SPHttpClientResponse) => {
        return response.json();
      })
      .then((jsonResponse: any) => {
        const permissionItems: IPermissionItem[] = [];
        jsonResponse.value.forEach((l: any) => {
          permissionItems.push({ key: l.Id, name: l.Title, permission: l.HasUniqueRoleAssignments ? 'Unique':'Inherits', isDefault: false, description: '', url: l.RootFolder.ServerRelativeUrl });
        });
        return permissionItems;
      });
  }

The Rest API endpoint retrieves all lists (here limited to basic list and library templates) together with the HasUniqueRoleAssignments attribute. The result is returned as custom array where the permission attribute is derived from HasUniqueRoleAssignments. Either it’s “Unique” or “Inherit”. Based on this in the UI either a button to “Re-inherit” (if “Unique”) or “Break” (if “Inherit”) is available. Both buttons execute further rest operations after a UI confirmation Dialog.

Confirm Dialog in SPFx with FluentUI
Confirm Dialog in SPFx with FluentUI
const confirmBreakPermissions = React.useCallback((listID: string) => {
    dialogContentProps.subText = 'Do you really want to break inherited list permissions?'
    setDialog(<Dialog
              hidden={false}
              onDismiss={hideDialog}
              dialogContentProps={dialogContentProps}
            >
              <DialogFooter>
                <PrimaryButton onClick={() => breakPermissionInheritance(listID)} text="OK" />
                <DefaultButton onClick={hideDialog} text="Cancel" />
              </DialogFooter>
            </Dialog>);
  }, [items]);

This React call back creates a Fluent UI Dialog component to be shown before executing any sensitive operation. The component is rendered from state and if the user clicks “OK” with another function finally the following service method(s) are called:

public async breakInheritListPermissions(currentSiteUrl: string, listID: string): Promise<boolean> {
    const requestUrl = currentSiteUrl + `/_api/web/lists(guid'${listID}')/breakroleinheritance(copyRoleAssignments=true, clearSubscopes=true)`;
    return this._spHttpClient.post(requestUrl, SPHttpClient.configurations.v1, {})
    .then((response: SPHttpClientResponse) => {
      if (response.ok) {
        return true;
      }
      else {
        return false;
      }
    })
    .catch((err: any) => {

Here the endpoint /breakroleinheritance us called on a given list while enforcing to copy existing permissions from current site (in the UI you are normally asked if to do so). The opposite is the ResetRoleInheritance() endpoint:

public async reInheritListPermissions(currentSiteUrl: string, listID: string): Promise<boolean> {
    const requestUrl = currentSiteUrl + `/_api/web/lists(guid'${listID}')/ResetRoleInheritance()`;
    return this._spHttpClient.post(requestUrl, SPHttpClient.configurations.v1, {})
    .then((response: SPHttpClientResponse) => {
      if (response.ok) {
        return true;
      }
      else {
        return false;
      }
    })
    .catch((err: any) => {

This were some operations on site and list permissions and how to execute them within SharePoint Framework (SPFx). In the next part of this little series based on a complete sample, I’ll show how to deal with another security topic: External Sharing and External Sharing links. Stay tuned, but meanwhile the whole sample is available in my GitHub repository.

Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 Development. He loves SharePoint Framework but also has a passion for Microsoft Graph and Teams Development.
He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich.
In 2021 he received his first Microsoft MVP award in M365 Development for his continuous community contributions.
Although if partially inspired by his daily work opinions are always personal.