As everyone knows by now, Microsoft is pushing all development out to the client-side. But most of the time, I find customers who desire customization want a user experience that is somewhat tailored to the current user. Like managers should see one thing when they log in, but regular users should see something else. That means that on the client-side, I need to be able to distinguish managers from other users. That’s normally done by assigning the users to groups. But in large organizations, that usually means Active Directory groups, which are then added to SharePoint groups. This leads to a problem, because from the client-side, there is no way to determine if the user has membership in a SharePoint group to which they’ve been added indirectly (i.e. through membership in an Active Directory group).
To demonstrate, I have a SharePoint site, and it has the following site group membership:
I’m only directly in the owners group, but I have membership in the Active Directory security groups Managers and HumanResources. So I’m indirectly in the members and visitors groups too.
If I go to Site Settings -> Site Permissions, click on Check Permissions, enter my user name, and click the Check Now button, I get the following dialog:
The result is as I would expect. It shows that I have Full Control, Edit, and Read through the owners, members and visitors groups respectively.
Now I run the following JavaScript, which is just a bit of jQuery to call the web service to get all groups to which the current user belongs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $.ajax({ url: _spPageContextInfo.webAbsoluteUrl + "/_api/web/currentuser/groups", async: false, headers: { 'accept': 'application/json;odata=nometadata' }, complete: function(request) { console.log("CurrentUser: " + JSON.stringify(request, null, 4)); my_loginName = request.responseJSON.LoginName; }, error: function(request) { console.log(JSON.stringify(request, null, 4)); } }); |
The JSON response is:
1 2 3 4 5 6 | { "value": [{ "Id": 7, "Title": "CSRDemos Owners", }] } |
Huh…that kind of sucks. It only shows me in the owners group. And the results are the same no matter what method I use to try to determine my SharePoint groups from the client side. ASMX and/or CSOM/JSOM both also only show me in the owners group. The reason is that all of these methods only return groups to which the user has been added directly. Any group that I’m in only through membership in an Active Directory security group does not come back.
This has also been the case in all versions of SharePoint that have any kind of API to access this stuff client-side (i.e. from SharePoint 2007 all the way up to SharePoint Online).
The only way around this is to write some server-side code. Specifically, I’m going to write a quick and dirty web service to give me all SharePoint groups for the current user (direct or indirect). To do that, I’m going to use the .Net classes PrincipalContext and UserPrincipal, both of which are in the System.DirectoryServices.AccountManagement namespace, so I need to add the following assembly reference to my web.config (really the web.config for each web application from which I want to be able to call this):
1 | <add assembly="System.DirectoryServices.AccountManagement, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> |
With that done, I can now write my web service, and here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | <@WebService Language="C#" Class="SPEasyForms.UserGroups"> using System; using System.Collections.Generic; using System.DirectoryServices.AccountManagement; using System.Web.Script.Services; using System.Web.Services; using Microsoft.SharePoint; namespace SPEasyForms { public class SharePointGroup { public int ID { get; set; } public string Name { get; set; } } [WebService (Namespace = "http://SPEasyForms/")] [WebServiceBinding (ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem (false)] [System.Web.Script.Services.ScriptService] public class UserGroups : System.Web.Services.WebService { /// /// Get all SharePoint groups for the current user. /// [WebMethod] [ScriptMethod (UseHttpGet = false, ResponseFormat = ResponseFormat.Json)] public SharePointGroup[] GetCurrentUserSharePointGroups () { // this will be converted to an array and returned List result = new List (); // save a couple things from the context to be used in the elevated privileges string user = SPContext.Current.Web.CurrentUser.LoginName; string webUrl = SPContext.Current.Web.Url; SPSecurity.RunWithElevatedPrivileges (() => { // call the helper method to get back the users expanded AD groups ListadGroups = new List (GetAuthorizationGroupsImpl (user)); using (SPSite site = new SPSite (webUrl)) { SPWeb web = site.RootWeb; SPGroupCollection groups = web.SiteGroups; // loop through all site collection groups foreach (SPGroup g in groups) { // loop through all members of the site collection group SPUserCollection users = g.Users; foreach (SPUser u in users) { // if the member is the current user, add the group to results // and break out to check the next group (note: can't user g.ContainsCurrentUser // because we're elevated, so current user is the system account) if (user == u.LoginName) { SharePointGroup current = new SharePointGroup () { ID : g.ID, Title : g.Name }; result.Add (current); } else { string samActName = u.Name.ToLower (); if (samActName.Contains ("\\")) { samActName = samActName.Substring (samActName.IndexOf ("\\") + 1); } // if the member is an AD group that is in the user's expanded list of // AD groups, add the group to results and break out to check the next group if (adGroups.Contains(samActName)) { SharePointGroup current = new SharePointGroup() { ID: g.ID, Title: g.Name }; result.Add(current); } } } } } }); return result.ToArray(); } [WebMethod] [ScriptMethod (UseHttpGet = false, ResponseFormat = ResponseFormat.Json)] public string[] GetCurrentUserAuthorizationGroups () { string[] result = null; string user = SPContext.Current.Web.CurrentUser.LoginName; SPSecurity.RunWithElevatedPrivileges (() => { result = GetAuthorizationGroupsImpl (user); }); return result; } private static string[] GetAuthorizationGroupsImpl (string user) { List result = new List (); // first initialize the context for the current domain using (PrincipalContext ctx = new PrincipalContext(ContextType.Domain)) { // find the user within the context UserPrincipal principal = (UserPrincipal)Principal.FindByIdentity(ctx, user); if (principal != null) { // get authorization groups returns all groups the user is a member of (even indirectly) PrincipalSearchResult groups = prindipal.GetAuthorizationGroups(); IEnumerator enumerator = groups.GetEnumerator(); while(enumerator.MoveNext()) { result.Add(enumerator.Current.SamAccountName.ToLower()); } } } return result.ToArray(); } } // end of class } // end of namespace |
For simplicity sake, I’ve done the entire service in a single file with no code behind, so it can just be dropped into the layouts directory and you can start using it. In any real world situation, it should really be compiled into a DLL and deployed as a WSP.
The code is actually pretty simple. At it’s heart, I use the PrindipalContext to get the current user as a UserPrincipal. I then call UserPrincipal.GetAuthorizationGroups(), which returns an expanded list of all groups to which the principal is a member (i.e. it expands out nested groups). Finally, I loop through all of the
SharePoint groups and check if the user, or one of the user’s AD groups, is a member of each SharePoint group.
Note that the classes PrincipalContext and UserPrincipal were introduced in .Net 4.0, which means this solution only works for SharePoint 2013 and beyond (and not counting Online of course, since you can’t deploy server side code in that environment).
Also note that I’m performing these operations at elevated privileges. This isn’t that big of an issue, since the only functionality I expose through this web service is that the user can get a complete list of their own SharePoint groups (or Active Directory groups), and there isn’t any security issue with that. If I wanted to change the interface such that it takes in the user name as a parameter (i.e. it could be used to retrieve groups for users other than the current user), that would be a different story. In that case, I would remove the elevated privileges, resulting in only privileged users being able to call the method successfully.
Once it’s in the layouts directory, just call it like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $.ajax({ url: _spPageContextInfo.webAbsoluteUrl + "/_layouts/15/MyUserGroups.asmx", async: false, headers: { 'accept': 'application/json;odata=nometadata' }, complete: function(request) { console.log("CurrentUser: " + JSON.stringify(request, null, 4)); my_loginName = request.responseJSON.LoginName; }, error: function(request) { console.log(JSON.stringify(request, null, 4)); } }); |
And the JSON response is:
1 2 3 4 5 6 7 8 9 10 | [{ "Id": 7, "Title": "CSRDemos Owners", },{ "Id": 8, "Title": "CSRDemos Members", },{ "Id": 9, "Title": "CSRDemos Visitors", }] |
And that’s more like it. It wasn’t that hard. Kind of makes you wonder why Microsoft hasn’t done it sometime in the last 12 years. The OOB groups web services are really pretty useless as they are.
Just remember that the limitations are that it only works on 2013 and later, but not in SharePoint Online. It can be done on older versions of SharePoint too, but probably only by calling Active Directory multiple times to expand out groups (possibly many times). And maybe it could be done in SharePoint Online using something like an Azure function, but if so, that’s a different blog post.
Reference