Take some of this with a pinch of salt – just some thoughts I had whilst playing with controlling access to an ADO.NET Data Service, haven’t necessarily got it worked out just yet.
When you first create an ADO.NET Data Service, one of the things that you’ll encounter is the need to configure access to your service-side resources (e.g. Customers, Orders, etc) and any service operations that you’ve built because, by default, no access is allowed.
From Reflector – when a message is first received for a data service, the WebDataService<T> base class picks it up and ultimately uses reflection to look for a public, static (void) method on your service class called InitializeService and it then calls that with an IWebDataServiceConfiguration as in;
public class MyService : WebDataService<NorthwindDataContext> { public static void InitializeService(IWebDataServiceConfiguration config) { config.SetResourceContainerAccessRule("*", ResourceContainerRights.All); } }
IWebDataServiceConfiguration lets you SetResourceContainerAccessRule () to control what access you allow to the resources made public from your “data class” (i.e. the <T> in WebDataService<T>) and also lets you SetServiceOperationAccessRule() to control what access you allow to your service operations (if any).
Into these methods you pass a name and a bunch of flags controlling access. As far as I can tell the name can be either a proper name such as “Orders” or it can be “*” to indicate “everything”. The parameter doesn’t appear to allow something like “O.*” or “O*”.
The flags that you can pass are different depending on whether you are controlling resources or service operations;
[Flags] public enum ResourceContainerRights { None = 0, ReadSingle = 1, ReadMultiple = 2, AllRead = 3, WriteAppend = 4, WriteUpdate = 8, WriteDelete = 16, AllWrite = 28, All = 31, }
[Flags] public enum ServiceOperationRights { None = 0, ReadSingle = 1, ReadMultiple = 2, AllRead = 3, All = 3, }
and you can use these in flexible ways such as;
public static void InitializeService(IWebDataServiceConfiguration config) { config.SetResourceContainerAccessRule("Orders", ResourceContainerRights.AllRead | ResourceContainerRights.WriteAppend); config.SetResourceContainerAccessRule("Customers", ResourceContainerRights.ReadSingle); }
and ReadSingle() is (to me) a very useful access control because not every user really needs to read the whole of a particular EntitySet?
AFAIK, this kind of access control is set in stone for the duration of the service and is not related to who is accessing the service and, whilst it is related to what resource they are accessing you might need more dynamic control than that offered by (e.g.) just being able to say “Insert – Yes”, “Delete – No”. It’s a good start but it’s not very granular.
Query/Update Interceptors allow us to inject some level of dynamic checking into what resources are being manipulated so that we can do something dynamic such as this when a caller tries to access our Orders resource;
[QueryInterceptor("Orders")] public IQueryable<Orders> Intercept(IQueryable<Orders> query) { // Dynamic check - you're only allowed to see the orders that // were created on the same day of the week as today. return (query.Where(order => order.OrderDate.HasValue && order.OrderDate.Value.DayOfWeek == DateTime.Now.DayOfWeek)); }
Note – that interception code won’t work if you’re using Entity Framework as it doesn’t support DayOfWeek. LINQ to SQL does.
This is fairly “easy” when you want to further restrict a query because you can just add on a Where clause and you’re done. If you wanted to inspect the query itself or change it in some way then you get into a lot more complexity because you have to start messing around with the IQueryable that you’re passed.
Now, if we want to do something based on who is running the query or performing a modification then we need to authenticate the user.
Data Services are offered over HTTP and rely on transport security so, for me, the most likely one of these that people would use is Basic Authentication over HTTPS.
With IIS, you switch on Basic Authentication for your .svc file and the web server will make sure that the user is authenticated before they get to your service by checking them against a Windows account. I wrote something about trying to do this against ASP.NET membership instead of Windows accounts over here and I used that particular implementation for the rest of what I write here.
With my user authenticated by Basic Authentication backed by ASP.NET Membership, the first thing that I wanted to do was try to do some kind of simple role-based restriction tied to my Query Interceptor.
So, logically I wanted to do;
[QueryInterceptor("Orders")] [PrincipalPermission(SecurityAction.Demand, Role = "users")] public IQueryable<Order> Intercept(IQueryable<Order> query) { return (query); }
Actually making this work took a little more effort than I was planning for.
Some steps I took – no guarantee whatsoever that this is correct I’m afraid;
- Configured IIS so that my website is using HTTPS with Anonymous Authentication using the IIS UI. This is a bit weird because my Basic Authentication module explicitly blocks anonymous requests. If I disable Anonymous then WCF gets upset so I left it enabled whereas the proper answer would probably be to stop WCF being upset. That’d perhaps involve using a custom binding.
- Configured ASP.NET to not authenticate the user with mode=”None”.
- Configured IIS so that my Basic Authentication module is plugged in using web.config (as in the previous post).
- Configured WCF to let it know that it needed to be compatible with ASP.NET;
<serviceHostingEnvironment aspNetCompatibilityEnabled ="true"> </serviceHostingEnvironment>
- Configured my service in order to add a custom authorization policy as in;
<services> <service name="MyService" behaviorConfiguration="serviceBehaviour"> <endpoint binding="webHttpBinding" bindingConfiguration="bindingConfig" contract="Microsoft.Data.Web.IRequestHandler"/> </service> </services> <bindings> <webHttpBinding> <binding name="bindingConfig"> <security mode="Transport"/> </binding> </webHttpBinding> </bindings> <behaviors> <serviceBehaviors> <behavior name="serviceBehaviour"> <serviceAuthorization principalPermissionMode="UseAspNetRoles"> <authorizationPolicies> <add policyType="AuthPolicy, BasicAuthWithMembershipModule"/> </authorizationPolicies> </serviceAuthorization> </behavior> </serviceBehaviors> </behaviors>
There’s quite a bit in there. The important bits are the request to UseAspNetRoles and also to plug in that custom authorization policy. I got the idea that I needed this to move an identity from the HttpContext.User to the world of WCF from Dominick but I admit that I hacked his class and so more than likely broke it in the process;
public class AuthPolicy : IAuthorizationPolicy { public bool Evaluate(EvaluationContext evaluationContext, ref object state) { HttpContext context = HttpContext.Current; if (context != null) { evaluationContext.Properties["Identities"] = new List<IIdentity>() { context.User.Identity }; } return true; } public System.IdentityModel.Claims.ClaimSet Issuer { get { return ClaimSet.System; } } public string Id { get { return "Mike's Auth Policy"; } } }
With all of those pieces in place, I can now hit against my service and if I request Orders then the identity that I’ve provided over Basic Auth (validated by Membership) will then be checked against the Users role (within Membership) before I’m allowed to make a request involving Orders.
Naturally, if I wanted something a bit more dynamic I could use IsInRole at runtime because the Principal is there for me to access from Thread.CurrentPrincipal.
So, I could write something like;
[QueryInterceptor("Orders")] [PrincipalPermission(SecurityAction.Demand, Role = "users")] public IQueryable<Order> Intercept(IQueryable<Order> query) { if (!Thread.CurrentPrincipal.IsInRole("globalUsers")) { query = query.Where(o => o.Customer.Country == "UK"); } return (query); }
meaning that you need to be in “users” to get hold of Orders at all and you need to be in “globalUsers” to be able to see ones outside of the UK.
Another place that you can add access control in WCF is to use a ServiceAuthorizationManager and you can do that with Data Services too as in;
public class AuthManager : ServiceAuthorizationManager { protected override bool CheckAccessCore(OperationContext operationContext) { return base.CheckAccessCore(operationContext); } }
and this will run for every operation rather than just intercept access to a particular server-side resource but you’re at a much lower level in that you’d have to (perhaps) reach into the OperationContext and its RequestContext to determine what data was coming in and whether you’d allow it or not. You plug this in via config as below;
<serviceBehaviors> <behavior name="serviceBehaviour"> <serviceAuthorization principalPermissionMode="UseAspNetRoles" serviceAuthorizationManagerType="AuthManager, BasicAuthWithMembershipModule">
I dropped the code that I was playing with here – bear in mind that it’s just the result of me playing around with a few things and (like everything on this site) is definitely not tested for any particular purpose.