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.
Step #5. Modify Web.Config.
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
great !!
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?
Yes, you are absolutely right. Signing required because I needed to deploy all assemblies in the GAC.