Tuesday, October 18, 2016

How to get events history for specific list or document library in Sharepoint

Sometime we need to retrieve events history for specific Sharepoint list or document library for particular period. Internally events log is stored in EventCache table in content database. In order to get it programmatically we can use SPChange* classes from Sharepoint object model. E.g. the following example shows how to get list of all documents which were added today:

   1: private static List<ItemChange> getListItemChanges(SPWeb web,
   2:     SPList list)
   3: {
   4:     try
   5:     {
   6:         if (list == null)
   7:         {
   8:             return new List<ItemChange>();
   9:         }
  10:  
  11:         var today = DateTime.Today;
  12:         var changeTokenStart = new SPChangeToken(
  13:             SPChangeCollection.CollectionScope.List, list.ID,
  14:             today.ToUniversalTime());
  15:         var changeTokenEnd = new SPChangeToken(
  16:             SPChangeCollection.CollectionScope.List, list.ID,
  17:             today.AddDays(1).ToUniversalTime());
  18:         var changeQuery = new SPChangeQuery(false, false);
  19:         changeQuery.Item = true;
  20:         changeQuery.Add = true;
  21:         changeQuery.Delete = false;
  22:         changeQuery.Update = false;
  23:         changeQuery.ChangeTokenStart = changeTokenStart;
  24:         changeQuery.ChangeTokenEnd = changeTokenEnd;
  25:         var changes = list.GetChanges(changeQuery);
  26:  
  27:         var listItemChanges = new List<ItemChange>();
  28:         foreach (SPChangeItem c in changes)
  29:         {
  30:             SPListItem item = null;
  31:             try
  32:             {
  33:                 item = list.GetItemById(c.Id);
  34:             }
  35:             catch (Exception x)
  36:             {
  37:                 Console.WriteLine("Error occured: {0}", x.Message);
  38:                 continue;
  39:             }
  40:  
  41:             if (item == null)
  42:             {
  43:                 continue;
  44:             }
  45:  
  46:             listItemChanges.Add(new ItemChange
  47:                 {
  48:                     Time =
  49: web.RegionalSettings.TimeZone.UTCToLocalTime(c.Time),
  50:                     ChangeType = c.ChangeType,
  51:                     FileName = item.File.Name,
  52:                     Url = SPUrlUtility.CombineUrl(web.Url, item.File.Url)
  53:                 });
  54:         }
  55:         return listItemChanges;
  56:     }
  57:     catch (Exception x)
  58:     {
  59:         Console.WriteLine("Exception occured: {0}\n{1}",
  60:             x.Message, x.StackTrace);
  61:         return new List<ItemChange>();
  62:     }
  63: }

Class ItemChange used in this example if POC helper class defined like this:

   1: public class ItemChange
   2: {
   3:     public DateTime Time { get; set; }
   4:     public SPChangeType ChangeType { get; set; }
   5:     public string FileName { get; set; }
   6:     public string Url { get; set; }
   7: }

At first we create instances of SPChangeToken class where specify change scope (List), id of specific list and start and end time for limiting search (lines 11-17). Then we create SPChangeQuery instance where specify that we want to get changes made for files in our list (line 19) and what kind of event types we want to track (lines 20-22). In our example we set it so only Add events should be returned, in order to return other event types specify true for Delete and Update properties. After that we pass start and end change tokens to appropriate properties of the query (lines 23-24), call SPList.GetChanges method and pass constructed SPChangeQuery object there. On lines 27-55 we iterate through returned results and create more convenient and simple collection of ItemChange objects which together with event time and change type also contain file name and url.

Note that there is also SPChangeQuery.FetchLimit property which by default is set to 1000. If your doclib may contain more changes for specific period, you need to add pagination logic to your code.

Saturday, October 15, 2016

One reason why SPFarm.CurrentUserIsAdministrator() returns false for farm administrators

If you need to check whether current user is farm admin in Sharepoint you may use SPFarm.CurrentUserIsAdministrator method. However you may face with the problem that it returns false even for users which belong to farm admins group. Some posts mention that starting from Sharepoint 2010 if you want to call this method from content web application (not from central admin web application context) you need to use overridden version which receives boolean parameter allowContentApplicationAccess:

   1: SPFarm.CurrentUserIsAdministrator(true)

The problem is that even with this call it may still return false. In order to get correct return value you need to set SPWebService.RemoteAdministratorAccessDenied property to false which can be done e.g. by PowerShell:

   1: $contentService =
   2: [Microsoft.SharePoint.Administration.SPWebService]::ContentService
   3: $contentService.RemoteAdministratorAccessDenied = $false
   4: $contentService.Update()

After that it will be possible to use SPFarm.CurrentUserIsAdministrator() method and do other actions which are not allowed by default, e.g. update SPPersistedObject instances in config database. Be aware however that more power requires more responsibility and with such configuration you should be more careful with security configuration in order to not allow malicious users to harm your system.

Friday, October 14, 2016

Retrieve paginated data from Azure AD via Microsoft Graph Client library

In this post I will show how to retrieve paginated data from Azure AD using MS Graph .Net client library. As we already saw from previous examples (see e.g. Create Azure AD group and set group owner using Microsoft Graph Client library) client library is adopted for asynchronous usage (we can call its methods asynchronously using async/await C# keywords). In addition to that it also restricts number of objects returned from single request, e.g. when you get list of users via GraphServiceClient.Users.Request().GetAsync() call it will return only 100 users. In order to get other users we will need to load all pages by 100 users each. The following example shows how to do it:

   1: static async void enumUsers()
   2: {
   3:     var graph = new GraphServiceClient(new AzureAuthenticationProvider());
   4:     try
   5:     {
   6:         var users = await graph.Users.Request().GetAsync();
   7:         int requestNumber = 1;
   8:         while (users.Count > 0)
   9:         {
  10:             Console.WriteLine("Request number: {0}", requestNumber++);
  11:             foreach (var u in users)
  12:             {
  13:                 Console.WriteLine("User: {0} ({1})", u.DisplayName,
  14:                     u.UserPrincipalName);
  15:             }
  16:  
  17:             if (users.NextPageRequest != null)
  18:             {
  19:                 users = await users.NextPageRequest.GetAsync();
  20:             }
  21:             else
  22:             {
  23:                 break;
  24:             }
  25:         }
  26:     }
  27:     catch (ServiceException x)
  28:     {
  29:         Console.WriteLine("Exception occured: {0}", x.Error);
  30:     }
  31: }
  32:  
  33:  
  34:  
  35: public class AzureAuthenticationProvider : IAuthenticationProvider
  36: {
  37:     public async Task AuthenticateRequestAsync(HttpRequestMessage request)
  38:     {
  39:         string clientId = ConfigurationManager.AppSettings["ClientId"];
  40:         string clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
  41:         string authority = ConfigurationManager.AppSettings["AuthorityUrl"];
  42:     
  43:         AuthenticationContext authContext = new AuthenticationContext(authority);
  44:     
  45:         ClientCredential creds = new ClientCredential(clientId, clientSecret);
  46:     
  47:         Console.WriteLine("before: get token");
  48:         AuthenticationResult authResult = await
  49: authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
  50:         Console.WriteLine("after: get token {0}", authResult.AccessToken);
  51:     
  52:         request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
  53:     }
  54: }

I.e. we call graph.Users.Request().GetAsync() first (line 6) and then call graph.Users.NextPageRequest.GetAsync() (line 19) until next page won’t be empty. For convenience I also added number of request made to Graph API to the console (line 10) so it will be visible how many requests are needed for iterating through all users. For authentication we use application id and secret in custom AzureAuthenticationProvider as described in the following article: Work with Azure AD via Microsoft Graph API.

Thursday, October 13, 2016

Camlex 4.2 and Camlex.Client 2.2 are released

I’m pleasant to announce that today new version of Camlex 4.2 and Camlex.Client 2.2 (Camlex version for client object model) were released. Camlex is free open source library for Sharepoint developers which simplifies creation of CAML queries via using of C# lambda expressions. Camlex is especially useful for creating of dynamic CAML queries when amount of conditions is not known in compile time.

Before to describe changes I need to say that credits of this release go to MarTiX codeplex user which implemented most of necessary functionality and submitted pull request to the Camlex. This is not first time when developers contribute to Camlex project and from my point of view this is great example of open source community cooperation. Thank you MarTiX for your contribution!

In version 4.2/2.2 support for LookupMulti field type was added. Now it became possible to create CAML queries which contain LookupMulti with possibility to add LookupId=”True” attribute to FieldRef like in the following example:

   1: var caml =
   2:     Camlex.Query()
   3:         .Where(x => x["Title"] > (DataTypes.LookupMultiId)"5" &&
   4:             x["Author"] == (DataTypes.LookupMultiValue)"Martin").ToString();

In result we will get the following CAML query:

   1: <Where>
   2:   <And>
   3:     <Gt>
   4:       <FieldRef Name="Title" LookupId="True" />
   5:       <Value Type="LookupMulti">5</Value>
   6:     </Gt>
   7:     <Eq>
   8:       <FieldRef Name="Author" />
   9:       <Value Type="LookupMulti">Martin</Value>
  10:     </Eq>
  11:   </And>
  12: </Where>

As usual code is available in Download section on Codepex and in Nuget:

Install-Package Camlex.NET.dll

or

Install-Package Camlex.Client.dll

There were several requests for this feature on Camlex project hosting site on Codeplex, so I hope that with new version of Camlex developers will produce more efficient code.

Monday, October 10, 2016

Create Azure AD group and set group owner using Microsoft Graph Client library

In one of my previous posts I wrote about basic configuration steps needed for working with Azure AD via Microsoft Graph Client library (Work with Azure AD via Microsoft Graph API). These steps included registering app within Azure AD and creating authentication provider for authenticating requests with client id and secret from registered app. In this post I will show how to create Azure AD group and set it’s owner programmatically. We will use the same AzureAuthenticationProvider which we used in previous example:

   1: static async void createGroupWithOwner()
   2: {
   3:     var graph = new GraphServiceClient(new AzureAuthenticationProvider());
   4:     try
   5:     {
   6:         string groupName = string.Format("NewGroup-{0}",
   7:             DateTime.Now.ToString("yyyy-MM-dd_HH_mm"));
   8:  
   9:         var groupInfo = new Group();
  10:         groupInfo.DisplayName = groupName;
  11:         groupInfo.MailEnabled = false;
  12:         groupInfo.MailNickname = groupName;
  13:         groupInfo.SecurityEnabled = true;
  14:  
  15:         var group = await graph.Groups.Request().AddAsync(groupInfo);
  16:  
  17:         User owner = null;
  18:         var users = await graph.Users.Request().GetAsync();
  19:         foreach (var u in users)
  20:         {
  21:             if (u.UserPrincipalName.Contains("username@example.com"))
  22:             {
  23:                 owner = u;
  24:                 break;
  25:             }
  26:         }
  27:         
  28:         if (user == null)
  29:         {
  30:             return;
  31:         }
  32:  
  33:         await graph.Groups[group.Id].Owners.References
  34:             .Request().AddAsync(owner);
  35:     }
  36:     catch (ServiceException x)
  37:     {
  38:         Console.WriteLine("Exception occured: {0}", x.Error);
  39:     }
  40: }
  41:  
  42:  
  43:  
  44: public class AzureAuthenticationProvider : IAuthenticationProvider
  45: {
  46:     public async Task AuthenticateRequestAsync(HttpRequestMessage request)
  47:     {
  48:         string clientId = ConfigurationManager.AppSettings["ClientId"];
  49:         string clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
  50:         string authority = ConfigurationManager.AppSettings["AuthorityUrl"];
  51:     
  52:         AuthenticationContext authContext = new AuthenticationContext(authority);
  53:     
  54:         ClientCredential creds = new ClientCredential(clientId, clientSecret);
  55:     
  56:         Console.WriteLine("before: get token");
  57:         AuthenticationResult authResult =
  58: await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds);
  59:         Console.WriteLine("after: get token {0}", authResult.AccessToken);
  60:     
  61:         request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
  62:     }
  63: }

At first we create group (lines 6-15) and add time stamp to the group name (although there may be groups in Azure AD with the same display name, for clearance distinguish them by name). After that we find user by email (lines 17-26) and finally add this user to groups owners (lines 28-34). For convenience I also added here code of AzureAuthenticationProvider from previous example.