Assume you have a task list in your Project Web Access (PWA) site or on one of the Project Web Sites (PWS) in your Project Server and you would like to restrict the users available in the Assigned To field (field type of ‘Person or Group‘) to users who are specified as Enterprise Resource in Project Server, that is running in the “classical” Project Server permission mode, and not in the new SharePoint Server permission mode. There is no synchronization configured between Active Directory groups and Project Server resources.
You can limit a ‘Person or Group‘ field to a specific SharePoint group, but there is no built-in solution to sync enterprise resources to SharePoint groups. In this post I show you, how to achieve that via PowerShell and the managed client object models of Project Server and SharePoint.
Note: You could get the login names of users assigned to the enterprise resources via REST as well (http://YourProjectServer/PWA/_api/ProjectServer/EnterpriseResources?$expand=User&$select=User/LoginName), but in my sample I still use the client object model of Project Server.
My goal was to create a PowerShell solution, because it makes it easy to change the code on the server without any kind of compiler. I first created a C# solution, because the language elements of C# (like extension methods, generics and LINQ) help us to write compact, effective and readable code. For example, since the language elements of PowerShell do not support the LINQ expressions, you cannot simply restrict the elements and their properties returned by a client object model request, as I illustrated my former posts here, here and here. Having the working C# source code, I included it in my PowerShell script as literal string and built the .NET application at runtime, just as I illustrated in this post. In the C# code I utilized an extension method to help automated batching of the client object model request. More about this solution can be read here.
The logic of the synchronization is simple: we read the list of all non-generic enterprise resources, and store the login names (it the user exists) as string in a generic list. Then read the members of the SharePoint group we are synchronizing and store their login names as string in another generic list. Finally, we add the missing users to the SharePoint group and remove the extra users from the group.
The final code is included here:
- $pwaUrl = "http://YourProjectServer/PWA";
- $grpName = "AllResourcesGroup";
- $referencedAssemblies = (
- "Microsoft.SharePoint.Client, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
- "Microsoft.SharePoint.Client.Runtime, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
- "Microsoft.ProjectServer.Client, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c",
- "System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
- $sourceCode = @"
- using System;
- using System.Linq;
- using System.Collections.Generic;
- using Microsoft.SharePoint.Client;
- using Microsoft.ProjectServer.Client;
- public static class Extensions
- {
- // batching to avoid
- // Microsoft.SharePoint.Client.ServerException: The request uses too many resources
- // https://msdn.microsoft.com/en-us/library/office/jj163082.aspx
- public static void ExecuteQueryBatch<T>(this ClientContext clientContext, IEnumerable<T> itemsToProcess, Action<T> action, int batchSize)
- {
- var counter = 1;
- foreach (var itemToProcess in itemsToProcess)
- {
- action(itemToProcess);
- counter++;
- if (counter > batchSize)
- {
- clientContext.ExecuteQuery();
- counter = 1;
- }
- }
- if (counter > 1)
- {
- clientContext.ExecuteQuery();
- }
- }
- }
- public static class Helper
- {
- public static void SyncGroupMembers(string pwaUrl, string grpName)
- {
- List<string> resLogins = new List<string>();
- List<string> grpLogins = new List<string>();
- var batchSize = 20;
- using (var projectContext = new ProjectContext(pwaUrl))
- {
- var resources = projectContext.EnterpriseResources;
- projectContext.Load(resources, rs => rs.Where(r => !r.IsGeneric).Include(r => r.User.LoginName));
- projectContext.ExecuteQuery();
- resLogins.AddRange(resources.ToList().Where(r => r.User.ServerObjectIsNull == false).ToList().Select(r => r.User.LoginName.ToLower()));
- }
- using (var clientContext = new ClientContext(pwaUrl))
- {
- var web = clientContext.Web;
- var grp = web.SiteGroups.GetByName(grpName);
- clientContext.Load(grp, g => g.Users.Include(u => u.LoginName));
- clientContext.ExecuteQuery();
- grpLogins.AddRange(grp.Users.ToList().ToList().Select(u => u.LoginName.ToLower()));
- var usersToAdd = resLogins.Where(l => !grpLogins.Contains(l));
- clientContext.ExecuteQueryBatch<string>(usersToAdd,
- new Action<string>(loginName =>
- {
- var user = web.EnsureUser(loginName);
- grp.Users.AddUser(user);
- }),
- batchSize);
- var usersToRemove = grpLogins.Where(l => !resLogins.Contains(l));
- clientContext.ExecuteQueryBatch<string>(usersToRemove,
- new Action<string>(loginName =>
- {
- grp.Users.RemoveByLoginName(loginName);
- }),
- batchSize);
- }
- }
- }
- "@
- Add-Type -ReferencedAssemblies $referencedAssemblies -TypeDefinition $sourceCode -Language CSharp;
- [Helper]::SyncGroupMembers($pwaUrl, $grpName)
