Sunday, 6 February 2022

Run With Elevated Privileges(App-Only) in SPFx webpart using Power Automate

  • SPFx Solution didn't support elevated permission using App-Only authentication, but with MS Power Automate, it's possible to use app-only elevated permission.
  • App-Only Token and elevated permission is got in MS Flow and in SPFX solution only call the flow using HttpClient object.
  • For example, in public facing sites Contact Us form, where a user will not have direct access to view or edit other items but he/she should be able to make an entry(create item) into the list. So even if the user don't have access to the list, you would like to collect information from them.Below is the Architecture diagram from Microsoft:

  1. Here in the diagram, user does not have access to the list but to input the data an interface has been provided which is build using SPFx webpart.
  2. User makes a POST request from SPFx webpart which triggers the MS Flow.
  3. Flow(HTTP) makes a POST request to ACS to retrieve the Access token.
  4. Once the ACS token received, SharePoint REST API POST call will perform using the token and that will create the item in SharePoint list. 

Make REST API Call from MS Flow:

Create and register an App in SPO with full control permissions

    1.       Login to SPO https://domainname.sharepoint.com/sites/LiakathDev/_layouts/15/appregnew.aspx  to generate Client ID and Secret ID.

    2.       Go to https://domainname.sharepoint.com/sites/LiakathDev/_layouts/15/appinv.aspx and register the app by entering the generated Client ID via lookup. Add the below XML to provide the full permissions to app and to elevate the permission at list level.

<AppPermissionRequests AllowAppOnlyPolicy="true">

<AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web/list   " Right="FullControl" />

</AppPermissionRequests>

    3.      Select the list from the dropdown list where you need elevated permission and click on ‘Trust It’.

    4.      Verify the registered app and Note the Tenant ID from the below link:

https://domainname.sharepoint.com/sites/LiakathDev/_layouts/15/appprincipals.aspx

Create Power Automate Flow

    1.       Create flow from blank template and select the trigger as “When a Http request is received” and add the below JSON in the ‘Request Body’:

{
    "type": "object",
    "properties": {
        "title": {
            "type": "string"
        }
    }
}

    2.       Add action ‘HTTP’ post to fetch the access token from ACS using REST API call.

Body: replace Client ID, Secret ID and Tenant ID.

Grant_type=client_credentials&client_id=<<ClientID>>@<<TenantID>>&client_secret=<<ClientSecretID>>&resource=00000003-0000-0ff1-ce00-000000000000/<<TenantName>>.sharepoint.com@<<TenantID>>

    3.       Add action “Parse JSON”  and include the below JSON in to read the access token.

{

    "type""object",

    "properties": {

        "token_type": {

            "type""string"

        },

        "access_token": {

            "type""string"

        }

    }

}

         

     4.  Make Rest API call using Power Automate Flow

a.       Add action “HTTP” post to make a REST API call.

      

    5.   Add action "Response" to send a response back based on the out received from the previous step. Include Status code in the body parameter as displayed in the screenshot below.

    6.       Save the Flow and Note the URL of the HTTP Request: HTTP Post URL

https://prod-19.centralindia.logic.azure.com:443/workflows/02f9d3f348ec4f8ebee1f70be0b608f8/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=XpvU6D81LOSEtAH3Nvl_qKeYfEWNnyvqU9v6JnsPzbs 

Call Power Automate Flow From SPFx webpart:

Create SPFx webpart

    1.       Create SPFx webpart and the template is: No framework.

    2.       Once the project is created open the VS Code using this command : Code .

    3.       Update the Webpart.ts file with the below code        

import { Version } from '@microsoft/sp-core-library';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
//import { escape } from '@microsoft/sp-lodash-subset';

import styles from './SpFxWebpartWebPart.module.scss';
import * as strings from 'SpFxWebpartWebPartStrings';
import { HttpClient, IHttpClientOptions, HttpClientResponse } from '@microsoft/sp-http';

export interface ISpFxWebpartWebPartProps {
  //description: string;
  flowURL: string;
  FlowURLLabel: string;
}
//import { SPComponentLoader } from '@microsoft/sp-loader';
//interface IResult {
  //status: string;
//}

export default class SpFxWebpartWebPart extends BaseClientSideWebPart<ISpFxWebpartWebPartProps> {

  //private _isDarkTheme: boolean = false;
  //private _environmentMessage: string = '';

  public render(): void {
    this.domElement.innerHTML = `
    <div class="${ styles.spFxWebpart }">
        <div class="${ styles.container }">
          <div class="${ styles.row }">
            <div class="${ styles.column }">
                 <span class="${styles.title} ms-Grid-col ms-u-sm12 block">
                   Item creation using SPFx & Flow
                </span>
                <div class="ms-Grid-col ms-u-sm12 block"><br/></div>
                <div class="ms-TextField ms-Grid-col ms-u-sm12 block">
                  <label class="ms-Label ms-Grid-col ms-u-sm4 block">Title</label>
                  <input id="spfxflowIm" class="ms-TextField-field ms-Grid-col ms-u-sm8 block" value="" type="text" placeholder="">
                </div>
                <div class="ms-TextField ms-Grid-col ms-u-sm12 block"><br/></div>
                <div class="ms-TextField ms-Grid-col ms-u-sm6 block"></div>
                <div class="ms-TextField ms-Grid-col ms-u-sm6 block">
                <button class="${styles.button} create-Button">
                <span class="${styles.label}">Create Item</span>
                </button></div>
                <div id="status"></div>
              </div>
            </div>
          </div>
        </div>
      </div>`;
      this.setButtonsEventHandlers();
  }

  private setButtonsEventHandlers(): void {
    const webPart: SpFxWebpartWebPart = this;
    this.domElement.querySelector('button.create-Button')!.addEventListener('click', () => { webPart.createItem(); });
  }

  private createItem(): void {
    let result: HTMLElement = document.getElementById("status")!;
    let responseText: string = "";
    //result.style.color = "white";
    result.innerText = "Updating item...";
    var itemTitle : string = (<HTMLInputElement>document.getElementById("spfxflowIm")).value;              
    const postURL = this.properties.flowURL;   
    const body: string = JSON.stringify({
      'title': itemTitle,
    });   
    const requestHeaders: Headers = new Headers();       
    requestHeaders.append('Content-type', 'application/json');
    requestHeaders.append('Cache-Control', 'no-cache');   
    const httpClientOptions: IHttpClientOptions = {
      body: body,
      headers: requestHeaders
    };     
    this.context.httpClient.post(
      postURL,
      HttpClient.configurations.v1,
      httpClientOptions).then((response: HttpClientResponse) => {
        response.json().then((responseJSON: JSON) => {
          responseText = JSON.stringify(responseJSON);            
        result.innerText = (responseText=="201")?"Item updated successfully":"Error received while updating item";
      });
    }).catch ((response: any) => {
      let errMsg: string = `Error = ${response.message}`;
      result.style.color = "red";
      console.log(errMsg);
      result.innerText = errMsg;
    });
}



  protected onInit(): Promise<void> {
    return this._getEnvironmentMessage().then(message => {
      //this._environmentMessage = message;
    });
  }


  private _getEnvironmentMessage(): Promise<string> {
    if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
      return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
        .then(context => {
          let environmentMessage: string = '';
          switch (context.app.host.name) {
            case 'Office': // running in Office
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
              break;
            case 'Outlook': // running in Outlook
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
              break;
            case 'Teams': // running in Teams
              environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
              break;
            default:
              throw new Error('Unknown host');
          }

          return environmentMessage;
        });
    }

    return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
  }

  protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
    if (!currentTheme) {
      return;
    }

    //this._isDarkTheme = !!currentTheme.isInverted;
    const {
      semanticColors
    } = currentTheme;

    if (semanticColors) {
      this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
      this.domElement.style.setProperty('--link', semanticColors.link || null);
      this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
    }

  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
                PropertyPaneTextField('flowURL', {
                  label: strings.FlowURLLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

    4.       Update the en-us.js file with the below code which is located under ‘loc’ folder (solutionpath > src > webparts > spFxWebpart > loc).

define([], function() {

  return {

    "PropertyPaneDescription": "Description",

    "BasicGroupName": "Group Name",

    "DescriptionFieldLabel": "Description Field",

    "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",

    "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",

    "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",

    "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",

    "AppSharePointEnvironment": "The app is running on SharePoint page",

    "AppTeamsTabEnvironment": "The app is running in Microsoft Teams",

    "AppOfficeEnvironment": "The app is running in office.com",

    "AppOutlookEnvironment": "The app is running in Outlook",

    "PropertyPaneDescription": "SPFx FLow",

    "BasicGroupName": "Flow Properties",

    "FlowURLLabel": "Flow URL"

  }

});

    5.       Update the mystring.d.ts file with the below code which is located under ‘loc’ colder(solutionpath > src > webparts > spFxWebpart > loc)

declare interface ISpFxWebpartWebPartStrings {

  PropertyPaneDescription: string;

  BasicGroupName: string;

  DescriptionFieldLabel: string;

  AppLocalEnvironmentSharePoint: string;

  AppLocalEnvironmentTeams: string;

  AppLocalEnvironmentOffice: string;

  AppLocalEnvironmentOutlook: string;

  AppSharePointEnvironment: string;

  AppTeamsTabEnvironment: string;

  AppOfficeEnvironment: string;

  AppOutlookEnvironment: string;

  FlowURLLabel: string;

}

 

declare module 'SpFxWebpartWebPartStrings' {

  const strings: ISpFxWebpartWebPartStrings;

  export = strings;

}

    6.       Run ‘Gulp Serve’ to test the webpart in Workbench url.

7.       Add the webpart on workbench url, edit the webpart and provide URL of the Flow noted in the above step: HTTP Post URL.

No comments:

Post a Comment