Monday, September 24, 2018

Access denied when try to get Azure AD group’s unseencount through Graph API

Some time ago I faced with interesting problem: when tried to get properties of Azure AD group using app token with app permissions (without available user context) through Graph API:

https://graph.microsoft.com/v1.0/groups/{groupId}?$select=visibility,unseencount

the following error was shown:

{
     "error": {
         "code": "ErrorAccessDenied",
         "message": "Access is denied. Check credentials and try again.",
         "innerError": {
             "request-id": "…",
             "date": "…"
         }
     }
}

Here is example from Postman:

01

Investigation showed that problem was caused by unseencount property. When I tried to remove it – another selected property (visibility) was returned successfully:

https://graph.microsoft.com/v1.0/groups/{groupId}?$select=visibility

{
     "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups(visibility)/$entity",
     "visibility": "Public"
}

02

What was even more stranger is that in Graph explorer it worked:

03

Communication with MS support both on forums (see Can't get group's unseenCount) and via Azure support ticket helped to figure out the reason of this problem: in Postman I used app token with app permissions, while in Graph explorer I was authenticated with my own account (see above). I.e. in Graph explorer delegated permissions were used. And there is known issue in MS Graph (see Known issues with Microsoft Graph): unseencount may be retrieved only using delegated permissions:

“Examples of group features that support only delegated permissions:

  • Group conversations, events, photo
  • External senders, accepted or rejected senders, group subscription
  • User favorites and unseen count
  • Microsoft Teams channels and chats”

Hope that this information will help someone.

Tuesday, September 11, 2018

Several problems when follow/unfollow Sharepoint site via REST API

If you need to implement follow/unfollow site functionality via javascript you most probably will find the following MS article Follow documents, sites, and tags by using the REST service in SharePoint which contains the following example which uses REST API:

$.ajax( {
	url: followingManagerEndpoint + "/isfollowed",
	type: "POST",
	data: JSON.stringify( { 
		"actor": {
			"__metadata": {
				"type":"SP.Social.SocialActorInfo"
			},
			"ActorType":2,
			"ContentUri":siteUrl,
			"Id":null
		} 
	}),
	headers: { 
		"accept":"application/json;odata=verbose",
		"content-type":"application/json;odata=verbose",
		"X-RequestDigest":$("#__REQUESTDIGEST").val()
	},
	success: function (responseData) { 
		...
	},
	error: requestFailed
});

However currently if you will try to run it you will get the following 400 Bad request error:

{"error":{"code":"-1, Microsoft.OData.Core.ODataException","message":"The property '__metadata' does not exist on type 'SP.Social.SocialActorInfo'. Make sure to only use property names that are defined by the type."}}

The problem is with __metadata property of the actor which seems to be outdated nowadays. In order to avoid this error just remove or comment __metadata from actor object.

Another problem is related with odata=verbose specified in Accept header. If you will try to follow site with it API will return the following 406 Not acceptable error:

{"error":{"code":"-1, Microsoft.SharePoint.Client.ClientServiceException","message":"The HTTP header ACCEPT is missing or its value is invalid."}}

In order to resolve it change odata=verbose to odata.metadata=minimal in Accept header. Here is the working code written with Typescript and SPFx:

context.spHttpClient.post(context.pageContext.web.absoluteUrl + "/_api/social.following/follow",
  SPHttpClient.configurations.v1, {
	headers: {
	  'Accept': 'application/json;odata.metadata=minimal',
	  'Content-type': 'application/json;odata=verbose',
	},
	body: JSON.stringify({
	  "actor": {
		  /*"__metadata": {
			  "type": "SP.Social.SocialActorInfo"
		  },*/
		  "ActorType": 2,
		  "ContentUri": site.url,
		  "Id": null
	  }
	})
  });

Hope that it will help someone.

Friday, September 7, 2018

How to delete navigation nodes with AuthoringLink types programmatically in Sharepoint

If you use structural navigation on your publishing Sharepoint site and navigation nodes are created as headings (NodeType = Heading), then it is quite straightforward to delete such navigation nodes programmatically. Here is how it can be done:

var web = ...
var pweb = PublishingWeb.GetPublishingWeb(web);
var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
var nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test"));
if (nodePage != null)
{
	nodePage.Delete();
}

In this example we delete navigation node with title “Test”. However if you will try to delete navigation nodes which were created as AuthoredLink* (see NodeTypes Enum):

  • AuthoredLink
  • AuthoredLinkPlain
  • AuthoredLinkToPage
  • AuthoredLinkToWeb

using the same code you will find that link is not get deleted. The workaround is to change NodeType property first to Heading and then delete the node:

var web = ...
var pweb = PublishingWeb.GetPublishingWeb(web);
var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
var nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test" &&
	(n.Properties != null && n.Properties["NodeType"] != null && n.Properties["NodeType"] is string &&
		(n.Properties["NodeType"] as string == "AuthoredLink" || (n.Properties["NodeType"] as string).StartsWith("AuthoredLink"))));

if (nodePage != null)
{
	nodePage.Properties["NodeType"] = "Heading";
	nodePage.Update();

	// reinitialize navigation nodes
	pweb = PublishingWeb.GetPublishingWeb(web);
	globalNavigation = pweb.Navigation.GlobalNavigationNodes;
	nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test);
	if (nodePage != null)
	{
		nodePage.Delete();
	}
}

After that AuthoredLink navigation node will be successfully deleted.

Tuesday, September 4, 2018

How to get modern Team or Communication sites using Search API in Sharepoint

In one of my previous posts I wrote how to create modern Sharepoint sites: How to create modern Team or Communication site in Sharepoint (quite basic post but necessary if you just stated to work with modern sites). In this article we will continue exploring modern sites and will see how to get list of modern Team or Communication sites using Search API. Using of Search API is preferable in many scenarios as you have all sites at once with single API call.

Let’s create modern Team site and explore it’s Site Pages doclib. In default list view let’s add additional column “Content Type” to see what content type is used for the default front page:

image

As you can see it uses “Site Page” content type. Modern Communication site also uses the same content type for front page.

So in order to get list of all modern sites we may query for pages created with “Site Page” content type. In order to make our search query language-independent we will use content type id instead of the name. In order to get it go to Site Pages doclib settings and click Site Page content type. Then copy content type id from query string. You will have something like that:

0x0101009D1CB255DA76424F860D91F20E6C4118…

where the rest will be unique for your doclib. Our query string will look like that then:

ContentTypeId:0x0101009D1CB255DA76424F860D91F20E6C4118*

which means return all pages which content type starts with specified id. If we will test it in the Search Query Tool we will have list of all modern Team and Communication sites in the tenant:

image

In order to get distinct list don’t forget to check “Trim duplicates” option and select at least Title and SPWebUrl managed properties which contain site title and url.

Monday, September 3, 2018

How to explore Sharepoint REST API endpoints

Sometimes you need to get list of available operations in the Sharepoint REST API endpoint. Let’s say we want to check operations available for /_api/SitePages endpoint.

First of all we need to get authentication cookies. In order to get them lunch Fiddler and open e.g. Sharepoint landing page (/_layouts/15/Sharepoint.aspx) which is opened from App launcher > Sharepoint. On this page there will be several REST API calls which will contain Cookies header. E.g. /_api/GroupSiteManager/CanUserCreateGroup:

image

From this Fiddler view copy value of Cookie header.

After that launch Postman and create request on endpoint in question: /_api/SitePages. In Headers section add Cookie, put value copied from Fiddler and click Send:

image

In the response it will return list of relative endpoint operations available under selected endpoint. In this example they are:

  • /_api/SitePages/CommunicationSite
  • /_api/SitePages/Pages
  • /_api/SitePages/PublishingSite

Note that this method doesn’t return POST/PUT endpoints unfortunately.

How to create modern Team or Communication site in Sharepoint

Modern Team/Communication sites are not displayed on the classic create site page in Sharepoint together with other “classic” web templates. In order to create them you need first click App launcher icon in top left corner and choose Sharepoint link there:

image

It will open Sharepoint landing page which will have Create site icon on the top:

image

After clicking on this icon you will be able to choose which modern site to create: Team or Communication:

image

And on the last step you have to specify site name, privacy and classification (later one is shown if classifications are configured for your tenant):

image

After clicking Next your modern Team or Classification site will be created.

Monday, August 27, 2018

Combine async redux actions in SPFx react components

If you work with SPFx and e.g. implement web part which uses react/redux for its components then you may face with need to combine multiple async redux actions into single one. Let’s assume that we have components which shows O365 groups and has appropriate properties and actions for it:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import * as GroupActions from 'groupActions';

export interface GroupListProps {
  groups: IGroup[];
  actions: {
    getGroups: GroupActions.IGetGroups,
  };
}

class GroupList extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groups) {
      this.props.actions.getGroups();
    }
  }

  public render() {
    return (
      this.props.groups ? <GroupsList items={this.props.groups} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
    getGroups: () => dispatch(GroupActions.getGroupsImpl()),
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

After that we decide to show both regular Sharepoint sites together with groups. We need to action for getting sites themselves (asynchronously using promises) and another action for combining groups and sites into single list. Both actions ae asynchronous. Let’s see how we may combine them to single one.

At first start with adding new action for sites:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import { ISite } from 'ISite';
import * as GroupActions from 'groupActions';
import * as SiteActions from 'siteActions';

export interface GroupListProps {
  groups: IGroup[];
  sites: ISite[];
  actions: {
    getGroups: GroupActions.IGetGroups,
	getSites: SiteActions.IGetSites,
  };
}

class GroupListContainer extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groups) {
      this.props.actions.getGroups();
    }

    if (!this.props.sites) {
      this.props.actions.getSites();
    }
  }

  public render() {
    return (
      this.props.groups ? <GroupsList items={this.props.groups} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
  sites: state.sites
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
    getGroups: () => dispatch(GroupActions.getGroupsImpl()),
	getSites: () => dispatch(SiteActions.getSitesImpl()),
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

Then create new combined action which calls 2 other actions (getGroups and getSites) via dispatch and additional action which will combine 2 groups and sites to single list:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import { ISite } from 'ISite';
import { IGroupOrSite } from 'IGroupOrSite';
import * as GroupActions from 'groupActions';
import * as SiteActions from 'siteActions';
import * as GroupOrSiteActions from 'groupOrSiteActions';

export interface GroupListProps {
  groups: IGroup[];
  sites: ISite[];
  groupsOrSites: IGroupOrSite[];
  actions: {
	getGroupsAndSites: GroupOrSiteActions.IGetGroupsAndSites
	combineGroupsAndSites: GroupOrSiteActions.ICombineGroupsAndSites
  };
}

class GroupListContainer extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groupsOrSites) {
      this.props.actions.getGroupsAndSites();
    }
  }

  public componentDidUpdate(prevProps: GroupListProps) {
    // Combine groups and sites
    if ((!isEqual(this.props.groups, prevProps.groups) || !isEqual(this.props.sites, prevProps.sites)) &&
	  this.props.groups && this.props.sites) {
	  this.props.actions.combineGroupsAndSites(this.props.groups, this.props.sites);
    }
  }
  
  public render() {
    return (
      this.props.groupsOrSites ? <GroupsList items={this.props.groupsOrSites} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
  sites: state.sites,
  groupsOrSites: state.groupsOrSites
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
	getGroupsAndSites: () => {
		dispatch(GroupActions.getGroupsImpl());
		return dispatch(SiteActions.getSitesImpl());
	},
	getGroupsAndSites: (groups: IGroup[], sites: ISite[]) => dispatch(GroupOrSiteActions.combineSitesImpl(groups, sites))
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

As result when you will call this combined action it will perform 2 async sub actions.