Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial generic device import functionality #166

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions duo_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from .accounts import Accounts
from .admin import Admin
from .auth import Auth
from .device import Device
from .client import __version__

__all__ = [
'Accounts',
'Admin',
'Auth',
'Device',
]
4 changes: 3 additions & 1 deletion duo_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ def to_list(value):
class Client(object):

def __init__(self, ikey, skey, host,
mkey=None,
ca_certs=DEFAULT_CA_CERTS,
sig_timezone='UTC',
user_agent=('Duo API Python/' + __version__),
Expand All @@ -170,6 +171,7 @@ def __init__(self, ikey, skey, host,
"""
self.ikey = ikey
self.skey = skey
self.mkey = mkey
self.host = host
self.port = port
self.sig_timezone = sig_timezone
Expand Down Expand Up @@ -426,7 +428,7 @@ def json_cursor_api_call(self, method, path, params, get_records_func):
:param get_records_func: Function that can be called to extract an
iterable of records from the parsed response
json.

:returns: Generator which will yield records from the api response(s).
"""

Expand Down
138 changes: 138 additions & 0 deletions duo_client/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Duo Security Device API reference client implementation.

"""
from __future__ import absolute_import

import six.moves.urllib

from . import client
import six
import warnings
import json

MAX_DEVICE_IDS = 1000
DEVICE_CACHE_STATUS = [
"active",
"pending"
]


class Device(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(Device, self).api_call(method, path, params)

def _validate_device_ids(self, device_ids):
# For some reason this is a list of dicts
if not isinstance(device_ids, list):
raise ValueError
for device_id_dict in device_ids:
if not isinstance(device_id_dict, dict):
raise ValueError
if 'device_id' not in device_id_dict:
raise ValueError
return True

def create_device_cache(self):
existing_caches = self.get_device_caches()
for cache in existing_caches:
cache_key = cache['cache_key']
if cache['status'] == "pending":
raise ValueError(
f"Cannot create a cache when a cache is pending ({cache_key})")
return self.json_api_call(
'POST',
f'/device/v1/management_systems/{self.mkey}/device_cache',
{}
)

def delete_device_cache(self, cache_key):
device_cache = self.get_device_cache_by_key(cache_key)
if device_cache['status'] != "pending":
raise ValueError(
f"Only Pending device caches can be deleted ({cache_key})")
return self.json_api_call(
'DELETE',
f'/device/v1/management_systems/{self.mkey}/device_cache/{cache_key}',
{}
)

def activate_device_cache(self, cache_key):
return self.json_api_call(
'POST',
f'/device/v1/management_systems/{self.mkey}/device_cache/{cache_key}/activate',
{}
)

def add_device_to_cache(self, cache_key, device_ids):
# We only return the last result
result = None
for device_start in range(0, len(device_ids), MAX_DEVICE_IDS):
device_end = device_start + MAX_DEVICE_IDS
if device_end > len(device_ids):
device_end = len(device_ids)
device_ids_chunk = device_ids[device_start:device_end]
self._validate_device_ids(device_ids_chunk)
params = {
'devices': json.dumps(device_ids_chunk),
}

result = self.json_api_call(
'POST',
f'/device/v1/management_systems/{self.mkey}/device_cache/{cache_key}/devices',
params
)
return result

def get_device_caches(self, status=None):
params = {}
if status:
if status not in DEVICE_CACHE_STATUS:
raise ValueError
params = {"status": status}
params = six.moves.urllib.parse.urlencode(params, doseq=True)
if params:
url = f'/device/v1/management_systems/{self.mkey}/device_cache?{params}'
else:
url = f'/device/v1/management_systems/{self.mkey}/device_cache'
return self.json_api_call(
'GET',
url,
{}
)

def get_device_cache_by_key(self, cache_key):
return self.json_api_call(
'GET',
f'/device/v1/management_systems/{self.mkey}/device_cache/{cache_key}',
{}
)

def get_device_cache_by_key(self, cache_key):
return self.json_api_call(
'GET',
f'/device/v1/management_systems/{self.mkey}/device_cache/{cache_key}',
{}
)

def activate_cache_with_devices(self, device_ids, cache_key=None):
# Remove any existing pending caches, since there can only be one
if not cache_key:
existing_caches = self.get_device_caches()
for cache in existing_caches:
if cache['status'] == 'pending':
cache_key = cache['cache_key']
self.delete_device_cache(cache_key)
device_cache = self.create_device_cache()
else:
device_cache = self.get_device_cache_by_key(cache_key)
if device_cache['status'] != 'pending':
raise ValueError("Specified Cache is not in pending state")
cache_key = device_cache['cache_key']
self.add_device_to_cache(cache_key, device_ids)
self.activate_device_cache(cache_key)
return self.get_device_cache_by_key(cache_key)
68 changes: 68 additions & 0 deletions examples/add_generic_devices_from_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/python
import duo_client
import argparse
import csv


def arg_parser():
parser = argparse.ArgumentParser()
parser.add_argument(
'--infile',
required=True,
help='The path to the input CSV file.'
)
parser.add_argument(
'--device_id_column',
default='device_id',
help='The name of the column in the csv file that has the device ID',
)
parser.add_argument(
'--mkey',
required=len(MKEY_CREDENTIALS.keys())>1,
help='The Duo Device API managment key: ',
choices=MKEY_CREDENTIALS.keys()
)
parser.add_argument(
'--ikey',
required=len(MKEY_CREDENTIALS.keys())>1,
help='The Duo Device API integration key: ',
choices=MKEY_CREDENTIALS.keys()
)
parser.add_argument(
'--skey',
required=len(MKEY_CREDENTIALS.keys())>1,
help='The Duo Device API secret key: ',
choices=MKEY_CREDENTIALS.keys()
)
parser.add_argument(
'--host',
required=len(MKEY_CREDENTIALS.keys())>1,
help='The Duo Device API hostname ("api-....duosecurity.com"): ',
choices=MKEY_CREDENTIALS.keys()
)
return parser


def upload_identifiers(device_api, csvfile, device_id_column):
device_ids = []
with open(args.infile) as csvfile:
reader = csv.DictReader(csvfile, skipinitialspace=True)
for row in reader:
if row[device_id_column]:
device_ids.append({'device_id': row[device_id_column]})
if not len(device_ids):
raise ValueError(
f'No device IDs read from input column: {device_id_column}')
self.device_api.activate_cache_with_devices(device_ids)


def main():
parser = arg_parser()
args = parser.parse_args()
device_api = duo_client.client.Client(
ikey=args.ikey,
skey=args.skey,
host=args.host,
mkey=args.mkey
)
upload_identifiers(device_api, args.infile, args.device_id_column)
Empty file added tests/device/__init__.py
Empty file.
Loading