diff --git a/README.md b/README.md index d096467..5c921c2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ **Admin** - https://www.duosecurity.com/docs/adminapi -**Accounts** - https://www.duosecurity.com/docs/accountsapi + - **Subaccount access** - https://duo.com/docs/adminapi#subaccounts **Activity** - The activity endpoint is in public preview and subject to change diff --git a/duo_client/accounts.py b/duo_client/accounts.py index 555dcfa..ecbf5a8 100644 --- a/duo_client/accounts.py +++ b/duo_client/accounts.py @@ -1,16 +1,29 @@ """ -Duo Security Accounts API reference client implementation. +Duo Security Admin API subaccount management reference client implementation. - + + +DEPRECATED: this module is deprecated and will be removed in a future release. +Use the equivalent methods on duo_client.Admin instead. """ from . import client class Accounts(client.Client): + """ + DEPRECATED: use duo_client.Admin instead. This client will be removed in a + future release. + + The subaccount methods below are duplicated on Admin and are frozen: they + will not be updated going forward. Any new subaccount API method is added + to Admin only, and fixes to these methods may not be mirrored here. + """ child_map = {} def get_child_accounts(self): """ Return a list of all child accounts of the integration's account. + + DEPRECATED: use Admin.get_child_accounts instead. Not maintained. """ params = {} response = self.json_api_call('POST', @@ -27,6 +40,8 @@ def get_child_accounts(self): def create_account(self, name): """ Create a new child account of the integration's account. + + DEPRECATED: use Admin.create_account instead. Not maintained. """ params = { 'name': name, @@ -39,6 +54,8 @@ def create_account(self, name): def delete_account(self, account_id): """ Delete a child account of the integration's account. + + DEPRECATED: use Admin.delete_account instead. Not maintained. """ params = { 'account_id': account_id, diff --git a/duo_client/admin.py b/duo_client/admin.py index 5413923..4de2a46 100644 --- a/duo_client/admin.py +++ b/duo_client/admin.py @@ -130,6 +130,21 @@ 'adminapi_read_resource': , 'adminapi_settings': , 'adminapi_write_resource': , + 'adminapi_subaccount_accounts': , + 'adminapi_subaccount_accounts_read': , + 'adminapi_subaccount_admins': , + 'adminapi_subaccount_admins_read': , + 'adminapi_subaccount_info': , + 'adminapi_subaccount_integrations': , + 'adminapi_subaccount_integrations_read': , + 'adminapi_subaccount_settings': , + 'adminapi_subaccount_settings_read': , + 'adminapi_subaccount_read_log': , + 'adminapi_subaccount_read_resource': , + 'adminapi_subaccount_write_resource': , + 'adminapi_subaccount_allow_to_set_permissions': , + 'adminapi_subaccount_user_limits': , + 'adminapi_subaccount_user_limits_read': , 'self_service_allowed': , 'enroll_policy': , 'username_normalization_policy': , @@ -216,6 +231,10 @@ class Admin(client.Client): account_id = None + # Shared with Accounts.child_map so that child api hostnames discovered by + # either client are visible to AccountAdmin. This is an alias, not a copy. + child_map = Accounts.child_map + def api_call(self, method, path, params): if self.account_id is not None: params['account_id'] = self.account_id @@ -2241,6 +2260,54 @@ def get_allowed_admin_auth_methods(self): ) return response + 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) + if response and isinstance(response, list): + for account in response: + account_id = account.get('account_id', None) + api_hostname = account.get('api_hostname', None) + if account_id and api_hostname: + Admin.child_map[account_id] = api_hostname + 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. + + account_id - The child account to delete. This is only honored on a + parent level client, i.e. one with no account_id of its + own. api_call() overwrites params['account_id'] with + self.account_id whenever that is set, so on an + account scoped client (any AccountAdmin, or an Admin with + account_id assigned) this argument is ignored and the + client's own account is the one deleted. + """ + params = { + 'account_id': account_id, + } + response = self.json_api_call('POST', + '/accounts/v1/account/delete', + params) + return response + def get_info_summary(self): """ Returns a summary of objects in the account. @@ -2636,7 +2703,22 @@ def create_integration(self, groups_allowed=None, self_service_allowed=None, sso=None, - user_access=None): + user_access=None, + adminapi_subaccount_accounts=None, + adminapi_subaccount_accounts_read=None, + adminapi_subaccount_admins=None, + adminapi_subaccount_admins_read=None, + adminapi_subaccount_info=None, + adminapi_subaccount_integrations=None, + adminapi_subaccount_integrations_read=None, + adminapi_subaccount_settings=None, + adminapi_subaccount_settings_read=None, + adminapi_subaccount_read_log=None, + adminapi_subaccount_read_resource=None, + adminapi_subaccount_write_resource=None, + adminapi_subaccount_allow_to_set_permissions=None, + adminapi_subaccount_user_limits=None, + adminapi_subaccount_user_limits_read=None): """Creates a new integration. name - The name of the integration (required) @@ -2659,12 +2741,30 @@ def create_integration(self, adminapi_read_resource - |None adminapi_settings - |None adminapi_write_resource - |None + adminapi_subaccount_accounts - |None + adminapi_subaccount_accounts_read - |None + adminapi_subaccount_admins - |None + adminapi_subaccount_admins_read - |None + adminapi_subaccount_info - |None + adminapi_subaccount_integrations - |None + adminapi_subaccount_integrations_read - |None + adminapi_subaccount_settings - |None + adminapi_subaccount_settings_read - |None + adminapi_subaccount_read_log - |None + adminapi_subaccount_read_resource - |None + adminapi_subaccount_write_resource - |None + adminapi_subaccount_allow_to_set_permissions - |None + adminapi_subaccount_user_limits - |None + adminapi_subaccount_user_limits_read - |None groups_allowed - self_service_allowed - |None sso - (optional) New argument for unreleased feature. Will return an error if used. Client will be updated again in the future when feature is released. + The adminapi_subaccount_* permissions apply only to 'adminapi' + integrations and are ignored for other integration types. + Returns the created integration. Raises RuntimeError on error. @@ -2707,6 +2807,51 @@ def create_integration(self, if adminapi_write_resource is not None: params['adminapi_write_resource'] = ( '1' if adminapi_write_resource else '0') + if adminapi_subaccount_accounts is not None: + params['adminapi_subaccount_accounts'] = ( + '1' if adminapi_subaccount_accounts else '0') + if adminapi_subaccount_accounts_read is not None: + params['adminapi_subaccount_accounts_read'] = ( + '1' if adminapi_subaccount_accounts_read else '0') + if adminapi_subaccount_admins is not None: + params['adminapi_subaccount_admins'] = ( + '1' if adminapi_subaccount_admins else '0') + if adminapi_subaccount_admins_read is not None: + params['adminapi_subaccount_admins_read'] = ( + '1' if adminapi_subaccount_admins_read else '0') + if adminapi_subaccount_info is not None: + params['adminapi_subaccount_info'] = ( + '1' if adminapi_subaccount_info else '0') + if adminapi_subaccount_integrations is not None: + params['adminapi_subaccount_integrations'] = ( + '1' if adminapi_subaccount_integrations else '0') + if adminapi_subaccount_integrations_read is not None: + params['adminapi_subaccount_integrations_read'] = ( + '1' if adminapi_subaccount_integrations_read else '0') + if adminapi_subaccount_settings is not None: + params['adminapi_subaccount_settings'] = ( + '1' if adminapi_subaccount_settings else '0') + if adminapi_subaccount_settings_read is not None: + params['adminapi_subaccount_settings_read'] = ( + '1' if adminapi_subaccount_settings_read else '0') + if adminapi_subaccount_read_log is not None: + params['adminapi_subaccount_read_log'] = ( + '1' if adminapi_subaccount_read_log else '0') + if adminapi_subaccount_read_resource is not None: + params['adminapi_subaccount_read_resource'] = ( + '1' if adminapi_subaccount_read_resource else '0') + if adminapi_subaccount_write_resource is not None: + params['adminapi_subaccount_write_resource'] = ( + '1' if adminapi_subaccount_write_resource else '0') + if adminapi_subaccount_allow_to_set_permissions is not None: + params['adminapi_subaccount_allow_to_set_permissions'] = ( + '1' if adminapi_subaccount_allow_to_set_permissions else '0') + if adminapi_subaccount_user_limits is not None: + params['adminapi_subaccount_user_limits'] = ( + '1' if adminapi_subaccount_user_limits else '0') + if adminapi_subaccount_user_limits_read is not None: + params['adminapi_subaccount_user_limits_read'] = ( + '1' if adminapi_subaccount_user_limits_read else '0') if groups_allowed is not None: params['groups_allowed'] = groups_allowed if self_service_allowed is not None: @@ -2840,7 +2985,22 @@ def update_integration(self, groups_allowed=None, self_service_allowed=None, sso=None, - user_access=None + user_access=None, + adminapi_subaccount_accounts=None, + adminapi_subaccount_accounts_read=None, + adminapi_subaccount_admins=None, + adminapi_subaccount_admins_read=None, + adminapi_subaccount_info=None, + adminapi_subaccount_integrations=None, + adminapi_subaccount_integrations_read=None, + adminapi_subaccount_settings=None, + adminapi_subaccount_settings_read=None, + adminapi_subaccount_read_log=None, + adminapi_subaccount_read_resource=None, + adminapi_subaccount_write_resource=None, + adminapi_subaccount_allow_to_set_permissions=None, + adminapi_subaccount_user_limits=None, + adminapi_subaccount_user_limits_read=None ): """Updates an integration. @@ -2862,6 +3022,21 @@ def update_integration(self, adminapi_read_resource - True|False|None adminapi_settings - True|False|None adminapi_write_resource - True|False|None + adminapi_subaccount_accounts - True|False|None + adminapi_subaccount_accounts_read - True|False|None + adminapi_subaccount_admins - True|False|None + adminapi_subaccount_admins_read - True|False|None + adminapi_subaccount_info - True|False|None + adminapi_subaccount_integrations - True|False|None + adminapi_subaccount_integrations_read - True|False|None + adminapi_subaccount_settings - True|False|None + adminapi_subaccount_settings_read - True|False|None + adminapi_subaccount_read_log - True|False|None + adminapi_subaccount_read_resource - True|False|None + adminapi_subaccount_write_resource - True|False|None + adminapi_subaccount_allow_to_set_permissions - True|False|None + adminapi_subaccount_user_limits - True|False|None + adminapi_subaccount_user_limits_read - True|False|None reset_secret_key - |None groups_allowed - self_service_allowed - True|False|None @@ -2869,6 +3044,9 @@ def update_integration(self, New argument for unreleased feature. Will return an error if used. Client will be updated again in the future when feature is released. + The adminapi_subaccount_* permissions apply only to 'adminapi' + integrations and are ignored for other integration types. + 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. @@ -2915,6 +3093,51 @@ def update_integration(self, if adminapi_write_resource is not None: params['adminapi_write_resource'] = ( '1' if adminapi_write_resource else '0') + if adminapi_subaccount_accounts is not None: + params['adminapi_subaccount_accounts'] = ( + '1' if adminapi_subaccount_accounts else '0') + if adminapi_subaccount_accounts_read is not None: + params['adminapi_subaccount_accounts_read'] = ( + '1' if adminapi_subaccount_accounts_read else '0') + if adminapi_subaccount_admins is not None: + params['adminapi_subaccount_admins'] = ( + '1' if adminapi_subaccount_admins else '0') + if adminapi_subaccount_admins_read is not None: + params['adminapi_subaccount_admins_read'] = ( + '1' if adminapi_subaccount_admins_read else '0') + if adminapi_subaccount_info is not None: + params['adminapi_subaccount_info'] = ( + '1' if adminapi_subaccount_info else '0') + if adminapi_subaccount_integrations is not None: + params['adminapi_subaccount_integrations'] = ( + '1' if adminapi_subaccount_integrations else '0') + if adminapi_subaccount_integrations_read is not None: + params['adminapi_subaccount_integrations_read'] = ( + '1' if adminapi_subaccount_integrations_read else '0') + if adminapi_subaccount_settings is not None: + params['adminapi_subaccount_settings'] = ( + '1' if adminapi_subaccount_settings else '0') + if adminapi_subaccount_settings_read is not None: + params['adminapi_subaccount_settings_read'] = ( + '1' if adminapi_subaccount_settings_read else '0') + if adminapi_subaccount_read_log is not None: + params['adminapi_subaccount_read_log'] = ( + '1' if adminapi_subaccount_read_log else '0') + if adminapi_subaccount_read_resource is not None: + params['adminapi_subaccount_read_resource'] = ( + '1' if adminapi_subaccount_read_resource else '0') + if adminapi_subaccount_write_resource is not None: + params['adminapi_subaccount_write_resource'] = ( + '1' if adminapi_subaccount_write_resource else '0') + if adminapi_subaccount_allow_to_set_permissions is not None: + params['adminapi_subaccount_allow_to_set_permissions'] = ( + '1' if adminapi_subaccount_allow_to_set_permissions else '0') + if adminapi_subaccount_user_limits is not None: + params['adminapi_subaccount_user_limits'] = ( + '1' if adminapi_subaccount_user_limits else '0') + if adminapi_subaccount_user_limits_read is not None: + params['adminapi_subaccount_user_limits_read'] = ( + '1' if adminapi_subaccount_user_limits_read else '0') if reset_secret_key is not None: params['reset_secret_key'] = '1' if groups_allowed is not None: @@ -3806,7 +4029,7 @@ def get_idv_status(self, user_id): class AccountAdmin(Admin): - """AccountAdmin manages a child account using an Accounts API integration.""" + """AccountAdmin manages a child account using an Admin API integration.""" def __init__(self, account_id, child_api_host=None, **kwargs): """Initializes an AccountAdmin for administering a child account. @@ -3816,7 +4039,7 @@ def __init__(self, account_id, child_api_host=None, **kwargs): See the Client base class for other parameters. """ if not child_api_host: - child_api_host = Accounts.child_map.get(account_id, None) + child_api_host = Admin.child_map.get(account_id, None) if child_api_host is None: child_api_host = kwargs.get('host') try: @@ -3829,9 +4052,9 @@ def __init__(self, account_id, child_api_host=None, **kwargs): self.account_id = account_id def get_child_api_host(self, account_id, **kwargs): - accounts_api = Accounts(**kwargs) - accounts_api.get_child_accounts() - return Accounts.child_map.get(account_id, kwargs['host']) + admin_api = Admin(**kwargs) + admin_api.get_child_accounts() + return Admin.child_map.get(account_id, kwargs['host']) def get_edition(self): """ diff --git a/examples/Accounts/README.md b/examples/Accounts/README.md index ba663de..4f088c2 100644 --- a/examples/Accounts/README.md +++ b/examples/Accounts/README.md @@ -1,14 +1,18 @@ -# Duo Accounts API Examples Overview +# Subaccount Management Examples Overview ## Examples -This folder contains various examples to illustrate the usage of the `Accounts` module within the -`duo_client_python` library. The Duo Accounts API is primarily intended for use by Managed Service -Partners (MSP) to assist in the automation of managing their child (customer) Duo accounts. +This folder contains various examples to illustrate the usage of the subaccount management methods of the `Admin` +module within the `duo_client_python` library. Subaccount management in Admin API is primarily intended for use by +Managed Service Partners (MSP) to assist in the automation of managing their child (customer) Duo accounts. -Use of the Duo Accounts API requires special access to be enabled. Please see the -[online documentation](https://www.duosecurity.com/docs/accountsapi) for more information. +The child account methods (`get_child_accounts`, `create_account`, `delete_account`) live on `Admin`. The `Accounts` +client also provides them, but it is deprecated and will be removed in a future release, and its copy of these +methods is frozen -- new subaccount API methods are added to `Admin` only. Use `Admin` for new code. + +Subaccount management in Admin API requires special access to be enabled. Please see the +[online documentation](https://duo.com/docs/adminapi#subaccounts) for more information. # Using diff --git a/examples/Accounts/create_child_account.py b/examples/Accounts/create_child_account.py index e66f552..83b1e62 100644 --- a/examples/Accounts/create_child_account.py +++ b/examples/Accounts/create_child_account.py @@ -1,5 +1,5 @@ """ -Example of Duo Accounts API child account creation +Example of Duo Admin API child account creation """ import duo_client @@ -27,12 +27,12 @@ def _get_next_arg(prompt, secure=False): def prompt_for_credentials() -> dict: """Collect required API credentials from command line prompts - :return: dictionary containing Duo Accounts API ikey, skey and hostname strings + :return: dictionary containing Duo Admin API ikey, skey and hostname strings """ - ikey = _get_next_arg('Duo Accounts API integration key ("DI..."): ') - skey = _get_next_arg('Duo Accounts API integration secret key: ', secure=True) - host = _get_next_arg('Duo Accounts API hostname ("api-....duosecurity.com"): ') + ikey = _get_next_arg('Duo Admin API integration key ("DI..."): ') + skey = _get_next_arg('Duo Admin API integration secret key: ', secure=True) + host = _get_next_arg('Duo Admin API hostname ("api-....duosecurity.com"): ') account_name = _get_next_arg('Name for new child account: ') return {"IKEY": ikey, "SKEY": skey, "APIHOST": host, "ACCOUNT_NAME": account_name} @@ -43,7 +43,7 @@ def main(): inputs = prompt_for_credentials() - account_client = duo_client.Accounts( + account_client = duo_client.Admin( ikey=inputs['IKEY'], skey=inputs['SKEY'], host=inputs['APIHOST'] diff --git a/examples/Accounts/create_integration_in_child_account.py b/examples/Accounts/create_integration_in_child_account.py index ab397d2..ada15a7 100644 --- a/examples/Accounts/create_integration_in_child_account.py +++ b/examples/Accounts/create_integration_in_child_account.py @@ -29,10 +29,10 @@ def _get_next_arg(prompt, secure=False): def prompt_for_credentials() -> dict: """Collect required API credentials from command line prompts - :return: dictionary containing Duo Accounts API ikey, skey and hostname strings + :return: dictionary containing Duo Admin API ikey, skey and hostname strings """ - answers = {'ikey': _get_next_arg('Duo Accounts API integration key ("DI..."): '), - 'skey': _get_next_arg('Duo Accounts API integration secret key: ', secure=True), + answers = {'ikey': _get_next_arg('Duo Admin API integration key ("DI..."): '), + 'skey': _get_next_arg('Duo Admin API integration secret key: ', secure=True), 'host': _get_next_arg('Duo API hostname of child account ("api-....duosecurity.com"): '), 'account_id': _get_next_arg('Child account ID: '), 'app_name': _get_next_arg('New application name: '), diff --git a/examples/Accounts/delete_child_account.py b/examples/Accounts/delete_child_account.py index 16f3416..251a451 100644 --- a/examples/Accounts/delete_child_account.py +++ b/examples/Accounts/delete_child_account.py @@ -1,5 +1,5 @@ """ -Example of Duo Accounts API child account deletiom +Example of Duo Admin API child account deletion """ import duo_client @@ -26,12 +26,12 @@ def _get_next_arg(prompt, secure=False): def prompt_for_credentials() -> dict: """Collect required API credentials from command line prompts - :return: dictionary containing Duo Accounts API ikey, skey and hostname strings + :return: dictionary containing Duo Admin API ikey, skey and hostname strings """ - ikey = _get_next_arg('Duo Accounts API integration key ("DI..."): ') - skey = _get_next_arg('Duo Accounts API integration secret key: ', secure=True) - host = _get_next_arg('Duo Accounts API hostname ("api-....duosecurity.com"): ') + ikey = _get_next_arg('Duo Admin API integration key ("DI..."): ') + skey = _get_next_arg('Duo Admin API integration secret key: ', secure=True) + host = _get_next_arg('Duo Admin API hostname ("api-....duosecurity.com"): ') account_id = _get_next_arg('ID of child account to delete: ') return {"IKEY": ikey, "SKEY": skey, "APIHOST": host, "ACCOUNT_ID": account_id} @@ -42,7 +42,7 @@ def main(): inputs = prompt_for_credentials() - account_client = duo_client.Accounts( + account_client = duo_client.Admin( ikey=inputs['IKEY'], skey=inputs['SKEY'], host=inputs['APIHOST'] diff --git a/examples/Accounts/get_account_edition.py b/examples/Accounts/get_account_edition.py index 7452b4a..75369fe 100644 --- a/examples/Accounts/get_account_edition.py +++ b/examples/Accounts/get_account_edition.py @@ -1,5 +1,5 @@ """ -Example of Duo Accounts API get child account edition +Example of Duo Admin API get child account edition """ import duo_client @@ -23,9 +23,9 @@ def _get_user_input(prompt, secure=False): def prompt_for_credentials() -> dict: """Collect required API credentials from command line prompts""" - ikey = _get_user_input('Duo Accounts API integration key ("DI..."): ') - skey = _get_user_input('Duo Accounts API integration secret key: ', secure=True) - host = _get_user_input('Duo Accounts API hostname ("api-....duosecurity.com"): ') + ikey = _get_user_input('Duo Admin API integration key ("DI..."): ') + skey = _get_user_input('Duo Admin API integration secret key: ', secure=True) + host = _get_user_input('Duo Admin API hostname ("api-....duosecurity.com"): ') account_id = _get_user_input('Child account ID: ') return { diff --git a/examples/Accounts/get_billing_and_telephony_credits.py b/examples/Accounts/get_billing_and_telephony_credits.py index 35f09f8..a279cc9 100644 --- a/examples/Accounts/get_billing_and_telephony_credits.py +++ b/examples/Accounts/get_billing_and_telephony_credits.py @@ -19,12 +19,12 @@ def get_next_input(prompt): def main(): """Program entry point""" - ikey=get_next_input('Accounts API integration key ("DI..."): ') - skey=get_next_input('Accounts API integration secret key: ') - host=get_next_input('Accounts API hostname ("api-....duosecurity.com"): ') + ikey=get_next_input('Admin API integration key ("DI..."): ') + skey=get_next_input('Admin API integration secret key: ') + host=get_next_input('Admin API hostname ("api-....duosecurity.com"): ') # Configuration and information about objects to create. - accounts_api = duo_client.Accounts( + accounts_api = duo_client.Admin( ikey=ikey, skey=skey, host=host, diff --git a/examples/Accounts/retrieve_account_list.py b/examples/Accounts/retrieve_account_list.py index 7118b8d..11165f8 100644 --- a/examples/Accounts/retrieve_account_list.py +++ b/examples/Accounts/retrieve_account_list.py @@ -26,12 +26,12 @@ def _get_next_arg(prompt, secure=False): def prompt_for_credentials() -> dict: """Collect required API credentials from command line prompts - :return: dictionary containing Duo Accounts API ikey, skey and hostname strings + :return: dictionary containing Duo Admin API ikey, skey and hostname strings """ - ikey = _get_next_arg('Duo Accounts API integration key ("DI..."): ') - skey = _get_next_arg('Duo Accounts API integration secret key: ', secure=True) - host = _get_next_arg('Duo Accounts API hostname ("api-....duosecurity.com"): ') + ikey = _get_next_arg('Duo Admin API integration key ("DI..."): ') + skey = _get_next_arg('Duo Admin API integration secret key: ', secure=True) + host = _get_next_arg('Duo Admin API hostname ("api-....duosecurity.com"): ') return {"IKEY": ikey, "SKEY": skey, "APIHOST": host} @@ -41,7 +41,7 @@ def main(): inputs = prompt_for_credentials() - account_client = duo_client.Accounts( + account_client = duo_client.Admin( ikey=inputs['IKEY'], skey=inputs['SKEY'], host=inputs['APIHOST'] diff --git a/examples/Accounts/retrieve_integrations_from_child_account.py b/examples/Accounts/retrieve_integrations_from_child_account.py index 82d7f9c..0cc67ef 100644 --- a/examples/Accounts/retrieve_integrations_from_child_account.py +++ b/examples/Accounts/retrieve_integrations_from_child_account.py @@ -7,13 +7,13 @@ parser = argparse.ArgumentParser() -duo_arg_group = parser.add_argument_group('Duo Accounts API Credentials') +duo_arg_group = parser.add_argument_group('Duo Admin API Credentials') duo_arg_group.add_argument('--ikey', - help='Duo Accounts API IKEY', + help='Duo Admin API IKEY', required=True ) duo_arg_group.add_argument('--skey', - help='Duo Accounts API Secret Key', + help='Duo Admin API Secret Key', required=True, ) duo_arg_group.add_argument('--host', @@ -26,7 +26,7 @@ ) args = parser.parse_args() -# It is important to note that we are using the IKEY/SKEY combination for an Accounts API integration in the +# It is important to note that we are using the IKEY/SKEY combination for an Admin API integration in the # parent account along with the api-hostname of a child account to create a new duo_client.Admin instance account_client = duo_client.Admin( ikey=args.ikey, diff --git a/examples/Accounts/set_account_edition.py b/examples/Accounts/set_account_edition.py index ea97701..e657e7b 100644 --- a/examples/Accounts/set_account_edition.py +++ b/examples/Accounts/set_account_edition.py @@ -1,5 +1,5 @@ """ -Example of Duo Accounts API set child account edition +Example of Duo Admin API set child account edition """ import duo_client @@ -18,9 +18,9 @@ def _get_user_input(prompt, secure=False): def prompt_for_credentials() -> dict: """Collect required API credentials from command line prompts""" - ikey = _get_user_input('Duo Accounts API integration key ("DI..."): ') - skey = _get_user_input('Duo Accounts API integration secret key: ', secure=True) - host = _get_user_input('Duo Accounts API hostname ("api-....duosecurity.com"): ') + ikey = _get_user_input('Duo Admin API integration key ("DI..."): ') + skey = _get_user_input('Duo Admin API integration secret key: ', secure=True) + host = _get_user_input('Duo Admin API hostname ("api-....duosecurity.com"): ') account_id = _get_user_input('Child account ID: ') account_apihost = _get_user_input('Child account api_hostname: ') account_edition = _get_user_input('Child account edition: ') diff --git a/examples/README.md b/examples/README.md index 803bd09..afe1433 100644 --- a/examples/README.md +++ b/examples/README.md @@ -15,13 +15,12 @@ The Duo Admin API provides access to endpoints that are primarily focused on Duo - Integration management - Policy management - Log extractions +- Subaccount management + +Subaccount management is primarily intended for use by Duo Managed Service Provider (MSP) partners. +See the `Accounts` folder for examples. ------- ### Auth API The Duo Auth API provides access to user enrollment and authentication services and is primarily intended for use by -application developers that want to integration Duo MFA functionality into their applications. - -------- -### Accounts API -The Duo Accounts API provides access to Duo account management functionality and is primarily intended for use by -Duo Managed Service Provider (MSP) partners. \ No newline at end of file +application developers that want to integration Duo MFA functionality into their applications. \ No newline at end of file diff --git a/tests/admin/test_accounts.py b/tests/admin/test_accounts.py new file mode 100644 index 0000000..d55916e --- /dev/null +++ b/tests/admin/test_accounts.py @@ -0,0 +1,61 @@ +import json + +import duo_client.admin +from .. import util +from .base import TestAdmin + + +class TestAccounts(TestAdmin): + def test_get_child_accounts(self): + """ Test to get child accounts. + """ + response = self.client_list.get_child_accounts() + response = response[0] + self.assertEqual(response['method'], 'POST') + self.assertEqual(response['uri'], '/accounts/v1/account/list') + self.assertEqual( + json.loads(response['body']), + { + 'account_id': self.client.account_id, + }) + + def test_create_account(self): + """ Test to create a child account. + """ + response = self.client.create_account('Test Account') + self.assertEqual(response['method'], 'POST') + self.assertEqual(response['uri'], '/accounts/v1/account/create') + self.assertEqual( + json.loads(response['body']), + { + 'name': 'Test Account', + 'account_id': self.client.account_id, + }) + + def test_delete_account(self): + """ Test to delete a child account. + """ + client = duo_client.admin.Admin('test_ikey', 'test_akey', 'example.com') + client._connect = lambda: util.MockHTTPConnection() + + response = client.delete_account('DA099999999999999999') + self.assertEqual(response['method'], 'POST') + self.assertEqual(response['uri'], '/accounts/v1/account/delete') + self.assertEqual( + json.loads(response['body']), + { + 'account_id': 'DA099999999999999999', + }) + + def test_delete_account_when_client_is_account_scoped(self): + """ Test that a client-level account_id overrides the delete target. + + Admin.api_call sets params['account_id'] from self.account_id, so an + account-scoped client cannot delete a different account. + """ + response = self.client.delete_account('DA099999999999999999') + self.assertEqual( + json.loads(response['body']), + { + 'account_id': self.client.account_id, + }) diff --git a/tests/admin/test_integration.py b/tests/admin/test_integration.py index 4f1aca5..cea38bd 100644 --- a/tests/admin/test_integration.py +++ b/tests/admin/test_integration.py @@ -5,6 +5,24 @@ import duo_client.admin from .base import TestAdmin +SUBACCOUNT_PERMISSIONS = [ + 'adminapi_subaccount_accounts', + 'adminapi_subaccount_accounts_read', + 'adminapi_subaccount_admins', + 'adminapi_subaccount_admins_read', + 'adminapi_subaccount_info', + 'adminapi_subaccount_integrations', + 'adminapi_subaccount_integrations_read', + 'adminapi_subaccount_settings', + 'adminapi_subaccount_settings_read', + 'adminapi_subaccount_read_log', + 'adminapi_subaccount_read_resource', + 'adminapi_subaccount_write_resource', + 'adminapi_subaccount_allow_to_set_permissions', + 'adminapi_subaccount_user_limits', + 'adminapi_subaccount_user_limits_read', +] + class TestIntegration(TestAdmin): def setUp(self): @@ -51,6 +69,39 @@ def test_create_integration(self): } ) + def test_create_integration_subaccount_permissions(self): + response = self.client.create_integration( + name="Subaccount integration", + integration_type="adminapi", + **{perm: True for perm in SUBACCOUNT_PERMISSIONS} + ) + + expected = { + "account_id": self.client.account_id, + "name": "Subaccount integration", + "type": "adminapi", + } + expected.update({perm: "1" for perm in SUBACCOUNT_PERMISSIONS}) + + self.assertEqual(response['method'], 'POST') + self.assertEqual(response['uri'], '/admin/v3/integrations') + self.assertEqual(json.loads(response['body']), expected) + + def test_update_integration_subaccount_permissions(self): + response = self.client.update_integration( + self.integration_key, + **{perm: False for perm in SUBACCOUNT_PERMISSIONS} + ) + + expected = {"account_id": self.client.account_id} + expected.update({perm: "0" for perm in SUBACCOUNT_PERMISSIONS}) + + self.assertEqual(response['method'], 'POST') + self.assertEqual( + response['uri'], + '/admin/v3/integrations/{}'.format(self.integration_key)) + self.assertEqual(json.loads(response['body']), expected) + def test_update_integration_success(self): response = self.client.update_integration( self.integration_key,