Skip to content

Commit dd195b1

Browse files
authored
Add v2 endpoint for telephony log (#208)
* Add v2 endpoint for telephony log * Fix test by correcting expected output
1 parent 3ed320d commit dd195b1

12 files changed

Lines changed: 370 additions & 151 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ dist
99
.idea
1010
env/
1111
py3env/
12+
.venv
13+
duo_client.egg-info
14+
*.DS_Store

duo_client/admin.py

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -174,44 +174,39 @@
174174
import six.moves.urllib
175175

176176
from . import client
177+
from .logs.telephony import Telephony
177178
import six
178179
import warnings
179180
import time
180181
import base64
181-
from datetime import datetime, timedelta
182+
from datetime import datetime, timedelta, timezone
182183

183-
USER_STATUS_ACTIVE = 'active'
184-
USER_STATUS_BYPASS = 'bypass'
185-
USER_STATUS_DISABLED = 'disabled'
186-
USER_STATUS_LOCKED_OUT = 'locked out'
184+
USER_STATUS_ACTIVE = "active"
185+
USER_STATUS_BYPASS = "bypass"
186+
USER_STATUS_DISABLED = "disabled"
187+
USER_STATUS_LOCKED_OUT = "locked out"
187188

188-
TOKEN_HOTP_6 = 'h6'
189-
TOKEN_HOTP_8 = 'h8'
190-
TOKEN_YUBIKEY = 'yk'
189+
TOKEN_HOTP_6 = "h6"
190+
TOKEN_HOTP_8 = "h8"
191+
TOKEN_YUBIKEY = "yk"
191192

192193
VALID_AUTHLOG_REQUEST_PARAMS = [
193-
'mintime',
194-
'maxtime',
195-
'limit',
196-
'sort',
197-
'next_offset',
198-
'event_types',
199-
'reasons',
200-
'results',
201-
'users',
202-
'applications',
203-
'groups',
204-
'factors',
205-
'api_version'
194+
"mintime",
195+
"maxtime",
196+
"limit",
197+
"sort",
198+
"next_offset",
199+
"event_types",
200+
"reasons",
201+
"results",
202+
"users",
203+
"applications",
204+
"groups",
205+
"factors",
206+
"api_version",
206207
]
207208

208-
VALID_ACTIVITY_REQUEST_PARAMS = [
209-
'mintime',
210-
'maxtime',
211-
'limit',
212-
'sort',
213-
'next_offset'
214-
]
209+
VALID_ACTIVITY_REQUEST_PARAMS = ["mintime", "maxtime", "limit", "sort", "next_offset"]
215210

216211

217212
class Admin(client.Client):
@@ -598,12 +593,12 @@ def get_activity_logs(self, **kwargs):
598593
"value" : <int: total objects in the time range>
599594
}
600595
}
601-
},
596+
}
602597
603598
Raises RuntimeError on error.
604599
"""
605600
params = {}
606-
today = datetime.utcnow()
601+
today = datetime.now(tz=timezone.utc)
607602
default_maxtime = int(today.timestamp() * 1000)
608603
default_mintime = int((today - timedelta(days=180)).timestamp() * 1000)
609604

@@ -622,8 +617,6 @@ def get_activity_logs(self, **kwargs):
622617
if 'limit' in params:
623618
params['limit'] = str(int(params['limit']))
624619

625-
626-
627620
response = self.json_api_call(
628621
'GET',
629622
'/admin/v2/logs/activity',
@@ -634,41 +627,64 @@ def get_activity_logs(self, **kwargs):
634627
row['host'] = self.host
635628
return response
636629

637-
def get_telephony_log(self,
638-
mintime=0):
630+
def get_telephony_log(self, mintime=0, api_version=1, **kwargs):
639631
"""
640632
Returns telephony log events.
641633
642634
mintime - Fetch events only >= mintime (to avoid duplicate
643-
records that have already been fetched)
644-
645-
Returns:
635+
records that have already been fetched)
636+
api_version - The API version of the handler to use.
637+
Currently, the default api version is v1, but the v1 API
638+
will be deprecated in a future version of the Duo Admin API.
639+
Please migrate to the v2 api at your earliest convenience.
640+
For details on the differences between v1 and v2,
641+
please see Duo's Admin API documentation. (Optional)
642+
643+
v1 Returns:
646644
[
647-
{'timestamp': <int:unix timestamp>,
648-
'eventtype': "telephony",
649-
'host': <str:host>,
650-
'context': <str:context>,
651-
'type': <str:type>,
652-
'phone': <str:phone number>,
653-
'credits': <str:credits>}, ...
645+
{
646+
'timestamp': <int:unix timestamp>,
647+
'eventtype': "telephony",
648+
'host': <str:host>,
649+
'context': <str:context>,
650+
'type': <str:type>,
651+
'phone': <str:phone number>,
652+
'credits': <str:credits>}
654653
]
654+
655+
v2 Returns:
656+
{
657+
"items": [
658+
{
659+
'context': <str>,
660+
'credits': <int: credits used>,
661+
'phone': <str:phone number>,
662+
'telephony_id': <str:UUID>,
663+
'ts': <str:ISO timestamp>,
664+
'txid': <str:UUID>,
665+
'type': <str:"sms" or "phone">,
666+
'eventtype': <str:"telephony">,
667+
'host': <str:application hostname>
668+
}
669+
],
670+
"metadata": {
671+
"next_offset": <str: comma seperated ts and offset value>
672+
"total_objects": {
673+
"relation" : <str: relational operator>
674+
"value" : <int: total objects in the time range>
675+
}
676+
}
677+
}
655678
656679
Raises RuntimeError on error.
657680
"""
658-
# Sanity check mintime as unix timestamp, then transform to string
659-
mintime = str(int(mintime))
660-
params = {
661-
'mintime': mintime,
662-
}
663-
response = self.json_api_call(
664-
'GET',
665-
'/admin/v1/logs/telephony',
666-
params,
667-
)
668-
for row in response:
669-
row['eventtype'] = 'telephony'
670-
row['host'] = self.host
671-
return response
681+
682+
if api_version not in [1,2]:
683+
raise ValueError("Invalid API Version")
684+
685+
if api_version == 2:
686+
return Telephony.get_telephony_logs_v2(self.json_api_call, self.host, **kwargs)
687+
return Telephony.get_telephony_logs_v1(self.json_api_call, self.host, mintime=mintime)
672688

673689
def get_users_iterator(self):
674690
"""

duo_client/logs/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from __future__ import absolute_import
2+
from .telephony import Telephony
3+
4+
__all__ = [
5+
'Telephony'
6+
]

duo_client/logs/telephony.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import Callable
2+
from duo_client.util import (
3+
get_params_from_kwargs,
4+
get_log_uri,
5+
get_default_request_times,
6+
)
7+
8+
VALID_TELEPHONY_V2_REQUEST_PARAMS = [
9+
"filters",
10+
"mintime",
11+
"maxtime",
12+
"limit",
13+
"sort",
14+
"next_offset",
15+
"account_id",
16+
]
17+
18+
LOG_TYPE = "telephony"
19+
20+
21+
class Telephony:
22+
@staticmethod
23+
def get_telephony_logs_v1(json_api_call: Callable, host: str, mintime=0):
24+
# Sanity check mintime as unix timestamp, then transform to string
25+
mintime = f"{int(mintime)}"
26+
params = {
27+
"mintime": mintime,
28+
}
29+
response = json_api_call(
30+
"GET",
31+
get_log_uri(LOG_TYPE, 1),
32+
params,
33+
)
34+
for row in response:
35+
row["eventtype"] = LOG_TYPE
36+
row["host"] = host
37+
return response
38+
39+
@staticmethod
40+
def get_telephony_logs_v2(json_api_call: Callable, host: str, **kwargs):
41+
params = {}
42+
default_mintime, default_maxtime = get_default_request_times()
43+
44+
params = get_params_from_kwargs(VALID_TELEPHONY_V2_REQUEST_PARAMS, **kwargs)
45+
46+
if "mintime" not in params:
47+
# If mintime is not provided, the script defaults it to 180 days in past
48+
params["mintime"] = default_mintime
49+
params["mintime"] = f"{int(params['mintime'])}"
50+
if "maxtime" not in params:
51+
# if maxtime is not provided, the script defaults it to now
52+
params["maxtime"] = default_maxtime
53+
params["maxtime"] = f"{int(params['maxtime'])}"
54+
if "limit" in params:
55+
params["limit"] = f"{int(params['limit'])}"
56+
57+
response = json_api_call(
58+
"GET",
59+
get_log_uri(LOG_TYPE, 2),
60+
params,
61+
)
62+
for row in response["items"]:
63+
row["eventtype"] = LOG_TYPE
64+
row["host"] = host
65+
return response

duo_client/util.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Dict, Sequence, Tuple
2+
from datetime import datetime, timedelta, timezone
3+
4+
5+
def get_params_from_kwargs(valid_params: Sequence[str], **kwargs) -> Dict:
6+
params = {}
7+
for k in kwargs:
8+
if kwargs[k] is not None and k in valid_params:
9+
params[k] = kwargs[k]
10+
return params
11+
12+
13+
def get_log_uri(log_type: str, version: int = 1) -> str:
14+
return f"/admin/v{version}/logs/{log_type}"
15+
16+
17+
def get_default_request_times() -> Tuple[int, int]:
18+
today = datetime.now(tz=timezone.utc)
19+
mintime = int((today - timedelta(days=180)).timestamp() * 1000)
20+
maxtime = int(today.timestamp() * 1000) - 120
21+
return mintime, maxtime

0 commit comments

Comments
 (0)