05-03-2023 02:16 PM
Hi,
I'm testing a draft workflow that modifies a user's attributes in Duo using the Duo Admin API and based on the "Duo - Admin - Add User to Group" atomic. Currently it just modifies user's notes:
When I use spaces in the notes parameter, it fails the workflow.
The workflow passes when there are no spaces (no characters to encode). Per the Hints in HTTP Request | SecureX orchestration (ciscosecurity.github.io), I think I already created the payload in an "Execute Python Script" block. But the payload is just text (notes=Some%20notes%20to%20add) not JSON. How do deal with encoded characters in the request body? I tried surrounding the [params_encoded$] in quotes but to no avail.
However, it works just fine in Python:
import requests
method = 'POST'
host = 'api-XXXXXXXX.duosecurity.com'
path = '/admin/v1/users/XXXXXXXXXXXXXX'
params = {'notes': 'Some notes to add'}
skey=<key>
ikey=<ikey>
c = canon(
method=method,
host=host,
path=path,
params=params
)
#print(c)
urlPath = c[-2]
pathParm = c[-1]
print(pathParm)
headRequest = signHeader(
method = method,
host = host,
path = path,
params = params,
skey=skey,
ikey=ikey)
headRequest['Host'] = host
headRequest['Content-Type'] = 'application/x-www-form-urlencoded'
response = requests.request(method=method,
url=url,
headers=headRequest,
data= pathParm)
print(response.status_code)
notes=Some%20notes%20to%20add
200
Thanks!
05-04-2023 02:18 AM
it sounds to me like you don't need that Pyhton block at all, as you can just format the payload directly in the HTTP request activity. that being said are you sure you are exporting the script variable correctly? why are you not exporting the variable `j` as a normal string? the script variable is not called `params` and you should not export it as secure string either.
05-04-2023 08:47 AM
Thanks @chrivand .
I am using the "Duo - Admin - Add User to Group" atomic as a guide for my workflow. The payload for the POST request needs to be formatted as URL-encoded key-value pairs (the same request format used by browsers to submit form data), Duo Admin API | Duo Security
Outputs the j variable {"group_id":"XXXX"} to params variable.
Takes the params variable and outputs it as group_id=XXXX to the params_enc variable.
Take the params_enc for the request body. This API call will fail in my workflow (to modify a user's notes) when the payload contains encoded characters (notes=Some%20notes%20to%20add). If the payload lacks any encoded characters, it works (notes=NoSpaces).
Fortunately, changing my workflow to alter a user's Duo status back to active works (status=active for the POST request body). Just curious if there is a workaround for an URL-encoded key-value pairs payload should I want to change a user's notes, name, or any other attribute that could possibly contain spaces or other special characters. Tried fumbling with a straight Python block to send the POST request.
05-09-2023 06:31 AM
hi! I did some asking around and please check out these 2 WFs (courtesy of Sudhir Desai):
{
"workflow": {
"unique_name": "definition_workflow_025JZ5L701VPZ4lXLAzAIimGPbT6InsMmk5",
"name": "Duo API POST Parameters Encoder",
"title": "Duo API POST Parameters Encoder",
"type": "generic.workflow",
"base_type": "workflow",
"variables": [
{
"schema_id": "datatype.secure_string",
"properties": {
"value": "",
"scope": "output",
"name": "post_data_outer",
"type": "datatype.secure_string",
"is_required": false,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZ70KBMQAK1VCx7LlNQRdSsOwTKeCzZc",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.string",
"properties": {
"value": "",
"scope": "input",
"name": "input_post_data",
"type": "datatype.string",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZASRBINMT7n0bNolCCzQMad2ZSj2UL7",
"object_type": "variable_workflow"
}
],
"properties": {
"atomic": {
"is_atomic": false
},
"delete_workflow_instance": false,
"description": "This atomic action will create a secure json string for parameters.",
"display_name": "Duo API POST Parameters Encoder",
"runtime_user": {
"target_default": true
},
"target": {
"no_target": true
}
},
"object_type": "definition_workflow",
"actions": [
{
"unique_name": "definition_activity_025JZ9TCJ44V86vCr6muba1LDSjG5PKC1lB",
"name": "Execute Python Script",
"title": "Execute Python Script",
"type": "python3.script",
"base_type": "activity",
"properties": {
"action_timeout": 180,
"continue_on_failure": false,
"display_name": "Execute Python Script",
"script": "\"\"\"\nFunction: _encode_post\nPurpose: This function encodes POST data to help in policy creation\n\"\"\"\npost_data_str = json.dumps($workflow.definition_workflow_025JZ5L701VPZ4lXLAzAIimGPbT6InsMmk5.input.variable_workflow_025JZASRBINMT7n0bNolCCzQMad2ZSj2UL7$)\nencoded = urllib.parse.quote_plus(post_data_str)\n$workflow.definition_workflow_025JZ5L701VPZ4lXLAzAIimGPbT6InsMmk5.output.variable_workflow_025JZ70KBMQAK1VCx7LlNQRdSsOwTKeCzZc$ = {\n \"payload\": encoded\n}",
"script_arguments": [
"",
"$workflow.definition_workflow_025JZ5L701VPZ4lXLAzAIimGPbT6InsMmk5.input.variable_workflow_025JZASRBINMT7n0bNolCCzQMad2ZSj2UL7$"
],
"script_queries": [
{
"script_query": "$workflow.definition_workflow_025JZ5L701VPZ4lXLAzAIimGPbT6InsMmk5.output.variable_workflow_025JZ70KBMQAK1VCx7LlNQRdSsOwTKeCzZc$",
"script_query_name": "output variable",
"script_query_type": "secure_string"
}
],
"skip_execution": false
},
"object_type": "definition_activity"
}
],
"categories": [
"category_025JZ9M4WAU6Y0SVu2pHIiv6ZiQYb8EChgT"
]
},
"categories": {
"category_025JZ9M4WAU6Y0SVu2pHIiv6ZiQYb8EChgT": {
"unique_name": "category_025JZ9M4WAU6Y0SVu2pHIiv6ZiQYb8EChgT",
"name": "Duo",
"title": "Duo",
"type": "basic.category",
"base_type": "category",
"category_type": "custom",
"object_type": "category"
}
}
}
{
"workflow": {
"unique_name": "definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd",
"name": "Duo API Sign Headers",
"title": "Duo API Sign Headers",
"type": "generic.workflow",
"base_type": "workflow",
"variables": [
{
"schema_id": "datatype.secure_string",
"properties": {
"value": "",
"scope": "input",
"name": "ikey",
"type": "datatype.secure_string",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZGOFIQ9BE2qmeJ6a4NrhoOceUAsWMF4",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.secure_string",
"properties": {
"value": "",
"scope": "input",
"name": "skey",
"type": "datatype.secure_string",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZGKU16SAR2kzFry870YXghVJWlb6YTn",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.string",
"properties": {
"value": "",
"scope": "input",
"name": "path",
"type": "datatype.string",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZFWEFTOZH6pLKWGWAQER44PVsm63zkE",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.string",
"properties": {
"value": "",
"scope": "input",
"name": "host",
"type": "datatype.string",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZFSOIX5C51uxszxjWHZBXZaF2cxbKhy",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.secure_string",
"properties": {
"value": "",
"scope": "output",
"name": "signed_header",
"type": "datatype.secure_string",
"is_required": false,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZHW3ZI29O3RBugfkj0cBlKQ1NWCMpl7",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.string",
"properties": {
"value": "",
"scope": "input",
"name": "method",
"type": "datatype.string",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZFO0KC6SO79p6xMBLws20gZwlUVhsWs",
"object_type": "variable_workflow"
},
{
"schema_id": "datatype.string",
"properties": {
"value": "{}",
"scope": "input",
"name": "params",
"type": "datatype.string",
"description": "parameters, defaults to {} for a GET method",
"is_required": true,
"is_invisible": false
},
"unique_name": "variable_workflow_025JZG9W2QC9L3IXHwBgTQ9ajDsBJKZsk9A",
"object_type": "variable_workflow"
}
],
"properties": {
"atomic": {
"is_atomic": false
},
"delete_workflow_instance": false,
"display_name": "Duo API Sign Headers",
"runtime_user": {
"target_default": true
},
"target": {
"no_target": true
}
},
"object_type": "definition_workflow",
"actions": [
{
"unique_name": "definition_activity_025JZF0DDBB2R04d1o3N47Bd3BNcjl6ufwS",
"name": "Execute Python Script",
"title": "Execute Python Script",
"type": "python3.script",
"base_type": "activity",
"properties": {
"action_timeout": 180,
"continue_on_failure": false,
"description": "This ",
"display_name": "Execute Python Script",
"script": "import base64\nimport email.utils\nimport hmac\nimport hashlib\nimport urllib\n\"\"\"\nfunction: sign\npurpose: this function can be used to construct the \"Authorization\" and \"Date\" headers\ninputs:\n method: GET, POST [uppercase]\n host: personal API url [lowercase]\n path: path to API method [lowercase]\n params: URL-encoded list of key=value request parameters [alphabetically sorted by key]\n skey: secret key\n ikey: integration key\n\noutputs: Return HTTP Basic Authentication (\"Authorization\" and \"Date\") headers.\n\"\"\" \n# create canonical string\nnow = email.utils.formatdate()\ncanon = [now, $workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZFO0KC6SO79p6xMBLws20gZwlUVhsWs$.upper(), $workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZFSOIX5C51uxszxjWHZBXZaF2cxbKhy$.lower(), $workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZFWEFTOZH6pLKWGWAQER44PVsm63zkE$]\nargs = []\nfor key in sorted($workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZG9W2QC9L3IXHwBgTQ9ajDsBJKZsk9A$.keys()):\n val = $workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZG9W2QC9L3IXHwBgTQ9ajDsBJKZsk9A$[key].encode(\"utf-8\")\n args.append(\n '%s=%s' % (urllib.parse.\n quote(key, '~'), urllib.parse.quote(val, '~')))\ncanon.append('&'.join(args))\ncanon = '\\n'.join(canon)\n\n# sign canonical string\nsig = hmac.new(bytes($workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZGKU16SAR2kzFry870YXghVJWlb6YTn$, encoding='utf-8'),\n bytes(canon, encoding='utf-8'),\n hashlib.sha1)\nauth = '%s:%s' % ($workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZGOFIQ9BE2qmeJ6a4NrhoOceUAsWMF4$, sig.hexdigest())\n\n# return headers\n$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.output.variable_workflow_025JZHW3ZI29O3RBugfkj0cBlKQ1NWCMpl7$ = {'Date': now, 'Authorization': 'Basic %s' % base64.b64encode(bytes(auth, encoding=\"utf-8\")).decode()}",
"script_arguments": [
"$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZFSOIX5C51uxszxjWHZBXZaF2cxbKhy$",
"$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZGOFIQ9BE2qmeJ6a4NrhoOceUAsWMF4$",
"$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZFO0KC6SO79p6xMBLws20gZwlUVhsWs$",
"$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZG9W2QC9L3IXHwBgTQ9ajDsBJKZsk9A$",
"$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZFWEFTOZH6pLKWGWAQER44PVsm63zkE$",
"$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.input.variable_workflow_025JZGKU16SAR2kzFry870YXghVJWlb6YTn$"
],
"script_queries": [
{
"script_query": "$workflow.definition_workflow_025JZE5IC0FFU271F0327WsKRA25FvJaryd.output.variable_workflow_025JZHW3ZI29O3RBugfkj0cBlKQ1NWCMpl7$",
"script_query_name": "signed header",
"script_query_type": "secure_string"
}
],
"skip_execution": false
},
"object_type": "definition_activity"
}
],
"categories": [
"category_025JZ9M4WAU6Y0SVu2pHIiv6ZiQYb8EChgT"
]
},
"categories": {
"category_025JZ9M4WAU6Y0SVu2pHIiv6ZiQYb8EChgT": {
"unique_name": "category_025JZ9M4WAU6Y0SVu2pHIiv6ZiQYb8EChgT",
"name": "Duo",
"title": "Duo",
"type": "basic.category",
"base_type": "category",
"category_type": "custom",
"object_type": "category"
}
}
}
05-09-2023 04:00 PM
Thanks again. I got it working when using multiple request parameters (notes and email) via Python Script blocks.
import sys,json
(notes,email) = sys.argv[1:]
d = {}
d["notes"] = notes
d["email"] = email
j = json.dumps(d)
import base64, email.utils, hmac, hashlib, urllib, requests, json, sys
# Sign Headers function
def signHeader(method, host, path, params, skey, ikey):
"""
Return HTTP Basic Authentication ("Authorization" and "Date") headers.
method, host, path: strings for request
params: dict of request parameters
skey: secret key
ikey: integration key
"""
# create canonical string
now = email.utils.formatdate()
canon = [now, method.upper(), host.lower(), path]
args = []
for key in sorted(params.keys()):
val = params[key].encode("utf-8")
args.append(
'%s=%s' % (urllib.parse.quote(key, '~'), urllib.parse.quote(val, '~')))
canon.append('&'.join(args))
canon = '\n'.join(canon)
# sign canonical string
sig = hmac.new(bytes(skey, encoding='utf-8'),
bytes(canon, encoding='utf-8'),
hashlib.sha1)
auth = '%s:%s' % (ikey, sig.hexdigest())
# return headers
return {'Date': now, 'Authorization': 'Basic %s' % base64.b64encode(bytes(auth, encoding="utf-8")).decode()}
# canonization function
def canon(method, host, path, params):
"""
Returns a list of the five-line request.
method, host, path: strings for request
params: dict of request parameters
"""
# create canonical string
now = email.utils.formatdate()
canon = [now, method.upper(), host.lower(), path]
args = []
for key in sorted(params.keys()):
val = params[key].encode("utf-8")
args.append(
'%s=%s' % (urllib.parse.quote(key, '~'), urllib.parse.quote(val, '~')))
canon.append('&'.join(args))
return canon
#POST Request
(method, host, path, params, skey, ikey) = sys.argv[1:]
method = method
host = host
path = path
params = json.loads(params)
skey = skey
ikey = ikey
c = canon(
method=method,
host=host,
path=path,
params=params
)
urlPath = c[-2]
pathParm = c[-1]
headRequest = signHeader(
method = method,
host = host,
path = path,
params = params,
skey=skey,
ikey=ikey)
headRequest['Host'] = host
headRequest['Content-Type'] = 'application/x-www-form-urlencoded'
url = f"https://{host}{urlPath}"
response = requests.request(method=method,
url=url,
headers=headRequest,
data= pathParm)
print(response.status_code)
Discover and save your favorite ideas. Come back to expert answers, step-by-step guides, recent topics, and more.
New here? Get started with these tips. How to use Community New member guide