Function HTTP_Authentication_Services(@Service, @Params)
/***********************************************************************************************************************

    This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
    permission from SRP Computer Solutions, Inc.

    Name        :   HTTP_Authentication_Services

    Description :   Handler program for all HTTP authentication.

    Notes       :   Authentication techniques will vary depending upon the application so the code in the
                    AuthenticateRequest service will need to be customized as necessary.

    Parameters  :
        Service         [in] -- Name of the service being requested
        Param1-10   [in/out] -- Additional request parameter holders
        Response       [out] -- Response to be sent back to the Controller (MCP) or requesting procedure

    History     :   (Date, Initials, Notes)
        02/25/15    dmb     [SRPFW-91] Original programmer.
        06/10/15    dmb     [SRPFW-91] Add checks to make sure Username and Password are populated before attempting to
                            authenticate against the USERS table. This prevents easy authentication if the USERS table
                            isn't being managed well.
        02/25/16    dmb     [SRPFW-108] Add support for the GetEnableAuthenticateFlag service. If disabled, then automatically
                            authenticate the user.
        02/25/16    dmb     [SRPFW-108] Add support for the GetRealmValue service. Use this instead of hard-coding the
                            realm.
        12/01/16    dmb     Update the AuthenticateRequest service to verify authentication requirements of the current
                            URL using the URLRequiresAuthentication service.
        07/01/17    dmb     [SRPFW-184] Refactor using Enhanced BASIC+ syntax.
        10/22/18    dmb     [SRPFW-253] Add support for checking for whitelisted IPs in the AuthenticateRequest service.
        10/31/18    dmb     [SRPFW-254] Add GetWebAccountPassword, SetWebAccountPassword, and ValidateWebAccountPassword
                            services.
        10/31/18    dmb     [SRPFW-254] Update the AuthenticateRequest service to use the ValidateWebAccountPassword
                            service rather than relying upon a hardcoded USERS table. 
        11/01/18    dmb     [SRPFW-256] Update NewPasswordTimeToLive$ equate to use the GetNewPasswordTimeToLive service
                            rather than the hardcoded value.
        11/01/18    dmb     [SRPFW-256] Update OldPasswordTimeToLive$ equate to use the GetOldPasswordTimeToLive service
                            rather than the hardcoded value.
        11/09/18    dmb     [SRPFW-256] Update ValidateWebAccountPassword service to implement the containment action if
                            too many failed password attempts have been attempted.
        11/20/18    dmb     [SRPFW-256] Add GetWebAccountEnabledStatus service. Update the AuthenticateRequest service
                            to use it before attempting to validate the password.
        11/21/18    dmb     [SRPFW-257] Add ResetWebAccountPassword service.
        11/21/18    dmb     [SRPFW-257] Update SetWebAccountPassword service to support a flag that ignores expiration
                            date.
        11/23/18    dmb     [SRPFW-257] Add SetAuthenticatedAccountID and GetAuthenticatedAccountID services.
        12/12/18    dmb     [SRPFW-257] Add SetAuthenticatedPassword and GetAuthenticatedPassword services.
        06/24/19    dmb     [SRPFW-276] Update the ValidateWebAccountPassword service to reset the invalid password
                            attempt counter for an account if a valid password is passed in.
        12/09/19    dmb     [SRPFW-296] Update all calls to Memory_Services to use a specific cache name.
        06/30/20    dmb     [SRPFW-313] Update the AuthenticateRequest service to return a 403 status code rather than
                            a 511 status code if the IP making the request is not permitted.
        07/27/20    dmb     [SRPFW-313] Replace references to the IPIsPermitted service with the IsIPPermitted service.

***********************************************************************************************************************/

#pragma precomp SRP_PreCompiler

$insert APP_INSERTS
$insert SERVICE_SETUP
$insert HTTP_INSERTS

Equ SecondsPerHour$                 to 60 * 60                                                      ; // 60 minutes * 60 seconds = 3600
Equ SecondsPerDay$                  to 24 * SecondsPerHour$                                         ; // 24 hours * 60 minutes * 60 seconds = 86400
Equ NewPasswordTimeToLive$          to HTTP_Services('GetNewPasswordTimeToLive') * SecondsPerHour$  ; // Convert hours to seconds
Equ OldPasswordTimeToLive$          to HTTP_Services('GetOldPasswordTimeToLive') * SecondsPerHour$  ; // Convert hours to seconds
Equ CacheName$                      to 'SRPHTTPFramework'

Declare function    Database_Services, RTI_CreateGUID
Declare subroutine  Database_Services

GoToService else
    Error_Services('Add', Service : ' is not a valid service request within the HTTP Authentication services module.')
end

Return Response OR ''


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Service Parameter Options
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Options BOOLEAN                     = True$, False$


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Services
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//----------------------------------------------------------------------------------------------------------------------
// AuthenticateRequest
//
// Returns a boolean value indicating the success of the authentication attempt. Default method is built around
// HTTP Basic Authentication.
//----------------------------------------------------------------------------------------------------------------------
Service AuthenticateRequest()

    // All response headers that need to be set, regardless of authentication, should be handled here.
    //  1. Access-Control-Allow-Origin must always be returned for CORS purposes.
    HTTP_Services('SetResponseHeaderField', 'Access-Control-Allow-Origin', '*')

    EnableAuthentication        = HTTP_Services('GetEnableAuthenticationFlag')
    FullEndPointURL             = HTTP_Services('GetFullEndPointURL')
    URLRequiresAuthentication   = HTTP_Services('URLRequiresAuthentication', FullEndPointURL)

    // Set the default status code and phrase if authentication fails.
    StatusCode      = 401
    StatusPhrase    = ''

    If EnableAuthentication AND URLRequiresAuthentication then
        HTTPMethod      = HTTP_Services('GetHTTPRequestMethod')
        HTTPRemoteAddr  = HTTP_Services('GetHTTPRemoteAddr')

        // Verify that the client IP is permitted. If there are no whitelisted IPs, then all IPs are permitted.
        IsIPPermitted = HTTP_Services('IsIPPermitted', HTTPRemoteAddr)
        If IsIPPermitted EQ True$ then
            If HTTPMethod _EQC 'OPTIONS' then
                // OPTIONS methods are never authenticated. Allow the user to be provisionally authenticated since the method
                // will remains as OPTIONS throughout the entire API.
                UserAuthenticated   = True$
            end else
                // Assume the user is not authenticated until otherwise proven.
                UserAuthenticated   = False$

                // The follow code provides a skeleton for support HTTP Basic authorization. This is a REST friendly
                // authentication protocol and is documented in the core HTTP specification. Because REST does not preserve the
                // state, all requests are authenticated regardless of previous authentication successes. HTTP Basic should
                // only be used if https:// is being used. Otherwise, the credentials are being passed through as plain text.


                // HTTP Basic uses the Authorization request header. However, the Authorization request header field does not
                // always work with web server products when being passed to a third-party service. So, if the standard header
                // returns nothing then check the custom X-Authorization request header.
                AuthorizationB64    = HTTP_Services('GetRequestHeaderField', 'Authorization')
                If AuthorizationB64 EQ '' then AuthorizationB64 = HTTP_Services('GetRequestHeaderField', 'X-Authorization')

                If AuthorizationB64 NE '' then
                    // All HTTP Basic credentials should be Base64 encoded (in addition to encrypted via https://). Decode
                    // the credentials.
                    Authorization   = SRP_Decode(AuthorizationB64[7, 999], 'BASE64')

                    // HTTP Basic credentials are always colon (:) delimited. Typically this will come through as
                    // Username:Password, but there could be other formats if the application requires it. For instance, for
                    // applications supporting multiple customers wherein each customer has their own group of users, the
                    // format could look like this CustomerID/Username:Password. This provides, in a sense, a three-part
                    // identifier. The following parsing logic would need to be adjusted as needed.
                    Username        = Authorization[1, ':']
                    Password        = Authorization[Col2() + 1, 999]
                    EnabledStatus   = HTTP_Authentication_Services('GetWebAccountEnabledStatus', Username)
                    If EnabledStatus EQ True$ then
                        // Only authenticate if a username and password is provided. This prevents authenticating in the event
                        // the USERS row is missing a password or the USERS table has a blank row.
                        If (Username NE '') AND (Password NE '') then
                            // Below is where you would place your logic to validate the username, password, and any other credentials
                            // that were passed in. This code uses the default HTTP Framework WEB_ACCOUNTS table.

                            UserAuthenticated   = HTTP_Authentication_Services('ValidateWebAccountPassword', Username, Password, False$)

                            // A successful login should set the WWW-Authenticate response header field with the appropriate value. The
                            // credentials are stored in memory so they can be retrieved by other services as needed.
                            If UserAuthenticated then
                                UserAuthenticated   = True$
                                HTTP_Authentication_Services('SetAuthenticatedAccountID', Username)
                                HTTP_Authentication_Services('SetAuthenticatedPassword', Password)
                                // The realm attribute is a part of the HTTP authentication specification and is used to help identify all
                                // resources that belong to the same authentication. Typically this will be the same value for all requests
                                // within the same application. The branded name or OpenInsight name of the application would be a good
                                // example to use here.
                                Realm               = HTTP_Services('GetRealmValue')
                                HTTP_Services('SetResponseHeaderField', 'WWW-Authenticate', 'xBasic realm="' : Realm : '"')
                            end
                        end
                    end else
                        // IP address making the request is not permitted. Do not authenticate the user.
                        StatusCode          = 403
                        StatusPhrase        = 'Account ' : Username : ' is disabled.'
                        UserAuthenticated   = False$
                    end
                end
            end
        end else
            // IP address making the request is not permitted. Do not authenticate the user.
            StatusCode        = 403
            StatusPhrase      = HTTPRemoteAddr : ' is not a permitted IP address.'
            UserAuthenticated = False$
        end
    end else
        // Force the user to be authenticated since authentication is not enabled.
        UserAuthenticated = True$
    end

    // Non-authenticated requests should have a 401 status code returned.
    If Not(UserAuthenticated) then
        HTTP_Services('SetResponseError', '', '', StatusCode, StatusPhrase, FullEndpointURL)
    end

    Response    = UserAuthenticated

end service


//----------------------------------------------------------------------------------------------------------------------
// CleanUp
//
// Runs any clean up processes as needed to prepare the engine for the next request.
//----------------------------------------------------------------------------------------------------------------------
Service CleanUp()

    // This service is called from HTTP_MCP before sending the response back to the caller. Any application specific
    // logic that stores data in memory or attaches customer specific database tables should be properly closed out
    // to avoid subsequent requests from having innappropriate access.

end service


//----------------------------------------------------------------------------------------------------------------------
// GetWebAccountEnabledStatus
//
// Gets the enabled status for the indicated web account.
//----------------------------------------------------------------------------------------------------------------------
Service GetWebAccountEnabledStatus(AccountID)

    EnabledStatus   = ''

    If AccountID NE '' then
        WebAccountRow   = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
        If Error_Services('NoError') then
            @DICT   = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
            @ID     = AccountID
            @RECORD = WebAccountRow
            EnabledStatus   = {ACCOUNT_ENABLED}
            If EnabledStatus NE True$ then EnabledStatus = False$   ; // Always default to disabled unless explicitly enabled.
        end
    end else
        Error_Services('Add', 'AccountID argument was missing in the ' : Service : ' service.')
    end

    Response    = EnabledStatus

end service


//----------------------------------------------------------------------------------------------------------------------
// GetWebAccountPassword
//
// Gets the current password for the indicated web account. If the CreateIfNew flag is set to True$, a new password will
// be generated if no password currently exists. This new password will be added to the web account.
//----------------------------------------------------------------------------------------------------------------------
Service GetWebAccountPassword(AccountID, CreateIfNew)

    Password    = ''

    If CreateIfNew NE True$ then CreateIfNew = False$

    If AccountID NE '' then
        WebAccountRow   = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
        If Error_Services('NoError') then
            @DICT   = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
            @ID     = AccountID
            @RECORD = WebAccountRow
            Begin Case
                Case ({CURRENT_PASSWORD} EQ '') AND (CreateIfNew EQ True$)
                    Password    = HTTP_Authentication_Services('ResetWebAccountPassword', AccountID, CurrentPassword)

                Case ({CURRENT_PASSWORD} EQ '') AND (CreateIfNew EQ False$)
                    Error_Services('Add', 'No password exists for Account ID ' : AccountID)

                Case Otherwise$
                    Password    = {CURRENT_PASSWORD}

            End Case
        end
    end else
        Error_Services('Add', 'AccountID argument was missing in the ' : Service : ' service.')
    end

    Response    = Password

end service


//----------------------------------------------------------------------------------------------------------------------
// ResetWebAccountPassword
//
// Resets the current password (or creates a new one) for the indicated web account. This new password will be added to
// the web account.
//----------------------------------------------------------------------------------------------------------------------
Service ResetWebAccountPassword(AccountID, CurrentPassword)

    Password    = ''

    If AccountID NE '' then
        WebAccountRow   = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
        If Error_Services('NoError') then
            @DICT   = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
            @ID     = AccountID
            @RECORD = WebAccountRow
            // Password is based on a random GUID and then encoded as Base64.
            Password    = RTI_CreateGUID('B')
            HTTP_Authentication_Services('SetWebAccountPassword', AccountID, CurrentPassword, Password, True$)
            If Error_Services('HasError') then Password = ''
        end
    end else
        Error_Services('Add', 'AccountID argument was missing in the ' : Service : ' service.')
    end

    Response    = Password

end service


//----------------------------------------------------------------------------------------------------------------------
// ValidateWebAccountPassword
//
// Validates the password for the indicated web account. If the CurrentOnly argument is set to True$, then only the
// current password associated with the web account will be validated. Otherwise, the old password will also be valided
// using the expiration date and time associated.
//----------------------------------------------------------------------------------------------------------------------
Service ValidateWebAccountPassword(AccountID, Password, CurrentOnly)

    Valid           = False$    ; // Assume False$ for now.
    ErrorMessage    = ''

    If CurrentOnly NE True$ then CurrentOnly = False$

    If (AccountID NE '') AND (Password NE '') then
        WebAccountRow   = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
        If Error_Services('NoError') then
            @DICT       = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
            @ID         = AccountID
            @RECORD     = WebAccountRow
            ThisSeconds = Date() * SecondsPerDay$ + Time()
            Begin Case
                Case Password EQ {CURRENT_PASSWORD}
                    ExpireSeconds   = {CURRENT_PASSWORD_EXPIRE_DATE} * SecondsPerDay$ + {CURRENT_PASSWORD_EXPIRE_TIME}
                    If ThisSeconds LE ExpireSeconds then
                        Valid           = True$
                    end else
                        ErrorMessage    = 'Password is expired. A new one needs to be requested.'
                    end

                Case (Password EQ {OLD_PASSWORD}) AND (CurrentOnly EQ False$)
                    ExpireSeconds   = {OLD_PASSWORD_EXPIRE_DATE} * SecondsPerDay$ + {OLD_PASSWORD_EXPIRE_TIME}
                    If ThisSeconds LE ExpireSeconds then
                        Valid           = True$
                    end else
                        ErrorMessage    = 'Password is expired. A new one needs to be requested.'
                    end

                Case Otherwise$
                    ErrorMessage    = 'Password is invalid.'

            End Case

            If ErrorMessage EQ '' then
                // Reset the number of invalid password attempts for the account.
                {INVALID_PASSWORD_ATTEMPTS} = 0
                Database_Services('WriteDataRow', 'WEB_ACCOUNTS', @ID, @RECORD, True$, False$, True$)
            end else
                // Update the total invalid password attempts for this server.
                Attempts                    = HTTP_Services('GetTotalInvalidPasswordAttempts')
                Attempts                   += 1
                HTTP_Services('SetTotalInvalidPasswordAttempts', Attempts)
                // Update the total invalid password attempts for this account.
                InvalidPasswordAttempts     = {INVALID_PASSWORD_ATTEMPTS} + 1
                {INVALID_PASSWORD_ATTEMPTS} = InvalidPasswordAttempts
                Database_Services('WriteDataRow', 'WEB_ACCOUNTS', @ID, @RECORD, True$, False$, True$)
                InvalidPasswordLimit        = HTTP_Services('GetInvalidPasswordLimit')
                If InvalidPasswordAttempts GE InvalidPasswordLimit then
                    ContainmentAction   = HTTP_Services('GetContainmentAction')
                    Begin Case
                        Case ContainmentAction _EQC 'Disable Server'
                            HTTP_Services('SetServerEnabled', False$)
                        Case ContainmentAction _EQC 'Quarantine Account'
                            {ACCOUNT_ENABLED}   = False$
                            WebAccountRow       = @RECORD
                            Database_Services('WriteDataRow', 'WEB_ACCOUNTS', AccountID, WebAccountRow, True$, False$, True$)
                    End Case
                    ActionDetails       = ''
                    ActionDetails<1>    = Fmt('Containment Action:', 'L#35') : ContainmentAction
                    ActionDetails<2>    = Fmt('Invalid Password Limit:', 'L#35') : InvalidPasswordLimit
                    ActionDetails<3>    = Fmt('Total Invalid Password Attempts:', 'L#35') : Attempts
                    ActionDetails<4>    = Fmt('Account ID:', 'L#35') : AccountID
                    ActionDetails<5>    = Fmt('Total Account Invalid Attempts:', 'L#35') : InvalidPasswordAttempts
                    HTTP_Authentication_Services('ContainmentActionNotification', ActionDetails)
                end
                Error_Services('Add', ErrorMessage)
            end
        end
    end else
        Error_Services('Add', 'AccountID or Password argument was missing in the ' : Service : ' service.')
    end

    Response    = Valid

end service


//----------------------------------------------------------------------------------------------------------------------
// SetWebAccountPassword
//
// Sets a new password for the indicated web account. If no current password already exists, then the new password will
// be added to the web account automatically. Otherwise, the current password will be verified before allowing a new
// password to be set.
//----------------------------------------------------------------------------------------------------------------------
Service SetWebAccountPassword(AccountID, CurrentPassword, NewPassword, OverrideExpireDate)

    If OverrideExpireDate NE True$ then OverrideExpireDate = False$

    If (AccountID NE '') AND (NewPassword NE '') then
        WebAccountRow   = Database_Services('ReadDataRow', 'WEB_ACCOUNTS', AccountID)
        If Error_Services('NoError') then
            @DICT   = Database_Services('GetTableHandle', 'DICT.WEB_ACCOUNTS')
            @ID     = AccountID
            @RECORD = WebAccountRow
            If {CURRENT_PASSWORD} EQ '' then
                // This is a new password for this web account. Accept the new password.
                CreateDate      = Date()
                CreateTime      = Time()
                CreateSeconds   = CreateDate * SecondsPerDay$ + CreateTime
                ExpireSeconds   = CreateSeconds + NewPasswordTimeToLive$
                ExpireDate      = Int(ExpireSeconds / SecondsPerDay$)
                ExpireTime      = Mod(ExpireSeconds, SecondsPerDay$)
                {CURRENT_PASSWORD}              = NewPassword
                {CURRENT_PASSWORD_CREATE_DATE}  = CreateDate
                {CURRENT_PASSWORD_CREATE_TIME}  = CreateTime
                {CURRENT_PASSWORD_EXPIRE_DATE}  = ExpireDate
                {CURRENT_PASSWORD_EXPIRE_TIME}  = ExpireTime
                WebAccountRow   = @RECORD
                Database_Services('WriteDataRow', 'WEB_ACCOUNTS', AccountID, WebAccountRow, True$, False$, True$)
            end else
                // A current password already exists.
                Valid   = HTTP_Authentication_Services('ValidateWebAccountPassword', AccountID, CurrentPassword, True$) OR (OverrideExpireDate EQ True$)
                If Valid EQ True$ then
                    Begin Case
                        Case CurrentPassword EQ NewPassword
                            // New password must be different than the current password.
                            Error_Services('Add', 'New password must be different than the current password.')

                        Case Otherwise$
                            // Current password is valid and new password is different.

                            // Make the current password the old passowrd. Reset the expiration date and time as
                            // needed.
                            CurrentPassword                 = {CURRENT_PASSWORD}
                            CurrentPasswordCreateDate       = {CURRENT_PASSWORD_CREATE_DATE}
                            CurrentPasswordCreateTime       = {CURRENT_PASSWORD_CREATE_TIME}
                            {OLD_PASSWORD}                  = CurrentPassword
                            {OLD_PASSWORD_CREATE_DATE}      = CurrentPasswordCreateDate
                            {OLD_PASSWORD_CREATE_TIME}      = CurrentPasswordCreateTime
                            ThisSeconds                     = Date() * SecondsPerDay$ + Time()
                            ExpireSeconds                   = ThisSeconds + OldPasswordTimeToLive$
                            ExpireDate                      = Int(ExpireSeconds / SecondsPerDay$)
                            ExpireTime                      = Mod(ExpireSeconds, SecondsPerDay$)
                            {OLD_PASSWORD_EXPIRE_DATE}      = ExpireDate
                            {OLD_PASSWORD_EXPIRE_TIME}      = ExpireTime

                            // Set the new password information.
                            CreateDate                      = Date()
                            CreateTime                      = Time()
                            CreateSeconds                   = CreateDate * SecondsPerDay$ + CreateTime
                            ExpireSeconds                   = CreateSeconds + NewPasswordTimeToLive$
                            ExpireDate                      = Int(ExpireSeconds / SecondsPerDay$)
                            ExpireTime                      = Mod(ExpireSeconds, SecondsPerDay$)
                            {CURRENT_PASSWORD}              = NewPassword
                            {CURRENT_PASSWORD_CREATE_DATE}  = CreateDate
                            {CURRENT_PASSWORD_CREATE_TIME}  = CreateTime
                            {CURRENT_PASSWORD_EXPIRE_DATE}  = ExpireDate
                            {CURRENT_PASSWORD_EXPIRE_TIME}  = ExpireTime
                            WebAccountRow   = @RECORD
                            Database_Services('WriteDataRow', 'WEB_ACCOUNTS', AccountID, WebAccountRow, True$, False$, True$)
                    End Case
                end
            end
        end
    end else
        Error_Services('Add', 'AccountID or NewPassword argument was missing in the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// SetAuthenticatedAccountID
//
// Sets the account ID that was successfully authenticated for this request.
//----------------------------------------------------------------------------------------------------------------------
Service SetAuthenticatedAccountID(AccountID)

    If AccountID NE '' then
        Memory_Services('SetValue', ServiceModule : '*AuthenticatedAccountID', AccountID, CacheName$)
    end
    
end service


//----------------------------------------------------------------------------------------------------------------------
// GetAuthenticatedAccountID
//
// Gets the successfully authenticated account ID for this request.
//----------------------------------------------------------------------------------------------------------------------
Service GetAuthenticatedAccountID()

    AccountID   = Memory_Services('GetValue', ServiceModule : '*AuthenticatedAccountID', '', '', CacheName$)

    Response    = AccountID

end service


//----------------------------------------------------------------------------------------------------------------------
// SetAuthenticatedPassword
//
// Sets the password that was successfully authenticated for this request.
//----------------------------------------------------------------------------------------------------------------------
Service SetAuthenticatedPassword(Password)

    If Password NE '' then
        Memory_Services('SetValue', ServiceModule : '*AuthenticatedPassword', Password, CacheName$)
    end
    
end service


//----------------------------------------------------------------------------------------------------------------------
// GetAuthenticatedPassword
//
// Gets the successfully authenticated password for this request.
//----------------------------------------------------------------------------------------------------------------------
Service GetAuthenticatedPassword()

    Password    = Memory_Services('GetValue', ServiceModule : '*AuthenticatedPassword', '', '', CacheName$)

    Response    = Password

end service


//----------------------------------------------------------------------------------------------------------------------
// ContainmentActionNotification
//
// Handles notification protocols when a containment breach has occured. This handler is mostly a placeholder for
// developers to add their own custom protocol action.
//----------------------------------------------------------------------------------------------------------------------
Service ContainmentActionNotification(ActionDetails)

end service


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

