Adapt Infinity Web Service for ESB Integration–Part 6

This post is part 6 in a series on integrating Blackbaud Infinity based applications such as Blackbaud Enterprise CRM (BBEC) into an Enterprise Service Bus (ESB) architecture using a WCF Workflow service hosted in Windows Server AppFabric.

  1. Part 1 – Intro
  2. Part 2 – ESB Message pass-through to BBEC workflow activity
  3. Part 3 – ESB Message translation to BBEC IDs and operations
  4. Part 4 – Deploying to Windows Server AppFabric on IIS
  5. Part 5 – Asynchronous message handling with SQL persistence

This post will cover securing access to the ESB adapter service.  I will show how to ensure that only authorized principals can send a message to the adapter service.

Inbound Security Authentication & Authorization

In part 4 of this series we moved our application from the local ASP.Net development web server to IIS.  In the process we realized we had to deal with the security requirements for the outgoing calls from the adapter service to the Infinity web service.  We used the Trusted Subsystem Model pattern where the account that the ESB adapter service is running under is treated as a trusted subsystem with a set of the least privileges needed to carry out only and exactly the operations that are used by the workflow service.  We placed the account of the workflow service into a BBEC System Role and gave it permissions to only the BBEC features that are used by the workflow.  This took care of the outgoing security from the adapter service into Infinity.

However, up until now we have failed to take into account the incoming security- which is a potentially glaring omission.  In the current implementation of our service up until Part 5 of this series we have a pretty wide open front door to add a new record to BBEC because our IIS application that hosts the ESB adapter is configured to allow Anonymous access.

image

This means that currently anyone who can make an HTTP POST to our endpoint on this server can send a message and the adapter will dutifully forward it on to the BBEC application even though the caller may be completely anonymous.  If we don’t correct this then if we have a mischievous and bored summer intern on staff we can probably look forward to a few prank “Seymore Butz” and “”Amanda Hugginkiss” Constituent records showing up in our database.  Obviously we need to plug this hole.

There are multiple ways we can seal off this service from unauthorized access.  A brute force approach could be to secure the service behind some kind of firewall (hardware or software) that only allows messages to this service from a well known server that is the actual ESB server.  Such hardening of access may be a good idea as a way to implement a Defense In Depth strategy, but I would feel much better if the service itself had its own implementation of proper user authentication and authorization in addition to any external access control.

AppFabric, where are you?

Securing access to the workflow service is an area that Workflow Foundation and AppFabric are surprisingly… how can I say this nicely…. not in the business of helping us out.  According to the AppFabric documentation on MSDN:

The primary goal of the AppFabric security model is to provide a simple, yet effective, mechanism for the majority of AppFabric users. Because of its integration with existing Windows, .NET Framework, IIS, and SQL Server security models, users can leverage existing security knowledge and skill sets to use the AppFabric security model. Specifically it uses Windows, .NET Framework, IIS, and SQL Server security concepts to enforce different levels of security on the WCF and WF applications it manages. Because AppFabric adds only minor enhancements to an already robust integrated Microsoft security picture, its security model is familiar to administrators who are knowledgeable in Microsoft security concepts. This results in a lower long-term total cost of ownership for AppFabric customers. If you are already familiar with these products and technologies, you can easily secure your application by following the guidance in the Security and Protection section.

Allow me to translate in my own words:

When it comes to securing access to the workflow service, go read up on how you do that with WCF because all an AppFabric service is really is just another WCF service.  AppFabric workflow services security punts to all that existing stuff so you get no help from WF or AppFabric beyond what comes with WCF.  Just secure it like you would any other WCF service.

This delegation to existing WCF functionality is something that I can respect because there are some good things about being compatible with existing concepts and implementation.  But I do feel a little disappointed that neither WF or AppFabric have done anything to take the level of abstraction up a notch with WCF authentication in the same way that they have with so much else.  In fact, in some ways it is actually more work to secure a workflow service because you can’t take advantage of the PrincipalPermissionAttribute that would allow you to declaratively decorate a WCF service method implemented in procedural code.  The result is that paradoxically you can use declarative security in procedural WCF code but you can’t use the same easy technique in declarative workflow services.  (Personally this makes me want to put a T in the middle of the WF abbreviation).

Interestingly, there is a project from Microsoft up on CodePlex called the WF Security Pack CTP 1 that includes an activity named “PrinciplePermissionScope” that is exactly the kind of security wrapper we need.  According to the documentation of the "CTP status" activity:

PrincipalPermissionScope

The PrincipalPermissionScope activity enforces authorization within the workflow by performing a principal permission check against a client-provided identity.

After a message is received (via a Receive activity within the scope of the Body), the authenticated client identity is checked against the principal permission values specified in the PrincipalPermissionName and PrincipalPermissionRole arguments. This permission demand is done in the same way as enforced by the PrincipalPermission class or attribute, and therefore it also supports ASP.NET Role Providers in addition to WindowsIdentity.

The following example demonstrates how to use a PrincipalPermissionScope activity to authorize an UpdatePurchaseOrder request submitted by a client who must be an Administrator:

image

The PrincipalPermissionScope is exactly the kind of declarative security mechanism that I would expect to be available with WF, however it is implemented in an open source project on CodePlex, not in the Framework itself as a first class supported feature.  We can hope that a future version of WF will include this activity, but in the meantime I would prefer to stay away from it as long as it remains in CTP status.

We finally get to write some code.

So if the bad news is that there is not a nice clean fully supported first class declarative mechanism for implementing authorization, then the good news is that we finally get to write some .Net code in our adapter implementation.  Securing our service will involve the following:

  1. Authentication – Our service will be configured for Integrated Windows Authentication.  Anonymous access will be denied
  2. Authorization – Our service will check if the caller is a member of a local security group named BBECAdapter_Callers and only accept the message if the calling identity is in that group.

#1 (authentication) is implemented by configuring the web.config file.

#2 (authorization) must be implemented in code by a class that inherits from System.ServiceModel.ServiceAuthorizationManager

Enable Windows Authentication

To enable Windows Authentication for the service edit the web.config system.webServer\Security element:

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>

    <security>
      <authentication>
        <windowsAuthentication enabled="true" />
        <anonymousAuthentication enabled="false"/>
      </authentication>
    </security>

  </system.webServer>

and in the system.serviceModel\bindings\basicHttpBinding element set the security mode to “TransportCredentialOnly” and the transport client credential type to “Windows”

<bindings>
      <basicHttpBinding>
        <!--This binding is used as the default for all HTTP / Port 80 requests-->
        <binding name="">

          <!--Security mode TransportCredentialOnly requires Windows Authentication-->
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
          
        </binding>
      </basicHttpBinding>
    </bindings>

Implement ServiceAuthorizationManager

While we are editing the web.config file we can go ahead and add a placeholder pointer to our ServiceAuthorizationManager implementation, which will be named ServiceAuthManager.  Add the <serviceAuthorization> behavior in the system.serviceModel\behaviors\serviceBehaviors\behavior element:

   <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true"/>

          <sqlWorkflowInstanceStore connectionStringName="BBECBusAdapterInstanceStoreConnection" />

          <serviceAuthorization serviceAuthorizationManagerType="BBECBusAdapter.ServiceAuthManager,BBECBusAdapter" />

        </behavior>
      </serviceBehaviors>
    </behaviors>

In my authorization logic I am going to check if the caller identity is in at least one of three local security groups:

  • Local Administrators Group
    • A local administrator of the web server should probably be allowed to do anything
  • The AS_Administrators group that AppFabric installs by default
    • Members of this group are intended to be administrators of all AppFabric apps, so it makes sense that they should be able to make calls to the adatper
  • A local security group named BBECBusAdapter_Callers
    • This is the group that the ESB calling principal should be placed in

Implementing this check is straightforward.  In the constructor of the ServiceAuthorizationManager I will grab the account SIDs for those three local security groups:

Public Class ServiceAuthManager
    Inherits ServiceAuthorizationManager

    'sid for local AS_Administrators group
    Private _appServerAdministratorsWindowsGroupSid As SecurityIdentifier

    'sid for local Administrtors group 
    Private _builtInAdminsSid As SecurityIdentifier

    'Sid for local security group BBECBusAdapter_Callers
    Private _esbAdapterServiceWorkflowUsers As SecurityIdentifier

    Public Sub New()
        MyBase.New()

        'builtin\Administrators
        _builtInAdminsSid = New SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, Nothing)

        'AS_Administrators
        _appServerAdministratorsWindowsGroupSid = GetAppServerAdministratorsWindowsGroup()

        'BBECBusAdapter_Callers
        _esbAdapterServiceWorkflowUsers = GetESBWorkflowServiceUsersWindowsGroup()


    End Sub

and then in CheckAccessCore I will simply call System.Security.WindowsPrincipal.IsInRole() for each of the three SIDs and return true if the incoming caller principal is in any of the groups.  Before returning False in the case where the caller is not in any role I will write a detailed error message to the Windows Application Event log.

 Protected Overrides Function CheckAccessCore(ByVal operationContext As System.ServiceModel.OperationContext) As Boolean

        If operationContext.ServiceSecurityContext.IsAnonymous Then

            RaiseWebHealthEvent("An anonymous request was made to the workflow service. WorkflowServiceAuthorizationManager.CheckAccesCore will return 'false' to deny access.")

            Return False

        End If

        Dim incomingCallerWinIdentity = operationContext.ServiceSecurityContext.WindowsIdentity
        Dim incomingCallerWinPrincipal = New WindowsPrincipal(incomingCallerWinIdentity)

        'ok if the incoming caller is in the Administrators local security group
        If incomingCallerWinPrincipal.IsInRole(_builtInAdminsSid) Then
            Return True
        End If

        'ok if the incoming caller is in the AS_Administrators local security group
        If _appServerAdministratorsWindowsGroupSid IsNot Nothing Then
            If incomingCallerWinPrincipal.IsInRole(_appServerAdministratorsWindowsGroupSid) Then

                Return True

            End If
        End If

        If _esbAdapterServiceWorkflowUsers IsNot Nothing Then
            'ok if the incoming caller is in the BBECBusAdapter_Callers group
            If incomingCallerWinPrincipal.IsInRole(_esbAdapterServiceWorkflowUsers) Then
                Return True

            End If
        End If


        'build detailed error message, log it, and return false
        Dim sb As New Text.StringBuilder
        sb.AppendLine("A request to the workflow service was made by an unauthorized account. WorkflowServiceAuthorizationManager.CheckAccesCore will return 'false' to deny access.")
        sb.AppendLine("caller WindowsIdentity.Name: " & incomingCallerWinIdentity.Name)
        sb.AppendLine("caller WindowsIdentity.AuthType: " & incomingCallerWinIdentity.AuthenticationType)

        RaiseWebHealthEvent(sb.ToString)


        Return False



    End Function

My solution now has 1 new .vb file in it, so I’ve got 2 .vb files and 3 .xaml files and a web.config, so the ratio of declarative to procedural code is still very high.

image

With the ServiceAuthManager now in place, when I run the WCF Test client I get an “Access Denied” message:

image

I am running on a machine with UAC enabled so I my Windows account is not in the local administrators group.  I am also not in the AS_Administrators group and I haven’t yet created a group called BBEC_BusAdapters yet.  The service is correctly denying me access to send a message to it.  In my implementation I used a Web Health event to log this authorization denial to the Windows Application event log.  The event message reads:

“A request to the workflow service was made by an unauthorized account. WorkflowServiceAuthorizationManager.CheckAccesCore will return ‘false’ to deny access.

caller WindowsIdentity.Name: bbdev\PaulG”

 

image

Having this kind of logging in place can be very helpful when trying to troubleshoot problems with the service.

To be able to successfully call the service I need to grant access to my Windows account by placing it in one of the groups that the ServiceAuthManager recognizes.  The PowerShell script below will create a local security group named BBEC_BusAdapters and add my account to it.

#create the local group named BBEC_BusAdapter            
#see http://blogs.technet.com/b/heyscriptingguy/archive/2010/11/24/use-powershell-to-create-local-windows-groups.aspx            
            
$localComputerName="paulgxps16"            
$groupName="BBECBusAdapter_Callers"            
            
$comp=[adsi]"WinNT://$localComputerName"            
$group=$comp.Create("Group",$groupName)            
$group.SetInfo()            
            
$group.description="Users in this group can call the BBECBusAdapter workflow service"            
$group.SetInfo()            
            
#add my Windows account to the group            
#see http://blogs.technet.com/b/heyscriptingguy/archive/2008/03/11/how-can-i-use-windows-powershell-to-add-a-domain-user-to-a-local-group.aspx            
            
$userName=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name            
$userName= $userName -replace ("\\","/")            
            
$userPath = "WinNT://$userName, user"            
            
$group.PSBase.Invoke("Add",$userPath)            

After running that script I have a new local security group named BBECBusAdapter_Callers:

image

After logging out and logging back in my session now has this group token in my identity and I can successfully call the ESB adapter service from the WCFTestclient.exe.

Where are we?

We have now tightened up the security of our ESB adapter service.  We made the following changes in this post:

  1. Modified web.config to require Windows Authentication
  2. Implemented a standard WCF ServiceAuthorizationManager class that checks that the caller is in at least one of a specific set of local security groups.

Web.config Source Code:

Here is the full web.config file for reference:

<configuration>
  <connectionStrings>
    <add
        name="appfx_connection"
        connectionString="applicationName=ESBAdapt;databaseName=BBInfinity_firebird;url=http://paulgxps16/bbappfx_firebird/appfxwebservice.asmx"
      />

    <add
      name="BBECBusAdapterInstanceStoreConnection"
      connectionString="server=paulgxps16;database=BBECBusAdapterInstanceStore;integrated security=true"
      />

  </connectionStrings>

  <system.web>
    <compilation debug="true" strict="false" explicit="true" targetFramework="4.0" />


    <healthMonitoring>
      <eventMappings>

        <add name="ServiceAuth Events" 
             type="BBECBusAdapter.ServiceAuthWebHealthEvent,BBECBusAdapter" />

      </eventMappings>
      <rules>

        <add name="Event Log ServiceAuth Events" 
             eventName="ServiceAuth Events" 
             provider="EventLogProvider" 
             profile="Critical" />

      </rules>
      
    </healthMonitoring>
    
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="true"/>

          <sqlWorkflowInstanceStore connectionStringName="BBECBusAdapterInstanceStoreConnection" />

          <serviceAuthorization serviceAuthorizationManagerType="BBECBusAdapter.ServiceAuthManager,BBECBusAdapter" />

        </behavior>
      </serviceBehaviors>
    </behaviors>


    <bindings>
      <basicHttpBinding>
        <!--This binding is used as the default for all HTTP / Port 80 requests-->
        <binding name="">

          <!--Security mode TransportCredentialOnly requires Windows Authentication-->
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
          
        </binding>
      </basicHttpBinding>
    </bindings>

    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>

    <security>
      <authentication>
        <windowsAuthentication enabled="true" />
        <anonymousAuthentication enabled="false"/>
      </authentication>
    </security>

  </system.webServer>
  <microsoft.applicationServer>
    <monitoring>
      <default enabled="true" connectionStringName="ApplicationServerMonitoringConnectionString" monitoringLevel="Troubleshooting" />
    </monitoring>
    <persistence>
      <instanceStores>
        <add name="BBECBusAdapterStore" provider="SqlPersistenceStoreProvider" connectionStringName="BBECBusAdapterInstanceStoreConnection" />
      </instanceStores>
    </persistence>
  </microsoft.applicationServer>
</configuration>

ServiceAuthManager source code:

and here is the code in our ServiceAuthManager class:

Imports System.ServiceModel
Imports System.Security.Principal
Imports System.Web.Management


Public Class ServiceAuthManager
    Inherits ServiceAuthorizationManager

    'sid for local AS_Administrators group
    Private _appServerAdministratorsWindowsGroupSid As SecurityIdentifier

    'sid for local Administrtors group 
    Private _builtInAdminsSid As SecurityIdentifier

    'Sid for local security group BBECBusAdapter_Callers
    Private _esbAdapterServiceWorkflowUsers As SecurityIdentifier

    Public Sub New()
        MyBase.New()

        'builtin\Administrators
        _builtInAdminsSid = New SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, Nothing)

        'AS_Administrators
        _appServerAdministratorsWindowsGroupSid = GetAppServerAdministratorsWindowsGroup()

        'BBECBusAdapter_Callers
        _esbAdapterServiceWorkflowUsers = GetESBWorkflowServiceUsersWindowsGroup()


    End Sub

    Private Function GetAppServerAdministratorsWindowsGroup() As SecurityIdentifier

        Dim appServerAdmins = GetAppServerAdministratorsWindowsGroupName()

        If Not String.IsNullOrEmpty(appServerAdmins) Then

            Dim account As New NTAccount(appServerAdmins)
            Try

                Return DirectCast(account.Translate(GetType(SecurityIdentifier)), SecurityIdentifier)

            Catch exNotMapped As IdentityNotMappedException
                'just report and eat the error.
                RaiseWebHealthEvent("Unable to map the App Server Administrators group to an NT account")


            End Try
        End If

        Return Nothing

    End Function
    Private Function GetESBWorkflowServiceUsersWindowsGroup() As SecurityIdentifier

        Dim groupName As String
        groupName = System.Configuration.ConfigurationManager.AppSettings.Get("BBECBusAdapter_AuthorizedWindowsGroup")

        If String.IsNullOrEmpty(groupName) Then
            groupName = "BBECBusAdapter_Callers"
        End If


        Dim account As New NTAccount(groupName)
        Dim identifier As SecurityIdentifier = Nothing

        Try

            identifier = DirectCast(account.Translate(GetType(SecurityIdentifier)), SecurityIdentifier)

        Catch exNotMapped As IdentityNotMappedException
            Dim msg As String = String.Format("Unable to map account {0} specified in the BBECBusAdapter_AuthorizedWindowsGroup app setting value. Error={1}", groupName, exNotMapped.Message)

            RaiseWebHealthEvent(msg)
            'just report and eat the error.

        End Try

        Return identifier


    End Function

    Private Function GetAppServerAdministratorsWindowsGroupName() As String

        'Use the WorkflowInstanceManagementElement setting for AuthorizedWindowsGroup.

        Dim behaviorsSection = TryCast(System.Configuration.ConfigurationManager.GetSection("system.serviceModel/behaviors"), System.ServiceModel.Configuration.BehaviorsSection)

        If behaviorsSection IsNot Nothing Then

            If behaviorsSection.ServiceBehaviors IsNot Nothing Then

                For Each section In behaviorsSection.ServiceBehaviors

                    Dim serviceBehaviorElm = TryCast(section, System.ServiceModel.Configuration.ServiceBehaviorElement)

                    If serviceBehaviorElm IsNot Nothing Then

                        For i = 0 To serviceBehaviorElm.Count
                            Dim item = serviceBehaviorElm.Item(i)

                            Dim wfManInstance = TryCast(item, System.ServiceModel.Activities.Configuration.WorkflowInstanceManagementElement)
                            If wfManInstance IsNot Nothing Then

                                ' RaiseWebHealthEvent("ServiceAuthManager windows group = " & wfManInstance.AuthorizedWindowsGroup)

                                Return wfManInstance.AuthorizedWindowsGroup
                            End If
                        Next


                    End If


                Next
            End If
        End If


        Return "AS_Administrators"


    End Function



    Protected Overrides Function CheckAccessCore(ByVal operationContext As System.ServiceModel.OperationContext) As Boolean

        If operationContext.ServiceSecurityContext.IsAnonymous Then

            RaiseWebHealthEvent("An anonymous request was made to the workflow service. WorkflowServiceAuthorizationManager.CheckAccesCore will return 'false' to deny access.")

            Return False

        End If

        Dim incomingCallerWinIdentity = operationContext.ServiceSecurityContext.WindowsIdentity
        Dim incomingCallerWinPrincipal = New WindowsPrincipal(incomingCallerWinIdentity)

        'ok if the incoming caller is in the Administrators local security group
        If incomingCallerWinPrincipal.IsInRole(_builtInAdminsSid) Then
            Return True
        End If

        'ok if the incoming caller is in the AS_Administrators local security group
        If _appServerAdministratorsWindowsGroupSid IsNot Nothing Then
            If incomingCallerWinPrincipal.IsInRole(_appServerAdministratorsWindowsGroupSid) Then

                Return True

            End If
        End If

        If _esbAdapterServiceWorkflowUsers IsNot Nothing Then
            'ok if the incoming caller is in the BBECBusAdapter_Callers group
            If incomingCallerWinPrincipal.IsInRole(_esbAdapterServiceWorkflowUsers) Then
                Return True

            End If
        End If


        'build detailed error message, log it, and return false
        Dim sb As New Text.StringBuilder
        sb.AppendLine("A request to the workflow service was made by an unauthorized account. WorkflowServiceAuthorizationManager.CheckAccesCore will return 'false' to deny access.")
        sb.AppendLine("caller WindowsIdentity.Name: " & incomingCallerWinIdentity.Name)
        sb.AppendLine("caller WindowsIdentity.AuthType: " & incomingCallerWinIdentity.AuthenticationType)

        RaiseWebHealthEvent(sb.ToString)


        Return False



    End Function


    Private Sub RaiseWebHealthEvent(msg As String)

        Dim evt As New ServiceAuthWebHealthEvent(msg, Me)
        Web.Management.WebBaseEvent.Raise(evt)

    End Sub


End Class


Public Class ServiceAuthWebHealthEvent
    Inherits System.Web.Management.WebBaseEvent

    Public Const SERVICEAUTH_EVENT_BASECODE As Integer = WebEventCodes.WebExtendedBase + 2400000

    Public Sub New(msg As String, source As Object)
        MyBase.New(msg, source, SERVICEAUTH_EVENT_BASECODE)
    End Sub

End Class

This entry was posted in Architecture, workflow and tagged , . Bookmark the permalink.

Leave a comment