Custom Workflow Activity for Setting a SharePoint Group Owner

In my last couple of posts I've looked at building custom workflow activities in Visual Studio 2012 to create sites and create groups in SharePoint 2013. In this post I'll walk you through how to build a workflow activity that changes the owner of a SharePoint group. This is slightly more challenging, as at the time of writing you cannot use the REST API to change the owner of a SharePoint group. Instead, you have to make a service call containing client-side object model (CSOM) XML. If I ever make it to San Diego, I owe Chris Givens several drinks for helping me figure out this approach and switching me on to the capabilities of CSOM XML.

Note: this is part of a series of posts on building workflow activities to manage sites, groups, users and permissions. For a complete list of posts, together with a more detailed walkthrough of how to build a custom workflow activity and make it available in SharePoint Designer, start at the beginning.


Fundamentals

You can't currently use the REST API to set the owner of a SharePoint group (if you want the details, check out this forum post). However, you can set the owner of a SharePoint group by calling the client.svc service and including an XML body that specifies the changes you want to make. Whenever you use client-side code in SharePoint, the client-side object model serializes your changes into XML and sends them to the client.svc service. By watching these service calls in a web debugger such as Fiddler, you can figure out how the XML is structured. You can then build your own XML strings in a workflow activity and send them directly to the client.svc service.

If you want to make a user the owner of a SharePoint group, you need to send a web request that resembles the following:

Endpoint:
{site collection URL}/_vti_bin/client.svc/ProcessQuery

HTTP method:
POST

Headers:
Content-Type: text/xml

Body:
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
  <Actions>
    <ObjectPath Id="1" ObjectPathId="2" />
    <SetProperty Id="3" ObjectPathId="4" Name="Owner">
      <Parameter ObjectPathId="2" />
    </SetProperty>
    <Method Name="Update" Id="5" ObjectPathId="4" />
  </Actions>
  <ObjectPaths>
    <Method Id="2" ParentId="6" Name="EnsureUser">
      <Parameters>
        <Parameter Type="String">{0}</Parameter>
      </Parameters>
    </Method>
    <Identity Id="4" Name="{1}:site:{2}:g:{3}" />
    <Identity Id="6" Name="{1}:site:{2}:web:{4}" />
  </ObjectPaths>
</Request>

Essentially, the request consists of two key elements:
  • The Actions element tells the service what you want to do.
  • The ObjectPaths element tells the service which objects you want to perform the actions on.
The values of the integer IDs don't matter. When the client-side object model sends a request, it generates these values randomly for the purpose of tracking objects between requests. The important thing is that the ObjectPathId attribute values used throughout match the Id attribute values in the Identity elements, as this is how the service correlates your actions and your object paths.

The XML body shown above includes various string placeholders that you'll need to replace before you call the service:
  • {0} is the user's login name.
  • {1} is the GUID of the SPObjectFactory class (always 740c6a0b-85e2-48a0-a494-e0f1759d4aa7).
  • {2} is the GUID of the current SPSite.
  • {3} is the integer ID of the group on which you want to set the owner.
  • {4} is the GUID of the SPWeb on which the user is listed (the root SPWeb will work).
Plug in the placeholder values and your request body should look something like this:
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
  <Actions>
    <ObjectPath Id="1" ObjectPathId="2" />
    <SetProperty Id="3" ObjectPathId="4" Name="Owner">
      <Parameter ObjectPathId="2" />
    </SetProperty>
    <Method Name="Update" Id="5" ObjectPathId="4" />
  </Actions>
  <ObjectPaths>
    <Method Id="2" ParentId="6" Name="EnsureUser">
      <Parameters>
        <Parameter Type="String">i:0#.w|jason\andya</Parameter>
      </Parameters>
    </Method>
    <Identity Id="4" Name="740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:fd4535b0-25f5-4fcf-9a10-961ce1c30db3:g:27" />
    <Identity Id="6" Name="740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:fd4535b0-25f5-4fcf-9a10-961ce1c30db3:web:6e504370-0ea5-48b1-ad12-8f5d6cd23b23" />
  </ObjectPaths>
</Request>

Notice the format of the GUIDs - no braces or any other adornments. The object path names do get very hard to read, but so long as you get your format string right once you won't have to worry about them again.

If you want to make another group (rather than a user) the owner of your group, you need to structure the XML body slightly differently:
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">
  <Actions>
    <SetProperty Id="1" ObjectPathId="2" Name="Owner">
      <Parameter ObjectPathId="3" />
    </SetProperty>
    <Method Name="Update" Id="4" ObjectPathId="2" />
  </Actions>
  <ObjectPaths>
    <Identity Id="2" Name="{0}:site:{1}:g:{2}" />
    <Identity Id="3" Name="{0}:site:{1}:g:{3}" />
  </ObjectPaths>
</Request>

In this case the placeholder values are as follows:
  • {0} is the GUID of the SPObjectFactory class (always 740c6a0b-85e2-48a0-a494-e0f1759d4aa7).
  • {1} is the GUID of the current SPSite.
  • {2} is the integer ID of the group on which you want to set the owner.
  • {3} is the integer ID of the group you want to make the owner.

Build the XAML File

At this point I'll assume that you know the basics of how to build custom workflow activities for SharePoint 2013 - if you want a more detailed walkthrough, take a look at my first post in this series. I'll start by defining the arguments:










In this case, I need the consumer of the workflow activity to specify the ID of the group they want to update, together with the login name of the new group owner. The activity will return the status code from the web service response.

Next, I'll define the variables I want to use within the activity:


























There's a lot going on here, so there are an exceptionally large number of variables. You'll see the purpose of each of these as we walk through the activity.

The complete activity looks like this (in two halves, as it's too long to fit on the screen):




























In this case, we've got a total of twelve child activities. I'll walk through each of these in turn.

Step 1: Get current site URL

In this activity, we want to get the URL of the current site collection. I've used a LookupWorkflowContextProperty activity to do this. In the activity properties, I'm looking up the current site URL and assigning it to the siteUrl variable.















Step 2: Assign (build service URL)

In this activity, we want to build on the site URL to form a service endpoint for our request. I've used an Assign activity to do this. In the activity properties, I'm concatenating the site URL and the site-relative service endpoint, and assigning it to the serviceUrl variable.














Step 3: LookupSPPrincipal

In our workflow arguments, we're asking the activity consumer to provide a login name for the new group owner (which could be a user or a group). Here, I'm using a LookupSPPrincipal activity to resolve the login name. LookupSPPrincipal is a useful activity as it's fairly forgiving - the user can specify a login name, a display name or an email address, and the activity will resolve it and return the properties of the principal.















The activity returns a JSON response body, which I'm assigning to the getPrincipalResponse DynamicValue variable.


Step 4: Get Principal ID

The JSON response returned by the LookupSPPrincipal activity resembles the following:
{"d":{
   "ResolvePrincipalInCurrentContext":{
      "__metadata":{
         "type":"SP.Utilities.PrincipalInfo"
      },
      "Department":null,
      "DisplayName":"Project Four Owners",
      "Email":null,
      "JobTitle":null,
      "LoginName":"Project Four Owners",
      "Mobile":null,
      "PrincipalId":27,
      "PrincipalType":4,
      "SIPAddress":null
   }
}}

We can use GetDynamicValue<T> activities to retrieve the properties we're interested in from this response. If the new group owner is a SharePoint group, we'll need to specify the integer ID in our request, so in this activity we retrieve the PrincipalId property:














Step 5: Get Principal Login Name

If our new group owner is a user, we'll need to specify the login name in our request. So in this activity we retrieve the LoginName property:














Step 6: Get Principal Type

Before we can construct the XML body for our service request, we need to know whether the new group owner is a user or a SharePoint group. We can determine this from the value of the PrincipalType property, so in this activity we retrieve the PrincipalType property:














Step 7: Get SPObjectFactory ID

When we construct the XML body for our service request, we need to include the GUID of the SPObjectFactory class in all our object paths. As I mentioned earlier, in the current version of SharePoint this is always 740c6a0b-85e2-48a0-a494-e0f1759d4aa7. Here, I've created a simple helper activity that returns the SPObjectFactory ID. I encapsulated it in a helper activity in case the GUID changes at a later date.











Step 8: Get Site Guid

Our service request XML body must also include the ID of the site collection that contains our group and its new owner. Here, I've created a simple helper activity that returns the GUID ID of the current site collection:












If you've got this far, you probably won't have any difficulty getting the site GUID. Essentially, you send a GET request to {site collection URL}/_api/site, and then retrieve the "d/Id" property from the response.

Step 9: Get Web Guid

If our new group owner is a user, our service request XML body must include the ID of the site (SPWeb) whose user information list contains the user (the root site will typically do it - the XML body includes an EnsureUser call that adds the user to the user information list if they're not on it already). Here, I've created a simple helper activity that returns the GUID ID of a specified site:












Again, getting the GUID of an SPWeb is pretty straightforward - you send a GET request to {site URL}/_api/web, and then retrieve the "d/Id" property from the response.

Step 10: Switch<Int32>

Things get a little bit messy at this point. We need to construct a different XML body for our service request depending on whether the new group owner is a user or a group. To do this, I've used a Switch<T> activity that switches on the value of the PrincipalType property. This is an SPPrincipalType enumeration value, where:
  • A value of 1 represents a SharePoint user.
  • A value of 2 represents a distribution list.
  • A value of 4 represents a security group.
  • A value of 8 represents a SharePoint group.
Case 8
If the value of the principalType variable is 8, we need to construct an XML body that specifies another SharePoint group as the new owner of our group:

























Notice that if you want to add more than one activity within a case, you need to encapsulate them within a Sequence activity. First, we create a format string with placeholders for our GUIDs, group IDs, etc:












The value here is the XML body I showed you at the start of this article, with whitespace removed and quotation marks escaped - here it is for copy-and-paste convenience:
@"<Request AddExpandoFieldTypeSuffix=""true"" SchemaVersion=""15.0.0.0"" LibraryVersion=""15.0.0.0"" ApplicationName="".NET Library"" xmlns=""http://schemas.microsoft.com/sharepoint/clientquery/2009""><Actions><SetProperty Id=""1"" ObjectPathId=""2"" Name=""Owner""><Parameter ObjectPathId=""3"" /></SetProperty><Method Name=""Update"" Id=""4"" ObjectPathId=""2"" /></Actions><ObjectPaths><Identity Id=""2"" Name=""{0}:site:{1}:g:{2}"" /><Identity Id=""3"" Name=""{0}:site:{1}:g:{3}"" /></ObjectPaths></Request>"

Notice that you do need to escape the quotation marks as shown - it appears that the service does not like attribute values in single quotes ('). Next we plug the values into our format string to generate our XML body:












The value here is as follows:
String.Format(xmlFormatString, factoryGuid.ToString(), siteGuid.ToString(), groupId.ToString(), principalId.ToString())

Case 1
If the value of the principalType variable is 1, we need to construct an XML body that specifies another SharePoint group as the new owner of our group:

























As you can see, the activities in this case are the same as the activities in the previous case. However, the values reflect the syntax required to make a user, rather than another group, the owner of our group. You need to set the XML format string to the following:
@"<Request AddExpandoFieldTypeSuffix=""true"" SchemaVersion=""15.0.0.0"" LibraryVersion=""15.0.0.0"" ApplicationName="".NET Library"" xmlns=""http://schemas.microsoft.com/sharepoint/clientquery/2009""><Actions><ObjectPath Id=""1"" ObjectPathId=""2"" /><SetProperty Id=""3"" ObjectPathId=""4"" Name=""Owner""><Parameter ObjectPathId=""2"" /></SetProperty><Method Name=""Update"" Id=""5"" ObjectPathId=""4"" /></Actions><ObjectPaths><Method Id=""2"" ParentId=""6"" Name=""EnsureUser""><Parameters><Parameter Type=""String"">{0}</Parameter></Parameters></Method><Identity Id=""4"" Name=""{1}:site:{2}:g:{3}"" /><Identity Id=""6"" Name=""{1}:site:{2}:web:{4}"" /></ObjectPaths></Request>"

Set the value of your XML body to the following:
String.Format(xmlFormatString, principalLoginName, factoryGuid.ToString(), siteGuid.ToString(), groupId.ToString(), webGuid.ToString())

Case 4
This case is required to address an idiosyncrasy in the LookupSPPrincipal activity. When you look up a SharePoint group, it appears to return a principal type of 4 rather than the expected 8. To deal with this, I've simply copied the contents of Case 8 into Case 4.

Step 11: HttpSend

At this point, you've set all the variable values you need in order to call the web service. The HttpSend activity is configured as follows:


























This is pretty straightforward - you're sending a POST request containing your XML body to the service URL you defined near the start of the activity. The only other thing you need to do at this point is to add a Content-Type header:



















Step 12: Assign

In this final task, we're simply converting the response status code into an SPD-friendly format and assigning it to an argument:













Build the Actions File

Our last task is to edit the .actions4 file so we can use the Set Group Owner activity in SharePoint Designer. To recap, the .actions4 file defines the sentence that appears in SPD when you add the activity to a workflow, together with the arguments defined in the workflow activity. If you've made it this far, this step should be pretty straightforward (For a more detailed explanation of what's going on in the .actions4 file, refer to the first post in this series.)

In this case, the .actions4 file should resemble the following:
<Action Name="Set Group Owner" ClassName="SiteManagementActivities.SetGroupOwner" Category="Site Management" AppliesTo="all">
  <RuleDesigner Sentence="Make %1 the owner of the group %2 (Output: %3)">
    <FieldBind  Field="ownerLoginName" Text="Principal Login Name" Id="1" />
    <FieldBind  Field="groupId" Text="Group ID" Id="2" />
    <FieldBind  Field="responseStatusCodeOut" Text="Response Status Code" Id="3" />
  </RuleDesigner>
  <Parameters>
    <Parameter Type="System.String, mscorlib" Direction="In" Name="ownerLoginName" />
    <Parameter Type="System.Int32, mscorlib" Direction="In" Name="groupId" />
    <Parameter Type="System.String, mscorlib" Direction="Out" Name="responseStatusCodeOut" />   
  </Parameters>
</Action>

And that concludes how to build a workflow activity that sets the owner of a SharePoint group. It's not pretty, but it works a treat.

Update 17/11/14: a sample Visual Studio solution containing these workflow activities is now available for download.

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi Jason, thanks for the informative post.
    Two questions though when using LookupSPPrincipal
    1. Why can't you use the GetDynamicValueProperties to get multiple properties?
    2. Why do you have to include the full path of the property? Im a bit mixed up about this because GetDynamicValueProperties allows to specify Entity type as the Principal and then get each property without defining the full path...which doestn work. I have posted a question to MSDN forums describing my problem
    http://social.msdn.microsoft.com/Forums/sharepoint/en-US/1337c1a3-92ca-489c-9d01-38eb965ab70d/lookupspprincipal-wont-associate-the-principal-properties-to-variables?forum=sharepointdevelopment

    ReplyDelete
  3. Hi Chris

    I had a similar experience to you - I couldn't get the GetDynamicValueProperties activity to work as it's supposed to. I suspect it only works if your Principal object in a flat JSON structure; i.e. if all the properties are top-level properties rather than nested. Which, needless to say, is no use in this case.

    Using individual GetDynamicValueProperty activities is a working alternative. It's a bit of a hassle to figure out the full property paths - I usually do it by capturing the JSON responses in Fiddler.

    ReplyDelete
  4. Hi Jason thanks for the reply.
    I managed to make it work by using GetDynamicValueProperties and passing the full path for each element so effectively i have all properties under one collection but you are right about the flat JSON structure; the full path is needed
    Also a small tip: No need for Fiddler. You can capture them by running the debugger from Visual Studio

    ReplyDelete
  5. Hi Jason,

    I'm trying to set the owner of a site group from a sharepoint designer 2013 list workflow (To elevate privileges). The problem is that "Call HTTP Web Service" workflow action doest not allow string values in "RequestContent" property (Only allows dictionaries), so the RequestContent can't be setted. Is there any way to do this?.

    ReplyDelete
    Replies
    1. Hi Julián - you're right in saying that you can't do it in SharePoint Designer. Building a custom workflow activity in Visual Studio as described in this article is how you work around the limitation.

      Delete

Post a Comment

Popular posts from this blog

Server-side activities have been updated

The target principal name is incorrect. Cannot generate SSPI context.

Custom Workflow Activity for Creating a SharePoint Site