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¶
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
}