Thursday, April 20, 2017

Overview of item-level permissions in Sharepoint lists

In Sharepoint lists you may configure so called item-level permissions: List settings > Advanced settings > Item-level permissions:

In this article I will show how this setting affects behavior of the lists. In our examples I will use calendar list and 2 different users with Contribute permissions: user1 and user2. user1 created event “test from user1”, and user2 – “test from user2”.

So first of all we need to distinguish 2 concepts:

  1. Item-level security
  2. Item-level permissions

Item-level security means “physical” permissions which you may assign on particular list item (e.g. from context menu shown for this list item Shared With > Advanced) when list items don’t inherit permissions from parent list (and as you probably know list item is minimal securable object in Sharepoint: Site > Web > List > Folder > Item). E.g. you may grant particular permission level to user, AD or Sharepoint group on the list item and list item will become secured by standard Sharepoint security mechanism.

Item-level permissions shown in List settings > Advanced settings don’t use standard Sharepoint security mechanism. It is more like adding filters to the list views to shown only items created by current user ([Me]) and adding additional checks on Save button in Add and Edit list forms (see below).

So let’s start with configuration shown above:

  • Read all items
  • Create and edit all items

In this case both user1 and user2 will see the same list view:

I.e. all users see events created by all users. Also both users are able to modify all events regardless of who created this event.

Now let’s check the following configuration:

  • Read items that were created by the user
  • Create and edit all items

In this case user1 will see only own event:

the same as user2:

In this case even if user1 knows direct url of edit form for event created by user1 (EditForm.aspx?ID=…), it won’t be possible to edit it: Sharepoint will show error “Item not found”. I.e. “Read items that were created by the user” setting is has priority over “Create and edit all items” in this case.

Next combination is this one:

  • Read items that were created by the user
  • Create items and edit items that were created by the user

In this case it will work in the same way as previous combination, i.e. users may see and edit only own events.

Next combination:

  • Read all items
  • Create items and edit items that were created by the user

In this case both users again will see all events:

Also there still will be Edit button available in the ribbon so e.g. user1 may select “test from user2” and click Edit – edit form will be successfully shown. But when user1 will click Save on this form Sharepoint will show error “You do not have access to this page”.

And the last combination when user can’t create or edit items:

  • Read all items/Read items that were created by the user
  • None

In this case Sharepoint will show “You do not have access to this page” error when user will try to create new event or edit any event, including own (which was probably created before this setting was turned on). Although new and edit forms still will be opened, error will be shown on clicking Save button.

And last thing which I would like to mention is that under Item-level permissions section in advanced list setting there is the following note:

Users with the Cancel Checkout permission can read and edit all items.

If you will go to Site settings > Site permissions > Permissions levels and will try to edit existing permission level or create new one, you won’t find exactly “Cancel Checkout” permission there. Instead there will be “Override List Bahaviors”:

Discard or check in a document which is checked out to another user, and change or override settings which allow users to read/edit only their own items

And actually this permission is meant under Item-level permissions instead of “Cancel Checkout”. This is all what I wanted to share about this topic. Hope that this information will help someone.

Thursday, April 6, 2017

Problem with SPWeb.EnsureUser method and FBA users with claims based authentication in Sharepoint

If you need to perform some action on FBA user in your Sharepoint site where claims authentication is used from outside of Sharepoint context (e.g. from console application) you may face with the following issue: when you will call web.EnsureUser(userName) method it will throw exception:

Specified user ‘username’ not found

There are several things which have to be done in order to make it possible to work with FBA users without Sharepoint context with claims based authentication:

1. Fake HTTP context after you get instance of SPWeb:

   1: HttpRequest request = new HttpRequest("", web.Url, "");
   2: HttpContext.Current = new HttpContext(request,
   3:     new HttpResponse(new StringWriter()));
   4: HttpContext.Current.Items["HttpHandlerSPWeb"] = web;

2. Use user name in full claims format, i.e.:

   1: var user = web.EnsureUser("i:0#.f|mymembershipprovider|username");

where instead of mymembershipprovider and username you should use your own membership provider name and user name.

3. The most tricky thing: from web.config of your FBA site zone you need to copy the following sections to the app.config of your console application:

  • connectionStrings
  • system.web/membership
  • system.web/roleManager

e.g.:

   1: <connectionStrings>
   2:   <add name="MyConnStr" connectionString="..." />
   3: </connectionStrings>
   4: <system.web>
   5:   <membership defaultProvider="i">
   6:     <providers>
   7:       <add name="i" type="Microsoft.SharePoint.Administration.Claims.SPClaimsAuthMembershipProvider,
   8: Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
   9:       <add connectionStringName="MyConnStr" name="MyMembershipProvider" ... />
  10:     </providers>
  11:   </membership>
  12:   <roleManager defaultProvider="c" enabled="true" cacheRolesInCookie="false">
  13:     <providers>
  14:       <add name="c" type="Microsoft.SharePoint.Administration.Claims.SPClaimsAuthRoleProvider,
  15: Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
  16:       <add connectionStringName="MyConnStr" applicationName="/" name="MyRoleProvider" ... />
  17:     </providers>
  18:   </roleManager>
  19: </system.web>

Here is the full working C# code which allows to get FBA user from console application:

   1: using (var site = new SPSite("http://example.com"))
   2: {
   3:     using (var web = site.OpenWeb())
   4:     {
   5:         web.AllowUnsafeUpdates = true;
   6:         HttpRequest request = new HttpRequest("", web.Url, "");
   7:         HttpContext.Current = new HttpContext(request,
   8:             new HttpResponse(new StringWriter()));
   9:         HttpContext.Current.Items["HttpHandlerSPWeb"] = web;
  10:  
  11:         var user = web.EnsureUser("i:0#.f|mymembershipprovider|username");
  12:         ...
  13:     }
  14: }

Wednesday, March 29, 2017

One reason of AADSTS65001: The user or administrator has not consented to use the application with ID error from adal.js

ADAL JS is a library which allows to write javascript code which interacts with Azure AD via MS Graph API. It uses OAuth2 flow for authenticating user against Azure AD and getting access token which then can be used for using Graph API. Samples and prerequisites for using ADAL JS are available on its GitHub page. Briefly you need to create new app in Azure AD (Azure portal > Active directory > Applications) and then update its app manifest to change oauth2AllowImplicitFlow property from default false value to true (see e.g. Azure AD OAuth2 implicit grant). But when you will try to launch the code you may get various AADSTS* errors. One of them is:

AADSTS65001: The user or administrator has not consented to use the application with ID '…'. Send an interactive authorization request for this user and resource

If you encountered with this error go to Azure AD app configuration page and set Delegated permission like shown on the picture below for scenario when we need to list O365 groups where user is a member (you will need to choose permissions appropriate for your case of course):

After that error should disappear.

One problem in configuring OAuth authentication for Web API 2

Recently I faced with the following problem when tried to configure OAuth token-based authentication for Web API 2 project. I used the following article as a general guide: Token Based Authentication using ASP.NET Web API 2, Owin, and Identity. When you create new Web API project VS creates also default routes configuration which look like this:

   1: public static class WebApiConfig
   2: {
   3:     public static void Config(HttpConfiguration config)
   4:     {
   5:         config.MapHttpAttributeRoutes();
   6:  
   7:         config.Routes.MapHttpRoute(
   8:             name: "DefaultApi",
   9:             routeTemplate: "{controller}/{id}",
  10:             defaults: new { id = RouteParameter.Optional }
  11:         );
  12:     }
  13: }

And if we will follow the same technique as described in the article above for configuring OAuth authentication we will add the following OAuth config to the startup:

   1: public static class OAuthConfig
   2: {
   3:     public static void Config(IAppBuilder app)
   4:     {
   5:         var OAuthServerOptions = new OAuthAuthorizationServerOptions()
   6:         {
   7:             AllowInsecureHttp = true,
   8:             TokenEndpointPath = new PathString("/token"),
   9:             AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
  10:             Provider = new AuthorizationServerProvider()
  11:         };
  12:  
  13:         app.UseOAuthAuthorizationServer(OAuthServerOptions);
  14:         app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
  15:     }
  16: }

where AuthorizationServerProvider is your custom token provider. Note on TokenEndPointPath property of OAuthServerOptions which contains path to the token generation end point. Now if you will try to get token by accessing http://example.com/token (where instead of http://example.com you need to use your web api host) you will get the following error:

{
  "Message": "No HTTP resource was found that matches the request URI 'http://example.com/token'.",
  "MessageDetail": "No type was found that matches the controller named 'token'."
}

The problem is fixed by commenting out default routing configuration in WebApiConfig shown above:

   1: public static class WebApiConfig
   2: {
   3:     public static void Config(HttpConfiguration config)
   4:     {
   5:         config.MapHttpAttributeRoutes();
   6:  
   7: //            config.Routes.MapHttpRoute(
   8: //                name: "DefaultApi",
   9: //                routeTemplate: "{controller}/{id}",
  10: //                defaults: new { id = RouteParameter.Optional }
  11: //            );
  12:     }
  13: }

But please note that after that it will be mandatory to decorate your controllers and actions by RoutePrefix and Route attributes correspondently;

   1: [RoutePrefix("Account")]
   2: public class AccountController : ApiControllerBase
   3: {
   4:     [AllowAnonymous]
   5:     [HttpPost]
   6:     [Route("Register")]
   7:     public IHttpActionResult Register(UserRegistrationModel userRegistration)
   8:     {
   9:         ...
  10:     }
  11: }

After that token generation endpoint should start work.

Friday, March 24, 2017

Interesting behavior of Sharepoint list views with filters on hidden fields

Sometimes we need to set filter in Sharepoint list view on the hidden field. As hidden fields are not shown in UI we can’t set such filters from the browser, but we can set them programmatically. E.g. suppose that we have field “IsPrivate” which is hidden so end users are not able to change value of this field and we want to show only non-private list items in the list view. List view filter will look like this in this case:

   1: <Where>
   2:   <Eq>
   3:     <FieldRef Name="IsPrivate" />
   4:     <Value Type="Boolean">false</Value>
   5:   </Eq>
   6: </Where>

Now if we will try to modify this list view in UI we will see that its filter section looks like there is no filter at all:

And which is more interesting is that when we will click Save button there – filter on hidden field will remain. I.e. if we will add sorting from UI ViewQuery property will look like this:

   1: <OrderBy>
   2:     <FieldRef Name="Title" />
   3: </OrderBy>
   4: <Where>
   5:     <Eq>
   6:         <FieldRef Name="IsPrivate" />
   7:         <Value Type="Boolean">false</Value>
   8:     </Eq>
   9: </Where>

even though Filter was shown empty in UI. Be aware of this tricky moment to not be confused in situations when filter uses hidden fields.

Monday, March 20, 2017

Problem with outdated value in ModifiedBy search managed property after copying the document

Recently we faced with interesting problem: there is functionality which copies documents from another site collection in Sharepoint Online via SP.RequestExecutor:

   1: var sourceExecutor = new SP.RequestExecutor(sourceSiteUrl);
   2: var targetExecutor = new SP.RequestExecutor(targetSiteUrl);
   3:  
   4: // Get form digest of target site collection
   5: jQuery.ajax({
   6:     url: targetSiteUrl + "/_api/contextinfo",
   7:     type: "POST",
   8:     headers: {
   9:         "Accept": "application/json;odata=verbose"
  10:     },
  11:     success: function (data) {
  12:         try {
  13:             var digest = data.d.GetContextWebInformation.FormDigestValue;
  14:  
  15:             // Build executor action to retrieve the file data.
  16:             var getFileAction = {
  17:                 url: sourceSiteUrl + "/_api/web/GetFileByServerRelativeUrl('" +
  18:                     sourceServerRelativeUrl + "')/$value",
  19:                 method: "GET",
  20:                 binaryStringResponseBody: true,
  21:                 success: function (getFileData) {
  22:                     // Get the binary data.
  23:                     var result = data.body;
  24:                     // Build executor action to copy the file data to the new location.
  25:                     var copyFileAction = {
  26:                         url: targetSiteUrl + "/_api/web/GetFolderByServerRelativeUrl('" +
  27:                             targetFolder.get_serverRelativeUrl() + "')/Files/Add(url='" +
  28:                             fileNameFromInput + "', overwrite=true)",
  29:                         method: "POST",
  30:                         headers: {
  31:                             "Accept": "application/json; odata=verbose",
  32:                             "X-RequestDigest": digest
  33:                         },
  34:                         contentType: "application/json;odata=verbose",
  35:                         binaryStringRequestBody: true,
  36:                         body: getFileData.body,
  37:                         success: function (copyFileData) {
  38:                             ...
  39:                         },
  40:                         error: function (ex) {
  41:                             ...
  42:                         }
  43:                     };
  44:  
  45:                     targetExecutor.executeAsync(copyFileAction);
  46:                 },
  47:                 error: function (ex) {
  48:                     ...
  49:                 }
  50:             };
  51:             sourceExecutor.executeAsync(getFileAction);
  52:         } catch (e) {
  53:             ...
  54:         }
  55:     },
  56:     error: function (ex) {
  57:         ...
  58:     }
  59: });

After file is successfully copied Modified By field shown in list views displays correct value – it shows account of the user which copied the document (i.e. under which account code above was executed). However when we tried to get the value of ModifiedBy managed properly from search index we got value which original file had in the source site collection before it was copied to target site collection (i.e. person which last modified the document in the source site collection). Recrawling of the list didn’t help. Also we tried to explicitly set value of Editor field in document metadata after file has been copied and it didn’t help either.

In order to avoid this issue the following workaround was used: at first as before we get list of the files from search index using javascript object model and then when we get last n results sorted in correct way for displaying in web part we make n requests to content database in order to get correct value of ModifiedBy field (remember that in list views correct account is shown in this field and list views show data from content database):

   1: var documentsItems = [];
   2: var ctx = SP.ClientContext.get_current();
   3: for (var i in documentsFromSearch) {
   4:     var item = documentsFromSearch[i];
   5:     var path = item.Path;
   6:  
   7:     var relativeUrl = _spPageContextInfo.webServerRelativeUrl +
   8:         path.replace(_spPageContextInfo.siteAbsoluteUrl, "");
   9:  
  10:     documentsItems.push({
  11:         RelativeURL: relativeUrl,
  12:         File: null,
  13:         // item["ModifiedBy"] from search contains incorrect  value
  14:         ModifiedBy: item["ModifiedBy"]
  15:     });
  16: }
  17:  
  18: for (var i in documentsItems) {
  19:     var doc = documentsItems[i];
  20:     doc.File = ctx.get_web().getFileByServerRelativeUrl(doc.RelativeURL);
  21:     ctx.load(doc.File, "ListItemAllFields");
  22: }
  23:  
  24: ctx.executeQueryAsync(
  25:     function () {
  26:         for (var i in documentsItems) {
  27:             var doc = documentsItems[i];
  28:             if (typeof (doc.File) == "undefined" || doc.File == null) {
  29:                 continue;
  30:             }
  31:             var item = doc.File.get_listItemAllFields();
  32:             if (typeof (item) == "undefined" || item == null) {
  33:                 continue;
  34:             }
  35:             doc.ModifiedBy = item.get_item("Editor").$5E_1;
  36:         }
  37:     },
  38:     function (sender, args) {
  39:         ...
  40:     });

In this code we at first create array of objects from results returned from search index (lines 3-16) and then for each object we get it’s File object and from it’s list item read Editor value (lines 18-40). Note that for getting editor’s display name here we used undocumented property $5E_1 of SP.FieldUserValue – more proper way is to get user’s id from it’s get_lookupId() method and load it in separate request from web’s user collection, so please be aware of that. Of course this approach is slower than original because it makes n requests to content database, but if data is loaded asynchronously and number of displayed files is not big, performance will remain acceptable and web part will display correct value in ModifiedBy column. Hope that this information will help someone.