In my last couple of posts I've looked at building custom workflow activities 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 placeholders 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.