Declarative authorization in REST services in SharePoint with F# and ServiceStack

This post is a short overview of my talk at Belarus SharePoint User Group at 2013/06/27.

The primary goals were to find an efficient declarative way to specify authorization on REST service (that knows about SharePoint built in security model) and try F# in SharePoint on a real-life problem. Service Stack was selected over ASP.NET Web API because I wanted to find a solution that operates in SharePoint 2010 on .NET 3.5.

This solution was inspired by the work of Matt Cowan “Building a Web API in SharePoint 2010 with ServiceStack“.

The solution can be re-implemented in 6 steps:

Step #1.  Build strongly typed version of Service Stack.

It is probably the hardest step of all. We need to download source code of following GitHub projects:

Sign all projects and rebuild them. You should get the following assemblies:

  • ServiceStack.dll 
  • ServiceStack.Common.dll
  • ServiceStack.Interfaces.dll
  • ServiceStack.OrmLite.dll
  • ServiceStack.OrmLite.SqlServer.dll
  • ServiceStack.Redis.dll
  • ServiceStack.ServiceInterface.dll
  • ServiceStack.Text.dll

Step #2. Create signed F# library project.

To be able to use F# in SharePoint we need to:

  • Sign F# library to be able to deploy it in GAC (It can be done using AssemblyKeyFileAttribute)
  • Add our custom library and Fsharp.Core.dll to the SharePoint Package additional assemblies list.

Step #3. Create custom SharePoint bootstrapper that starts Service Stack.

 

open System
open Microsoft.SharePoint.ApplicationRuntime
open ServiceStack.WebHost.Endpoints

type ServiceStackGlobalAppHost() =
    inherit AppHostBase("Service Stack Web Services", 
                           typeof<ServiceStackGlobalAppHost>.Assembly) 

    override this.Configure(container) =
        //Set JSON web services to return idiomatic JSON camelCase properties
        ServiceStack.Text.JsConfig.EmitCamelCaseNames <- true
        base.SetConfig(EndpointHostConfig(
                        ServiceStackHandlerFactoryPath = "_layouts/api"));

type ServiceStackGlobalApplication() =
    inherit SPHttpApplication()
    member this.Application_Start(sender:obj, e:EventArgs) =
        (new ServiceStackGlobalAppHost()).Init()

Step #4. Register custom bootstrapper  in Global.asax.

spRESR_globalASAX

Step #5. Modify Web.Config.

spRESR_webConfig

Step #4-5: Both steps can be automated in WebApplication level feature.

open System
open System.IO
open Microsoft.SharePoint
open Microsoft.SharePoint.Administration

let private CONFIG_MOD_OWNER_NAME = "myWebConfigEntries"

/// Container to hold info about our modifications to the web.config.
type private WebConfigEntry(name, xPath, value, modificationType, keepOnDeactivate) =
    member this.Prepare() =
        SPWebConfigModification(
            name, xPath, 
            Owner = CONFIG_MOD_OWNER_NAME, 
            Sequence = 0u,
            Type = modificationType, 
            Value = value)

let private configEntries = 
    [ WebConfigEntry(
        "location[@path='_layouts/api']",
        "configuration",
        sprintf 
           """<location path="_layouts/api">
                <system.web>
                  <authorization>
                    <allow users="*" />
                  </authorization>
                  <httpHandlers>
                    <add path="*" type="%s" verb="*" />
                  </httpHandlers>
                </system.web>
                <system.webServer>
                  <modules runAllManagedModulesForAllRequests="true" />
                  <validation validateIntegratedModeConfiguration="false" />
                  <handlers>
                    <add path="*" name="ServiceStack.Factory" type="%s" verb="*" preCondition="integratedMode" resourceType="Unspecified" />
                  </handlers>
                </system.webServer>
              </location>"""
              typeof<ServiceStack.WebHost.Endpoints.ServiceStackHttpHandlerFactory>.AssemblyQualifiedName
              typeof<ServiceStack.WebHost.Endpoints.ServiceStackHttpHandlerFactory>.AssemblyQualifiedName,
        SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode,
        false)]

type InfrastructureEventReceiver() =
    inherit SPFeatureReceiver()

    let updateAsaxFiles (webApp:SPWebApplication) asaxFileContents = 
        typeof<SPUrlZone> 
        |> Enum.GetValues
        |> Seq.cast<SPUrlZone>
        |> Seq.map (fun x ->
            Path.Combine(webApp.GetIisSettingsWithFallback(x).Path.ToString(), "global.asax"))
        |> Seq.distinct
        |> Seq.filter File.Exists
        |> Seq.iter (fun file -> File.WriteAllText(file, asaxFileContents, Text.Encoding.UTF8))

    override this.FeatureActivated properties =
        match properties.Feature.Parent with
        | 😕 SPWebApplication as webApp when webApp.IsAdministrationWebApplication ->
            failwith "Feature cannot be activated on Central Administration"
        | 😕 SPWebApplication as webApp when not <| webApp.IsAdministrationWebApplication ->
            // Update web.config
            configEntries
            |> Seq.map (fun x -> x.Prepare())
            |> Seq.iter (fun x -> webApp.WebConfigModifications.Add(x))
            webApp.Farm.Servers.GetValue<SPWebService>().ApplyWebConfigModifications();
            webApp.Update();

            // Update globax.asax file(s)
            sprintf """<%%@ Assembly Name="Microsoft.SharePoint"%%><%%@ Assembly Name="%s"%%><%%@ Application Language="C#" Inherits="%s" %%>"""
                typeof<ServiceStackGlobalApplication>.Assembly.FullName
                typeof<ServiceStackGlobalApplication>.AssemblyQualifiedName
            |> updateAsaxFiles webApp
        | _ -> failwithf "properties.Feature.Parent is unknown" 

    override this.FeatureDeactivating properties =
        match properties.Feature.Parent with
        | 😕 SPWebApplication as webApp ->
            // Update web.config
            webApp.WebConfigModifications
            |> Seq.filter (fun mods -> mods.Owner = CONFIG_MOD_OWNER_NAME)
            |> Seq.iter (fun mods -> webApp.WebConfigModifications.Remove(mods) |> ignore)
            webApp.Farm.Servers.GetValue<SPWebService>().ApplyWebConfigModifications();
            webApp.Update();

            // Update globax.asax file(s)
            """<%@ Assembly Name="Microsoft.SharePoint"%><%@ Application Language="C#" Inherits="Microsoft.SharePoint.ApplicationRuntime.SPHttpApplication"%>"""
            |> updateAsaxFiles webApp
        | _ -> failwithf "properties.Feature.Parent is unknown" 

Step #6: Implement custom authorization filter that familiar with SharePoint security model.

open System
open System.Net
open ServiceStack.ServiceHost
open ServiceStack.ServiceInterface
open ServiceStack.WebHost.Endpoints.Extensions

[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Method, 
                 Inherited = true, AllowMultiple = true)>]
type AllowedSharePointGroupsAttribute(applyTo, groupStr: string) as this =
    inherit RequestFilterAttribute()
    do this.ApplyTo <- applyTo
    do this.Priority <- (int) RequestFilterPriority.RequiredRole;

    let groups = groupStr.Split([|','|], StringSplitOptions.RemoveEmptyEntries)
                 |> Seq.map (fun x -> x.Trim())
                 |> Set.ofSeq
    let isUserHasAnyGroup () =
        match (Microsoft.SharePoint.SPContext.Current) with
        | null -> false
        | context ->
            seq { 
                for group in context.Web.CurrentUser.Groups do
                    yield group.Name }
            |> Seq.fold 
                (fun state groupName -> state || (groups.Contains groupName))
                false

    new (groups) = AllowedSharePointGroupsAttribute(ApplyTo.All, groups)

    override this.Execute(req, res, requestDto) =
        match isUserHasAnyGroup() with
        | true -> ignore()
        | false ->
            res.StatusCode <- (int)HttpStatusCode.Forbidden;
            res.StatusDescription <- "Invalid Group";
            res.EndServiceStackRequest();

Result: REST service with declarative SharePoint security.

open System
open ServiceStack.ServiceHost
open ServiceStack.WebHost.Endpoints
open ServiceStack.ServiceInterface
open Microsoft.SharePoint

open System.Collections.Generic

[<AllowedSharePointGroups("Admin")>]
[<Route("/lists")>]
type GetLists() =
    interface IReturn<List<string>>

type ListsService() =
    inherit Service()
    member this.Any (request:GetLists) =
        seq{ for list in SPContext.Current.Web.Lists do
                    yield list.Title }

Bonus: .NET REST client library for free.

#r "ServiceStack.Common.dll"
#r "ServiceStack.Text.dll"
#r "ServiceStack.dll"
#r "Application.Core.dll"

open ServiceStack.ServiceHost
open ServiceStack.ServiceClient.Web

let getServiceClient baseUri = 
    let client = new JsonServiceClient(baseUri)
    let lines = System.IO.File.ReadAllLines(@"D:\SecretCredentials.txt")
    client.Credentials <- System.Net.NetworkCredential(lines.[0], lines.[1])
    client

let client = getServiceClient "http://localhost/_layouts/api"

client.Get(HelloService.Hello(Name="SharePoint"))
client.Post(ListsService.GetLists()) |> Seq.toArray

4 thoughts on “Declarative authorization in REST services in SharePoint with F# and ServiceStack

  1. Thanks Sergey!

    With the help of your slides I managed to get a F# services project in my ServiceStack project. I now host the F# ones on a /v2/ route. Since I still have the C# ones I have both service assemblies referenced in my AppHostBase call.

    I did not need to sign the dll’s – I guess signing is for the SharePoint deployment, right?

Leave a comment