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.