From d8233240ddb0d86be0575db1eb8d4b5a72214198 Mon Sep 17 00:00:00 2001 From: igorski-r7 <99184344+igorski-r7@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:23:35 +0100 Subject: [PATCH] Duo Auth - 18610 - Updated dependencies and SDK to the latest version (#3055) --- plugins/duo_auth/.CHECKSUM | 10 +- plugins/duo_auth/Dockerfile | 21 +- plugins/duo_auth/bin/komand_duo_auth | 42 +- plugins/duo_auth/help.md | 160 +- .../komand_duo_auth/actions/__init__.py | 4 +- .../komand_duo_auth/actions/auth/__init__.py | 2 +- .../komand_duo_auth/actions/auth/action.py | 83 +- .../komand_duo_auth/actions/auth/schema.py | 46 +- .../komand_duo_auth/connection/__init__.py | 2 +- .../komand_duo_auth/connection/connection.py | 24 +- .../komand_duo_auth/connection/schema.py | 24 +- .../komand_duo_auth/tasks/__init__.py | 2 + .../komand_duo_auth/triggers/__init__.py | 3 +- plugins/duo_auth/plugin.spec.yaml | 96 +- plugins/duo_auth/requirements.txt | 3 +- plugins/duo_auth/setup.py | 8 +- plugins/duo_auth/unit_test/__init__.py | 4 + .../unit_test/responses/auth.json.resp | 5 + .../unit_test/responses/auth_async.json.resp | 3 + .../unit_test/responses/auth_device.json.resp | 6 + .../unit_test/responses/bad_json.json.resp | 1 + plugins/duo_auth/unit_test/test_auth.py | 67 + plugins/duo_auth/unit_test/utils.py | 39 + .../vendor/duo_client_python/.gitignore | 11 - .../vendor/duo_client_python/.travis.yml | 13 - .../duo_auth/vendor/duo_client_python/LICENSE | 30 - .../vendor/duo_client_python/MANIFEST.in | 8 - .../vendor/duo_client_python/README.md | 42 - .../duo_client_python/apache-license-2.0.txt | 202 -- .../duo_client_python/duo_client/__init__.py | 11 - .../duo_client_python/duo_client/accounts.py | 37 - .../duo_client_python/duo_client/admin.py | 2262 ----------------- .../duo_client_python/duo_client/auth.py | 191 -- .../duo_client_python/duo_client/auth_v1.py | 132 - .../duo_client_python/duo_client/ca_certs.pem | 120 - .../duo_client_python/duo_client/client.py | 479 ---- .../duo_client/https_wrapper.py | 150 -- .../duo_client_python/duo_client/verify.py | 59 - .../examples/create_user_and_phone.py | 67 - .../examples/report_auths_by_country.py | 48 - .../examples/report_users_and_phones.py | 46 - .../examples/splunk/duo.conf | 12 - .../examples/splunk/splunk.py | 244 -- .../verify_phone_number_with_voice_call.py | 37 - .../duo_client_python/requirements-dev.txt | 3 - .../vendor/duo_client_python/requirements.txt | 1 - .../vendor/duo_client_python/setup.cfg | 7 - .../vendor/duo_client_python/setup.py | 44 - .../duo_client_python/tests/__init__.py | 0 .../duo_client_python/tests/test_admin.py | 295 --- .../duo_client_python/tests/test_client.py | 393 --- .../vendor/duo_client_python/tests/util.py | 117 - 52 files changed, 432 insertions(+), 5284 deletions(-) create mode 100644 plugins/duo_auth/komand_duo_auth/tasks/__init__.py create mode 100644 plugins/duo_auth/unit_test/__init__.py create mode 100644 plugins/duo_auth/unit_test/responses/auth.json.resp create mode 100644 plugins/duo_auth/unit_test/responses/auth_async.json.resp create mode 100644 plugins/duo_auth/unit_test/responses/auth_device.json.resp create mode 100644 plugins/duo_auth/unit_test/responses/bad_json.json.resp create mode 100644 plugins/duo_auth/unit_test/test_auth.py create mode 100644 plugins/duo_auth/unit_test/utils.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/.gitignore delete mode 100755 plugins/duo_auth/vendor/duo_client_python/.travis.yml delete mode 100755 plugins/duo_auth/vendor/duo_client_python/LICENSE delete mode 100755 plugins/duo_auth/vendor/duo_client_python/MANIFEST.in delete mode 100755 plugins/duo_auth/vendor/duo_client_python/README.md delete mode 100755 plugins/duo_auth/vendor/duo_client_python/apache-license-2.0.txt delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/__init__.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/accounts.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/admin.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/auth.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/auth_v1.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/ca_certs.pem delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/client.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/duo_client/https_wrapper.py delete mode 100644 plugins/duo_auth/vendor/duo_client_python/duo_client/verify.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/examples/create_user_and_phone.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/examples/report_auths_by_country.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/examples/report_users_and_phones.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/examples/splunk/duo.conf delete mode 100755 plugins/duo_auth/vendor/duo_client_python/examples/splunk/splunk.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/examples/verify_phone_number_with_voice_call.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/requirements-dev.txt delete mode 100755 plugins/duo_auth/vendor/duo_client_python/requirements.txt delete mode 100755 plugins/duo_auth/vendor/duo_client_python/setup.cfg delete mode 100755 plugins/duo_auth/vendor/duo_client_python/setup.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/tests/__init__.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/tests/test_admin.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/tests/test_client.py delete mode 100755 plugins/duo_auth/vendor/duo_client_python/tests/util.py diff --git a/plugins/duo_auth/.CHECKSUM b/plugins/duo_auth/.CHECKSUM index 37c6a08b40..8cf0a804d4 100644 --- a/plugins/duo_auth/.CHECKSUM +++ b/plugins/duo_auth/.CHECKSUM @@ -1,15 +1,15 @@ { - "spec": "9a9da19b82f3ad4f23ab7a0725695422", - "manifest": "48735bedd2e407af0784bdd90e706896", - "setup": "d0fc4557b513a4944506a2e885cd0a32", + "spec": "ff85d692a483975710c09a54310d6e74", + "manifest": "c0f72d9ea137585ca83d612bfd3c23bb", + "setup": "426658221d9fe1e3eca5dca34163072a", "schemas": [ { "identifier": "auth/schema.py", - "hash": "fbc5545cf3c14ba63cabf57255f3273d" + "hash": "3730109fc4e5ae4a226cec9d4caaed6c" }, { "identifier": "connection/schema.py", - "hash": "b4dd28f9a040343bb03350fc8a29d096" + "hash": "3507436916c3cb62d4113a3d5092ac33" } ] } \ No newline at end of file diff --git a/plugins/duo_auth/Dockerfile b/plugins/duo_auth/Dockerfile index cb5575eda9..06dd7d384b 100644 --- a/plugins/duo_auth/Dockerfile +++ b/plugins/duo_auth/Dockerfile @@ -1,19 +1,20 @@ -FROM komand/python-3-37-slim-plugin:3 +FROM --platform=linux/amd64 rapid7/insightconnect-python-3-slim-plugin:6.2.3 -# Add any custom package dependencies here -# NOTE: Add pip packages to requirements.txt +LABEL organization=rapid7 +LABEL sdk=python -# End package dependencies - -# Add source code WORKDIR /python/src + ADD ./plugin.spec.yaml /plugin.spec.yaml -ADD . /python/src +ADD ./requirements.txt /python/src/requirements.txt -# Install pip dependencies RUN if [ -f requirements.txt ]; then pip install -r requirements.txt; fi -# Install plugin +ADD . /python/src + RUN python setup.py build && python setup.py install -ENTRYPOINT ["/usr/local/bin/komand_duo_auth"] \ No newline at end of file +# User to run plugin code. The two supported users are: root, nobody +USER nobody + +ENTRYPOINT ["/usr/local/bin/komand_duo_auth"] diff --git a/plugins/duo_auth/bin/komand_duo_auth b/plugins/duo_auth/bin/komand_duo_auth index 6abdf5b85a..4d111f8ecc 100755 --- a/plugins/duo_auth/bin/komand_duo_auth +++ b/plugins/duo_auth/bin/komand_duo_auth @@ -1,30 +1,44 @@ #!/usr/bin/env python -# GENERATED BY KOMAND SDK - DO NOT EDIT -import komand -from komand_duo_auth import connection, actions, triggers - +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT +import os +import json +from sys import argv Name = "Duo Auth API" Vendor = "rapid7" -Version = "1.0.3" -Description = "Duo's Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth plugin for Rapid7 InsightConnect enables users to create and send two-factor authentication push notifications" +Version = "1.0.4" +Description = "[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth InsightConnect plugin enables users to create and send push notifications from within automation workflows" -class ICONDuoAuth(komand.Plugin): - def __init__(self): - super(self.__class__, self).__init__( +def main(): + if 'http' in argv: + if os.environ.get("GUNICORN_CONFIG_FILE"): + with open(os.environ.get("GUNICORN_CONFIG_FILE")) as gf: + gunicorn_cfg = json.load(gf) + if gunicorn_cfg.get("worker_class", "sync") == "gevent": + from gevent import monkey + monkey.patch_all() + elif 'gevent' in argv: + from gevent import monkey + monkey.patch_all() + + import insightconnect_plugin_runtime + from komand_duo_auth import connection, actions, triggers, tasks + + class ICONDuoAuth(insightconnect_plugin_runtime.Plugin): + def __init__(self): + super(self.__class__, self).__init__( name=Name, vendor=Vendor, version=Version, description=Description, connection=connection.Connection() - ) - self.add_action(actions.Auth()) + ) + self.add_action(actions.Auth()) + - -def main(): """Run plugin""" - cli = komand.CLI(ICONDuoAuth()) + cli = insightconnect_plugin_runtime.CLI(ICONDuoAuth()) cli.run() diff --git a/plugins/duo_auth/help.md b/plugins/duo_auth/help.md index e7f9a15181..dcc1a33877 100644 --- a/plugins/duo_auth/help.md +++ b/plugins/duo_auth/help.md @@ -1,7 +1,6 @@ # Description -[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and -security health of their devices before they connect to the apps they use. The Duo Auth InsightConnect plugin enables users to create and send push notifications from within automation workflows. +[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth InsightConnect plugin enables users to create and send push notifications from within automation workflows # Key Features @@ -13,119 +12,116 @@ security health of their devices before they connect to the apps they use. The D * Requires a Duo secret key * Requires a Duo hostname -# Documentation - -## Setup +# Supported Product Versions -|Name|Type|Default|Required|Description|Enum| -|----|----|-------|--------|-----------|----| -|hostname|string|None|True|Enter the Duo API hostname and secret key|None| -|integration_key|credential_secret_key|None|True|API integration key|None| -|secret_key|credential_secret_key|None|True|API secret key|None| - -## Technical Details +* Duo Client 5.3.0 -### Actions - -#### Auth +# Documentation -This action is used to perform second-factor authentication. +## Setup -##### Options +The connection configuration accepts the following parameters: -The "Options" field is used to specify additional parameters that may be necessary depending on the authentication factor selected. "Options" accepts the following parameters in JSON format `username`, `passcode`, `pushinfo`, `type`. +|Name|Type|Default|Required|Description|Enum|Example|Placeholder|Tooltip| +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +|hostname|string|None|True|Enter the Duo API hostname and secret key|None|ExampleHostname|None|None| +|integration_key|credential_secret_key|None|True|API integration key|None|{"secretKey": "9de5069c5afe602b2ea0a04b66beb2c0"}|None|None| +|secret_key|credential_secret_key|None|True|API secret key|None|{"secretKey": "9de5069c5afe602b2ea0a04b66beb2c0"}|None|None| Example input: ``` { - "type": "Transfer", - "pushinfo": { - "hello": "world", - "host": "suspicious-host" - } + "hostname": "ExampleHostname", + "integration_key": { + "secretKey": "9de5069c5afe602b2ea0a04b66beb2c0" + }, + "secret_key": { + "secretKey": "9de5069c5afe602b2ea0a04b66beb2c0" + } } ``` -###### Push - -|Parameter|Required?|Description| -|---------|---------|-----------| -|device|Required|ID of the device. This device must have the "push" capability. You may also specify "auto" to use the first of the user's devices with the "push" capability.| -|type|Optional|This string is displayed in the Duo Mobile app before the word "request". The default is "Login", so the phrase "Login request" appears in the push notification text and on the request details screen. You may want to specify "Transaction", "Transfer", etc.| -|display_username|Optional|String to display in Duo Mobile in place of the user's Duo username.| -|pushinfo|Optional|A set of URL-encoded key/value pairs with additional contextual information associated with this authentication attempt. The Duo Mobile app will display this information to the user. For example: from=login%20portal&domain=example.com. The URL-encoded string's total length must be less than 20,000 bytes.| - -###### Passcode - -|Parameter|Required?|Description| -|---------|---------|-----------| -|passcode|true|Passcode entered by the user.| +## Technical Details -###### Phone +### Actions -|Parameter|Required?|Description| -|---------|---------|-----------| -|device|true|ID of the device to call. This device must have the "phone" capability. You may also specify "auto" to use the first of the user's devices with the "phone" capability.| -###### SMS +#### Auth -|Parameter|Required?|Description| -|---------|---------|-----------| -|device|true|ID of the device to send passcodes to. This device must have the "sms" capability. You may also specify "auto" to use the first of the user's devices with the "sms" capability.| +This action is used to perform second-factor authentication ##### Input -|Name|Type|Default|Required|Description|Enum| -|----|----|-------|--------|-----------|----| -|username|string|None|False|Username is required if user_id is not provided|None| -|user_id|string|None|False|User ID|None| -|factor|string|auto|False|Factor to use for authentication|['auto', 'push', 'passcode', 'sms', 'phone']| -|device|string|auto|False|Device ID to use for auth|None| -|async|bool|None|False|Set to true for an async response|None| -|ipaddr|string|None|False|The IP address of the user to be authenticated, in dotted quad format. This will cause an 'allow' response to be sent if appropriate for requests from a trusted network|None| -|options|object|None|False|Additional options required by the API|None| +|Name|Type|Default|Required|Description|Enum|Example|Placeholder|Tooltip| +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | +|async|bool|None|False|Set to true for an async response|None|False|None|None| +|device|string|auto|False|Device ID to use for auth|None|auto|None|None| +|factor|string|auto|False|Factor to use for authentication|["auto", "push", "passcode", "sms", "phone"]|auto|None|None| +|ipaddr|string|None|False|The IP address of the user to be authenticated, in dotted quad format. This will cause an 'allow' response to be sent if appropriate for requests from a trusted network|None|192.168.0.1|None|None| +|options|object|None|False|Additional options required by the API. This field is used to specify additional parameters that may be necessary depending on the authentication factor selected. Accepts the following parameters in JSON format `username`, `passcode`, `pushinfo`, `type`|None|{"type":"Transfer","pushinfo":{"hello":"world","host":"suspicious-host"}}|None|None| +|user_id|string|None|False|Permanent, unique identifier for the user as generated by Duo upon user creation. Exactly one of user_id or username must be specified|None|DUYHV6TJBC3O4RITS1WC|None|None| +|username|string|None|False|Unique identifier for the user that is commonly specified by your application during user creation. This value may also represent a username alias assigned to a user. Exactly one of user_id or username must be specified|None|user@example.com|None|None| + +Example input: -##### Output +``` +{ + "async": false, + "device": "auto", + "factor": "auto", + "ipaddr": "192.168.0.1", + "options": { + "pushinfo": { + "hello": "world", + "host": "suspicious-host" + }, + "type": "Transfer" + }, + "user_id": "DUYHV6TJBC3O4RITS1WC", + "username": "user@example.com" +} +``` -|Name|Type|Required|Description| -|----|----|--------|-----------| -|status|string|False|Status| -|status_msg|string|False|Status message| -|trusted_device_token|string|False|Trusted device token| -|result|string|False|Either allow or deny| -|txid|string|False|TX ID| +##### Output +|Name|Type|Required|Description|Example| +| :--- | :--- | :--- | :--- | :--- | +|result|string|False|Either "allow" or "deny". If "allow" was returned, your application should grant access to the user. If "deny", it should not|allow| +|status|string|False|String detailing the progress or outcome of the authentication attempt|allow| +|status_msg|string|False|The message describing the status of the authentication attempt. If the authentication attempt was denied, it may identify a reason|Success. Logging you in...| +|trusted_device_token|string|False|A string containing a token for that trusted device|REkxS00Ld4ddEVTRZOUlYMEldJ05HwUldRRThJR1VTNE0=|35|835c28ca9b042e05e| +|txid|string|False|A transaction ID|45f7c92b-f45f-4862-8545-e0f58e78075a| + Example output: ``` - { - "log": "Connect: Connecting..\n", - "status": "ok", - "meta": {}, - "output": { - "result": "allow", - "status": "allow", - "status_msg": "Success. Logging you in..." - } - + "result": "allow", + "status": "allow", + "status_msg": "Success. Logging you in...", + "trusted_device_token": "REkxS00Ld4ddEVTRZOUlYMEldJ05HwUldRRThJR1VTNE0=|35|835c28ca9b042e05e", + "txid": "45f7c92b-f45f-4862-8545-e0f58e78075a" +} ``` - ### Triggers + +*This plugin does not contain any triggers.* +### Tasks + +*This plugin does not contain any tasks.* -This plugin does not contain any triggers. - -### Custom Output Types - -_This plugin does not contain any custom output types._ +### Custom Types + +*This plugin does not contain any custom output types.* ## Troubleshooting - -This plugin does not contain any troubleshooting information. + +*This plugin does not contain a troubleshooting.* # Version History +* 1.0.4 - Updated dependencies and SDK to the latest version * 1.0.3 - Upgraded `duo_client` in requirements.txt to version `4.0.0` | Upgraded `duo_client` in vendor folder to version `4.0.0` | Use input and output constants | Change docker image from `komand/python-3-plugin:2` to `komand/python-3-37-slim-plugin:3` to reduce plugin image size * 1.0.2 - New spec and help.md format for the Extension Library * 1.0.1 - Support `type` parameter as `push_type` in the `options` input of the Auth action @@ -135,7 +131,9 @@ This plugin does not contain any troubleshooting information. # Links +* [Duo](https://duo.com/) + ## References * [Duo](https://duo.com/) -* [Duo Auth API V2](https://duo.com/docs/authapi) +* [Duo Auth API V2](https://duo.com/docs/authapi) \ No newline at end of file diff --git a/plugins/duo_auth/komand_duo_auth/actions/__init__.py b/plugins/duo_auth/komand_duo_auth/actions/__init__.py index 7470313b59..1384cbae70 100755 --- a/plugins/duo_auth/komand_duo_auth/actions/__init__.py +++ b/plugins/duo_auth/komand_duo_auth/actions/__init__.py @@ -1,2 +1,4 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT + from .auth.action import Auth + diff --git a/plugins/duo_auth/komand_duo_auth/actions/auth/__init__.py b/plugins/duo_auth/komand_duo_auth/actions/auth/__init__.py index 697302d793..ad021ee227 100755 --- a/plugins/duo_auth/komand_duo_auth/actions/auth/__init__.py +++ b/plugins/duo_auth/komand_duo_auth/actions/auth/__init__.py @@ -1,2 +1,2 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT from .action import Auth diff --git a/plugins/duo_auth/komand_duo_auth/actions/auth/action.py b/plugins/duo_auth/komand_duo_auth/actions/auth/action.py index e15b158122..9800ce6300 100755 --- a/plugins/duo_auth/komand_duo_auth/actions/auth/action.py +++ b/plugins/duo_auth/komand_duo_auth/actions/auth/action.py @@ -1,51 +1,60 @@ -import komand -import urllib +from urllib.parse import urlencode -from komand.exceptions import PluginException -from .schema import AuthInput, AuthOutput, Input +import insightconnect_plugin_runtime +from insightconnect_plugin_runtime.exceptions import PluginException +from insightconnect_plugin_runtime.helper import clean +from .schema import AuthInput, AuthOutput, Component, Input, Output -class Auth(komand.Action): + +class Auth(insightconnect_plugin_runtime.Action): def __init__(self): super(self.__class__, self).__init__( name="auth", - description="Perform second-factor authentication", + description=Component.DESCRIPTION, input=AuthInput(), output=AuthOutput(), ) def run(self, params={}): - """Run action""" - opts = params.get(Input.OPTIONS) or {} - push_info = opts.get("pushinfo") - - if push_info: - push_info = urllib.parse.urlencode(push_info) - - user_id = None - if params.get(Input.USER_ID): - user_id = params.get(Input.USER_ID) - - username = None - if params.get(Input.USERNAME): - username = params.get(Input.USERNAME) - - if (username and user_id) or (user_id is None and username is None): + # START INPUT BINDING - DO NOT REMOVE - ANY INPUTS BELOW WILL UPDATE WITH YOUR PLUGIN SPEC AFTER REGENERATION + user_id = params.get(Input.USER_ID, "") or None + username = params.get(Input.USERNAME, "") or None + device = params.get(Input.DEVICE, "") or None + factor = params.get(Input.FACTOR, "") + ip_address = params.get(Input.IPADDR, "") + async_ = params.get(Input.ASYNC, False) + opts = params.get(Input.OPTIONS, {}) or {} + # END INPUT BINDING - DO NOT REMOVE + + if (username and user_id) or (not username and not user_id): raise PluginException(cause="Wrong input", assistance="Only user_id or username should be used. Not both.") - response = self.connection.auth_api.auth( - factor=params[Input.FACTOR], - username=username, - user_id=user_id, - ipaddr=params.get(Input.IPADDR), - async_txn=params.get(Input.ASYNC), - type=opts.get("type"), - display_username=username, - pushinfo=push_info, - device=params.get(Input.DEVICE), - passcode=opts.get("passcode"), + if push_info := opts.get("pushinfo"): + push_info = urlencode(push_info) + + try: + response = self.connection.auth_api.auth( + factor=factor, + username=username, + user_id=user_id, + ipaddr=ip_address, + async_txn=async_, + display_username=username, + pushinfo=push_info, + device=device, + type=opts.get("type"), + passcode=opts.get("passcode"), + ) + except Exception as error: + raise PluginException(preset=PluginException.Preset.UNKNOWN, data=error) + + return clean( + { + Output.RESULT: response.get(Output.RESULT), + Output.STATUS: response.get(Output.STATUS), + Output.STATUS_MSG: response.get(Output.STATUS_MSG), + Output.TRUSTED_DEVICE_TOKEN: response.get(Output.TRUSTED_DEVICE_TOKEN), + Output.TXID: response.get(Output.TXID), + } ) - return response - - def test(self): - pass diff --git a/plugins/duo_auth/komand_duo_auth/actions/auth/schema.py b/plugins/duo_auth/komand_duo_auth/actions/auth/schema.py index 510273afbc..175011411c 100755 --- a/plugins/duo_auth/komand_duo_auth/actions/auth/schema.py +++ b/plugins/duo_auth/komand_duo_auth/actions/auth/schema.py @@ -1,5 +1,5 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT -import komand +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT +import insightconnect_plugin_runtime import json @@ -15,7 +15,7 @@ class Input: OPTIONS = "options" USER_ID = "user_id" USERNAME = "username" - + class Output: RESULT = "result" @@ -23,10 +23,10 @@ class Output: STATUS_MSG = "status_msg" TRUSTED_DEVICE_TOKEN = "trusted_device_token" TXID = "txid" - -class AuthInput(komand.Input): - schema = json.loads(""" + +class AuthInput(insightconnect_plugin_runtime.Input): + schema = json.loads(r""" { "type": "object", "title": "Variables", @@ -46,7 +46,6 @@ class AuthInput(komand.Input): }, "factor": { "type": "string", - "title": "Factor", "description": "Factor to use for authentication", "default": "auto", "enum": [ @@ -60,29 +59,29 @@ class AuthInput(komand.Input): }, "ipaddr": { "type": "string", - "title": "Ipaddr", "description": "The IP address of the user to be authenticated, in dotted quad format. This will cause an 'allow' response to be sent if appropriate for requests from a trusted network", "order": 4 }, "options": { "type": "object", "title": "Options", - "description": "Additional options required by the API", + "description": "Additional options required by the API. This field is used to specify additional parameters that may be necessary depending on the authentication factor selected. Accepts the following parameters in JSON format `username`, `passcode`, `pushinfo`, `type`", "order": 7 }, "user_id": { "type": "string", - "title": "User Id", - "description": "User ID", + "title": "User ID", + "description": "Permanent, unique identifier for the user as generated by Duo upon user creation. Exactly one of user_id or username must be specified", "order": 1 }, "username": { "type": "string", "title": "Username", - "description": "Username is required if user_id is not provided", + "description": "Unique identifier for the user that is commonly specified by your application during user creation. This value may also represent a username alias assigned to a user. Exactly one of user_id or username must be specified", "order": 2 } - } + }, + "definitions": {} } """) @@ -90,8 +89,8 @@ def __init__(self): super(self.__class__, self).__init__(self.schema) -class AuthOutput(komand.Output): - schema = json.loads(""" +class AuthOutput(insightconnect_plugin_runtime.Output): + schema = json.loads(r""" { "type": "object", "title": "Variables", @@ -99,34 +98,35 @@ class AuthOutput(komand.Output): "result": { "type": "string", "title": "Result", - "description": "Either allow or deny", + "description": "Either \"allow\" or \"deny\". If \"allow\" was returned, your application should grant access to the user. If \"deny\", it should not", "order": 1 }, "status": { "type": "string", "title": "Status", - "description": "Status", + "description": "String detailing the progress or outcome of the authentication attempt", "order": 2 }, "status_msg": { "type": "string", - "title": "Status Msg", - "description": "Status message", + "title": "Status Message", + "description": "The message describing the status of the authentication attempt. If the authentication attempt was denied, it may identify a reason", "order": 3 }, "trusted_device_token": { "type": "string", "title": "Trusted Device Token", - "description": "Trusted device token", + "description": "A string containing a token for that trusted device", "order": 4 }, "txid": { "type": "string", - "title": "Txid", - "description": "TX ID", + "title": "TX ID", + "description": "A transaction ID", "order": 5 } - } + }, + "definitions": {} } """) diff --git a/plugins/duo_auth/komand_duo_auth/connection/__init__.py b/plugins/duo_auth/komand_duo_auth/connection/__init__.py index a515dcf6b0..c78d3356be 100755 --- a/plugins/duo_auth/komand_duo_auth/connection/__init__.py +++ b/plugins/duo_auth/komand_duo_auth/connection/__init__.py @@ -1,2 +1,2 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT from .connection import Connection diff --git a/plugins/duo_auth/komand_duo_auth/connection/connection.py b/plugins/duo_auth/komand_duo_auth/connection/connection.py index 23eff89c5d..b04bb2eb08 100755 --- a/plugins/duo_auth/komand_duo_auth/connection/connection.py +++ b/plugins/duo_auth/komand_duo_auth/connection/connection.py @@ -1,20 +1,30 @@ -import komand -from .schema import ConnectionSchema, Input +from typing import Dict # Custom imports below import duo_client +import insightconnect_plugin_runtime +from insightconnect_plugin_runtime.exceptions import ConnectionTestException + +from .schema import ConnectionSchema, Input -class Connection(komand.Connection): +class Connection(insightconnect_plugin_runtime.Connection): def __init__(self): super(self.__class__, self).__init__(input=ConnectionSchema()) self.auth_api = None def connect(self, params={}): self.logger.info("Connect: Connecting..") - self.auth_api = duo_client.Auth( - ikey=params.get(Input.INTEGRATION_KEY).get("secretKey"), - skey=params.get(Input.SECRET_KEY).get("secretKey"), - host=params.get(Input.HOSTNAME), + ikey=params.get(Input.INTEGRATION_KEY, {}).get("secretKey", "").strip(), + skey=params.get(Input.SECRET_KEY, {}).get("secretKey", "").strip(), + host=params.get(Input.HOSTNAME, "").strip(), ) + + def test(self) -> Dict[str, bool]: + try: + self.auth_api.ping() + self.auth_api.check() + return {"success": True} + except Exception as error: + raise ConnectionTestException(preset=ConnectionTestException.Preset.UNKNOWN, data=error) diff --git a/plugins/duo_auth/komand_duo_auth/connection/schema.py b/plugins/duo_auth/komand_duo_auth/connection/schema.py index 4ee3eeb822..c546e44dce 100755 --- a/plugins/duo_auth/komand_duo_auth/connection/schema.py +++ b/plugins/duo_auth/komand_duo_auth/connection/schema.py @@ -1,5 +1,5 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT -import komand +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT +import insightconnect_plugin_runtime import json @@ -7,10 +7,10 @@ class Input: HOSTNAME = "hostname" INTEGRATION_KEY = "integration_key" SECRET_KEY = "secret_key" - -class ConnectionSchema(komand.Input): - schema = json.loads(""" + +class ConnectionSchema(insightconnect_plugin_runtime.Input): + schema = json.loads(r""" { "type": "object", "title": "Variables", @@ -23,13 +23,11 @@ class ConnectionSchema(komand.Input): }, "integration_key": { "$ref": "#/definitions/credential_secret_key", - "title": "Integration Key", "description": "API integration key", "order": 1 }, "secret_key": { "$ref": "#/definitions/credential_secret_key", - "title": "Secret Key", "description": "API secret key", "order": 2 } @@ -45,18 +43,18 @@ class ConnectionSchema(komand.Input): "type": "object", "title": "Credential: Secret Key", "description": "A shared secret key", + "required": [ + "secretKey" + ], "properties": { "secretKey": { "type": "string", "title": "Secret Key", - "displayType": "password", "description": "The shared secret key", - "format": "password" + "format": "password", + "displayType": "password" } - }, - "required": [ - "secretKey" - ] + } } } } diff --git a/plugins/duo_auth/komand_duo_auth/tasks/__init__.py b/plugins/duo_auth/komand_duo_auth/tasks/__init__.py new file mode 100644 index 0000000000..7020c9a4ad --- /dev/null +++ b/plugins/duo_auth/komand_duo_auth/tasks/__init__.py @@ -0,0 +1,2 @@ +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT + diff --git a/plugins/duo_auth/komand_duo_auth/triggers/__init__.py b/plugins/duo_auth/komand_duo_auth/triggers/__init__.py index bace8db897..7020c9a4ad 100755 --- a/plugins/duo_auth/komand_duo_auth/triggers/__init__.py +++ b/plugins/duo_auth/komand_duo_auth/triggers/__init__.py @@ -1 +1,2 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT + diff --git a/plugins/duo_auth/plugin.spec.yaml b/plugins/duo_auth/plugin.spec.yaml index d677f0f01e..a2c5ed1dad 100644 --- a/plugins/duo_auth/plugin.spec.yaml +++ b/plugins/duo_auth/plugin.spec.yaml @@ -5,95 +5,143 @@ name: duo_auth title: Duo Auth API vendor: rapid7 support: rapid7 +supported_versions: ["Duo Client 5.3.0"] status: [] -description: Duo's Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth plugin for Rapid7 InsightConnect enables users to create and send two-factor authentication push notifications -version: 1.0.3 +description: "[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth InsightConnect plugin enables users to create and send push notifications from within automation workflows" +version: 1.0.4 +connection_version: 1 resources: source_url: https://github.com/rapid7/insightconnect-plugins/tree/master/plugins/duo_auth license_url: https://github.com/rapid7/insightconnect-plugins/blob/master/LICENSE vendor_url: https://duo.com tags: -- duo -- auth + - duo + - auth hub_tags: use_cases: [devops] keywords: [duo, auth] features: [] +sdk: + type: slim + version: 6.2.3 + user: nobody +key_features: + - Send push notifications for two-factor authentication +requirements: + - Requires a Duo integration key + - Requires a Duo secret key + - Requires a Duo hostname +version_history: + - "1.0.4 - Updated dependencies and SDK to the latest version" + - "1.0.3 - Upgraded `duo_client` in requirements.txt to version `4.0.0` | Upgraded `duo_client` in vendor folder to version `4.0.0` | Use input and output constants | Change docker image from `komand/python-3-plugin:2` to `komand/python-3-37-slim-plugin:3` to reduce plugin image size" + - "1.0.2 - New spec and help.md format for the Extension Library" + - "1.0.1 - Support `type` parameter as `push_type` in the `options` input of the Auth action" + - "1.0.0 - Update to v2 Python plugin architecture | Support web server mode | Update to new credential types | Add example output" + - "0.1.1 - SSL bug fix in SDK" + - "0.1.0 - Initial plugin" +links: + - "[Duo](https://duo.com/)" +references: + - "[Duo](https://duo.com/)" + - "[Duo Auth API V2](https://duo.com/docs/authapi)" connection: integration_key: type: credential_secret_key description: API integration key required: true + example: '{"secretKey": "9de5069c5afe602b2ea0a04b66beb2c0"}' secret_key: type: credential_secret_key description: API secret key required: true + example: '{"secretKey": "9de5069c5afe602b2ea0a04b66beb2c0"}' hostname: title: API Hostname description: Enter the Duo API hostname and secret key type: string required: true + example: ExampleHostname actions: auth: title: Auth description: Perform second-factor authentication input: user_id: + title: User ID + description: Permanent, unique identifier for the user as generated by Duo upon user creation. Exactly one of user_id or username must be specified type: string - description: User ID required: false + example: "DUYHV6TJBC3O4RITS1WC" username: + title: Username + description: Unique identifier for the user that is commonly specified by your application during user creation. This value may also represent a username alias assigned to a user. Exactly one of user_id or username must be specified type: string - description: Username is required if user_id is not provided required: false + example: user@example.com factor: type: string description: Factor to use for authentication - default: auto enum: - - auto - - push - - passcode - - sms - - phone + - auto + - push + - passcode + - sms + - phone required: false + default: auto + example: auto ipaddr: type: string - description: The IP address of the user to be authenticated, in dotted quad - format. This will cause an 'allow' response to be sent if appropriate for - requests from a trusted network + description: The IP address of the user to be authenticated, in dotted quad format. This will cause an 'allow' response to be sent if appropriate for requests from a trusted network required: false + example: 192.168.0.1 async: - type: bool + title: Async description: Set to true for an async response + type: bool required: false + example: false device: - type: string + title: Device description: Device ID to use for auth - default: auto + type: string required: false + default: auto + example: auto options: + title: Options + description: Additional options required by the API. This field is used to specify additional parameters that may be necessary depending on the authentication factor selected. Accepts the following parameters in JSON format `username`, `passcode`, `pushinfo`, `type` type: object - description: Additional options required by the API required: false + example: '{"type":"Transfer","pushinfo":{"hello":"world","host":"suspicious-host"}}' output: result: + title: Result + description: Either "allow" or "deny". If "allow" was returned, your application should grant access to the user. If "deny", it should not type: string - description: Either allow or deny required: false + example: allow status: + title: Status + description: String detailing the progress or outcome of the authentication attempt type: string - description: Status required: false + example: allow status_msg: + title: Status Message + description: The message describing the status of the authentication attempt. If the authentication attempt was denied, it may identify a reason type: string - description: Status message required: false + example: Success. Logging you in... trusted_device_token: - description: Trusted device token + title: Trusted Device Token + description: A string containing a token for that trusted device type: string required: false + example: REkxS00Ld4ddEVTRZOUlYMEldJ05HwUldRRThJR1VTNE0=|35|835c28ca9b042e05e txid: - description: TX ID + title: TX ID + description: A transaction ID type: string required: false + example: 45f7c92b-f45f-4862-8545-e0f58e78075a diff --git a/plugins/duo_auth/requirements.txt b/plugins/duo_auth/requirements.txt index 077c7db966..22781f2fc0 100755 --- a/plugins/duo_auth/requirements.txt +++ b/plugins/duo_auth/requirements.txt @@ -1,4 +1,5 @@ # List third-party dependencies here, separated by newlines. # All dependencies must be version-pinned, eg. requests==1.2.0 # See: https://pip.pypa.io/en/stable/user_guide/#requirements-files -duo_client==4.0.0 +duo_client==5.3.0 +parameterized==0.9.0 diff --git a/plugins/duo_auth/setup.py b/plugins/duo_auth/setup.py index 176b8b9948..0831e96c1a 100644 --- a/plugins/duo_auth/setup.py +++ b/plugins/duo_auth/setup.py @@ -1,14 +1,14 @@ -# GENERATED BY KOMAND SDK - DO NOT EDIT +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT from setuptools import setup, find_packages setup(name="duo_auth-rapid7-plugin", - version="1.0.3", - description="Duo's Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth plugin for Rapid7 InsightConnect enables users to create and send two-factor authentication push notifications", + version="1.0.4", + description="[Duo](https://duo.com/)'s Trusted Access platform verifies the identity of your users with two-factor authentication and security health of their devices before they connect to the apps they use. The Duo Auth InsightConnect plugin enables users to create and send push notifications from within automation workflows", author="rapid7", author_email="", url="", packages=find_packages(), - install_requires=['komand'], # Add third-party dependencies to requirements.txt, not here! + install_requires=['insightconnect-plugin-runtime'], # Add third-party dependencies to requirements.txt, not here! scripts=['bin/komand_duo_auth'] ) diff --git a/plugins/duo_auth/unit_test/__init__.py b/plugins/duo_auth/unit_test/__init__.py new file mode 100644 index 0000000000..d9ae09fc16 --- /dev/null +++ b/plugins/duo_auth/unit_test/__init__.py @@ -0,0 +1,4 @@ +# GENERATED BY INSIGHT-PLUGIN - DO NOT EDIT +import sys + +sys.path.append("../") \ No newline at end of file diff --git a/plugins/duo_auth/unit_test/responses/auth.json.resp b/plugins/duo_auth/unit_test/responses/auth.json.resp new file mode 100644 index 0000000000..a3f84a3dbe --- /dev/null +++ b/plugins/duo_auth/unit_test/responses/auth.json.resp @@ -0,0 +1,5 @@ +{ + "result": "allow", + "status": "allow", + "status_msg": "Success. Logging you in..." +} diff --git a/plugins/duo_auth/unit_test/responses/auth_async.json.resp b/plugins/duo_auth/unit_test/responses/auth_async.json.resp new file mode 100644 index 0000000000..c1123b0b30 --- /dev/null +++ b/plugins/duo_auth/unit_test/responses/auth_async.json.resp @@ -0,0 +1,3 @@ +{ + "txid": "45f7c92b-f45f-4862-8545-e0f58e78075a" +} diff --git a/plugins/duo_auth/unit_test/responses/auth_device.json.resp b/plugins/duo_auth/unit_test/responses/auth_device.json.resp new file mode 100644 index 0000000000..b02d0a18ce --- /dev/null +++ b/plugins/duo_auth/unit_test/responses/auth_device.json.resp @@ -0,0 +1,6 @@ +{ + "result": "allow", + "status": "allow", + "status_msg": "Success. Logging you in...", + "trusted_device_token": "REkxS00Ld4ddEVTRZOUlYMEldJ05HwUldRRThJR1VTNE0=|35|835c28ca9b042e05e" +} diff --git a/plugins/duo_auth/unit_test/responses/bad_json.json.resp b/plugins/duo_auth/unit_test/responses/bad_json.json.resp new file mode 100644 index 0000000000..f54e86c928 --- /dev/null +++ b/plugins/duo_auth/unit_test/responses/bad_json.json.resp @@ -0,0 +1 @@ +WRONG JSON diff --git a/plugins/duo_auth/unit_test/test_auth.py b/plugins/duo_auth/unit_test/test_auth.py new file mode 100644 index 0000000000..75073ad245 --- /dev/null +++ b/plugins/duo_auth/unit_test/test_auth.py @@ -0,0 +1,67 @@ +import os +import sys + +from insightconnect_plugin_runtime.exceptions import PluginException + +sys.path.append(os.path.abspath("../")) + +from typing import Any, Dict +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from komand_duo_auth.actions.auth import Auth +from komand_duo_auth.actions.auth.schema import Input, Output +from parameterized import parameterized + +from utils import MockResponse, Util + +STUB_PARAMETERS = {Input.USER_ID: "ExampleUserID"} + + +class TestAuth(TestCase): + def setUp(self) -> None: + self.action = Util.default_connector(Auth()) + + @parameterized.expand( + [ + ( + STUB_PARAMETERS, + "auth", + {Output.RESULT: "allow", Output.STATUS: "allow", Output.STATUS_MSG: "Success. Logging you in..."}, + ), + ( + {**STUB_PARAMETERS, Input.ASYNC: True}, + "auth_async", + {Output.TXID: "45f7c92b-f45f-4862-8545-e0f58e78075a"}, + ), + ( + {**STUB_PARAMETERS, Input.DEVICE: "1234567"}, + "auth_device", + { + Output.RESULT: "allow", + Output.STATUS: "allow", + Output.STATUS_MSG: "Success. Logging you in...", + Output.TRUSTED_DEVICE_TOKEN: "REkxS00Ld4ddEVTRZOUlYMEldJ05HwUldRRThJR1VTNE0=|35|835c28ca9b042e05e", + }, + ), + ] + ) + @patch("duo_client.client.Client.json_api_call") + def test_auth( + self, + input_parameters: Dict[str, Any], + response_filename: str, + expected: Dict[str, Any], + mock_request: MagicMock, + ) -> None: + mock_request.return_value = MockResponse(response_filename, 200).json() + response = self.action.run(input_parameters) + self.assertEqual(response, expected) + mock_request.assert_called() + + @patch("duo_client.client.Client.json_api_call") + def test_auth_error(self, mock_request: MagicMock) -> None: + mock_request.side_effect = MockResponse + with self.assertRaises(PluginException) as context: + self.action.run(STUB_PARAMETERS) + self.assertEqual(context.exception.preset, PluginException.Preset.UNKNOWN) diff --git a/plugins/duo_auth/unit_test/utils.py b/plugins/duo_auth/unit_test/utils.py new file mode 100644 index 0000000000..9d4fc07f13 --- /dev/null +++ b/plugins/duo_auth/unit_test/utils.py @@ -0,0 +1,39 @@ +import json +import logging +import os +import sys + +sys.path.append(os.path.abspath("../")) + +from insightconnect_plugin_runtime.action import Action +from komand_duo_auth.connection.connection import Connection +from komand_duo_auth.connection.schema import Input + +STUB_CONNECTION = { + Input.HOSTNAME: "ExampleHostname", + Input.SECRET_KEY: {"secretKey": "ExampleSecretKey"}, + Input.INTEGRATION_KEY: {"secretKey": "ExampleIntegrationKey"}, +} + + +class Util: + @staticmethod + def default_connector(action: Action) -> Action: + default_connection = Connection() + default_connection.logger = logging.getLogger("connection logger") + default_connection.connect(STUB_CONNECTION) + action.connection = default_connection + action.logger = logging.getLogger("action logger") + return action + + +class MockResponse: + def __init__(self, filename: str, status_code: int) -> None: + self.filename = filename + self.status_code = status_code + + def json(self): + with open( + os.path.join(os.path.dirname(os.path.realpath(__file__)), f"responses/{self.filename}.json.resp") + ) as file: + return json.load(file) diff --git a/plugins/duo_auth/vendor/duo_client_python/.gitignore b/plugins/duo_auth/vendor/duo_client_python/.gitignore deleted file mode 100755 index 9de2d1e96b..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -*.pyc -*.pyo -*.swp -*~ -.gitconfig -MANIFEST -build -dist -.idea -env/ -py3env/ diff --git a/plugins/duo_auth/vendor/duo_client_python/.travis.yml b/plugins/duo_auth/vendor/duo_client_python/.travis.yml deleted file mode 100755 index d2f1363e46..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -python: - - '2.7' - - '3.3' - - '3.4' - - '3.5' - - '3.6' - - '3.7-dev' -install: - - pip install -r requirements.txt - - pip install -r requirements-dev.txt -script: - - nose2 diff --git a/plugins/duo_auth/vendor/duo_client_python/LICENSE b/plugins/duo_auth/vendor/duo_client_python/LICENSE deleted file mode 100755 index 43d0e6362e..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright (c) 2013, Duo Security, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -Note: The open-source component https_wrapper.py included with this -distribution is under the terms of the Apache License, Version 2.0, a -copy of which has been included as 'apache-license-2.0.txt'. diff --git a/plugins/duo_auth/vendor/duo_client_python/MANIFEST.in b/plugins/duo_auth/vendor/duo_client_python/MANIFEST.in deleted file mode 100755 index 32fbda77fe..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include apache-license-2.0.txt -include LICENSE -recursive-include examples * -include tests/*.py -include README.md -include duo_client/ca_certs.pem -include requirements.txt -include requirements-dev.txt diff --git a/plugins/duo_auth/vendor/duo_client_python/README.md b/plugins/duo_auth/vendor/duo_client_python/README.md deleted file mode 100755 index e7ec335474..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Overview - -[![Build Status](https://travis-ci.org/duosecurity/duo_client_python.svg?branch=master)](https://travis-ci.org/duosecurity/duo_client_python) - -**Auth** - https://www.duosecurity.com/docs/authapi - -**Admin** - https://www.duosecurity.com/docs/adminapi - -**Accounts** - https://www.duosecurity.com/docs/accountsapi - -# Installing - -Development: - -``` -$ git clone https://github.com/duosecurity/duo_client_python.git -$ cd duo_client_python -$ pip install --requirement requirements.txt -$ pip install --requirement requirements-dev.txt -``` - -System: - -``` -$ pip install duo_client -``` - -# Using - -See the `examples` folder for how to use this library. - -# Testing - -``` -$ nose2 -``` - -# Linting - -``` -$ flake8 --ignore=E501 duo_client/ tests/ -``` diff --git a/plugins/duo_auth/vendor/duo_client_python/apache-license-2.0.txt b/plugins/duo_auth/vendor/duo_client_python/apache-license-2.0.txt deleted file mode 100755 index d645695673..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/apache-license-2.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/__init__.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/__init__.py deleted file mode 100755 index 16b1a253f7..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import absolute_import -from .accounts import Accounts -from .admin import Admin -from .auth import Auth -from .client import __version__ - -__all__ = [ - 'Accounts', - 'Admin', - 'Auth', -] diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/accounts.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/accounts.py deleted file mode 100755 index 964805d776..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/accounts.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Duo Security Accounts API reference client implementation. - - -""" -from __future__ import absolute_import -from . import client - - -class Accounts(client.Client): - def get_child_accounts(self): - """ - Return a list of all child accounts of the integration's account. - """ - params = {} - response = self.json_api_call("POST", "/accounts/v1/account/list", params) - return response - - def create_account(self, name): - """ - Create a new child account of the integration's account. - """ - params = { - "name": name, - } - response = self.json_api_call("POST", "/accounts/v1/account/create", params) - return response - - def delete_account(self, account_id): - """ - Delete a child account of the integration's account. - """ - params = { - "account_id": account_id, - } - response = self.json_api_call("POST", "/accounts/v1/account/delete", params) - return response diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/admin.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/admin.py deleted file mode 100755 index 883b82126b..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/admin.py +++ /dev/null @@ -1,2262 +0,0 @@ -""" -Duo Security Administration API reference client implementation. - - - - -USERS - -User objects are returned in the following format: - - {"username": , - "user_id": , - "realname": , - "status": , - "notes": , - "last_login": |None, - "phones": [, ...], - "tokens": [, ...]} - -User status is one of: - - USER_STATUS_ACTIVE, USER_STATUS_BYPASS, USER_STATUS_DISABLED, - USER_STATUS_LOCKED_OUT - -Note: USER_STATUS_LOCKED_OUT can only be set by the system. You cannot - set a user to be in the USER_STATUS_LOCKED_OUT state. - - -ENDPOINTS - -Endpoint objects are returned in the following format: - - {"username": , - "email": , - "epkey": , - "os family": , - "os version": , - "model": , - "type": , - "browsers": [, - "browser version": , - "flash version": , - "java version": } - - -PHONES - -Phone objects are returned in the following format: - - {"phone_id": , - "number": , - "extension": |'', - "predelay": |None, - "postdelay": |None, - "type": |"Unknown", - "platform": |"Unknown", - "activated": , - "sms_passcodes_sent": , - "users": [, ...]} - - -DESKTOP_TOKENS - -Desktop token objects are returned in the following format: - - {"desktoptoken_id": , - "name": , - "platform": |"Unknown", - "type"": "Desktop Token", - "users": [, ...]} - -TOKENS - -Token objects are returned in the following format: - - {"type": , - "serial": , - "token_id": , - "totp_step": or null, - "users": [, ...]} - -Token type is one of: - - TOKEN_HOTP_6, TOKEN_HOTP_8, TOKEN_YUBIKEY - - -SETTINGS - -Settings objects are returned in the following format: - - {'inactive_user_expiration': |0, - 'sms_message': , - 'name': , - 'sms_batch': , - 'lockout_threshold': - 'lockout_expire_duration': |0, - 'sms_expiration': |0, - 'log_retention_days': |0, - 'sms_refresh': , - 'telephony_warning_min': ', - 'minimum_password_length': , - 'password_requires_upper_alpha': , - 'password_requires_lower_alpha': , - 'password_requires_numeric': , - 'password_requires_special': , - 'security_checkup_enabled': , - } - - -INTEGRATIONS - -Integration objects are returned in the following format: - - {'adminapi_admins': , - 'adminapi_info': , - 'adminapi_integrations': , - 'adminapi_read_log': , - 'adminapi_read_resource': , - 'adminapi_settings': , - 'adminapi_write_resource': , - 'self_service_allowed': , - 'enroll_policy': , - 'username_normalization_policy': , - 'greeting': , - 'integration_key': , - 'name': , - 'notes': , - 'secret_key': , - 'type': , - 'visual_style': } - -See the adminapi docs for possible values for enroll_policy, visual_style, ip_whitelist, -and type. - -ERRORS - -Methods will raise a RuntimeError when an error is encountered. When -the call returns a HTTP status other than 200, error will be populated with -the following fields: - - message - String description of the error encountered such as - "Received 404 Not Found" - status - HTTP status such as 404 (int) - reason - Status description such as "Not Found" - data - Detailed error code such as - {"code": 40401, "message": "Resource not found", "stat": "FAIL"} -""" -from __future__ import absolute_import - -import six.moves.urllib - -from . import client -import six -import warnings -import time - -USER_STATUS_ACTIVE = "active" -USER_STATUS_BYPASS = "bypass" -USER_STATUS_DISABLED = "disabled" -USER_STATUS_LOCKED_OUT = "locked out" - -TOKEN_HOTP_6 = "h6" # noqa: B105 -TOKEN_HOTP_8 = "h8" # noqa: B105 -TOKEN_YUBIKEY = "yk" # noqa: B105 - -VALID_AUTHLOG_REQUEST_PARAMS = [ - "mintime", - "maxtime", - "limit", - "sort", - "next_offset", - "event_types", - "reasons", - "results", - "users", - "applications", - "groups", - "factors", - "api_version", -] - - -class Admin(client.Client): - account_id = None - - def api_call(self, method, path, params): - if self.account_id is not None: - params["account_id"] = self.account_id - return super(Admin, self).api_call(method, path, params) - - @classmethod - def _canonicalize_ip_whitelist(klass, ip_whitelist): - if isinstance(ip_whitelist, six.string_types): - return ip_whitelist - else: - return ",".join(ip_whitelist) - pass - - @staticmethod - def _canonicalize_bypass_codes(codes): - if isinstance(codes, six.string_types): - return codes - else: - return ",".join([str(int(code)) for code in codes]) - - def get_administrator_log(self, mintime=0): - """ - Returns administrator log events. - - mintime - Fetch events only >= mintime (to avoid duplicate - records that have already been fetched) - - Returns: - [ - {'timestamp': , - 'eventtype': "administrator", - 'host': , - 'username': , - 'action': , - 'object': |None, - 'description': |None}, ... - ] - - is one of: - 'admin_login', - 'admin_create', 'admin_update', 'admin_delete', - 'customer_update', - 'group_create', 'group_update', 'group_delete', - 'integration_create', 'integration_update', 'integration_delete', - 'phone_create', 'phone_update', 'phone_delete', - 'user_create', 'user_update', 'user_delete' - - Raises RuntimeError on error. - """ - # Sanity check mintime as unix timestamp, then transform to string - mintime = str(int(mintime)) - params = { - "mintime": mintime, - } - response = self.json_api_call( - "GET", - "/admin/v1/logs/administrator", - params, - ) - for row in response: - row["eventtype"] = "administrator" - row["host"] = self.host - return response - - def get_authentication_log(self, api_version=1, **kwargs): - """ - Returns authentication log events. - - api_version - The api version of the handler to use. Currently, the - default api version is v1, but the v1 api will be - deprecated in a future version of the Duo Admin API. - Please migrate to the v2 api at your earliest convenience. - For details on the differences between v1 and v2, - please see Duo's Admin API documentation. (Optional) - - API Version v1: - - mintime - Fetch events only >= mintime (to avoid duplicate - records that have already been fetched) - - Returns: - [ - {'timestamp': , - 'eventtype': "authentication", - 'host': , - 'username': , - 'factor': , - 'result': , - 'ip': , - 'new_enrollment': , - 'integration': , - 'location': { - 'state': '', - 'city': '', - 'country': '' - } - }] - - Raises RuntimeError on error. - - API Version v2: - - mintime (required) - Unix timestamp in ms; fetch records >= mintime - maxtime (required) - Unix timestamp in ms; fetch records <= mintime - limit - Number of results to limit to - next_offset - Used to grab the next set of results from a previous response - sort - Sort order to be applied - users - List of user ids to filter on - groups - List of group ids to filter on - applications - List of application ids to filter on - results - List of results to filter to filter on - reasons - List of reasons to filter to filter on - factors - List of factors to filter on - event_types - List of event_types to filter on - - Returns: - { - "authlogs": [ - { - "access_device": { - "ip": , - "location": { - "city": , - "state": , - "country": , - "name": - }, - "auth_device": { - "ip": , - "location": { - "city": , - "state": , - "country": - }, - "event_type": , - "factor": , - "result": , - "timestamp": , - "user": { - "key": , - "name": - } - } - ], - "metadata": { - "next_offset": [ - , - - ], - "total_objects": - } - } - - Raises RuntimeError on error. - """ - - if api_version not in [1, 2]: - raise ValueError("Invalid API Version") - - params = {} - - if api_version == 1: # v1 - params["mintime"] = kwargs["mintime"] if "mintime" in kwargs else 0 - # Sanity check mintime as unix timestamp, then transform to string - params["mintime"] = "{:d}".format(int(params["mintime"])) - warnings.warn( - "The v1 Admin API for retrieving authentication log events " - "will be deprecated in a future release of the Duo Admin API. " - "Please migrate to the v2 API.", - DeprecationWarning, - ) - else: # v2 - for k in kwargs: - if kwargs[k] is not None and k in VALID_AUTHLOG_REQUEST_PARAMS: - params[k] = kwargs[k] - - if "mintime" not in params: - params["mintime"] = (int(time.time()) - 86400) * 1000 - # Sanity check mintime as unix timestamp, then transform to string - params["mintime"] = "{:d}".format(int(params["mintime"])) - - if "maxtime" not in params: - params["maxtime"] = int(time.time()) * 1000 - # Sanity check maxtime as unix timestamp, then transform to string - params["maxtime"] = "{:d}".format(int(params["maxtime"])) - - response = self.json_api_call( - "GET", - "/admin/v{}/logs/authentication".format(api_version), - params, - ) - - if api_version == 1: - for row in response: - row["eventtype"] = "authentication" - row["host"] = self.host - else: - for row in response["authlogs"]: - row["eventtype"] = "authentication" - row["host"] = self.host - return response - - def get_telephony_log(self, mintime=0): - """ - Returns telephony log events. - - mintime - Fetch events only >= mintime (to avoid duplicate - records that have already been fetched) - - Returns: - [ - {'timestamp': , - 'eventtype': "telephony", - 'host': , - 'context': , - 'type': , - 'phone': , - 'credits': }, ... - ] - - Raises RuntimeError on error. - """ - # Sanity check mintime as unix timestamp, then transform to string - mintime = str(int(mintime)) - params = { - "mintime": mintime, - } - response = self.json_api_call( - "GET", - "/admin/v1/logs/telephony", - params, - ) - for row in response: - row["eventtype"] = "telephony" - row["host"] = self.host - return response - - def get_users(self): - """ - Returns list of users. - - - Returns list of user objects. - - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/users", {}) - return response - - def get_user_by_id(self, user_id): - """ - Returns user specified by user_id. - - user_id - User to fetch - - Returns user object. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id - response = self.json_api_call("GET", path, {}) - return response - - def get_users_by_name(self, username): - """ - Returns user specified by username. - - username - User to fetch - - Returns a list of 0 or 1 user objects. - - Raises RuntimeError on error. - """ - params = { - "username": username, - } - response = self.json_api_call("GET", "/admin/v1/users", params) - return response - - def add_user( - self, - username, - realname=None, - status=None, - notes=None, - email=None, - firstname=None, - lastname=None, - alias1=None, - alias2=None, - alias3=None, - alias4=None, - ): - """ - Adds a user. - - username - Username - realname - User's real name (optional) - status - User's status, defaults to USER_STATUS_ACTIVE - notes - Comment field (optional) - email - Email address (optional) - firstname - User's given name for ID Proofing (optional) - lastname - User's surname for ID Proofing (optional) - alias1..alias4 - Aliases for the user's primary username (optional) - - Returns newly created user object. - - Raises RuntimeError on error. - """ - params = { - "username": username, - } - if realname is not None: - params["realname"] = realname - if status is not None: - params["status"] = status - if notes is not None: - params["notes"] = notes - if email is not None: - params["email"] = email - if firstname is not None: - params["firstname"] = firstname - if lastname is not None: - params["lastname"] = lastname - if alias1 is not None: - params["alias1"] = alias1 - if alias2 is not None: - params["alias2"] = alias2 - if alias3 is not None: - params["alias3"] = alias3 - if alias4 is not None: - params["alias4"] = alias4 - response = self.json_api_call("POST", "/admin/v1/users", params) - return response - - def update_user( - self, - user_id, - username=None, - realname=None, - status=None, - notes=None, - email=None, - firstname=None, - lastname=None, - alias1=None, - alias2=None, - alias3=None, - alias4=None, - ): - """ - Update username, realname, status, or notes for a user. - - user_id - User ID - username - Username (optional) - realname - User's real name (optional) - status - User's status, defaults to USER_STATUS_ACTIVE - notes - Comment field (optional) - email - Email address (optional) - firstname - User's given name for ID Proofing (optional) - lastname - User's surname for ID Proofing (optional) - - Returns updated user object. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id - params = {} - if username is not None: - params["username"] = username - if realname is not None: - params["realname"] = realname - if status is not None: - params["status"] = status - if notes is not None: - params["notes"] = notes - if email is not None: - params["email"] = email - if firstname is not None: - params["firstname"] = firstname - if lastname is not None: - params["lastname"] = lastname - if alias1 is not None: - params["alias1"] = alias1 - if alias2 is not None: - params["alias2"] = alias2 - if alias3 is not None: - params["alias3"] = alias3 - if alias4 is not None: - params["alias4"] = alias4 - response = self.json_api_call("POST", path, params) - return response - - def delete_user(self, user_id): - """ - Deletes a user. If the user is already deleted, does nothing. - - user_id - User ID - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id - return self.json_api_call("DELETE", path, {}) - - def enroll_user(self, username, email, valid_secs=None): - """ - Enroll a user and send them an enrollment email. - - username - Username - email - Email address - valid_secs - Seconds before the enrollment link expires - (if 0 it never expires) - - Returns nothing. - - Raises RuntimeError on error. - """ - path = "/admin/v1/users/enroll" - params = { - "username": username, - "email": email, - } - - if valid_secs is not None: - params["valid_secs"] = str(valid_secs) - - return self.json_api_call("POST", path, params) - - def add_user_bypass_codes(self, user_id, count=None, valid_secs=None, remaining_uses=None, codes=None): - """ - Replace a user's bypass codes with new codes. - - user_id - User ID - count - Number of new codes to randomly generate - valid_secs - Seconds before codes expire (if 0 they will never expire) - remaining_uses - The number of times this code can be used (0 is unlimited) - codes - Optionally provide custom codes, otherwise will be random - count and codes are mutually exclusive - - Returns a list of newly created codes. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/bypass_codes" - params = {} - - if count is not None: - params["count"] = str(int(count)) - - if valid_secs is not None: - params["valid_secs"] = str(int(valid_secs)) - - if remaining_uses is not None: - params["reuse_count"] = str(int(remaining_uses)) - - if codes is not None: - params["codes"] = self._canonicalize_bypass_codes(codes) - - return self.json_api_call("POST", path, params) - - def get_user_bypass_codes(self, user_id): - """Gets a list of bypass codes associated with a user. - - Params: - user_id (str) - The user id. - - Returns: - A list of bypass code dicts. - - Notes: - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/bypass_codes" - return self.json_api_call("GET", path, {}) - - def get_user_phones(self, user_id): - """ - Returns an array of phones associated with the user. - - user_id - User ID - - Returns list of phone objects. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/phones" - return self.json_api_call("GET", path, {}) - - def add_user_phone(self, user_id, phone_id): - """ - Associates a phone with a user. - - user_id - User ID - phone_id - Phone ID - - Returns nothing. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/phones" - params = { - "phone_id": phone_id, - } - return self.json_api_call("POST", path, params) - - def delete_user_phone(self, user_id, phone_id): - """ - Dissociates a phone from a user. - - user_id - User ID - phone_id - Phone ID - - Returns nothing. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/phones/" + phone_id - params = {} - return self.json_api_call("DELETE", path, params) - - def get_user_tokens(self, user_id): - """ - Returns an array of hardware tokens associated with the user. - - user_id - User ID - - Returns list of hardware token objects. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/tokens" - params = {} - return self.json_api_call("GET", path, params) - - def add_user_token(self, user_id, token_id): - """ - Associates a hardware token with a user. - - user_id - User ID - token_id - Token ID - - Returns nothing. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/tokens" - params = { - "token_id": token_id, - } - return self.json_api_call("POST", path, params) - - def delete_user_token(self, user_id, token_id): - """ - Dissociates a hardware token from a user. - - user_id - User ID - token_id - Hardware token ID - - Returns nothing. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - token_id = six.moves.urllib.parse.quote_plus(str(token_id)) - path = "/admin/v1/users/" + user_id + "/tokens/" + token_id - return self.json_api_call("DELETE", path, {}) - - def get_user_u2ftokens(self, user_id): - """Returns an array of u2ftokens associated - with a user. - - Params: - user_id (str) - The user id. - - Returns: - An array of u2ftoken dicts. - - Notes: - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/u2ftokens" - return self.json_api_call("GET", path, {}) - - def get_user_groups(self, user_id): - """ - Returns an array of groups associated with the user. - - user_id - User ID - - Returns list of groups objects. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/groups" - return self.json_api_call("GET", path, {}) - - def add_user_group(self, user_id, group_id): - """ - Associates a group with a user. - - user_id - User ID - group_id - Group ID - - Returns nothing. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/groups" - params = {"group_id": group_id} - return self.json_api_call("POST", path, params) - - def delete_user_group(self, user_id, group_id): - """ - Dissociates a group from a user. - - user_id - User ID - group_id - Group ID - - Returns nothing. - - Raises RuntimeError on error. - """ - user_id = six.moves.urllib.parse.quote_plus(str(user_id)) - path = "/admin/v1/users/" + user_id + "/groups/" + group_id - params = {} - return self.json_api_call("DELETE", path, params) - - def get_endpoints(self): - """ - Returns list of endpoints. - - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/endpoints", {}) - return response - - def get_phones(self): - """ - Returns list of phones. - - - Returns list of phone objects. - - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/phones", {}) - return response - - def get_phone_by_id(self, phone_id): - """ - Returns a phone specified by phone_id. - - phone_id - Phone ID - - Returns phone object. - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones/" + phone_id - response = self.json_api_call("GET", path, {}) - return response - - def get_phones_by_number(self, number, extension=None): - """ - Returns a phone specified by number and extension. - - number - Phone number - extension - Phone number extension (optional) - - Returns list of 0 or 1 phone objects. - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones" - params = {"number": number} - if extension is not None: - params["extension"] = extension - response = self.json_api_call("GET", path, params) - return response - - def add_phone( - self, - number=None, - extension=None, - name=None, - type=None, - platform=None, - predelay=None, - postdelay=None, - ): - """ - Adds a phone. - - number - Phone number (optional). - extension - Phone number extension (optional). - name - Phone name (optional). - type - The phone type (optional). - platform - The phone platform (optional). - predelay - Number of seconds to wait after the number picks up - before dialing the extension (optional). - postdelay - Number of seconds to wait after the extension is - dialed before the speaking the prompt (optional). - - Returns phone object. - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones" - params = {} - if number is not None: - params["number"] = number - if extension is not None: - params["extension"] = extension - if name is not None: - params["name"] = name - if type is not None: - params["type"] = type - if platform is not None: - params["platform"] = platform - if predelay is not None: - params["predelay"] = predelay - if postdelay is not None: - params["postdelay"] = postdelay - response = self.json_api_call("POST", path, params) - return response - - def update_phone( - self, - phone_id, - number=None, - extension=None, - name=None, - type=None, - platform=None, - predelay=None, - postdelay=None, - ): - """ - Update a phone. - - number - Phone number (optional) - extension - Phone number extension (optional). - name - Phone name (optional). - type - The phone type (optional). - platform - The phone platform (optional). - predelay - Number of seconds to wait after the number picks up - before dialing the extension (optional). - postdelay - Number of seconds to wait after the extension is - dialed before the speaking the prompt (optional). - - Returns phone object. - - Raises RuntimeError on error. - """ - phone_id = six.moves.urllib.parse.quote_plus(str(phone_id)) - path = "/admin/v1/phones/" + phone_id - params = {} - if number is not None: - params["number"] = number - if extension is not None: - params["extension"] = extension - if name is not None: - params["name"] = name - if type is not None: - params["type"] = type - if platform is not None: - params["platform"] = platform - if predelay is not None: - params["predelay"] = predelay - if postdelay is not None: - params["postdelay"] = postdelay - response = self.json_api_call("POST", path, params) - return response - - def delete_phone(self, phone_id): - """ - Deletes a phone. If the phone has already been deleted, does nothing. - - phone_id - Phone ID. - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones/" + phone_id - params = {} - return self.json_api_call("DELETE", path, params) - - def send_sms_activation_to_phone( - self, phone_id, valid_secs=None, install=None, installation_msg=None, activation_msg=None - ): - """ - Generate a Duo Mobile activation code and send it to the phone via - SMS, optionally sending an additional message with a PATH to - install Duo Mobile. - - phone_id - Phone ID. - valid_secs - The number of seconds activation code should be valid for. - Default: 86400 seconds (one day). - install - '1' to also send an installation SMS message before the - activation message; '0' to not send. Default: '0'. - installation_msg - Custom installation message template to send to - the user if install was 1. Must contain - , which is replaced with the - installation URL. - activation_msg - Custom activation message template. Must contain - , which is replaced with the activation URL. - - Returns: { - "activation_barcode": "https://api-abcdef.duosecurity.com/frame/qr?value=duo%3A%2F%2Factivation-code", - "activation_msg": "To activate the Duo Mobile app, click this link: https://m-abcdef.duosecurity.com/iphone/7dmi4Oowz5g3J47FARLs", - "installation_msg": "Welcome to Duo! To install the Duo Mobile app, click this link: http://m-abcdef.duosecurity.com", - "valid_secs": 3600 - } - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones/" + phone_id + "/send_sms_activation" - params = {} - if valid_secs is not None: - params["valid_secs"] = str(valid_secs) - if install is not None: - params["install"] = str(int(bool(install))) - if installation_msg is not None: - params["installation_msg"] = installation_msg - if activation_msg is not None: - params["activation_msg"] = activation_msg - return self.json_api_call("POST", path, params) - - def create_activation_url(self, phone_id, valid_secs=None, install=None): - """ - Create an activation code for Duo Mobile. - - phone_id - Phone ID. - valid_secs - The number of seconds activation code should be valid for. - Default: 86400 seconds (one day). - install - '1' to also return an installation_url for Duo - Mobile; '0' to not return. Default: '0'. - - Returns: { - "activation_barcode": "https://api-abcdef.duosecurity.com/frame/qr?value=duo%3A%2F%2Factivation-code", - "activation_url": "https://m-abcdef.duosecurity.com/iphone/7dmi4Oowz5g3J47FARLs", - "valid_secs": 3600 - } - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones/" + phone_id + "/activation_url" - params = {} - if valid_secs is not None: - params["valid_secs"] = str(valid_secs) - if install is not None: - params["install"] = str(int(bool(install))) - return self.json_api_call("POST", path, params) - - def send_sms_installation(self, phone_id, installation_msg=None): - """ - Send a message via SMS describing how to install Duo Mobile. - - phone_id - Phone ID. - installation_msg - Custom installation message template to send to - the user if install was 1. Must contain - , which is replaced with the - installation URL. - - Returns: { - "installation_msg": "Welcome to Duo! To install the Duo Mobile app, click this link: http://m-abcdef.duosecurity.com", - } - - Raises RuntimeError on error. - """ - path = "/admin/v1/phones/" + phone_id + "/send_sms_installation" - params = {} - if installation_msg is not None: - params["installation_msg"] = installation_msg - return self.json_api_call("POST", path, params) - - def get_desktoptokens(self): - """ - Returns list of desktop tokens. - - Returns list of desktop token objects. - - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/desktoptokens", {}) - return response - - def get_desktoptoken_by_id(self, desktoptoken_id): - """ - Returns a desktop token specified by dtoken_id. - - desktoptoken_id - Desktop Token ID - - Returns desktop token object. - - Raises RuntimeError on error. - """ - path = "/admin/v1/desktoptokens/" + desktoptoken_id - response = self.json_api_call("GET", path, {}) - return response - - def add_desktoptoken(self, platform, name=None): - """ - Adds a desktop token. - - Returns desktop token object. - - platform - The desktop token platform. - name - Desktop token name (optional). - - Raises RuntimeError on error. - """ - params = { - "platform": platform, - } - if name is not None: - params["name"] = name - response = self.json_api_call("POST", "/admin/v1/desktoptokens", params) - return response - - def delete_desktoptoken(self, desktoptoken_id): - """ - Deletes a desktop token. If the desktop token has already been deleted, - does nothing. - - desktoptoken_id - Desktop token ID. - - Returns nothing. - - Raises RuntimeError on error. - """ - path = "/admin/v1/desktoptokens/" + six.moves.urllib.parse.quote_plus(desktoptoken_id) - params = {} - return self.json_api_call("DELETE", path, params) - - def update_desktoptoken(self, desktoptoken_id, platform=None, name=None): - """ - Update a desktop token. - - Returns desktop token object. - - name - Desktop token name (optional). - platform - The desktop token platform (optional). - - Raises RuntimeError on error. - """ - desktoptoken_id = six.moves.urllib.parse.quote_plus(str(desktoptoken_id)) - path = "/admin/v1/desktoptokens/" + desktoptoken_id - params = {} - if platform is not None: - params["platform"] = platform - if name is not None: - params["name"] = name - response = self.json_api_call("POST", path, params) - return response - - def activate_desktoptoken(self, desktoptoken_id, valid_secs=None): - """ - Generates an activation code for a desktop token. - - Returns activation info like: - { - 'activation_msg': , - 'activation_url': , - 'valid_secs': } - - Raises RuntimeError on error. - """ - - params = {} - if valid_secs: - params["valid_secs"] = str(valid_secs) - quoted_id = six.moves.urllib.parse.quote_plus(desktoptoken_id) - response = self.json_api_call("POST", "/admin/v1/desktoptokens/%s/activate" % quoted_id, params) - return response - - def get_tokens(self): - """ - Returns list of tokens. - - - Returns list of token objects. - """ - params = {} - response = self.json_api_call("GET", "/admin/v1/tokens", params) - return response - - def get_token_by_id(self, token_id): - """ - Returns a token. - - token_id - Token ID - - Returns a token object. - """ - token_id = six.moves.urllib.parse.quote_plus(str(token_id)) - path = "/admin/v1/tokens/" + token_id - params = {} - response = self.json_api_call("GET", path, params) - return response - - def get_tokens_by_serial(self, type, serial): - """ - Returns a token. - - type - Token type, one of TOKEN_HOTP_6, TOKEN_HOTP_8, TOKEN_YUBIKEY - serial - Token serial number - - Returns a list of 0 or 1 token objects. - """ - params = { - "type": type, - "serial": serial, - } - response = self.json_api_call("GET", "/admin/v1/tokens", params) - return response - - def delete_token(self, token_id): - """ - Deletes a token. If the token is already deleted, does nothing. - - token_id - Token ID - """ - token_id = six.moves.urllib.parse.quote_plus(str(token_id)) - path = "/admin/v1/tokens/" + token_id - return self.json_api_call("DELETE", path, {}) - - def add_hotp6_token(self, serial, secret, counter=None): - """ - Add a HOTP6 token. - - serial - Token serial number - secret - HOTP secret - counter - Initial counter value (default: 0) - - Returns newly added token object. - """ - path = "/admin/v1/tokens" - params = {"type": "h6", "serial": serial, "secret": secret} - if counter is not None: - params["counter"] = str(int(counter)) - response = self.json_api_call("POST", path, params) - return response - - def add_hotp8_token(self, serial, secret, counter=None): - """ - Add a HOTP8 token. - - serial - Token serial number - secret - HOTP secret - counter - Initial counter value (default: 0) - - Returns newly added token object. - """ - path = "/admin/v1/tokens" - params = {"type": "h8", "serial": serial, "secret": secret} - if counter is not None: - params["counter"] = str(int(counter)) - response = self.json_api_call("POST", path, params) - return response - - def add_totp6_token(self, serial, secret, totp_step=None): - """ - Add a TOTP6 token. - - serial - Token serial number - secret - TOTP secret - totp_step - Time step (default: 30 seconds) - - Returns newly added token object. - """ - path = "/admin/v1/tokens" - params = {"type": "t6", "serial": serial, "secret": secret} - if totp_step is not None: - params["totp_step"] = str(int(totp_step)) - response = self.json_api_call("POST", path, params) - return response - - def add_totp8_token(self, serial, secret, totp_step=None): - """ - Add a TOTP8 token. - - serial - Token serial number - secret - TOTP secret - totp_step - Time step (default: 30 seconds) - - Returns newly added token object. - """ - path = "/admin/v1/tokens" - params = {"type": "t8", "serial": serial, "secret": secret} - if totp_step is not None: - params["totp_step"] = str(int(totp_step)) - response = self.json_api_call("POST", path, params) - return response - - def update_token(self, token_id, totp_step=None): - """ - Update a token. - - totp_step - Time step (optional) - - Returns token object. - - Raises RuntimeError on error. - """ - token_id = six.moves.urllib.parse.quote_plus(str(token_id)) - path = "/admin/v1/tokens/" + token_id - params = {} - if totp_step is not None: - params["totp_step"] = totp_step - response = self.json_api_call("POST", path, params) - return response - - def add_yubikey_token(self, serial, private_id, aes_key): - """ - Add a Yubikey AES token. - - serial - Token serial number - secret - HOTP secret - - Returns newly added token object. - """ - path = "/admin/v1/tokens" - params = {"type": "yk", "serial": serial, "private_id": private_id, "aes_key": aes_key} - response = self.json_api_call("POST", path, params) - return response - - def resync_hotp_token(self, token_id, code1, code2, code3): - """ - Resync HOTP counter. The user must generate 3 consecutive OTP - from their token and input them as code1, code2, and code3. This - function will scan ahead in the OTP sequence to find a counter - that resyncs with those 3 codes. - - token_id - Token ID - code1 - First OTP from token - code2 - Second OTP from token - code3 - Third OTP from token - - Returns nothing on success. - """ - token_id = six.moves.urllib.parse.quote_plus(str(token_id)) - path = "/admin/v1/tokens/" + token_id + "/resync" - params = {"code1": code1, "code2": code2, "code3": code3} - return self.json_api_call("POST", path, params) - - def get_settings(self): - """ - Returns customer settings. - - Returns a settings object. - - Raises RuntimeError on error. - """ - return self.json_api_call("GET", "/admin/v1/settings", {}) - - def update_settings( # noqa: MC0001 - self, - lockout_threshold=None, - lockout_expire_duration=None, - inactive_user_expiration=None, - log_retention_days=None, - sms_batch=None, - sms_expiration=None, - sms_refresh=None, - sms_message=None, - fraud_email=None, - fraud_email_enabled=None, - keypress_confirm=None, - keypress_fraud=None, - timezone=None, - telephony_warning_min=None, - caller_id=None, - push_enabled=None, - voice_enabled=None, - sms_enabled=None, - mobile_otp_enabled=None, - u2f_enabled=None, - user_telephony_cost_max=None, - minimum_password_length=None, - password_requires_upper_alpha=None, - password_requires_lower_alpha=None, - password_requires_numeric=None, - password_requires_special=None, - helpdesk_bypass=None, - helpdesk_bypass_expiration=None, - reactivation_url=None, - reactivation_integration_key=None, - security_checkup_enabled=None, - ): - """ - Update settings. - - lockout_threshold - |None - lockout_expire_duration - |0|None - inactive_user_expiration - |None - log_retention_days - |0|None - sms_batch - |None - sms_expiration - |None - sms_refresh - True|False|None - sms_message - |None - fraud_email - |None - fraud_email_enabled - True|False|None - keypress_confirm - |None - keypress_fraud - |None - timezone - |None - telephony_warning_min - - caller_id - - push_enabled - True|False|None - sms_enabled = True|False|None - voice_enabled = True|False|None - mobile_otp_enabled = True|False|None - u2f_enabled = True|False|None - user_telephony_cost_max = - minimum_password_length = |None, - password_requires_upper_alpha = True|False|None - password_requires_lower_alpha = True|False|None - password_requires_numeric = True|False|None - password_requires_special = True|False|None - helpdesk_bypass - "allow"|"limit"|"deny"|None - helpdesk_bypass_expiration - |0 - reactivation_url = |None - reactivation_integration_key = |None - security_checkup_enabled = True|False|None - - Returns updated settings object. - - Raises RuntimeError on error. - - """ - params = {} - if lockout_threshold is not None: - params["lockout_threshold"] = str(lockout_threshold) - if lockout_expire_duration is not None: - params["lockout_expire_duration"] = str(lockout_expire_duration) - if inactive_user_expiration is not None: - params["inactive_user_expiration"] = str(inactive_user_expiration) - if log_retention_days is not None: - params["log_retention_days"] = str(log_retention_days) - if sms_batch is not None: - params["sms_batch"] = str(sms_batch) - if sms_expiration is not None: - params["sms_expiration"] = str(sms_expiration) - if sms_refresh is not None: - params["sms_refresh"] = "1" if sms_refresh else "0" - if sms_message is not None: - params["sms_message"] = sms_message - if fraud_email is not None: - params["fraud_email"] = fraud_email - if fraud_email_enabled is not None: - params["fraud_email_enabled"] = fraud_email_enabled - if keypress_confirm is not None: - params["keypress_confirm"] = keypress_confirm - if keypress_fraud is not None: - params["keypress_fraud"] = keypress_fraud - if timezone is not None: - params["timezone"] = timezone - if telephony_warning_min is not None: - params["telephony_warning_min"] = str(telephony_warning_min) - if caller_id is not None: - params["caller_id"] = caller_id - if push_enabled is not None: - params["push_enabled"] = "1" if push_enabled else "0" - if sms_enabled is not None: - params["sms_enabled"] = "1" if sms_enabled else "0" - if voice_enabled is not None: - params["voice_enabled"] = "1" if voice_enabled else "0" - if mobile_otp_enabled is not None: - params["mobile_otp_enabled"] = "1" if mobile_otp_enabled else "0" - if u2f_enabled is not None: - params["u2f_enabled"] = "1" if u2f_enabled else "0" - if user_telephony_cost_max is not None: - params["user_telephony_cost_max"] = str(user_telephony_cost_max) - if minimum_password_length is not None: - params["minimum_password_length"] = str(minimum_password_length) - if password_requires_upper_alpha is not None: - params["password_requires_upper_alpha"] = "1" if password_requires_upper_alpha else "0" - if password_requires_lower_alpha is not None: - params["password_requires_lower_alpha"] = "1" if password_requires_lower_alpha else "0" - if password_requires_numeric is not None: - params["password_requires_numeric"] = "1" if password_requires_numeric else "0" - if password_requires_special is not None: - params["password_requires_special"] = "1" if password_requires_special else "0" - if helpdesk_bypass is not None: - params["helpdesk_bypass"] = str(helpdesk_bypass) - if helpdesk_bypass_expiration is not None: - params["helpdesk_bypass_expiration"] = str(helpdesk_bypass_expiration) - if reactivation_url is not None: - params["reactivation_url"] = reactivation_url - if reactivation_integration_key is not None: - params["reactivation_integration_key"] = reactivation_integration_key - if security_checkup_enabled is not None: - params["security_checkup_enabled"] = security_checkup_enabled - - if not params: - raise TypeError("No settings were provided") - - response = self.json_api_call("POST", "/admin/v1/settings", params) - return response - - def set_allowed_admin_auth_methods( - self, - push_enabled=None, - sms_enabled=None, - voice_enabled=None, - mobile_otp_enabled=None, - yubikey_enabled=None, - hardware_token_enabled=None, - ): - params = {} - if push_enabled is not None: - params["push_enabled"] = "1" if push_enabled else "0" - if sms_enabled is not None: - params["sms_enabled"] = "1" if sms_enabled else "0" - if mobile_otp_enabled is not None: - params["mobile_otp_enabled"] = "1" if mobile_otp_enabled else "0" - if hardware_token_enabled is not None: - params["hardware_token_enabled"] = "1" if hardware_token_enabled else "0" - if yubikey_enabled is not None: - params["yubikey_enabled"] = "1" if yubikey_enabled else "0" - if voice_enabled is not None: - params["voice_enabled"] = "1" if voice_enabled else "0" - response = self.json_api_call("POST", "/admin/v1/admins/allowed_auth_methods", params) - return response - - def get_allowed_admin_auth_methods(self): - params = {} - response = self.json_api_call("GET", "/admin/v1/admins/allowed_auth_methods", params) - return response - - def get_info_summary(self): - """ - Returns a summary of objects in the account. - - - Raises RuntimeError on error. - """ - params = {} - response = self.json_api_call("GET", "/admin/v1/info/summary", params) - return response - - def get_info_telephony_credits_used(self, mintime=None, maxtime=None): - """ - Returns number of telephony credits used during the time period. - - mintime - Limit report to data for events after this UNIX - timestamp. Defaults to thirty days ago. - maxtime - Limit report to data for events before this UNIX - timestamp. Defaults to the current time. - - Raises RuntimeError on error. - """ - params = {} - if mintime is not None: - params["mintime"] = mintime - if maxtime is not None: - params["maxtime"] = maxtime - response = self.json_api_call("GET", "/admin/v1/info/telephony_credits_used", params) - return response - - def get_authentication_attempts(self, mintime=None, maxtime=None): - """ - Returns counts of authentication attempts, broken down by result. - - mintime - Limit report to data for events after this UNIX - timestamp. Defaults to thirty days ago. - maxtime - Limit report to data for events before this UNIX - timestamp. Defaults to the current time. - - Returns: { - "ERROR": , - "FAILURE": , - "FRAUD": , - "SUCCESS": - } - - where each integer is the number of authentication attempts with - that result. - - Raises RuntimeError on error. - """ - params = {} - if mintime is not None: - params["mintime"] = mintime - if maxtime is not None: - params["maxtime"] = maxtime - response = self.json_api_call("GET", "/admin/v1/info/authentication_attempts", params) - return response - - def get_user_authentication_attempts(self, mintime=None, maxtime=None): - """ - Returns number of unique users with each possible authentication result. - - mintime - Limit report to data for events after this UNIX - timestamp. Defaults to thirty days ago. - maxtime - Limit report to data for events before this UNIX - timestamp. Defaults to the current time. - - Returns: { - "ERROR": , - "FAILURE": , - "FRAUD": , - "SUCCESS": - } - - where each integer is the number of users who had at least one - authentication attempt ending with that result. (These counts are - thus always less than or equal to those returned by - get_authentication_attempts.) - - Raises RuntimeError on error. - """ - params = {} - if mintime is not None: - params["mintime"] = mintime - if maxtime is not None: - params["maxtime"] = maxtime - response = self.json_api_call("GET", "/admin/v1/info/user_authentication_attempts", params) - return response - - def get_groups(self): - """ - Returns a list of groups. - """ - return self.json_api_call("GET", "/admin/v1/groups", {}) - - def get_group(self, group_id, api_version=1): - """ - Returns a group by the group id. - - group_id - The id of group (Required) - api_version - The api version of the handler to use. Currently, the - default api version is v1, but the v1 api will be - deprecated in a future version of the Duo Admin API. - Please migrate to the v2 api at your earliest convenience. - For details on the differences between v1 and v2, - please see Duo's Admin API documentation. (Optional) - """ - if api_version == 1: - url = "/admin/v1/groups/" - warnings.warn( - "The v1 Admin API for group details will be deprecated " - "in a future release of the Duo Admin API. Please migrate to " - "the v2 API.", - DeprecationWarning, - ) - elif api_version == 2: - url = "/admin/v2/groups/" - else: - raise ValueError("Invalid API Version") - - return self.json_api_call("GET", url + group_id, {}) - - def get_group_users(self, group_id, limit=100, offset=0): - """ - Get a paginated list of users associated with the specified - group. - - group_id - The id of the group (Required) - limit - The maximum number of records to return. Maximum is 500. (Optional) - offset - The offset of the first record to return. (Optional) - """ - return self.json_api_call( - "GET", - "/admin/v2/groups/" + group_id + "/users", - { - "limit": str(limit), - "offset": str(offset), - }, - ) - - def create_group( - self, - name, - desc=None, - status=None, - push_enabled=None, - sms_enabled=None, - voice_enabled=None, - mobile_otp_enabled=None, - u2f_enabled=None, - ): - """ - Create a new group. - - name - The name of the group (Required) - desc - Group description (Optional) - status - Group authentication status (Optional) - push_enabled - Push factor restriction (Optional) - sms_enabled - SMS factor restriction (Optional) - voice_enabled - Voice factor restriction (Optional) - mobile_otp_enabled - Mobile OTP restriction <>True/False (Optional) - """ - params = { - "name": name, - } - if desc is not None: - params["desc"] = desc - if status is not None: - params["status"] = status - if push_enabled is not None: - params["push_enabled"] = "1" if push_enabled else "0" - if sms_enabled is not None: - params["sms_enabled"] = "1" if sms_enabled else "0" - if voice_enabled is not None: - params["voice_enabled"] = "1" if voice_enabled else "0" - if mobile_otp_enabled is not None: - params["mobile_otp_enabled"] = "1" if mobile_otp_enabled else "0" - if u2f_enabled is not None: - params["u2f_enabled"] = "1" if u2f_enabled else "0" - response = self.json_api_call("POST", "/admin/v1/groups", params) - return response - - def delete_group(self, group_id): - """ - Delete a group by group_id - - group_id - The id of the group (Required) - """ - return self.json_api_call("DELETE", "/admin/v1/groups/" + group_id, {}) - - def modify_group( - self, - group_id, - name=None, - desc=None, - status=None, - push_enabled=None, - sms_enabled=None, - voice_enabled=None, - mobile_otp_enabled=None, - u2f_enabled=None, - ): - """ - Modify a group - - group_id - The id of the group to modify (Required) - name - New group name (Optional) - desc - New group description (Optional) - status - Group authentication status (Optional) - push_enabled - Push factor restriction (Optional) - sms_enabled - SMS factor restriction (Optional) - voice_enabled - Voice factor restriction (Optional) - mobile_otp_enabled - Mobile OTP restriction (Optional) - u2f_enabled - u2f restriction (Optional) - """ - params = {} - if name is not None: - params["name"] = name - if desc is not None: - params["desc"] = desc - if status is not None: - params["status"] = status - if push_enabled is not None: - params["push_enabled"] = "1" if push_enabled else "0" - if sms_enabled is not None: - params["sms_enabled"] = "1" if sms_enabled else "0" - if voice_enabled is not None: - params["voice_enabled"] = "1" if voice_enabled else "0" - if mobile_otp_enabled is not None: - params["mobile_otp_enabled"] = "1" if mobile_otp_enabled else "0" - if u2f_enabled is not None: - params["u2f_enabled"] = "1" if u2f_enabled else "0" - response = self.json_api_call("POST", "/admin/v1/groups/" + group_id, params) - return response - - def get_integrations(self): - """ - Returns list of integrations. - - - Returns list of integration objects. - - Raises RuntimeError on error. - """ - params = {} - response = self.json_api_call("GET", "/admin/v1/integrations", params) - return response - - def get_integration(self, integration_key): - """ - Returns the requested integration. - - integration_key - The ikey of the integration to get - - Returns list of integration objects. - - Raises RuntimeError on error. - """ - params = {} - response = self.json_api_call("GET", "/admin/v1/integrations/" + integration_key, params) - return response - - def create_integration( # noqa: MC0001 - self, - name, - integration_type, - visual_style=None, - greeting=None, - notes=None, - enroll_policy=None, - username_normalization_policy=None, - adminapi_admins=None, - adminapi_info=None, - adminapi_integrations=None, - adminapi_read_log=None, - adminapi_read_resource=None, - adminapi_settings=None, - adminapi_write_resource=None, - trusted_device_days=None, - ip_whitelist=None, - ip_whitelist_enroll_policy=None, - groups_allowed=None, - self_service_allowed=None, - ): - """Creates a new integration. - - name - The name of the integration (required) - integration_type - (required) - See adminapi docs for possible values. - visual_style - (optional, default 'default') - See adminapi docs for possible values. - greeting - (optional, default '') - notes - (optional, uses default setting) - enroll_policy - (optional, default 'enroll') - username_normalization_policy - (optional, default 'none') - trusted_device_days - |None - ip_whitelist - |None - See adminapi docs for more details. - ip_whitelist_enroll_policy - - See adminapi docs for more details. - adminapi_admins - |None - adminapi_info - |None - adminapi_integrations - |None - adminapi_read_log - |None - adminapi_read_resource - |None - adminapi_settings - |None - adminapi_write_resource - |None - groups_allowed - - self_service_allowed - |None - - Returns the created integration. - - Raises RuntimeError on error. - - """ - params = {} - if name is not None: - params["name"] = name - if integration_type is not None: - params["type"] = integration_type - if visual_style is not None: - params["visual_style"] = visual_style - if greeting is not None: - params["greeting"] = greeting - if notes is not None: - params["notes"] = notes - if username_normalization_policy is not None: - params["username_normalization_policy"] = username_normalization_policy - if enroll_policy is not None: - params["enroll_policy"] = enroll_policy - if trusted_device_days is not None: - params["trusted_device_days"] = str(trusted_device_days) - if ip_whitelist is not None: - params["ip_whitelist"] = self._canonicalize_ip_whitelist(ip_whitelist) - if ip_whitelist_enroll_policy is not None: - params["ip_whitelist_enroll_policy"] = ip_whitelist_enroll_policy - if adminapi_admins is not None: - params["adminapi_admins"] = "1" if adminapi_admins else "0" - if adminapi_info is not None: - params["adminapi_info"] = "1" if adminapi_info else "0" - if adminapi_integrations is not None: - params["adminapi_integrations"] = "1" if adminapi_integrations else "0" - if adminapi_read_log is not None: - params["adminapi_read_log"] = "1" if adminapi_read_log else "0" - if adminapi_read_resource is not None: - params["adminapi_read_resource"] = "1" if adminapi_read_resource else "0" - if adminapi_settings is not None: - params["adminapi_settings"] = "1" if adminapi_settings else "0" - if adminapi_write_resource is not None: - params["adminapi_write_resource"] = "1" if adminapi_write_resource else "0" - if groups_allowed is not None: - params["groups_allowed"] = groups_allowed - if self_service_allowed is not None: - params["self_service_allowed"] = "1" if self_service_allowed else "0" - response = self.json_api_call("POST", "/admin/v1/integrations", params) - return response - - def delete_integration(self, integration_key): - """Deletes an integration. - - integration_key - The integration key of the integration to delete. - - Raises RuntimeError on error. - - """ - integration_key = six.moves.urllib.parse.quote_plus(str(integration_key)) - path = "/admin/v1/integrations/%s" % integration_key - return self.json_api_call("DELETE", path, {}) - - def update_integration( # noqa: MC0001 - self, - integration_key, - name=None, - visual_style=None, - greeting=None, - notes=None, - enroll_policy=None, - username_normalization_policy=None, - adminapi_admins=None, - adminapi_info=None, - adminapi_integrations=None, - adminapi_read_log=None, - adminapi_read_resource=None, - adminapi_settings=None, - adminapi_write_resource=None, - reset_secret_key=None, - trusted_device_days=None, - ip_whitelist=None, - ip_whitelist_enroll_policy=None, - groups_allowed=None, - self_service_allowed=None, - ): - """Updates an integration. - - integration_key - The key of the integration to update. (required) - name - The name of the integration (optional) - visual_style - (optional, default 'default') - See adminapi docs for possible values. - greeting - Voice greeting (optional, default '') - notes - internal use (optional, uses default setting) - enroll_policy - <'enroll'|'allow'|'deny'> (optional, default 'enroll') - trusted_device_days - |None - ip_whitelist - |None - See adminapi docs for more details. - ip_whitelist_enroll_policy - - See adminapi docs for more details. - adminapi_admins - |None - adminapi_info - True|False|None - adminapi_integrations - True|False|None - adminapi_read_log - True|False|None - adminapi_read_resource - True|False|None - adminapi_settings - True|False|None - adminapi_write_resource - True|False|None - reset_secret_key - |None - groups_allowed - - self_service_allowed - True|False|None - - If any value other than None is provided for 'reset_secret_key' - (for example, 1), then a new secret key will be generated for the - integration. - - Returns the created integration. - - Raises RuntimeError on error. - - """ - integration_key = six.moves.urllib.parse.quote_plus(str(integration_key)) - path = "/admin/v1/integrations/%s" % integration_key - params = {} - if name is not None: - params["name"] = name - if visual_style is not None: - params["visual_style"] = visual_style - if greeting is not None: - params["greeting"] = greeting - if notes is not None: - params["notes"] = notes - if enroll_policy is not None: - params["enroll_policy"] = enroll_policy - if username_normalization_policy is not None: - params["username_normalization_policy"] = username_normalization_policy - if trusted_device_days is not None: - params["trusted_device_days"] = str(trusted_device_days) - if ip_whitelist is not None: - params["ip_whitelist"] = self._canonicalize_ip_whitelist(ip_whitelist) - if ip_whitelist_enroll_policy is not None: - params["ip_whitelist_enroll_policy"] = ip_whitelist_enroll_policy - if adminapi_admins is not None: - params["adminapi_admins"] = "1" if adminapi_admins else "0" - if adminapi_info is not None: - params["adminapi_info"] = "1" if adminapi_info else "0" - if adminapi_integrations is not None: - params["adminapi_integrations"] = "1" if adminapi_integrations else "0" - if adminapi_read_log is not None: - params["adminapi_read_log"] = "1" if adminapi_read_log else "0" - if adminapi_read_resource is not None: - params["adminapi_read_resource"] = "1" if adminapi_read_resource else "0" - if adminapi_settings is not None: - params["adminapi_settings"] = "1" if adminapi_settings else "0" - if adminapi_write_resource is not None: - params["adminapi_write_resource"] = "1" if adminapi_write_resource else "0" - if reset_secret_key is not None: - params["reset_secret_key"] = "1" # noqa: B105 - if groups_allowed is not None: - params["groups_allowed"] = groups_allowed - if self_service_allowed is not None: - params["self_service_allowed"] = "1" if self_service_allowed else "0" - - if not params: - raise TypeError("No new values were provided") - - response = self.json_api_call("POST", path, params) - return response - - def get_admins(self): - """ - Returns list of administrators. - - - Returns list of administrator objects. See the adminapi docs. - - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/admins", {}) - return response - - def get_admin(self, admin_id): - """ - Returns an administrator. - - admin_id - The id of the administrator. - - Returns an administrator. See the adminapi docs. - - Raises RuntimeError on error. - """ - admin_id = six.moves.urllib.parse.quote_plus(str(admin_id)) - path = "/admin/v1/admins/%s" % admin_id - response = self.json_api_call("GET", path, {}) - return response - - def add_admin(self, name, email, phone, password, role=None): - """ - Create an administrator and adds it to a customer. - - name - - email - - phone - - password - - role - - - Returns the added administrator. See the adminapi docs. - - Raises RuntimeError on error. - """ - params = {} - if name is not None: - params["name"] = name - if email is not None: - params["email"] = email - if phone is not None: - params["phone"] = phone - if password is not None: - params["password"] = password - if role is not None: - params["role"] = role - response = self.json_api_call("POST", "/admin/v1/admins", params) - return response - - def update_admin( - self, - admin_id, - name=None, - phone=None, - password=None, - password_change_required=None, - ): - """ - Update one or more attributes of an administrator. - - admin_id - The id of the administrator. - name - (optional) - phone - (optional) - password - (optional) - password_change_required - (optional) - - Returns the updated administrator. See the adminapi docs. - - Raises RuntimeError on error. - """ - admin_id = six.moves.urllib.parse.quote_plus(str(admin_id)) - path = "/admin/v1/admins/%s" % admin_id - params = {} - if name is not None: - params["name"] = name - if phone is not None: - params["phone"] = phone - if password is not None: - params["password"] = password - if password_change_required is not None: - params["password_change_required"] = password_change_required - response = self.json_api_call("POST", path, params) - return response - - def delete_admin(self, admin_id): - """ - Deletes an administrator. - - admin_id - The id of the administrator. - - Raises RuntimeError on error. - """ - admin_id = six.moves.urllib.parse.quote_plus(str(admin_id)) - path = "/admin/v1/admins/%s" % admin_id - return self.json_api_call("DELETE", path, {}) - - def reset_admin(self, admin_id): - """ - Resets the admin lockout. - - admin_id - - - Raises RuntimeError on error. - """ - admin_id = six.moves.urllib.parse.quote_plus(str(admin_id)) - path = "/admin/v1/admins/%s/reset" % admin_id - return self.json_api_call("POST", path, {}) - - def activate_admin(self, email, send_email=False, valid_days=None): - """ - Generates an activate code for an administrator and optionally - emails the administrator. - - email - - valid_days - (optional) - send_email - (optional) - - Returns { - "email": , - "valid_days": - "link": - "message": - "email_sent": - "code": - } - - See the adminapi docs for updated return values. - - Raises RuntimeError on error. - """ - params = {} - if email is not None: - params["email"] = email - if send_email is not None: - params["send_email"] = "1" if send_email else "0" - if valid_days is not None: - params["valid_days"] = str(valid_days) - response = self.json_api_call("POST", "/admin/v1/admins/activate", params) - return response - - def get_logo(self): - """ - Returns current logo's PNG data or raises an error if none is set. - - Raises RuntimeError on error. - """ - response, data = self.api_call("GET", "/admin/v1/logo", params={}) - content_type = response.getheader("Content-Type") - if content_type and content_type.startswith("image/"): - return data - else: - return self.parse_json_response(response, data) - - def update_logo(self, logo): - """ - Set a logo that will appear in future Duo Mobile activations. - - logo - - - Raises RuntimeError on error. - """ - params = { - "logo": logo.encode("base64"), - } - return self.json_api_call("POST", "/admin/v1/logo", params) - - def delete_logo(self): - return self.json_api_call("DELETE", "/admin/v1/logo", params={}) - - def get_u2ftokens(self): - """Returns a list of u2ftokens. - - Returns: - Returns a list of u2ftoken dicts. - - Notes: - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/u2ftokens", {}) - return response - - def get_u2ftoken_by_id(self, registration_id): - """Returns u2ftoken specified by registration_id. - - Params: - registration_id (str): The registration id of the - u2ftoken to fetch. - - Returns: - Returns a u2ftoken dict. - - Notes: - Raises RuntimeError on error. - """ - registration_id = six.moves.urllib.parse.quote_plus(str(registration_id)) - path = "/admin/v1/u2ftokens/" + registration_id - response = self.json_api_call("GET", path, {}) - return response - - def delete_u2ftoken(self, registration_id): - """Deletes a u2ftoken. If the u2ftoken is already - deleted, does nothing. - - Params: - registration_id (str): The registration id of the - u2ftoken. - - Notes: - Raises RuntimeError on error. - """ - registration_id = six.moves.urllib.parse.quote_plus(str(registration_id)) - path = "/admin/v1/u2ftokens/" + registration_id - response = self.json_api_call("DELETE", path, {}) - return response - - def get_bypass_codes(self): - """Gets a list of bypass codes. - - Returns: - Returns a list of bypass code dicts. - - Notes: - Raises RuntimeError on error. - """ - response = self.json_api_call("GET", "/admin/v1/bypass_codes", {}) - return response - - def delete_bypass_code_by_id(self, bypass_code_id): - """Deletes a bypass code. If the bypass code is already - deleted, does nothing. - - Params: - bypass_code_id (str): The id of the bypass code. - - Notes: - Raises RuntimeError on error. - """ - registration_id = six.moves.urllib.parse.quote_plus(str(bypass_code_id)) - path = "/admin/v1/bypass_codes/" + bypass_code_id - response = self.json_api_call("DELETE", path, {}) - return response diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/auth.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/auth.py deleted file mode 100755 index 3868a5c9f3..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/auth.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -Duo Security Auth API reference client implementation. - - -""" -from __future__ import absolute_import -from . import client - - -class Auth(client.Client): - def ping(self): - """ - Determine if the Duo service is up and responding. - - Returns information about the Duo service state: { - 'time': , - } - """ - return self.json_api_call("GET", "/auth/v2/ping", {}) - - def check(self): - """ - Determine if the integration key, secret key, and signature - generation are valid. - - Returns information about the Duo service state: { - 'time': , - } - """ - return self.json_api_call("GET", "/auth/v2/check", {}) - - def logo(self): - """ - Retrieve the user-supplied logo. - - Returns the logo on success, raises RuntimeError on failure. - """ - response, data = self.api_call("GET", "/auth/v2/logo", {}) - content_type = response.getheader("Content-Type") - if content_type and content_type.startswith("image/"): - return data - else: - return self.parse_json_response(response, data) - - def enroll(self, username=None, valid_secs=None, bypass_codes=None): - """ - Create a new user and associated numberless phone. - - Returns activation information: { - 'activation_barcode': , - 'activation_code': , - 'bypass_codes': , - 'user_id': , - 'username': , - 'valid_secs': , - } - """ - params = {} - if username is not None: - params["username"] = username - if valid_secs is not None: - valid_secs = str(int(valid_secs)) - params["valid_secs"] = valid_secs - if bypass_codes is not None: - bypass_codes = str(int(bypass_codes)) - params["bypass_codes"] = bypass_codes - return self.json_api_call("POST", "/auth/v2/enroll", params) - - def enroll_status(self, user_id, activation_code): - """ - Check if a user has been enrolled yet. - - Returns a string constant indicating whether the user has been - enrolled or the code remains unclaimed. - """ - params = { - "user_id": user_id, - "activation_code": activation_code, - } - response = self.json_api_call("POST", "/auth/v2/enroll_status", params) - return response - - def preauth(self, username=None, user_id=None, ipaddr=None, trusted_device_token=None): - """ - Determine if and with what factors a user may authenticate or enroll. - - See the adminapi docs for parameter and response information. - """ - params = {} - if username is not None: - params["username"] = username - if user_id is not None: - params["user_id"] = user_id - if ipaddr is not None: - params["ipaddr"] = ipaddr - if trusted_device_token is not None: - params["trusted_device_token"] = trusted_device_token - response = self.json_api_call("POST", "/auth/v2/preauth", params) - return response - - def auth( - self, - factor, - username=None, - user_id=None, - ipaddr=None, - async_txn=False, - type=None, - display_username=None, - pushinfo=None, - device=None, - passcode=None, - ): - """ - Perform second-factor authentication for a user. - - If async_txn is True, returns: { - 'txid': , - } - - Otherwise, returns: { - 'result': , - 'status': , - 'status_msg': , - } - - If Trusted Devices is enabled, async_txn is not True, and status is - 'allow', another item is returned: - - * trusted_device_token: - """ - params = { - "factor": factor, - "async": str(int(async_txn)), - } - if username is not None: - params["username"] = username - if user_id is not None: - params["user_id"] = user_id - if ipaddr is not None: - params["ipaddr"] = ipaddr - if type is not None: - params["type"] = type - if display_username is not None: - params["display_username"] = display_username - if pushinfo is not None: - params["pushinfo"] = pushinfo - if device is not None: - params["device"] = device - if passcode is not None: - params["passcode"] = passcode - response = self.json_api_call("POST", "/auth/v2/auth", params) - return response - - def auth_status(self, txid): - """ - Longpoll for the status of an asynchronous authentication call. - - Returns a dict with four items: - - * waiting: True if the authentication attempt is still in progress - and the caller can continue to poll, else False. - - * success: True if the authentication request has completed and - was a success, else False. - - * status: String constant identifying the request's state. - - * status_msg: Human-readable string describing the request state. - - If Trusted Devices is enabled, another item is returned when success - is True: - - * trusted_device_token: String token to bypass second-factor - authentication for this user during an admin-defined period. - """ - params = { - "txid": txid, - } - status = self.json_api_call("GET", "/auth/v2/auth_status", params) - response = { - "waiting": (status.get("result") == "waiting"), - "success": (status.get("result") == "allow"), - "status": status.get("status", ""), - "status_msg": status.get("status_msg", ""), - } - - if "trusted_device_token" in status: - response["trusted_device_token"] = status["trusted_device_token"] - - return response diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/auth_v1.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/auth_v1.py deleted file mode 100755 index ef7d9260ae..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/auth_v1.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -Duo Security Auth API v1 reference client implementation. - - -""" -from __future__ import absolute_import - -from . import client - - -FACTOR_AUTO = "auto" -FACTOR_PASSCODE = "passcode" -FACTOR_PHONE = "phone" -FACTOR_SMS = "sms" -FACTOR_PUSH = "push" - -PHONE1 = "phone1" -PHONE2 = "phone2" -PHONE3 = "phone3" -PHONE4 = "phone4" -PHONE5 = "phone5" - - -class AuthV1(client.Client): - auth_details = False - - def ping(self): - """ - Returns True if and only if the Duo service is up and responding. - """ - response = self.json_api_call("GET", "/rest/v1/ping", {}) - return response == "pong" - - def check(self): - """ - Returns True if and only if the integration key, secret key, and - signature generation are valid. - """ - response = self.json_api_call("GET", "/rest/v1/check", {}) - return response == "valid" - - def logo(self): - """ - Retrieve the user-supplied logo. - - Returns the logo on success, raises RuntimeError on failure. - """ - response, data = self.api_call("GET", "/rest/v1/logo", {}) - content_type = response.getheader("Content-Type") - if content_type and content_type.startswith("image/"): - return data - else: - return self.parse_json_response(response, data) - - def preauth(self, username, ipaddr=None): - params = { - "user": username, - } - if ipaddr is not None: - params["ipaddr"] = ipaddr - response = self.json_api_call("POST", "/rest/v1/preauth", params) - return response - - def auth( - self, - username, - factor=FACTOR_PHONE, - auto=None, - passcode=None, - phone=PHONE1, - pushinfo=None, - ipaddr=None, - async_txn=False, - ): - """ - Returns True if authentication was a success, else False. - - If 'async_txn' is True, returns txid of the authentication transaction. - """ - params = { - "user": username, - "factor": factor, - } - if async_txn: - params["async"] = "1" - if pushinfo is not None: - params["pushinfo"] = pushinfo - if ipaddr is not None: - params["ipaddr"] = ipaddr - - if factor == FACTOR_AUTO: - params["auto"] = auto - elif factor == FACTOR_PASSCODE: - params["code"] = passcode - elif factor == FACTOR_PHONE: - params["phone"] = phone - elif factor == FACTOR_SMS: - params["phone"] = phone - elif factor == FACTOR_PUSH: - params["phone"] = phone - - response = self.json_api_call("POST", "/rest/v1/auth", params) - if self.auth_details: - return response - if async_txn: - return response["txid"] - return response["result"] == "allow" - - def status(self, txid): - """ - Returns a 3-tuple: - (complete, success, description) - - complete - True if the authentication request has - completed, else False. - success - True if the authentication request has - completed and was a success, else False. - description - A string describing the current status of the - authentication request. - """ - params = { - "txid": txid, - } - response = self.json_api_call("GET", "/rest/v1/status", params) - complete = False - success = False - if "result" in response: - complete = True - success = response["result"] == "allow" - description = response["status"] - - return (complete, success, description) diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/ca_certs.pem b/plugins/duo_auth/vendor/duo_client_python/duo_client/ca_certs.pem deleted file mode 100755 index 8e282ab6cc..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/ca_certs.pem +++ /dev/null @@ -1,120 +0,0 @@ -subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Assured ID Root CA ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv -b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c -JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP -mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ -wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 -VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ -AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB -AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW -BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun -pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC -dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf -fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm -NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx -H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE----- - -subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB -CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 -nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt -43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P -T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 -gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR -TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw -DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr -hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg -06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF -PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls -YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- - -subject= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j -ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 -LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug -RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm -+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW -PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM -xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB -Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 -hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg -EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA -FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec -nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z -eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF -hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 -Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep -+OkuE6N36B9K ------END CERTIFICATE----- - -subject= /C=US/O=SecureTrust Corporation/CN=SecureTrust CA ------BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz -MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv -cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz -Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO -0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao -wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj -7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS -8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT -BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg -JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 -6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ -3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm -D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS -CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE----- - -subject= /C=US/O=SecureTrust Corporation/CN=Secure Global CA ------BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK -MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x -GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx -MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg -Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ -iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa -/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ -jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI -HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 -sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w -gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF -MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw -KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG -AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L -URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO -H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm -I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY -iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE----- diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/client.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/client.py deleted file mode 100755 index 67986262eb..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/client.py +++ /dev/null @@ -1,479 +0,0 @@ -""" -Low level functions for generating Duo Web API calls and parsing results. -""" -from __future__ import absolute_import -from __future__ import print_function -import six - -__version__ = "4.0.0" - -import base64 -import collections -import datetime -import email.utils -import hashlib -import hmac -import json -import os -import socket -import ssl -import sys - -try: - # For the optional demonstration CLI program. - import argparse -except ImportError as e: - argparse_error = e - argparse = None - -try: - # Only needed if signing requests with timezones other than UTC. - import pytz -except ImportError as e: - pytz = None - pytz_error = e - -from .https_wrapper import CertValidatingHTTPSConnection - -DEFAULT_CA_CERTS = os.path.join(os.path.dirname(__file__), "ca_certs.pem") - - -def canon_params(params): - """ - Return a canonical string version of the given request parameters. - """ - # this is normalized the same as for OAuth 1.0, - # http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 - args = [] - for (key, vals) in sorted((six.moves.urllib.parse.quote(key, "~"), vals) for (key, vals) in list(params.items())): - for val in sorted(six.moves.urllib.parse.quote(val, "~") for val in vals): - args.append("%s=%s" % (key, val)) - return "&".join(args) - - -def canonicalize(method, host, uri, params, date, sig_version): - """ - Return a canonical string version of the given request attributes. - - * method: string HTTP method - * host: string hostname - * uri: string uri path - * params: string containing request params - * date: date string for request - * sig_version: signature version integer - """ - if sig_version == 1: - canon = [ - method.upper(), - host.lower(), - uri, - canon_params(params), - ] - elif sig_version == 2: - canon = [ - date, - method.upper(), - host.lower(), - uri, - canon_params(params), - ] - elif sig_version == 3: - # sig_version 3 is json only - canon = [ - date, - method.upper(), - host.lower(), - uri, - params, - ] - elif sig_version == 4: - # sig_version 4 is json only - canon = [ - date, - method.upper(), - host.lower(), - uri, - "", - hashlib.sha512(params.encode("utf-8")).hexdigest(), - ] - else: - raise ValueError("Unknown signature version: {}".format(sig_version)) - return "\n".join(canon) - - -def sign(ikey, skey, method, host, uri, date, sig_version, params, digestmod=hashlib.sha1): - """ - Return basic authorization header line with a Duo Web API signature. - """ - canonical = canonicalize(method, host, uri, params, date, sig_version) - if isinstance(skey, six.text_type): - skey = skey.encode("utf-8") - if isinstance(canonical, six.text_type): - canonical = canonical.encode("utf-8") - - sig = hmac.new(skey, canonical, digestmod) - auth = "%s:%s" % (ikey, sig.hexdigest()) - - if isinstance(auth, six.text_type): - auth = auth.encode("utf-8") - b64 = base64.b64encode(auth) - if not isinstance(b64, six.text_type): - b64 = b64.decode("utf-8") - return "Basic %s" % b64 - - -def normalize_params(params): - """ - Return copy of params with strings listified - and unicode strings utf-8 encoded. - """ - # urllib cannot handle unicode strings properly. quote() excepts, - # and urlencode() replaces them with '?'. - def encode(value): - if isinstance(value, six.text_type): - return value.encode("utf-8") - return value - - def to_list(value): - if value is None or isinstance(value, six.string_types): - return [value] - return value - - return dict((encode(key), [encode(v) for v in to_list(value)]) for (key, value) in list(params.items())) - - -class Client(object): - def __init__( - self, - ikey, - skey, - host, - ca_certs=DEFAULT_CA_CERTS, - sig_timezone="UTC", - user_agent=("Duo API Python/" + __version__), - timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - digestmod=hashlib.sha1, - sig_version=2, - ): - """ - ca_certs - Path to CA pem file. - """ - self.ikey = ikey - self.skey = skey - self.host = host - self.port = None - self.sig_timezone = sig_timezone - if ca_certs is None: - ca_certs = DEFAULT_CA_CERTS - self.ca_certs = ca_certs - self.user_agent = user_agent - self.set_proxy(host=None, proxy_type=None) - self.digestmod = digestmod - self.sig_version = sig_version - - # Default timeout is a sentinel object - if timeout is socket._GLOBAL_DEFAULT_TIMEOUT: - self.timeout = timeout - else: - self.timeout = float(timeout) - - if sig_version == 4 and digestmod != hashlib.sha512: - raise ValueError("sha512 required for sig_version 4") - - def set_proxy(self, host, port=None, headers=None, proxy_type="CONNECT"): - """ - Configure proxy for API calls. Supported proxy_type values: - - 'CONNECT' - HTTP proxy with CONNECT. - None - Disable proxy. - """ - if proxy_type not in ("CONNECT", None): - raise NotImplementedError("proxy_type=%s" % (proxy_type,)) - self.proxy_headers = headers - self.proxy_host = host - self.proxy_port = port - self.proxy_type = proxy_type - - def api_call(self, method, path, params): - """ - Call a Duo API method. Return a (response, data) tuple. - - * method: HTTP request method. E.g. "GET", "POST", or "DELETE". - * path: Full path of the API endpoint. E.g. "/auth/v2/ping". - * params: dict mapping from parameter name to stringified value, - or a dict to be converted to json. - """ - if self.sig_version in (1, 2): - params = normalize_params(params) - elif self.sig_version in (3, 4): - # Raises if params are not a dict that can be converted - # to json. - params = self.canon_json(params) - - if self.sig_timezone == "UTC": - now = email.utils.formatdate() - elif pytz is None: - raise pytz_error - else: - d = datetime.datetime.now(pytz.timezone(self.sig_timezone)) - now = d.strftime("%a, %d %b %Y %H:%M:%S %z") - - auth = sign( - self.ikey, - self.skey, - method, - self.host, - path, - now, - self.sig_version, - params, - self.digestmod, - ) - headers = { - "Authorization": auth, - "Date": now, - "Host": self.host, - } - - if self.user_agent: - headers["User-Agent"] = self.user_agent - - if method in ["POST", "PUT"]: - if self.sig_version in (3, 4): - headers["Content-type"] = "application/json" - body = params - else: - headers["Content-type"] = "application/x-www-form-urlencoded" - body = six.moves.urllib.parse.urlencode(params, doseq=True) - uri = path - else: - body = None - uri = path + "?" + six.moves.urllib.parse.urlencode(params, doseq=True) - - encoded_headers = {} - for k, v in headers.items(): - if isinstance(k, six.text_type): - k = k.encode("ascii") - if isinstance(v, six.text_type): - v = v.encode("ascii") - encoded_headers[k] = v - - return self._make_request(method, uri, body, encoded_headers) - - def _connect(self): - # Host and port for the HTTP(S) connection to the API server. - if self.ca_certs == "HTTP": - api_port = 80 - else: - api_port = 443 - if self.port is not None: - api_port = self.port - - # Host and port for outer HTTP(S) connection if proxied. - if self.proxy_type is None: - host = self.host - port = api_port - elif self.proxy_type == "CONNECT": - host = self.proxy_host - port = self.proxy_port - else: - raise NotImplementedError("proxy_type=%s" % (self.proxy_type,)) - - # Create outer HTTP(S) connection. - if self.ca_certs == "HTTP": - conn = six.moves.http_client.HTTPConnection(host, port) - elif self.ca_certs == "DISABLE": - kwargs = {} - if hasattr(ssl, "_create_unverified_context"): - # httplib.HTTPSConnection validates certificates by - # default in Python 2.7.9+. - kwargs["context"] = ssl._create_unverified_context() # noqa: B323 - conn = six.moves.http_client.HTTPSConnection(host, port, **kwargs) # noqa: B309 - else: - conn = CertValidatingHTTPSConnection(host, port, ca_certs=self.ca_certs) - - # Override default socket timeout if requested. - conn.timeout = self.timeout - - # Configure CONNECT proxy tunnel, if any. - if self.proxy_type == "CONNECT": - if hasattr(conn, "set_tunnel"): # 2.7+ - conn.set_tunnel(self.host, api_port, self.proxy_headers) - elif hasattr(conn, "_set_tunnel"): # 2.6.3+ - # pylint: disable=E1103 - conn._set_tunnel(self.host, api_port, self.proxy_headers) - # pylint: enable=E1103 - - return conn - - def _make_request(self, method, uri, body, headers): - conn = self._connect() - if self.proxy_type == "CONNECT": - # Ensure the request uses the correct protocol and Host. - if self.ca_certs == "HTTP": - api_proto = "http" - else: - api_proto = "https" - uri = "".join((api_proto, "://", self.host, uri)) - conn.request(method, uri, body, headers) - response = conn.getresponse() - data = response.read() - self._disconnect(conn) - return (response, data) - - def _disconnect(self, conn): - conn.close() - - def json_api_call(self, method, path, params): - """ - Call a Duo API method which is expected to return a JSON body - with a 200 status. Return the response data structure or raise - RuntimeError. - """ - (response, data) = self.api_call(method, path, params) - return self.parse_json_response(response, data) - - def parse_json_response(self, response, data): - """ - Return the parsed data structure or raise RuntimeError. - """ - - def raise_error(msg): - error = RuntimeError(msg) - error.status = response.status - error.reason = response.reason - error.data = data - raise error - - if not isinstance(data, six.text_type): - data = data.decode("utf-8") - if response.status != 200: - try: - data = json.loads(data) - if data["stat"] == "FAIL": - if "message_detail" in data: - raise_error( - "Received %s %s (%s)" - % ( - response.status, - data["message"], - data["message_detail"], - ) - ) - else: - raise_error( - "Received %s %s" - % ( - response.status, - data["message"], - ) - ) - except (ValueError, KeyError, TypeError): - pass - raise_error( - "Received %s %s" - % ( - response.status, - response.reason, - ) - ) - try: - data = json.loads(data) - if data["stat"] != "OK": - raise_error("Received error response: %s" % data) - return data["response"] - except (ValueError, KeyError, TypeError): - raise_error("Received bad response: %s" % data) - - @classmethod - def canon_json(cls, params): - if not isinstance(params, dict): - raise ValueError("JSON request must be an object.") - return json.dumps(params, sort_keys=True, separators=(",", ":")) - - -def output_response(response, data, headers=None): - """ - Print response, parsed, sorted, and pretty-printed if JSON - """ - if not headers: - headers = [] - print(response.status, response.reason) - for header in headers: - val = response.getheader(header) - if val is not None: - print("%s: %s" % (header, val)) - try: - if not isinstance(data, six.text_type): - data = data.decode("utf-8") - data = json.loads(data) - data = json.dumps(data, sort_keys=True, indent=4) - except ValueError: - pass - print(data) - - -def main(): - if argparse is None: - raise argparse_error - parser = argparse.ArgumentParser() - # named arguments - parser.add_argument("--ikey", required=True, help="Duo integration key") - parser.add_argument("--skey", required=True, help="Duo integration secret key") - parser.add_argument("--host", required=True, help="Duo API hostname") - parser.add_argument("--method", required=True, help="HTTP request method") - parser.add_argument("--path", required=True, help="API endpoint path") - parser.add_argument("--ca", default=DEFAULT_CA_CERTS) - parser.add_argument("--sig-version", type=int, default=2) - parser.add_argument("--sig-timezone", default="UTC") - parser.add_argument( - "--show-header", - action="append", - default=[], - metavar="Header-Name", - help="Show specified response header(s) (default: only output body).", - ) - parser.add_argument("--file-args", default=[]) - # optional positional arguments are used for GET/POST params, name=val - parser.add_argument("param", nargs="*") - args = parser.parse_args() - - client = Client( - ikey=args.ikey, - skey=args.skey, - host=args.host, - ca_certs=args.ca, - sig_version=args.sig_version, - sig_timezone=args.sig_timezone, - ) - - params = collections.defaultdict(list) - for p in args.param: - try: - (k, v) = p.split("=", 1) - except ValueError: - sys.exit("Error: Positional argument %s is not " "in key=value format." % (p,)) - params[k].append(v) - - # parse which arguments are filenames - file_args = args.file_args - if args.file_args: - file_args = file_args.split(",") - - for (k, v) in list(params.items()): - if k in file_args: # value is a filename, replace with contents - if len(v) != 1: - # file arguments cannot have multiple values - raise NotImplementedError - (v,) = v - with open(v, "rb") as val: - params[k] = base64.b64encode(val.read()) - else: - params[k] = v - - (response, data) = client.api_call(args.method, args.path, params) - output_response(response, data, args.show_header) - - -if __name__ == "__main__": - main() diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/https_wrapper.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/https_wrapper.py deleted file mode 100755 index e157b4e59e..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/https_wrapper.py +++ /dev/null @@ -1,150 +0,0 @@ -### The following code was adapted from: -### https://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py - -# Copyright 2007 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -"""Extensions to allow HTTPS requests with SSL certificate validation.""" -from __future__ import absolute_import - - -import six.moves.http_client -import re -import socket -import six.moves.urllib -import ssl - - -class InvalidCertificateException(six.moves.http_client.HTTPException): - """Raised when a certificate is provided with an invalid hostname.""" - - def __init__(self, host, cert, reason): - """Constructor. - - Args: - host: The hostname the connection was made to. - cert: The SSL certificate (as a dictionary) the host returned. - """ - six.moves.http_client.HTTPException.__init__(self) - self.host = host - self.cert = cert - self.reason = reason - - def __str__(self): - return ( - "Host %s returned an invalid certificate (%s): %s\n" - "To learn more, see " - "http://code.google.com/appengine/kb/general.html#rpcssl" % (self.host, self.reason, self.cert) - ) - - -class CertValidatingHTTPSConnection(six.moves.http_client.HTTPConnection): - """An HTTPConnection that connects over SSL and validates certificates.""" - - default_port = six.moves.http_client.HTTPS_PORT - - def __init__(self, host, port=None, key_file=None, cert_file=None, ca_certs=None, strict=None, **kwargs): - """Constructor. - - Args: - host: The hostname. Can be in 'host:port' form. - port: The port. Defaults to 443. - key_file: A file containing the client's private key - cert_file: A file containing the client's certificates - ca_certs: A file contianing a set of concatenated certificate authority - certs for validating the server against. - strict: When true, causes BadStatusLine to be raised if the status line - can't be parsed as a valid HTTP/1.0 or 1.1 status line. - """ - six.moves.http_client.HTTPConnection.__init__(self, host, port, strict, **kwargs) - self.key_file = key_file - self.cert_file = cert_file - self.ca_certs = ca_certs - if self.ca_certs: - self.cert_reqs = ssl.CERT_REQUIRED - else: - self.cert_reqs = ssl.CERT_NONE - - def _GetValidHostsForCert(self, cert): - """Returns a list of valid host globs for an SSL certificate. - - Args: - cert: A dictionary representing an SSL certificate. - Returns: - list: A list of valid host globs. - """ - if "subjectAltName" in cert: - return [x[1] for x in cert["subjectAltName"] if x[0].lower() == "dns"] - else: - return [x[0][1] for x in cert["subject"] if x[0][0].lower() == "commonname"] - - def _ValidateCertificateHostname(self, cert, hostname): - """Validates that a given hostname is valid for an SSL certificate. - - Args: - cert: A dictionary representing an SSL certificate. - hostname: The hostname to test. - Returns: - bool: Whether or not the hostname is valid for this certificate. - """ - hosts = self._GetValidHostsForCert(cert) - for host in hosts: - host_re = host.replace(".", "\.").replace("*", "[^.]*") - if re.search("^%s$" % (host_re,), hostname, re.I): - return True - return False - - def connect(self): - "Connect to a host on a given (SSL) port." - self.sock = socket.create_connection((self.host, self.port), self.timeout) - if self._tunnel_host: - self._tunnel() - self.sock = ssl.wrap_socket( # noqa: B504 - self.sock, - keyfile=self.key_file, - certfile=self.cert_file, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ) - if self.cert_reqs & ssl.CERT_REQUIRED: - cert = self.sock.getpeercert() - cert_validation_host = self._tunnel_host or self.host - hostname = cert_validation_host.split(":", 0)[0] - if not self._ValidateCertificateHostname(cert, hostname): - raise InvalidCertificateException(hostname, cert, "hostname mismatch") - - -class CertValidatingHTTPSHandler(six.moves.urllib.request.HTTPSHandler): - """An HTTPHandler that validates SSL certificates.""" - - def __init__(self, **kwargs): - """Constructor. Any keyword args are passed to the httplib handler.""" - super(CertValidatingHTTPSHandler, self).__init__(self) - self._connection_args = kwargs - - def https_open(self, req): - def http_class_wrapper(host, **kwargs): - full_kwargs = dict(self._connection_args) - full_kwargs.update(kwargs) - return CertValidatingHTTPSConnection(host, **full_kwargs) - - try: - return self.do_open(http_class_wrapper, req) - except six.moves.urllib.error.URLError as e: - if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: - raise InvalidCertificateException(req.host, "", e.reason.args[1]) - raise - - https_request = six.moves.urllib.request.HTTPSHandler.do_request_ diff --git a/plugins/duo_auth/vendor/duo_client_python/duo_client/verify.py b/plugins/duo_auth/vendor/duo_client_python/duo_client/verify.py deleted file mode 100644 index 26c980ca19..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/duo_client/verify.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Duo Security Verify API reference client implementation. - - -""" -from __future__ import absolute_import -from . import client - - -class Verify(client.Client): - def call( - self, - phone, - extension=None, - predelay=None, - postdelay=None, - message="The PIN is ", - digits=None, - ): - """ - Return a (PIN, txid) tuple from the response for a call API call. - """ - params = { - "phone": phone, - "message": message, - } - if extension is not None: - params["extension"] = extension - if predelay is not None: - params["predelay"] = predelay - if postdelay is not None: - params["postdelay"] = postdelay - if digits is not None: - params["digits"] = str(int(digits)) - response = self.json_api_call("POST", "/verify/v1/call", params) - return (response["pin"], response["txid"]) - - def status(self, txid): - """ - Return the response for a status API call. - """ - params = { - "txid": txid, - } - response = self.json_api_call("GET", "/verify/v1/status", params) - return response - - def sms(self, phone, message="The PIN is ", digits=None): - """ - Return the PIN from the response for a SMS API call. - """ - params = { - "phone": phone, - "message": message, - } - if digits is not None: - params["digits"] = str(int(digits)) - response = self.json_api_call("POST", "/verify/v1/sms", params) - return response["pin"] diff --git a/plugins/duo_auth/vendor/duo_client_python/examples/create_user_and_phone.py b/plugins/duo_auth/vendor/duo_client_python/examples/create_user_and_phone.py deleted file mode 100755 index 7d3e8c1370..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/examples/create_user_and_phone.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/python -from __future__ import absolute_import -from __future__ import print_function -import pprint -import sys - -import duo_client -from six.moves import input - -argv_iter = iter(sys.argv[1:]) - - -def get_next_arg(prompt): - try: - return next(argv_iter) - except StopIteration: - return input(prompt) - - -# Configuration and information about objects to create. -admin_api = duo_client.Admin( - ikey=get_next_arg('Admin API integration key ("DI..."): '), - skey=get_next_arg("integration secret key: "), - host=get_next_arg('API hostname ("api-....duosecurity.com"): '), -) - -USERNAME = get_next_arg("user login name: ") -REALNAME = get_next_arg("user full name: ") - -# Refer to http://www.duosecurity.com/docs/adminapi for more -# information about phone types and platforms. -PHONE_NUMBER = get_next_arg("phone number (e.g. +1-555-123-4567): ") -PHONE_TYPE = get_next_arg("phone type (e.g. mobile): ") -PHONE_PLATFORM = get_next_arg("phone platform (e.g. google android): ") - -# Create and return a new user object. -user = admin_api.add_user( - username=USERNAME, - realname=REALNAME, -) -print("Created user:") -pprint.pprint(user) - -# Create and return a new phone object. -phone = admin_api.add_phone( - number=PHONE_NUMBER, - type=PHONE_TYPE, - platform=PHONE_PLATFORM, -) -print("Created phone:") -pprint.pprint(phone) - -# Associate the user with the phone. -admin_api.add_user_phone( - user_id=user["user_id"], - phone_id=phone["phone_id"], -) -print("Added phone", phone["number"], "to user", user["username"]) - -# Send two SMS messages to the phone with information about installing -# the app for PHONE_PLATFORM and activating it with this Duo account. -act_sent = admin_api.send_sms_activation_to_phone( - phone_id=phone["phone_id"], - install="1", -) -print("SMS activation sent to", phone["number"] + ":") -pprint.pprint(act_sent) diff --git a/plugins/duo_auth/vendor/duo_client_python/examples/report_auths_by_country.py b/plugins/duo_auth/vendor/duo_client_python/examples/report_auths_by_country.py deleted file mode 100755 index 3719f74a44..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/examples/report_auths_by_country.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -from __future__ import absolute_import -import csv -import sys -import duo_client -import json -from six.moves import input - -argv_iter = iter(sys.argv[1:]) - - -def get_next_arg(prompt): - try: - return next(argv_iter) - except StopIteration: - return input(prompt) - - -# Configuration and information about objects to create. -admin_api = duo_client.Admin( - ikey=get_next_arg('Admin API integration key ("DI..."): '), - skey=get_next_arg("integration secret key: "), - host=get_next_arg('API hostname ("api-....duosecurity.com"): '), -) - -# Retrieve log info from API: -logs = admin_api.get_authentication_log() - -# Count authentications by country: -counts = dict() -for log in logs: - country = log["location"]["country"] - if country != "": - counts[country] = counts.get(country, 0) + 1 - -# Print CSV of country, auth count: -auths_descending = sorted(counts.items(), reverse=True) -reporter = csv.writer(sys.stdout) -print("[+] Report of auth counts by country:") -reporter.writerow(("Country", "Auth Count")) -for row in auths_descending: - reporter.writerow( - [ - row[0], - row[1], - ] - ) diff --git a/plugins/duo_auth/vendor/duo_client_python/examples/report_users_and_phones.py b/plugins/duo_auth/vendor/duo_client_python/examples/report_users_and_phones.py deleted file mode 100755 index cdcd8e30c5..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/examples/report_users_and_phones.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -from __future__ import absolute_import -from __future__ import print_function -import csv -import sys - -import duo_client -from six.moves import input - -argv_iter = iter(sys.argv[1:]) - - -def get_next_arg(prompt): - try: - return next(argv_iter) - except StopIteration: - return input(prompt) - - -# Configuration and information about objects to create. -admin_api = duo_client.Admin( - ikey=get_next_arg('Admin API integration key ("DI..."): '), - skey=get_next_arg("integration secret key: "), - host=get_next_arg('API hostname ("api-....duosecurity.com"): '), -) - -# Retrieve user info from API: -users = admin_api.get_users() - -# Print CSV of username, phone number, phone type, and phone platform: -# -# (If a user has multiple phones, there will be one line printed per -# associated phone.) -reporter = csv.writer(sys.stdout) -print("[+] Report of all users and associated phones:") -reporter.writerow(("Username", "Phone Number", "Type", "Platform")) -for user in users: - for phone in user["phones"]: - reporter.writerow( - [ - user["username"], - phone["number"], - phone["type"], - phone["platform"], - ] - ) diff --git a/plugins/duo_auth/vendor/duo_client_python/examples/splunk/duo.conf b/plugins/duo_auth/vendor/duo_client_python/examples/splunk/duo.conf deleted file mode 100755 index c41116ea76..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/examples/splunk/duo.conf +++ /dev/null @@ -1,12 +0,0 @@ -[duo] -; admin api integration key -ikey = - -; admin api secret key -skey = - -; api- -host = - -; HTTP proxy support -;http_proxy = http://host[:port] diff --git a/plugins/duo_auth/vendor/duo_client_python/examples/splunk/splunk.py b/plugins/duo_auth/vendor/duo_client_python/examples/splunk/splunk.py deleted file mode 100755 index 9c17128da3..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/examples/splunk/splunk.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/python -from __future__ import absolute_import -from __future__ import print_function -import six.moves.configparser -import optparse -import os -import sys -import time - -import duo_client - -# For proxy support -from urlparse import urlparse - - -class BaseLog(object): - def __init__(self, admin_api, path, logname): - self.admin_api = admin_api - self.path = path - self.logname = logname - - self.mintime = 0 - self.events = [] - - def get_events(self): - raise NotImplementedError - - def print_events(self): - raise NotImplementedError - - def get_last_timestamp_path(self): - """ - Returns the path to the file containing the timestamp of the last - event fetched. - """ - filename = self.logname + "_last_timestamp_" + self.admin_api.host - path = os.path.join(self.path, filename) - return path - - def get_mintime(self): - """ - Updates self.mintime which is the minimum timestamp of - log events we want to fetch. - self.mintime is > all event timestamps we have already fetched. - """ - try: - # Only fetch events that come after timestamp of last event - path = self.get_last_timestamp_path() - self.mintime = int(open(path).read().strip()) + 1 - except IOError: - pass - - def write_last_timestamp(self): - """ - Store last_timestamp so that we don't fetch the same events again - """ - if not self.events: - # Do not update last_timestamp - return - - last_timestamp = 0 - for event in self.events: - last_timestamp = max(last_timestamp, event["timestamp"]) - - path = self.get_last_timestamp_path() - f = open(path, "w") - f.write(str(last_timestamp)) - f.close() - - def run(self): - """ - Fetch new log events and print them. - """ - self.events = [] - self.get_mintime() - self.get_events() - self.print_events() - self.write_last_timestamp() - - -class AdministratorLog(BaseLog): - def __init__(self, admin_api, path): - BaseLog.__init__(self, admin_api, path, "administrator") - - def get_events(self): - self.events = self.admin_api.get_administrator_log( - mintime=self.mintime, - ) - - def print_events(self): - """ - Print events in a format suitable for Splunk. - """ - for event in self.events: - event["ctime"] = time.ctime(event["timestamp"]) - event["actionlabel"] = { - "admin_login": "Admin Login", - "admin_create": "Create Admin", - "admin_update": "Update Admin", - "admin_delete": "Delete Admin", - "customer_update": "Update Customer", - "group_create": "Create Group", - "group_udpate": "Update Group", - "group_delete": "Delete Group", - "integration_create": "Create Integration", - "integration_update": "Update Integration", - "integration_delete": "Delete Integration", - "phone_create": "Create Phone", - "phone_update": "Update Phone", - "phone_delete": "Delete Phone", - "user_create": "Create User", - "user_update": "Update User", - "user_delete": "Delete User", - }.get(event["action"], event["action"]) - - fmtstr = ( - "%(timestamp)s," - 'host="%(host)s", ' - 'eventtype="%(eventtype)s", ' - 'username="%(username)s", ' - 'action="%(actionlabel)s"' - ) - if event["object"]: - fmtstr += ', object="%(object)s"' - if event["description"]: - fmtstr += ', description="%(description)s"' - - print(fmtstr % event) - - -class AuthenticationLog(BaseLog): - def __init__(self, admin_api, path): - BaseLog.__init__(self, admin_api, path, "authentication") - - def get_events(self): - self.events = self.admin_api.get_authentication_log( - mintime=self.mintime, - ) - - def print_events(self): - """ - Print events in a format suitable for Splunk. - """ - for event in self.events: - event["ctime"] = time.ctime(event["timestamp"]) - - fmtstr = ( - "%(timestamp)s," - 'host="%(host)s", ' - 'eventtype="%(eventtype)s", ' - 'username="%(username)s", ' - 'factor="%(factor)s", ' - 'result="%(result)s", ' - 'reason="%(reason)s", ' - 'ip="%(ip)s", ' - 'integration="%(integration)s", ' - 'newenrollment="%(new_enrollment)s"' - ) - - print(fmtstr % event) - - -class TelephonyLog(BaseLog): - def __init__(self, admin_api, path): - BaseLog.__init__(self, admin_api, path, "telephony") - - def get_events(self): - self.events = self.admin_api.get_telephony_log( - mintime=self.mintime, - ) - - def print_events(self): - """ - Print events in a format suitable for Splunk. - """ - for event in self.events: - event["ctime"] = time.ctime(event["timestamp"]) - event["host"] = self.admin_api.host - - fmtstr = ( - "%(timestamp)s," - 'host="%(host)s", ' - 'eventtype="%(eventtype)s", ' - 'context="%(context)s", ' - 'type="%(type)s", ' - 'phone="%(phone)s", ' - 'credits="%(credits)s"' - ) - - print(fmtstr % event) - - -def admin_api_from_config(config_path): - """ - Return a duo_client.Admin object created using the parameters - stored in a config file. - """ - config = six.moves.configparser.ConfigParser() - config.read(config_path) - config_d = dict(config.items("duo")) - ca_certs = config_d.get("ca_certs", None) - if ca_certs is None: - ca_certs = config_d.get("ca", None) - - ret = duo_client.Admin( - ikey=config_d["ikey"], - skey=config_d["skey"], - host=config_d["host"], - ca_certs=ca_certs, - ) - - http_proxy = config_d.get("http_proxy", None) - if http_proxy is not None: - proxy_parsed = urlparse(http_proxy) - proxy_host = proxy_parsed.hostname - proxy_port = proxy_parsed.port - ret.set_proxy(host=proxy_host, port=proxy_port) - - return ret - - -def main(): - parser = optparse.OptionParser(usage="%prog []") - (options, args) = parser.parse_args(sys.argv[1:]) - - if len(sys.argv) == 1: - config_path = os.path.abspath(__file__) - config_path = os.path.dirname(config_path) - config_path = os.path.join(config_path, "duo.conf") - else: - config_path = os.path.abspath(sys.argv[1]) - - admin_api = admin_api_from_config(config_path) - - # Use the directory of the config file to store the last event tstamps - path = os.path.dirname(config_path) - - for logclass in (AdministratorLog, AuthenticationLog, TelephonyLog): - log = logclass(admin_api, path) - log.run() - - -if __name__ == "__main__": - main() diff --git a/plugins/duo_auth/vendor/duo_client_python/examples/verify_phone_number_with_voice_call.py b/plugins/duo_auth/vendor/duo_client_python/examples/verify_phone_number_with_voice_call.py deleted file mode 100755 index e5286cdd1b..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/examples/verify_phone_number_with_voice_call.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/python -from __future__ import absolute_import -from __future__ import print_function -import sys - -import duo_client -from six.moves import input - -argv_iter = iter(sys.argv[1:]) - - -def get_next_arg(prompt): - try: - return next(argv_iter) - except StopIteration: - return input(prompt) - - -# You can find this information in the integrations section -# where you signed up for Duo. -verify_api = duo_client.Verify( - ikey=get_next_arg('Verify API integration key ("DI..."): '), - skey=get_next_arg("integration secret key: "), - host=get_next_arg('API hostname ("api-....duosecurity.com"): '), -) - -# Please use your valid telephone number with country code, area -# code, and 7 digit number. For example: PHONE = '+1-313-555-5555' -PHONE_NUMBER = get_next_arg('phone number ("+1-313-555-5555"): ') - -(pin, txid) = verify_api.call(phone=PHONE_NUMBER) -print("Sent PIN: %s" % pin) -state = "" -while state != "ended": - status_res = verify_api.status(txid=txid) - print(status_res["event"], "event:", status_res["info"]) - state = status_res["state"] diff --git a/plugins/duo_auth/vendor/duo_client_python/requirements-dev.txt b/plugins/duo_auth/vendor/duo_client_python/requirements-dev.txt deleted file mode 100755 index 58a3942eae..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -nose2 -flake8 -pytz diff --git a/plugins/duo_auth/vendor/duo_client_python/requirements.txt b/plugins/duo_auth/vendor/duo_client_python/requirements.txt deleted file mode 100755 index ffe2fce498..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -six diff --git a/plugins/duo_auth/vendor/duo_client_python/setup.cfg b/plugins/duo_auth/vendor/duo_client_python/setup.cfg deleted file mode 100755 index e527289318..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[bdist_wheel] -# The code is written to work on both Python 2 and Python 3. -universal=1 - -[bdist_rpm] -requires=python-six -build_requires=python-nose diff --git a/plugins/duo_auth/vendor/duo_client_python/setup.py b/plugins/duo_auth/vendor/duo_client_python/setup.py deleted file mode 100755 index c1b16e595d..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import absolute_import -from distutils.core import setup - -import os.path - -import duo_client - -requirements_filename = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'requirements.txt') - -with open(requirements_filename) as fd: - install_requires = [i.strip() for i in fd.readlines()] - -requirements_dev_filename = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'requirements-dev.txt') - -with open(requirements_dev_filename) as fd: - tests_require = [i.strip() for i in fd.readlines()] - -long_description_filename = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'README.md') - -with open(long_description_filename) as fd: - long_description = fd.read() - -setup( - name='duo_client', - version=duo_client.__version__, - description='Reference client for Duo Security APIs', - long_description=long_description, - long_description_content_type='text/markdown', - author='Duo Security, Inc.', - author_email='support@duosecurity.com', - url='https://github.com/duosecurity/duo_client_python', - packages=['duo_client'], - package_data={'duo_client': ['ca_certs.pem']}, - license='BSD', - classifiers=[ - 'Programming Language :: Python', - 'License :: OSI Approved :: BSD License', - ], - install_requires=install_requires, - tests_require=tests_require, -) diff --git a/plugins/duo_auth/vendor/duo_client_python/tests/__init__.py b/plugins/duo_auth/vendor/duo_client_python/tests/__init__.py deleted file mode 100755 index e69de29bb2..0000000000 diff --git a/plugins/duo_auth/vendor/duo_client_python/tests/test_admin.py b/plugins/duo_auth/vendor/duo_client_python/tests/test_admin.py deleted file mode 100755 index 001ddaf260..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/tests/test_admin.py +++ /dev/null @@ -1,295 +0,0 @@ -from __future__ import absolute_import -import unittest -import warnings -from . import util -import duo_client.admin - - -class TestAdmin(unittest.TestCase): - def setUp(self): - self.client = duo_client.admin.Admin("test_ikey", "test_akey", "example.com") - self.client.account_id = "DA012345678901234567" - # monkeypatch client's _connect() - self.client._connect = lambda: util.MockHTTPConnection() - - # if you are wanting to simulate getting lists of objects - # rather than a single object - self.client_list = duo_client.admin.Admin("test_ikey", "test_akey", "example.com") - self.client_list.account_id = "DA012345678901234567" - self.client_list._connect = lambda: util.MockHTTPConnection(data_response_should_be_list=True) - - # if you are wanting to get a response from a call to get - # authentication logs - self.client_authlog = duo_client.admin.Admin("test_ikey", "test_akey", "example.com") - self.client_authlog.account_id = "DA012345678901234567" - self.client_authlog._connect = lambda: util.MockHTTPConnection(data_response_from_get_authlog=True) - - # GET with no params - def test_get_users(self): - response = self.client.get_users() - self.assertEqual(response["method"], "GET") - self.assertEqual(response["uri"], "/admin/v1/users?account_id=%s" % self.client.account_id) - self.assertEqual(response["body"], None) - - # GET with params - def test_get_users_by_name(self): - response = self.client.get_users_by_name("foo") - (uri, args) = response["uri"].split("?") - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/users") - self.assertEqual(util.params_to_dict(args), {"username": ["foo"], "account_id": [self.client.account_id]}) - self.assertEqual(response["body"], None) - response = self.client.get_users_by_name("foo") - (uri, args) = response["uri"].split("?") - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/users") - self.assertEqual(util.params_to_dict(args), {"username": ["foo"], "account_id": [self.client.account_id]}) - self.assertEqual(response["body"], None) - - # POST with params - def test_add_user(self): - # all params given - response = self.client.add_user( - "foo", - realname="bar", - status="active", - notes="notes", - email="foobar@baz.com", - firstname="fName", - lastname="lName", - alias1="alias1", - alias2="alias2", - alias3="alias3", - alias4="alias4", - ) - self.assertEqual(response["method"], "POST") - self.assertEqual(response["uri"], "/admin/v1/users") - self.assertEqual( - util.params_to_dict(response["body"]), - { - "realname": ["bar"], - "notes": ["notes"], - "username": ["foo"], - "status": ["active"], - "email": ["foobar%40baz.com"], - "firstname": ["fName"], - "lastname": ["lName"], - "account_id": [self.client.account_id], - "alias1": ["alias1"], - "alias2": ["alias2"], - "alias3": ["alias3"], - "alias4": ["alias4"], - }, - ) - # defaults - response = self.client.add_user("bar") - self.assertEqual(response["method"], "POST") - self.assertEqual(response["uri"], "/admin/v1/users") - self.assertEqual( - util.params_to_dict(response["body"]), - {"username": ["bar"], "account_id": [self.client.account_id]}, - ) - - def test_update_user(self): - response = self.client.update_user( - "DU012345678901234567", - username="foo", - realname="bar", - status="active", - notes="notes", - email="foobar@baz.com", - firstname="fName", - lastname="lName", - alias1="alias1", - alias2="alias2", - alias3="alias3", - alias4="alias4", - ) - self.assertEqual(response["method"], "POST") - self.assertEqual(response["uri"], "/admin/v1/users/DU012345678901234567") - self.assertEqual( - util.params_to_dict(response["body"]), - { - "account_id": [self.client.account_id], - "realname": ["bar"], - "notes": ["notes"], - "username": ["foo"], - "status": ["active"], - "email": ["foobar%40baz.com"], - "firstname": ["fName"], - "lastname": ["lName"], - "account_id": [self.client.account_id], - "alias1": ["alias1"], - "alias2": ["alias2"], - "alias3": ["alias3"], - "alias4": ["alias4"], - }, - ) - - def test_get_endpoints(self): - response = self.client.get_endpoints() - self.assertEqual(response["method"], "GET") - (uri, args) = response["uri"].split("?") - self.assertEqual(uri, "/admin/v1/endpoints") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_user_u2ftokens(self): - """Test to get u2ftokens by user id.""" - response = self.client.get_user_u2ftokens("DU012345678901234567") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/users/DU012345678901234567/u2ftokens") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_u2ftokens_with_params(self): - """Test to get u2ftokens with params.""" - response = list(self.client_list.get_u2ftokens())[0] - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/u2ftokens") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client_list.account_id]}) - - def test_get_u2ftokens_without_params(self): - """Test to get u2ftokens without params.""" - response = list(self.client_list.get_u2ftokens())[0] - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/u2ftokens") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client_list.account_id]}) - - def test_get_u2ftoken_by_id(self): - """Test to get u2ftoken by registration id.""" - response = self.client.get_u2ftoken_by_id("DU012345678901234567") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/u2ftokens/DU012345678901234567") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_delete_u2ftoken(self): - """Test to delete u2ftoken by registration id.""" - response = self.client.delete_u2ftoken("DU012345678901234567") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "DELETE") - self.assertEqual(uri, "/admin/v1/u2ftokens/DU012345678901234567") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_user_bypass_codes(self): - """Test to get bypass codes by user id.""" - response = self.client.get_user_bypass_codes("DU012345678901234567") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/users/DU012345678901234567/bypass_codes") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_bypass_codes(self): - """Test to get bypass codes.""" - response = list(self.client_list.get_bypass_codes())[0] - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/bypass_codes") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client_list.account_id]}) - - def test_delete_bypass_code_by_id(self): - """Test to delete a bypass code by id.""" - response = self.client.delete_bypass_code_by_id("DU012345678901234567") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "DELETE") - self.assertEqual(uri, "/admin/v1/bypass_codes/DU012345678901234567") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_authentication_log_v1(self): - """Test to get authentication log on version 1 api.""" - response = self.client_list.get_authentication_log(api_version=1)[0] - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/logs/authentication") - self.assertEqual(util.params_to_dict(args)["account_id"], [self.client_list.account_id]) - - def test_get_authentication_log_v2(self): - """Test to get authentication log on version 1 api.""" - response = self.client_authlog.get_authentication_log(api_version=2) - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v2/logs/authentication") - self.assertEqual(util.params_to_dict(args)["account_id"], [self.client_authlog.account_id]) - - def test_get_groups(self): - """Test for getting list of all groups""" - response = self.client.get_groups() - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/groups") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_group_v1(self): - """Test for v1 API of getting specific group details""" - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - response = self.client.get_group("ABC123", api_version=1) - uri, args = response["uri"].split("?") - - # Assert deprecation warning generated - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[0].category, DeprecationWarning)) - self.assertIn("Please migrate to the v2 API", str(w[0].message)) - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v1/groups/ABC123") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_group_v2(self): - """Test for v2 API of getting specific group details""" - response = self.client.get_group("ABC123", api_version=2) - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v2/groups/ABC123") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_get_group_users(self): - """Test for getting list of users associated with a group""" - response = self.client.get_group_users("ABC123") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "GET") - self.assertEqual(uri, "/admin/v2/groups/ABC123/users") - self.assertEqual( - util.params_to_dict(args), - { - "account_id": [self.client.account_id], - "limit": ["100"], - "offset": ["0"], - }, - ) - - def test_delete_group(self): - """Test for deleting a group""" - response = self.client.delete_group("ABC123") - uri, args = response["uri"].split("?") - - self.assertEqual(response["method"], "DELETE") - self.assertEqual(uri, "/admin/v1/groups/ABC123") - self.assertEqual(util.params_to_dict(args), {"account_id": [self.client.account_id]}) - - def test_modify_group(self): - """Test for modifying a group""" - response = self.client.modify_group("ABC123") - self.assertEqual(response["method"], "POST") - self.assertEqual(response["uri"], "/admin/v1/groups/ABC123") - self.assertEqual(util.params_to_dict(response["body"]), {"account_id": [self.client.account_id]}) - - -if __name__ == "__main__": - unittest.main() diff --git a/plugins/duo_auth/vendor/duo_client_python/tests/test_client.py b/plugins/duo_auth/vendor/duo_client_python/tests/test_client.py deleted file mode 100755 index c403599a5a..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/tests/test_client.py +++ /dev/null @@ -1,393 +0,0 @@ -from __future__ import absolute_import -import hashlib -import unittest -import six.moves.urllib -import duo_client.client -from . import util -import base64 - -JSON_BODY = { - "data": "abc123", - "alpha": ["a", "b", "c", "d"], - "info": { - "test": 1, - "another": 2, - }, -} -JSON_STRING = '{"alpha":["a","b","c","d"],"data":"abc123","info":{"another":2,"test":1}}' - - -class TestQueryParameters(unittest.TestCase): - """ - Tests for the proper canonicalization of query parameters for signing. - """ - - def assert_canon_params(self, params, expected): - params = duo_client.client.normalize_params(params) - self.assertEqual( - duo_client.client.canon_params(params), - expected, - ) - - def test_zero_params(self): - self.assert_canon_params( - {}, - "", - ) - - def test_one_param(self): - self.assert_canon_params( - {"realname": ["First Last"]}, - "realname=First%20Last", - ) - - def test_two_params(self): - self.assert_canon_params( - {"realname": ["First Last"], "username": ["root"]}, - "realname=First%20Last&username=root", - ) - - def test_list_string(self): - """A list and a string will both get converted.""" - self.assert_canon_params( - {"realname": "First Last", "username": ["root"]}, "realname=First%20Last&username=root" - ) - - def test_printable_ascii_characters(self): - self.assert_canon_params( - { - "digits": ["0123456789"], - "letters": ["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"], - "punctuation": ["!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"], - "whitespace": ["\t\n\x0b\x0c\r "], - }, - "digits=0123456789&letters=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&punctuation=%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~&whitespace=%09%0A%0B%0C%0D%20", - ) - - def test_unicode_fuzz_values(self): - self.assert_canon_params( - { - "bar": ["\u2815\uaaa3\u37cf\u4bb7\u36e9\ucc05\u668e\u8162\uc2bd\ua1f1"], - "baz": ["\u0df3\u84bd\u5669\u9985\ub8a4\uac3a\u7be7\u6f69\u934a\ub91c"], - "foo": ["\ud4ce\ud6d6\u7938\u50c0\u8a20\u8f15\ufd0b\u8024\u5cb3\uc655"], - "qux": ["\u8b97\uc846-\u828e\u831a\uccca\ua2d4\u8c3e\ub8b2\u99be"], - }, - "bar=%E2%A0%95%EA%AA%A3%E3%9F%8F%E4%AE%B7%E3%9B%A9%EC%B0%85%E6%9A%8E%E8%85%A2%EC%8A%BD%EA%87%B1&baz=%E0%B7%B3%E8%92%BD%E5%99%A9%E9%A6%85%EB%A2%A4%EA%B0%BA%E7%AF%A7%E6%BD%A9%E9%8D%8A%EB%A4%9C&foo=%ED%93%8E%ED%9B%96%E7%A4%B8%E5%83%80%E8%A8%A0%E8%BC%95%EF%B4%8B%E8%80%A4%E5%B2%B3%EC%99%95&qux=%E8%AE%97%EC%A1%86-%E8%8A%8E%E8%8C%9A%EC%B3%8A%EA%8B%94%E8%B0%BE%EB%A2%B2%E9%A6%BE", - ) - - def test_unicode_fuzz_keys_and_values(self): - self.assert_canon_params( - { - "\u469a\u287b\u35d0\u8ef3\u6727\u502a\u0810\ud091\xc8\uc170": [ - "\u0f45\u1a76\u341a\u654c\uc23f\u9b09\uabe2\u8343\u1b27\u60d0" - ], - "\u7449\u7e4b\uccfb\u59ff\ufe5f\u83b7\uadcc\u900c\ucfd1\u7813": [ - "\u8db7\u5022\u92d3\u42ef\u207d\u8730\uacfe\u5617\u0946\u4e30" - ], - "\u7470\u9314\u901c\u9eae\u40d8\u4201\u82d8\u8c70\u1d31\ua042": [ - "\u17d9\u0ba8\u9358\uaadf\ua42a\u48be\ufb96\u6fe9\ub7ff\u32f3" - ], - "\uc2c5\u2c1d\u2620\u3617\u96b3F\u8605\u20e8\uac21\u5934": [ - "\ufba9\u41aa\ubd83\u840b\u2615\u3e6e\u652d\ua8b5\ud56bU" - ], - }, - "%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU", - ) - - def test_sort_order_with_common_prefix(self): - self.assert_canon_params( - { - "foo_bar": "2", - "foo": "1", - }, - "foo=1&foo_bar=2", - ) - - -class TestCanonicalize(unittest.TestCase): - """ - Tests of the canonicalization of request attributes and parameters - for signing. - """ - - def test_v1(self): - test = { - "host": "foO.BAr52.cOm", - "method": "PoSt", - "params": { - "\u469a\u287b\u35d0\u8ef3\u6727\u502a\u0810\ud091\xc8\uc170": [ - "\u0f45\u1a76\u341a\u654c\uc23f\u9b09\uabe2\u8343\u1b27\u60d0" - ], - "\u7449\u7e4b\uccfb\u59ff\ufe5f\u83b7\uadcc\u900c\ucfd1\u7813": [ - "\u8db7\u5022\u92d3\u42ef\u207d\u8730\uacfe\u5617\u0946\u4e30" - ], - "\u7470\u9314\u901c\u9eae\u40d8\u4201\u82d8\u8c70\u1d31\ua042": [ - "\u17d9\u0ba8\u9358\uaadf\ua42a\u48be\ufb96\u6fe9\ub7ff\u32f3" - ], - "\uc2c5\u2c1d\u2620\u3617\u96b3F\u8605\u20e8\uac21\u5934": [ - "\ufba9\u41aa\ubd83\u840b\u2615\u3e6e\u652d\ua8b5\ud56bU" - ], - }, - "uri": "/Foo/BaR2/qux", - } - test["params"] = duo_client.client.normalize_params(test["params"]) - self.assertEqual( - duo_client.client.canonicalize(sig_version=1, date=None, **test), - "POST\nfoo.bar52.com\n/Foo/BaR2/qux\n%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU", - ) - - def test_v2(self): - test = { - "date": "Fri, 07 Dec 2012 17:18:00 -0000", - "host": "foO.BAr52.cOm", - "method": "PoSt", - "params": { - "\u469a\u287b\u35d0\u8ef3\u6727\u502a\u0810\ud091\xc8\uc170": [ - "\u0f45\u1a76\u341a\u654c\uc23f\u9b09\uabe2\u8343\u1b27\u60d0" - ], - "\u7449\u7e4b\uccfb\u59ff\ufe5f\u83b7\uadcc\u900c\ucfd1\u7813": [ - "\u8db7\u5022\u92d3\u42ef\u207d\u8730\uacfe\u5617\u0946\u4e30" - ], - "\u7470\u9314\u901c\u9eae\u40d8\u4201\u82d8\u8c70\u1d31\ua042": [ - "\u17d9\u0ba8\u9358\uaadf\ua42a\u48be\ufb96\u6fe9\ub7ff\u32f3" - ], - "\uc2c5\u2c1d\u2620\u3617\u96b3F\u8605\u20e8\uac21\u5934": [ - "\ufba9\u41aa\ubd83\u840b\u2615\u3e6e\u652d\ua8b5\ud56bU" - ], - }, - "uri": "/Foo/BaR2/qux", - } - test["params"] = duo_client.client.normalize_params(test["params"]) - self.assertEqual( - duo_client.client.canonicalize(sig_version=2, **test), - "Fri, 07 Dec 2012 17:18:00 -0000\nPOST\nfoo.bar52.com\n/Foo/BaR2/qux\n%E4%9A%9A%E2%A1%BB%E3%97%90%E8%BB%B3%E6%9C%A7%E5%80%AA%E0%A0%90%ED%82%91%C3%88%EC%85%B0=%E0%BD%85%E1%A9%B6%E3%90%9A%E6%95%8C%EC%88%BF%E9%AC%89%EA%AF%A2%E8%8D%83%E1%AC%A7%E6%83%90&%E7%91%89%E7%B9%8B%EC%B3%BB%E5%A7%BF%EF%B9%9F%E8%8E%B7%EA%B7%8C%E9%80%8C%EC%BF%91%E7%A0%93=%E8%B6%B7%E5%80%A2%E9%8B%93%E4%8B%AF%E2%81%BD%E8%9C%B0%EA%B3%BE%E5%98%97%E0%A5%86%E4%B8%B0&%E7%91%B0%E9%8C%94%E9%80%9C%E9%BA%AE%E4%83%98%E4%88%81%E8%8B%98%E8%B1%B0%E1%B4%B1%EA%81%82=%E1%9F%99%E0%AE%A8%E9%8D%98%EA%AB%9F%EA%90%AA%E4%A2%BE%EF%AE%96%E6%BF%A9%EB%9F%BF%E3%8B%B3&%EC%8B%85%E2%B0%9D%E2%98%A0%E3%98%97%E9%9A%B3F%E8%98%85%E2%83%A8%EA%B0%A1%E5%A4%B4=%EF%AE%A9%E4%86%AA%EB%B6%83%E8%90%8B%E2%98%95%E3%B9%AE%E6%94%AD%EA%A2%B5%ED%95%ABU", - ) - - def test_v2_with_json(self): - expected = ( - "Tue, 04 Jul 2017 14:12:00\n" - "POST\n" - "foo.bar52.com\n" - "/Foo/BaR2/qux\n" - '{"alpha":["a","b","c","d"],"data":"abc123","info":{"another":2,"test":1}}' - ) - params = duo_client.client.Client.canon_json(JSON_BODY) - actual = duo_client.client.canonicalize( - "POST", - "foO.BaR52.cOm", - "/Foo/BaR2/qux", - params, - "Tue, 04 Jul 2017 14:12:00", - sig_version=3, - ) - - self.assertEqual(actual, expected) - - def test_v4_with_json(self): - hashed_body = hashlib.sha512(JSON_STRING.encode("utf-8")).hexdigest() - expected = "Tue, 04 Jul 2017 14:12:00\n" "POST\n" "foo.bar52.com\n" "/Foo/BaR2/qux\n\n" + hashed_body - params = duo_client.client.Client.canon_json(JSON_BODY) - actual = duo_client.client.canonicalize( - "POST", - "foO.BaR52.cOm", - "/Foo/BaR2/qux", - params, - "Tue, 04 Jul 2017 14:12:00", - sig_version=4, - ) - - self.assertEqual(actual, expected) - - def test_invalid_signature_version_raises(self): - params = duo_client.client.Client.canon_json(JSON_BODY) - with self.assertRaises(ValueError) as e: - duo_client.client.canonicalize( - "POST", - "foO.BaR52.cOm", - "/Foo/BaR2/qux", - params, - "Tue, 04 Jul 2017 14:12:00", - sig_version=999, - ) - self.assertEqual(e.exception.args[0], "Unknown signature version: {}".format(999)) - - -class TestSign(unittest.TestCase): - """ - Tests for proper signature creation for a request. - """ - - def test_hmac_sha1(self): - test = { - "date": "Fri, 07 Dec 2012 17:18:00 -0000", - "host": "foO.BAr52.cOm", - "method": "PoSt", - "params": { - "\u469a\u287b\u35d0\u8ef3\u6727\u502a\u0810\ud091\xc8\uc170": [ - "\u0f45\u1a76\u341a\u654c\uc23f\u9b09\uabe2\u8343\u1b27\u60d0" - ], - "\u7449\u7e4b\uccfb\u59ff\ufe5f\u83b7\uadcc\u900c\ucfd1\u7813": [ - "\u8db7\u5022\u92d3\u42ef\u207d\u8730\uacfe\u5617\u0946\u4e30" - ], - "\u7470\u9314\u901c\u9eae\u40d8\u4201\u82d8\u8c70\u1d31\ua042": [ - "\u17d9\u0ba8\u9358\uaadf\ua42a\u48be\ufb96\u6fe9\ub7ff\u32f3" - ], - "\uc2c5\u2c1d\u2620\u3617\u96b3F\u8605\u20e8\uac21\u5934": [ - "\ufba9\u41aa\ubd83\u840b\u2615\u3e6e\u652d\ua8b5\ud56bU" - ], - }, - "uri": "/Foo/BaR2/qux", - } - test["params"] = duo_client.client.normalize_params(test["params"]) - ikey = "test_ikey" - actual = duo_client.client.sign( - sig_version=2, ikey=ikey, skey="gtdfxv9YgVBYcF6dl2Eq17KUQJN2PLM2ODVTkvoT", **test - ) - expected = "f01811cbbf9561623ab45b893096267fd46a5178" - expected = ikey + ":" + expected - if isinstance(expected, six.text_type): - expected = expected.encode("utf-8") - expected = base64.b64encode(expected).strip() - if not isinstance(expected, six.text_type): - expected = expected.decode("utf-8") - expected = "Basic " + expected - self.assertEqual(actual, expected) - - def test_hmac_sha1_json(self): - ikey = "test_ikey" - actual = duo_client.client.sign( - sig_version=3, - ikey=ikey, - skey="gtdfxv9YgVBYcF6dl2Eq17KUQJN2PLM2ODVTkvoT", - date="Tue, 04 Jul 2017 14:12:00", - host="foO.BAr52.cOm", - method="POST", - params=duo_client.client.Client.canon_json(JSON_BODY), - uri="/Foo/BaR2/qux", - ) - - sig = "7bf8cf95d689091cf7fdb72178f16d1c19ef92c1" - auth = "%s:%s" % (ikey, sig) - auth = auth.encode("utf-8") - b64 = base64.b64encode(auth) - b64 = b64.decode("utf-8") - expected = "Basic %s" % b64 - self.assertEqual(actual, expected) - - -class TestRequest(unittest.TestCase): - """Tests for the request created by api_call and json_api_call.""" - - # usful args for testing - args_in = {"foo": ["bar"], "baz": ["qux", "quux=quuux", "foobar=foobar&barbaz=barbaz"]} - args_out = dict((key, [six.moves.urllib.parse.quote(v) for v in val]) for (key, val) in list(args_in.items())) - - def setUp(self): - self.client = duo_client.client.Client("test_ikey", "test_akey", "example.com") - # monkeypatch client's _connect() - self.client._connect = lambda: util.MockHTTPConnection() - - def test_api_call_get_no_params(self): - (response, dummy) = self.client.api_call("GET", "/foo/bar", {}) - self.assertEqual(response.method, "GET") - self.assertEqual(response.uri, "/foo/bar?") - - def test_api_call_post_no_params(self): - (response, dummy) = self.client.api_call("POST", "/foo/bar", {}) - self.assertEqual(response.method, "POST") - self.assertEqual(response.uri, "/foo/bar") - self.assertEqual(response.body, "") - - def test_api_call_get_params(self): - (response, dummy) = self.client.api_call("GET", "/foo/bar", self.args_in) - self.assertEqual(response.method, "GET") - (uri, args) = response.uri.split("?") - self.assertEqual(uri, "/foo/bar") - self.assertEqual(util.params_to_dict(args), self.args_out) - - def test_api_call_post_params(self): - (response, dummy) = self.client.api_call("POST", "/foo/bar", self.args_in) - self.assertEqual(response.method, "POST") - self.assertEqual(response.uri, "/foo/bar") - self.assertEqual(util.params_to_dict(response.body), self.args_out) - - def test_json_api_call_get_no_params(self): - response = self.client.json_api_call("GET", "/foo/bar", {}) - self.assertEqual(response["method"], "GET") - self.assertEqual(response["uri"], "/foo/bar?") - self.assertEqual(response["body"], None) - - def test_json_api_call_post_no_params(self): - response = self.client.json_api_call("POST", "/foo/bar", {}) - self.assertEqual(response["method"], "POST") - self.assertEqual(response["uri"], "/foo/bar") - self.assertEqual(response["body"], "") - - def test_json_api_call_get_params(self): - response = self.client.json_api_call("GET", "/foo/bar", self.args_in) - self.assertEqual(response["method"], "GET") - (uri, args) = response["uri"].split("?") - self.assertEqual(uri, "/foo/bar") - self.assertEqual(util.params_to_dict(args), self.args_out) - - def test_json_api_call_post_params(self): - response = self.client.json_api_call("POST", "/foo/bar", self.args_in) - self.assertEqual(response["method"], "POST") - self.assertEqual(response["uri"], "/foo/bar") - self.assertEqual(util.params_to_dict(response["body"]), self.args_out) - - -class TestJsonRequests(unittest.TestCase): - def setUp(self): - self.client = duo_client.client.Client( - "test_ikey", "test_akey", "example.com", sig_timezone="America/Detroit", sig_version=3 - ) - # monkeypatch client's _connect() - self.client._connect = lambda: util.MockHTTPConnection() - - def test_json_post(self): - (response, dummy) = self.client.api_call("POST", "/foo/bar", JSON_BODY) - - self.assertEqual(response.method, "POST") - self.assertEqual(response.uri, "/foo/bar") - self.assertEqual(response.body, JSON_STRING) - - self.assertIn("Content-type", response.headers) - self.assertEqual(response.headers["Content-type"], "application/json") - self.assertIn("Authorization", response.headers) - - def test_json_fails_with_bad_args(self): - with self.assertRaises(ValueError) as e: - (response, dummy) = self.client.api_call("POST", "/foo/bar", "") - self.assertEqual(e.exception.args[0], "JSON request must be an object.") - - def test_json_put(self): - (response, dummy) = self.client.api_call("PUT", "/foo/bar", JSON_BODY) - - self.assertEqual(response.method, "PUT") - self.assertEqual(response.uri, "/foo/bar") - self.assertEqual(response.body, JSON_STRING) - - self.assertIn("Content-type", response.headers) - self.assertEqual(response.headers["Content-type"], "application/json") - self.assertIn("Authorization", response.headers) - - def test_json_request(self): - client = duo_client.client.Client( - "test_ikey", "test_akey", "example.com", sig_timezone="America/Detroit", sig_version=3 - ) - client._connect = lambda: util.MockHTTPConnection() - - (response, dummy) = client.api_call("POST", "/foo/bar", JSON_BODY) - - self.assertEqual(response.method, "POST") - self.assertEqual(response.uri, "/foo/bar") - self.assertEqual(response.body, JSON_STRING) - - self.assertIn("Content-type", response.headers) - self.assertEqual(response.headers["Content-type"], "application/json") - self.assertIn("Authorization", response.headers) - - -if __name__ == "__main__": - unittest.main() diff --git a/plugins/duo_auth/vendor/duo_client_python/tests/util.py b/plugins/duo_auth/vendor/duo_client_python/tests/util.py deleted file mode 100755 index 5a829302a8..0000000000 --- a/plugins/duo_auth/vendor/duo_client_python/tests/util.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import absolute_import -import json -import collections - -from json import JSONEncoder -import duo_client -import six - - -class MockObjectJsonEncoder(json.JSONEncoder): - def default(self, obj): - return getattr(obj.__class__, "to_json")(obj) - - -# put params in a dict to avoid inconsistent ordering -def params_to_dict(param_str): - param_dict = collections.defaultdict(list) - for (key, val) in (param.split("=") for param in param_str.split("&")): - param_dict[key].append(val) - return param_dict - - -class MockHTTPConnection(object): - """ - Mock HTTP(S) connection that returns a dummy JSON response. - """ - - status = 200 # success! - - def __init__(self, data_response_should_be_list=False, data_response_from_get_authlog=False): - # if a response object should be a list rather than - # a dict, then set this flag to true - self.data_response_should_be_list = data_response_should_be_list - self.data_response_from_get_authlog = data_response_from_get_authlog - - def dummy(self): - return self - - _connect = _disconnect = close = getresponse = dummy - - def read(self): - response = self.__dict__ - - if self.data_response_should_be_list: - response = [self.__dict__] - - if self.data_response_from_get_authlog: - response["authlogs"] = [] - - return json.dumps({"stat": "OK", "response": response}, cls=MockObjectJsonEncoder) - - def request(self, method, uri, body, headers): - self.method = method - self.uri = uri - self.body = body - - self.headers = {} - for k, v in headers.items(): - if isinstance(k, six.binary_type): - k = k.decode("ascii") - if isinstance(v, six.binary_type): - v = v.decode("ascii") - self.headers[k] = v - - -class MockJsonObject(object): - def to_json(self): - return {"id": id(self)} - - -class CountingClient(duo_client.client.Client): - def __init__(self, *args, **kwargs): - super(CountingClient, self).__init__(*args, **kwargs) - self.counter = 0 - - def _make_request(self, *args, **kwargs): - self.counter += 1 - return super(CountingClient, self)._make_request(*args, **kwargs) - - -class MockPagingHTTPConnection(MockHTTPConnection): - def __init__(self, objects=None): - if objects is not None: - self.objects = objects - - def dummy(self): - return self - - _connect = _disconnect = close = getresponse = dummy - - def read(self): - metadata = {} - metadata["total_objects"] = len(self.objects) - if self.offset + self.limit < len(self.objects): - metadata["next_offset"] = self.offset + self.limit - if self.offset > 0: - metadata["prev_offset"] = max(self.offset - self.limit, 0) - - return json.dumps( - { - "stat": "OK", - "response": self.objects[self.offset : self.offset + self.limit], - "metadata": metadata, - }, - cls=MockObjectJsonEncoder, - ) - - def request(self, method, uri, body, headers): - self.method = method - self.uri = uri - self.body = body - self.headers = headers - parsed = six.moves.urllib.parse.urlparse(uri) - params = six.moves.urllib.parse.parse_qs(parsed.query) - - self.limit = int(params["limit"][0]) - self.offset = int(params["offset"][0])