Skip to content

Example Webhook Configurations

P6 Authentication and Authorization Webhook events can be consumed by any Http listening service or device.

However, this page explains a common use of Webhooks with a Platform 6 instance: updating workflow assignees as users are added, updated and removed.

Instance Webhook Definition

Webhook Edit

Camel Route For Inbound Notification POSTs

A REST route to receive the webhook notification POSTs

rest( '/' )
.description('Inbound P6Auth Webhooks')
.post( 'hooksin')
.consumes( 'application/json' )
.to( 'p6cmb://scripts?platform6.request.action=execute&id=P6AuthWebhookIn' )
.id('POST_WebhooksIn')

Camel Route For POST Aggregation

An aggregator pattern route to batch all received webhook notification events for three minutes before passing to a script to process

Note

This is important to avoid being overloaded with multiple webhook notifications when a batch of users is added or a significant peak of user auto-provisioning takes place within a short space of time. Aggregation will smooth the load that high volume webhook notifications can create

import org.apache.camel.processor.aggregate.GroupedMessageAggregationStrategy

p6.camel.getCtx().addRoutes(new RouteBuilder() {

    void configure() {

        from('direct:hookbatch')
            .description('Webhook Aggregator')
            .aggregate(header('X-P6Auth-Hook-UUID'), new GroupedMessageAggregationStrategy()).completionTimeout(180000) 
            .to('p6cmb://scripts?platform6.request.action=execute&id=BatchWebhookProcessor')
            .routeId('WebhookAggregator')
    }
})

Webhook Processing/Aggregation Scripts

P6AuthWebhookIn.groovy

Called by the REST POST route. Basic validation of the senders secret is performed before forwarding to the aggregator route

// Validate the secret
if( p6.pipeline.get('X-P6auth-Secret') != 'mysecret' ){

    p6.pipeline.put('CamelHttpResponseCode', '401')
    p6.log.error('Webhook notification with invalid shared secret rejected!')

} else {

    // Send this notification to the aggregator
    def headers = [:]
    p6.pipeline.variables().each() {
        headers["${it}"] = p6.pipeline.get("${it}")
    }

    def exchange = [
      body: p6.pipeline.get('body'),
      headers: headers
    ]
    p6.camel.endpointExchangeWaitInput('direct:hookbatch', exchange)

    // 200 Response
    p6.pipeline.put('CamelHttpResponseCode', '200')
}

BatchWebhookProcessor.groovy

Called by the aggregator route no more than once every three minutes (if events are received). It processes a list of webhook notifications.

The JSON notification is parsed and the email and event type extracted. If any single user/event causes the isUserEmailAnAssignee method to return true, the inline workflow sync is invoked.

Note

This is only an example and the path(s) used in isUserEmailAnAssignee() will need customisation

import groovy.json.JsonSlurper

boolean isUserEmailAnAssignee( email, types) {
    types.each { tp->
        if( tp == 'DELETE'){
            // Was a current assignee?
            if( p6.workflow.isAssignee(email, true)){
                return true
            }
        } else {
            // 'ADD/UPDATE' and org path matches workflow assignee path?
            p6.users.getOrgPaths(email, P6User.AssociationType.BRANCH).each { path ->
                if( path == '/MyWorkflow/Path'){
                    return true
                }
            }
        }
    }
    false
}

def listOfMaps = new JsonSlurper().parseText(p6.pipeline.get('p6AggregateList'))

// Create a map using email keyset to avoid duplicates
def mapEmails = [:].withDefault { [] }

listOfMaps.each { mp ->
    def jsonBody = new String(mp['body'].decodeBase64(), 'utf-8')

    def notificationMap = new JsonSlurper().parseText(jsonBody)
    def attributesMap = notificationMap['attributes']

    def email = attributesMap['email']
    def type = notificationMap['type']
    if(!mapEmails[email].contains(type)){
        mapEmails[email] << type
    }
}

// If ANY of the email notifications infer a sync is required... do a single workflow.sync and finish
mapEmails.any { keyEmail, valList ->
    if(isUserEmailAnAssignee(keyEmail, valList)){
        p6.log.debug('-------- Workflow Assignee Changes Detected --------')
        p6.workflow.syncInlineAssignees(){id, msg ->
            p6.log.debug('    Syncing work item: ' + id + ', Message: ' + msg)
            true
        }
        return true
    }
    return
}