четверг, 7 мая 2020 г.

Using SharePoint REST APIs from Azure Functions with .net core


There is often a need to build C# APIs that update something or look something up from SharePoint online. Currently, if using Azure Functions and .net core, we are left with only one officially supported option: Using the Graph APIs.

To use Graph APIs we must register an app in Azure AD App registrations and grant it app permission to SharePoint-related scopes. The problem with this approach is that we cannot limit the app permissions to a certain site or list – it can only cover the entire tenant. This approach works for demo tenants, but often not possible in enterprise environments.

With SharePoint app registrations created with appregnew.aspx it is possible to limit the app permissions by using the app permissions XML, but these apps cannot be used with graph APIs. Using these apps required CSOM or SharePoint PnP packages, which are not available for .net core. 

It seems that we are forced to either create classic web apps with Web APIs or use Azure Functions v1 that still support .net framework. These are valid options, but they sound outdated in 2020.

 I was looking for something better, and I found one good option to use with .net core. You can use the SharePoint REST APIs directly without any third party libraries, but you need to obtain an access token via your own code. 

To obtain the access token, we would need a SharePoint app registration created with /_layouts/15/appregnew.aspx.

First, we need to get the OAuth2 endpoint for your tenant. To do it, I created the following function that goes to tenants endpoint reference page, and pickes the Oauth2 endpoint URL:

private static async Task<string> GetAuthUrl(string realm)
{
    var url = "https://accounts.accesscontrol.windows.net/metadata/json/1?realm=" + realm;
    HttpClient hc = new HttpClient();
    var r = await (await hc.GetAsync(url)).Content.ReadAsAsync<AccessControlMetadata>();
    return r.endpoints.FirstOrDefault(e => e.protocol == "OAuth2")?.location;
}

The next step is to get the access token. Here is the a function for that:

public static async Task<string> GetAccessToken(Uri siteUrl, string clientId, string tenantId, string clientSecret)
{
    var authUrl = await GetAuthUrl(tenantId);
    // This is the SharePoint Service Principal ID, it is the same in all tenants
    string serviceId = "00000003-0000-0ff1-ce00-000000000000";
    var _resource = $"{serviceId}/{siteUrl.Authority}@{tenantId}";
    var fullClientId = $"{clientId}@{tenantId}";
    HttpClient hc = new HttpClient();
    FormUrlEncodedContent content = new FormUrlEncodedContent(new KeyValuePair<string, string>[] {
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
        new KeyValuePair<string, string>("client_id", fullClientId),
        new KeyValuePair<string, string>("client_secret", clientSecret),
        new KeyValuePair<string, string>("resource", _resource)
    });
    HttpResponseMessage httpResponseMessage = await hc.PostAsync(authUrl, content);
    var result = await httpResponseMessage.Content.ReadAsAsync<AcsResults>();
    return result.access_token;
}

With these 2 functions, you can then use the SharePoint REST APIs without any NuGet packages or dependencies:

var url = "https://inyushin.sharepoint.com/sites/test";
var accessToken = await GetAccessToken(new Uri(url), clientId, tenantId, clientSecret);
using (var hc = new HttpClient())
{
    hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    hc.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata=verbose"));
   
    var r = await hc.GetAsync("https://inyushin.sharepoint.com/sites/test/_api/web?$select=Title");
    var str = await r.Content.ReadAsStringAsync();
}

I hope you find this useful. Feel free to comment or propose improvements.

вторник, 13 октября 2015 г.

Display SharePoint list item or folder permissions in a list view

It is a common scenario that users need to create a document library with folders, so that each folder would have unique permissions. To do so, the site owner breaks permission inheritance on the folders and assigns the permissions as needed. In the end, the permission model gets complicated, and the challenge challenge is to quickly find what are the actual permissions on a specific folder or a specific list item at a glance.

The Out of the Box tools include a column called "Shared With", which allows to quickly access the list of users who have access to the item, but when there are more than 2-3 groups it just shows shared with "Multiple people". To overcome this limitation and show all groups and users who have access to the item, I created a list column column with Client Side Rendering technique and some JavaScript. The column displays a list of all users and groups who have access to the folder or document, and renders a link that points directly to the "Manage Permissions" page for this item. When you have to set up permissions for tens of folders, this saves some grey hair.

The end result is on the picture below.

Permissions column shows the actual permissions on each folder and list item








The main challenge in implementing this column was to get CSR (Client Side Rendering) to work together with Minimal Download Strategy enabled or disabled. To do so, there is some additional startup code in the JavaScript file:

    Ivan.PermissionsField.Functions.RegisterField = function () {
        SPClientTemplates.TemplateManager.RegisterTemplateOverrides(Ivan.PermissionsField)
    }

    Ivan.PermissionsField.Functions.MdsRegisterField = function () {
        var thisUrl = _spPageContextInfo.siteServerRelativeUrl
            + '/SiteAssets/JSLink.js';
        Ivan.PermissionsField.Functions.RegisterField();
        RegisterModuleInit(thisUrl, Ivan.PermissionsField.Functions.RegisterField)
    }

    if (typeof _spPageContextInfo != "undefined" && _spPageContextInfo != null) {
        Ivan.PermissionsField.Functions.MdsRegisterField()
    } else {
        Ivan.PermissionsField.Functions.RegisterField()
    }

Once the script file is deployed to the site's "Site Assets" folder, the only step that remains is to create a basic column called Permissions and assign a ScriptLink to it. In my case it is done with a C# console app that uses the SharePoint Client API to perform the actions:


var fieldName = "Permissions";
var ctx = GetClientContext(url);
ctx.Load(ctx.Web.Fields, fc=>fc.Include(f=>f.Title));
ctx.ExecuteQuery();
if(ctx.Web.Fields.ToList().FindIndex(f=>f.Title == fieldName) == -1)
{
    // Create a field called Permissions
    var newField = "<Field Type=\"Text\" DisplayName=\"Permissions\" Name=\"Permissions\"/>";
    ctx.Web.Fields.AddFieldAsXml(newField, false, AddFieldOptions.DefaultValue);
    ctx.ExecuteQuery();
}
var field = ctx.Web.Fields.GetByTitle(fieldName);
ctx.Load(field);
ctx.ExecuteQuery();
field.JSLink = "clienttemplates.js|~site/SiteAssets/PermissionField.js";
field.Update();
ctx.ExecuteQuery();


The entire JS file can be downloaded from here. Please, feel free to improve the solution.
If you use it, please mention my blog as the origin.



четверг, 17 марта 2011 г.

Creating State workflows with SharePoint Designer and InfoPath


I just want to share our experience about creating quite complex workflows with SharePoint designer. 

The main problem with SPD is that you can’t create loops in your workflow – it can only proceed to next step or wait for something. This is why, for example, it is hard to implement things like custom approval workflows with SPD, as they might require returning the document to some previous state and getting additional reviews/comments.

In the internet there was an idea that you can start another workflow by creating a list item in some dummy list, and if needed you can implement some kind of looping with this technique.

We considered this as too much of unneeded customization and complications, and came to a new approach. Firs of all, you need an InfoPath form that will store 2 additional fields: ProcessState and PrevProcessState. Each action that the user makes in the InfoPath form will save the current value of ProcessState into PrevProcessState and update ProcessState to a new value.

Then you create a WF that starts every time an item is changed and create many steps that have something like “If ProcessState=XXX and PrevProcessState=YYY then …”. Using this idea you can implement any workflow you need with SPD+InfoPath. Each of this IF statements in the workflow will represent a transition between 2 states in the workflow, therefore in the end you get a way to create State Machine Workflows instead of Activity WFs. I think it is a great possibility. 

People Picker performance in SharePoint 2010


When you have large and complicated AD structure you will probably run into problems with PeoplePicker controls in SharePoint. Either they will be working slowly or not working at all. We faced a problem that they were not possible to use because of the slowness.

According to official MSDN documentation, PeoplePicker is first sending a DNS query to determine the nearest Global Catalog server, then sends several LDAP requests. Sometimes the DNS can affect the performance, and sometimes it is the LDAP itself. Network latency also might be an issue. 

In our case the poor performance of People Picker could not be explained by latency/server load. Typing my last name "Inyushin" in the control and clicking the "Check user names" icon in people picker caused a huge delay - about 5 minutes. When using samAccountName or email the people picker was working quickly but searching by last name or first name was too slow. Another interesting symptom was that searching users using the search dialog was working normally.

We started to investigate and ended up with the following solution:

1. Configured the people picker by using the following PowerShell script to avoid querying AD when possible:

$web = get-spweb("http://webapp_root")
$webapp = $web.Site.WebApplication
$ps = $webapp.PeoplePickerSettings
$ps.ActiveDirectoryRestrictIsolatedNameLevel=$true
$webapp.Update()

2. Configured people picker to search accounts in our domains by using the following STSADM commands

stsadm.exe -o setapppassword -password P@SSWORD
 stsadm.exe -o setproperty -pn peoplepicker-searchadforests -pv "domain:d1.company.com,d1\account_name,P@SSWORD;domain:d2.company.com,d2\account_name,P@SSWORD"
Having done so the people picker started to work quickly and resolve the user names within seconds.

пятница, 4 марта 2011 г.

Modifying the SharePoint Taxonomy Fields in code

While investigating this issue, I have found several posts that sugested a quite complicated way to modify taxonomy fields through code. Here comes some code that would to exactly that in a quite simple way. I have found this method in internet, but modified it to use the AnchorId of the taxonomy field, as otherwise it won't work with fields that are not mapped to the root of a term store.


public static void SetTaxonomyFieldValue(SPWeb web, SPListItem item, SPField field,
 string value, bool isAddingValuesToTermSetAllowed)
{
    // Gets the taxonomy field instance
    TaxonomyField managedField = field as TaxonomyField;
    // Gets the current taxonomy session
    TaxonomySession session = new TaxonomySession(web.Site, false);
    // Gets the term store (by SspId)
    var termStoreCol = session.TermStores[managedField.SspId];
    // Gets the terms of a specific term set (by TermSetId)
    var parentTerm = termStoreCol.GetTerm(managedField.TermSetId, managedField.AnchorId);
    var termCollection = parentTerm.Terms;
    if (isAddingValuesToTermSetAllowed)
    {
        if (!parentTerm.Terms.Any(t => t.Labels.Any(l => l.Value == value)))
        {
            parentTerm.CreateTerm(value, 1033);
            termStoreCol.CommitAll();
            SetTaxonomyFieldValue(web, item, field, value, false);
            return;
        }
    }
    // Sets the field value using the list of terms
    if (managedField.AllowMultipleValues)
    {
        var listTerms = new List<Term>();
        listTerms.Add(termCollection[value]);
        managedField.SetFieldValue(item, listTerms);
    }
    else
    {
        managedField.SetFieldValue(item, termCollection[value]);
    }
    item.Update();
}