sharepoint-people-picker.jpg

SharePoint People Picker Lessons Learned

Bo is a Principal Consultant for ThreeWill. He has 18 years of full lifecycle software development experience.

Recently I was working on a custom Angular 5 application running in a SharePoint Online site collection and needed to develop a custom people picker control.  Fortunately, I’d built a SharePoint people picker previously for a SharePoint 2013 on-premise Angular application.  Not wanting to re-invent the wheel, I simply pulled my shared components and necessary service layer code over and Bob’s your uncle.

A few weeks later, and completely by accident I noticed some differences in the results I was getting back when logged in as one user versus another. The differences appeared to be related to whether I was logged in as a tenant user or a guest user.  Oddly, my tenant-based, site collection administrator was NOT getting as many users back as my guest user who was a member of a different tenant.

As it turns out, my code was apparently really using the wrong REST API or at least wrong for what I was hoping to achieve. I was using the REST API SP.Utilities.Utility.SearchPrincipalsUsingContextWeb that has been around forever and has served me well.  However, what I should have been using is SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.ClientPeoplePickerSearchUser which is really a more robust and useful API for finding users.  Especially, if you are building a custom people picker on top of the API and might need to deal with tenant and guest users.

Below are some code samples and JSON outputs of both approaches to visualize what I learned the hard way.  Note that both approaches utilize the PnP.  Honestly, I wouldn’t build a client based SharePoint application today without PnP.  Whether the application is React or Angular, PnP is your friend.

Using the “old” way (sp.utility.searchPrincipals) you can see my return JSON object is a little “deep” but I just need to iterate over the result.SearchPrincipalsUsingContextWeb.results to get my users. Nothing fancy here, I’m just passing the search text from the user and trying to find all users that match.

When logged in as a tenant user [email protected] and searching for “Bo G” I only received a single result, my self in this case. Why I wasn’t finding the guest version of me really stumped me. Especially given the results I was seeing for the guest version of me below

{
    "d": {
        "SearchPrincipalsUsingContextWeb": {
            "__metadata": {
                "type": "Collection(SP.Utilities.PrincipalInfo)"
            },
            "results": [
                {
                    "Department": "Delivery",
                    "DisplayName": "Bo George (Admin)",
                    "Email": "[email protected]",
                    "JobTitle": "Principal Consultant",
                    "LoginName": "i:0#.f|membership|[email protected]",
                    "Mobile": "",
                    "PrincipalId": -1,
                    "PrincipalType": 1,
                    "SIPAddress": null
                }
            ]
        }
    }
}

When logged in as a GUEST user [email protected] and searching for “Bo G” I received two results, my self plus the tenant version of me. This baffled me because if I would expect any filtering it might be that guests should only find other guests.  It appeared to find all users in the site collection, which is what I wanted for my tenant user as well.

{
    "d": {
        "SearchPrincipalsUsingContextWeb": {
            "__metadata": {
                "type": "Collection(SP.Utilities.PrincipalInfo)"
            },
            "results": [
                {
                    "Department": "Delivery",
                    "DisplayName": "Bo George (Admin)",
                    "Email": "[email protected]",
                    "JobTitle": "Principal Consultant",
                    "LoginName": "i:0#.f|membership|[email protected]",
                    "Mobile": null,
                    "PrincipalId": 8,
                    "PrincipalType": 4,
                    "SIPAddress": "[email protected]"
                },
                {
                    "Department": null,
                    "DisplayName": "Bo George",
                    "Email": "[email protected]",
                    "JobTitle": null,
                    "LoginName": "i:0#.f|membership|bgeorge_threewill.com#ext#@bothreewill.onmicrosoft.com",
                    "Mobile": null,
                    "PrincipalId": 12,
                    "PrincipalType": 4,
                    "SIPAddress": null
                }
            ]
        }
    }
}

Since all the above was really just throwing me for a loop so I went on a quest and discovered what I think is a new way (and hopefully right way). I haven’t found any guidance saying we should definitely query for people using this API but so far in my experience, it gives me everything I want to know and more. More importantly, using this new way gives me the results I would expect to see given each type of user tenant or guest. This includes dealing with the obvious concerns about GUEST users from across different tenants.

Using the “new” way (sp.profiles.clientPeoplePickerSearchUser) you can see I just need to iterate over the result to get my users. No deep object hierarchy just to get to what I need to iterate over, it already feels better.

Ahh! Now these were the results I was expecting. Actually, they were even better as they also tell me more readily which users are guests in case I need to know that sort of thing.

{
    "d": {
        "ClientPeoplePickerSearchUser": [
            {
                "Key": "i:0#.f|membership|bgeorge_threewill.com#ext#@bothreewill.onmicrosoft.com",
                "DisplayText": "Bo George",
                "IsResolved": true,
                "Description": "bgeorge_threewill.com#EXT#@bothreewill.onmicrosoft.com",
                "EntityType": "User",
                "EntityData": {
                    "IsAltSecIdPresent": "True",
                    "ObjectId": "9f565d53-d508-4628-b56e-ffbd42a3e3f7",
                    "Title": "[email protected]",
                    "Email": "[email protected]",
                    "MobilePhone": "",
                    "PrincipalType": "GUEST_USER",
                    "OtherMails": "[email protected]",
                    "Department": ""
                },
                "MultipleMatches": [],
                "ProviderName": "ExtranetUsers",
                "ProviderDisplayName": "Extranet Users"
            },
            {
                "Key": "i:0#.f|membership|[email protected]",
                "DisplayText": "Bo George (Admin)",
                "IsResolved": true,
                "Description": "[email protected]",
                "EntityType": "User",
                "EntityData": {
                    "IsAltSecIdPresent": "False",
                    "ObjectId": "357a79fa-3555-4fcb-8187-8b71761317f2",
                    "Title": "Principal Consultant",
                    "Email": "[email protected]",
                    "MobilePhone": "",
                    "OtherMails": "[email protected]",
                    "Department": "Delivery"
                },
                "MultipleMatches": [],
                "ProviderName": "Tenant",
                "ProviderDisplayName": "Tenant"
            }
        ]
    }
}

I often try to internalize lessons from things like this and what I’ve filed away in the back of my mind for the next time something strange like this happens (and there will surely be a next time) is that even when you’ve been relying on the REST APIs to keep your code as platform version independent there can still be times that it is.  While SharePoint Online is based on earlier on-premise versions of SharePoint, it has moved on and that’s a good thing.


Related Posts

Bo GeorgeSharePoint People Picker Lessons Learned