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\\~\\!\\@\\#\\$\\%\\^\\&amp;\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?", 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.

No Comments