Wednesday, December 24, 2014

Elevating User permissions in SharePoint Hosted App Web

Copyright from: www.casvaniersel.com


Lately we’ve done some SharePoint Hosted App development and came across a Permission Issue when it comes to the App Web. Also inspired by this thread on the MSDN forums: http://social.msdn.microsoft.com/Forums/sharepoint/en-US/a34b1a2d-40d4-46d8-838b-1ca90dc860a2/user-permissions-inside-a-sharepoint-hosted-app-web we decided to follow up on this challenge.

Host Web vs. App Web

There are lots of different opinions when it comes to the subject of Host Web vs. App Web. Many people believe that in order to be completely isolated from “Out of the Box” SharePoint, all App data must be stored and managed within the App Web. This eliminates the need of complex App Permissions on the Host Web and the risk of rejection by users that don’t want to install the App because of those. The drawback of this however in my experience is that by default an App Web fully inherits it’s permissions from the Host Web. Meaning users that have read permissions on the Host Web aren’t allowed to create, update or delete the data stored in Lists in the App Web. In some scenario’s you want even users that are just visitors on the Host Web, to work with your App and therefor be able to update data stored in the App Web. What to do?

Setting Permissions in the App Web

Fortunately for us, the App Web is very similar to just any SharePoint SPWeb object. It’s possible to manage the Permissions in the App Web the same way we do on the Host Web, by using the Applications Page “/_layouts/15/user.aspx”. Of course this page is only available to users with the right permissions on the Host Web (due to the inheritance). But if you navigate to this page from your App Web we can see what’s going on with the Permissions.
PermissionsAppWeb
If you have a List deployed in the App Web that holds App Data just add a QueryString to this page “?List=[ListId]” and you’ll be able to see the permissions of that specific List.

Break the chain

In our case we needed All Users to be contributors to this List but without touching the Host Web’s permissions. Because we had just one List deployed in our App we decided to Break the Inheritance on the App Web level and so the List inherits all Permissions from the App Web level. We needed a one time event by a user that is allowed to do this on the App Web in the first place. This is were we need App Permissions on the Host Web. The idea is to let an admin install the App. When asked for App Permissions, the admin grant the permissions thereby redirecting him to the App Web after installation and allow us to let him “Initialize” the App. That initialization step executes some JavaScript that breaks role inheritance and adding Contribute Permissions to the group Everyone on the App Web.

App Installation

When dealing with App Permissions obvisouly the “Least Priviliges” Approach is best practice. In this particular scenario it doesn’t really matter what App Permissions are granted in terms of abuse. Since the App doesn’t have any interaction with the Host Web we don’t load any libraries that let users make Cross Domain calls back to the Host Web. So even if the App Permissions are set to “Manage Web” users will not likely to be able to make CSOM calls back to the Host Web unless they are very skilled. In this example I’ll use the Manage Web Permissions for the App.
AppPermissions
Now when an Admin Trusts this App he will immediately be redirected to the Default Page of the App Web.
Redirect
Now we have the event set up to get some cool script going for changing the App Web’s permissions.

Break Role Inheritance

In order to break the Role Inheritance and add a new Role Assignment we used the example from Yuri Leontyev:http://spsite.pro/Blog/Post/3/SharePoint-2013-REST-API-%E2%80%93-How-to-set-Unique-Permissions-(Item-Level-Permissions)
My function to break role inheritance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
P42.UpdateAppPermissions = function ($) {
 var execute = function (appWebUrl) {
  var deferred = $.Deferred();
  $.ajax({
    url: appWebUrl + "/_api/web/breakroleinheritance(copyRoleAssignments = true, clearSubscopes = true)",
    type: 'POST',
    headers:{ "accept": "application/json;odata=verbose", "content-type": "application/json;odata=verbose", "X-RequestDigest": $("#__REQUESTDIGEST").val() },
    success: function () { deferred.resolve(true); },
    error: function (sender) { deferred.reject(sender); }
  });
  return deferred;
 };
 return {
  execute: execute
 }
}(jQuery);
This will only break the role inheritance between the Host Web and the App Web, but also copies the default permissions.
UniquePermissions

Add Role Assignments

The second part is to get the Principal ID of the group “Everyone” and add a Role Assignment that says Everyone has Contribute permissions. We will need two functions for that.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
P42.GetEveryone = function ($) {
 var execute = function (appWebUrl) {
  var url = "/_api/web/siteusers?$filter=Title eq 'Everyone'&$select=Id";
  var deferred = $.Deferred();
  $.ajax({
    url: appWebUrl + url,
    type: 'GET',
    headers: { "accept": "application/json;odata=verbose" },
    success: function (data) { deferred.resolve(data); },
    error: function (sender) { deferred.reject(sender); }
  });
  return deferred;
 };
 return {
  execute: execute
 }
}(jQuery);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
P42.AddEveryone = function ($) {
 var execute = function (appWebUrl, principalId) {
  var deferred = $.Deferred();
  var ctx = SP.ClientContext.get_current();
  var web = ctx.get_web();
  var roleDefinition = web.get_roleDefinitions().getById(1073741827);
  // Create a new RoleDefinitionBindingCollection
  var newBindings = SP.RoleDefinitionBindingCollection.newObject(ctx);
  // Add the role to the collection
  newBindings.add(roleDefinition);
  // Get the RoleAssignmentCollection for the target list
  var assignments = web.get_roleAssignments();
  // Add the user to the target list and assign the use to the new RoleDefinitionBindingCollection
  var roleAssignment = assignments.add(web.getUserById(principalId), newBindings);
  ctx.executeQueryAsync(function () { deferred.resolve(); }, function (sender, args) { deferred.reject(sender, args); });
  return deferred;
 };
 return {
  execute: execute
 }
}(jQuery);
To obtain the ID of the default Contribute permission we can use a simple REST call on any SharePoint Web. This ID is fixed and always usable. From this overview we can also see other Permissions we could set if we like.
https://your_sp_site.sharepoint.com/_api/web/roledefinitions​ 
image

Run Once

Ok now the script part only has to run once. In order for that to happen there are lots of different options, but the one I always remembered from early SharePoint Development is setting a Property in the property bag that indicates the script has already been executed. I’ve created two functions, one to check if the Property has been set and one to actually set it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
P42.GetInitProp = function ($) {
 var execute = function () {
  var deferred = $.Deferred();
  var url = _spPageContextInfo.webAbsoluteUrl + "/_api/web/AllProperties/?$select=" + "_AppInit";
  $.ajax({
    url: url,
    type: 'GET',
    headers: { "accept": "application/json;odata=verbose" },
    success: function (data) { deferred.resolve(data); },
    error: function (sender) { deferred.reject(sender); }
  });
  return deferred;
 };
 return {
  execute: execute
 }
}(jQuery);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
P42.SetInitProp = function ($) {
 var execute = function (value) {
  var deferred = $.Deferred();
  var ctx = SP.ClientContext.get_current();
  var web = ctx.get_web();
  var properties = web.get_allProperties();
  properties.set_item("_AppInit", value);
  web.update();
  ctx.load(web);
  ctx.executeQueryAsync(function () { deferred.resolve(); }, function (sender, args) { deferred.reject(sender, args); });
  return deferred;
 };
 return {
  execute: execute
 }
}(jQuery);
On the default page we can use this piece to check the Property.
1
2
3
4
5
6
7
8
9
P42.GetInitProp.execute().promise().then(function (data) {
 if (data.d.OData__x005f_AppInit) {
  jQuery('#AfterInit').show();
 }else {
  jQuery('#PreInit').show();
 }
}, function (sender, args) {
 //Error
});
Where my property in this case is “_AppInit”.

Promises

Now the final piece of this puzzle is to make all these functions run in controlled flow. You may have noticed there is quite some dependency on one another hence the jQuery Deferred that is in all of these functions. The one function that makes all these calls happen is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
initApp = function (appWebUrl) {
 P42.UpdateAppPermissions.execute(appWebUrl).promise().then(function (success) {
  P42.GetEveryone.execute(appWebUrl).promise().then(function (data) {
   var principalId = data.d.results[0].Id;
   P42.AddEveryone.execute(appWebUrl, principalId).promise().then(function () {
    //Create Web Property
    P42.SetInitProp.execute(true).promise().then(function () {
      //Success! Do other stuff
     },
     function (sender, args) {
      console.log(args.get_message() + '\n' + args.get_stackTrace());
    });
   }, function (sender, args) {
    console.log(args.get_message() + '\n' + args.get_stackTrace());
   });
  }, function (sender, args) {
   console.log(args.get_message() + '\n' + args.get_stackTrace());
  });
 }, function (sender, args) {
  console.log(args.get_message() + '\n' + args.get_stackTrace());
 });
};
Basically this is it. We do need to pass the AppWebUrl to this method. I think some of this code can actually be done nicer, but just for this example it should work.
image

Final Considerations

With these pieces of code it is easy to set the App Permissions the way you want to in one initial step however this workaround is not really ideal. Even though it’s hard and users have to be real code guru’s, it is possible to abuse the App Permissions we granted. Using GetScript and the Cross Domain Library it’s possible to hack your way into the Host Web,  but frankly this is always the case when using App Permissions.
Besides that using an Initial step that must be done by an Admin is also not ideal. Still with the given tools at the moment I think this is the best solution yet to take control of the permissions in the App Web. Hopefully this will improve in the future.
And finally another great explanation on why Breaking Role Inheritance is not that good of a practice. A very well explained 3 part Blog Post.
But remember with SharePoint it always depends!