WCF Data Services and Custom Authorization
In the last post about Decoding
Messages in WCF Data Services I showed a code sample about how to decode an
incoming WCF Message in a Data Service. In this case I will show how we can use
this decoded message inside a ServiceAuthorizationManager derived class to perform
some authorization
depending on the content (i.e. grant access to some entities according to
the logged on user).
The following configuration set an
authorization manager using Windows Authentication.
<configuration> <system.serviceModel> <services> <service name="MyDataService"
behaviorConfiguration="securityBehavior"> <endpoint contract="System.Data.Services.IRequestHandler" binding="webHttpBinding" bindingConfiguration="WebHttpWindowsAuth"> </service> </services> <behaviors> <serviceBehaviors> <behavior name="catalogServiceBehavior"> <serviceMetadata httpGetEnabled="true"/> <serviceDebug includeExceptionDetailInFaults="true"/> <serviceAuthorization serviceAuthorizationManagerType="MyServiceAuthorization"/> <serviceSecurityAudit serviceAuthorizationAuditLevel="Failure" auditLogLocation="Application" messageAuthenticationAuditLevel="Failure"/> </behavior> </serviceBehaviors> <behaviors> <bindings> <webHttpBinding> <binding name="WebHttpWindowsAuth" maxBufferSize="65536" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" transferMode="StreamedResponse"> <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" /> <security mode="TransportCredentialOnly"> <transport clientCredentialType="Windows" /> </security> </binding> </webHttpBinding> </bindings> </system.serviceModel> </configuration> |
As you can see, we can also set the audit feature to let
WCF log any security issue on the message. Regarding the binding, we set the
required webHttpBinding to use the max settings for message size assuming that
we may need to send large messages (beware of DOS attacks). We also set the
security setting for using Windows authentication.
Now we can implement the “CheckAccess(OperationContext operationContext, ref Message
message)”
overload of our ServiceAuthorizationManager dervived class
(MyServiceAuthorization in config above) and decode the message so we can so
something usefull with its content.
public override
bool CheckAccess(OperationContext
operationContext, ref Message message) { if (IsGet()) return
true; if (!CanDecodeMessage(ref message)) return
false; if (IsUserAllowed()) return
true; return false; } |
As you can see, we want to allow read access so we bail
out for GET verbs using the IsGet() function
described below. After that, if we cannot decode the message we simply
deny access. Otherwise we perform our custom authorization and we are done.
private static
bool IsGet() { return
RequestMethod().Equals("GET", StringComparison.OrdinalIgnoreCase); } private static
bool IsDelete() { return
RequestMethod().Equals("DELETE",
StringComparison.OrdinalIgnoreCase); } private static
string RequestMethod() { object
propertyValue; if (OperationContext.Current.IncomingMessageProperties.TryGetValue(HttpRequestMessageProperty.Name, out propertyValue)) { HttpRequestMessageProperty
rqMessageProperty = HttpRequestMessageProperty)propertyValue; return
rqMessageProperty.Method; } return string.Empty; } private static
bool CanDecodeMessage(ref Message message) { string decodedMsg = DecodeMessage(ref message); if
(decodedMsg == null) { return IsDelete(); } return
IsMultipart(decodedMsg); } |
Notice that “DecodeMessage(ref
message)”
is described in a previous
post.
Now an interesting section is how we detect is the
incoming message is actually a multipart message
which is typically used in SaveBatch
operations.
Here we have the inspection of the multipart message and we can also extract a specific entity and check for access. Notice that we use the Atom10ItemFormatter class along with some other syndication classes to make it easier the parsing operation which is a simplification of the object materialization mechanism used by Data Services.
private static
bool IsMultipart(string
decodedMsg) { bool
isMultipart = false; foreach (Match m in
xmlContentFromMime.Matches(decodedMsg)) { isMultipart |= HaveEntity(m.Value); } if (false == isMultipart) { isMultipart |=
deleteContentFromMime.Matches(decodedMsg).OfType<Match>(). Where(m => m.Value !=
DataServicesMetadataNamespace).Count() > 0; } return
isMultipart; } private static
bool HaveEntity(string
rawData) { Atom10ItemFormatter
atomFormatter; if(XmlUtility.TryParse<Atom10ItemFormatter>(rawData,
out atomFormatter)) { ThrowOnSensitiveEntity(atomFormatter); XmlSyndicationContent
content = atomFormatter.Item.Content as XmlSyndicationContent; return
(content != null); } return false; } private static
void ThrowOnSensitiveEntity (Atom10ItemFormatter atomFormatter) { if
(atomFormatter.Item.Categories. FirstOrDefault(c =>
c.Name.Equals(typeof(MySensitiveEntity).FullName,
StringComparison.OrdinalIgnoreCase)) != null) { throw new SecurityException(Properties.Resources.AccessDenied); } } |
We use a couple of regular expressions to parse the xml sections in the multipart and also to detect when we have a DELETE section.
private const
string DataServicesMetadataNamespace = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; private static
Regex xmlContentFromMime = new Regex(@"<\?xml[^>]+>\s*<\s*(\w+).*?<\s*/\s*\1>",
RegexOptions.Singleline | RegexOptions.Compiled); private static
Regex deleteContentFromMime = new Regex("http://([\\w+?\\.\\w+])+([a-zA-Z0-9\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?",
RegexOptions.Singleline | RegexOptions.Compiled); |
Finally after decoding the message, we can perform our validation.
For that, we can use the result of the message decoding and parsing with the
auxiliary classes shown above and also do some mapping with the incoming user
identity.
Here is a sample of how we can get the incoming identity and
perform some authorization with a pre-configured group or groups (AuthorizedGroup)
that we can read from configuration or some external repository.
private bool
IsUserAllowed () { var
groups = GetWindowsGroups(); return groups.Contains(AuthorizedGroup,
StringComparer.OrdinalIgnoreCase); } private static
IEnumerable<string>
GetWindowsGroups() { OperationContext
operationContext = OperationContext.Current; return
operationContext == null ? new List<string>() :
GetWindowsGroups(operationContext.ServiceSecurityContext.WindowsIdentity); } private static
IEnumerable<string>
GetWindowsGroups(WindowsIdentity identity) { return
identity.Groups != null ?
identity.Groups.Translate(typeof(NTAccount)). Select<IdentityReference, string>(i
=> i.Value) : new string[0]; } |
An interesting part of this implementation is that it can be
completely decoupled from the actual service implementation so we can very easily
switch between different authorization strategies with a simple config change.