Skip to content

Commit 54429ff

Browse files
djacob-pdrafuseljohncoleman83
authored
EF-4826 - audit user role changes (#114)
* EF-4826 - audit user role changes * Script works! * Actor rendering. * Write to CSV. * Documentation. * Docs updates. * One more docs update. * Rename var. * Unnecessary option. * Clean up role tiers. * More docs. * adds support for a new argument * Update get_info_on_all_users/README.md Co-authored-by: David John Coleman II <jcoleman@pagerduty.com> * Add only updates option. * Update get_info_on_all_users/README.md Co-authored-by: David John Coleman II <jcoleman@pagerduty.com> * Update get_info_on_all_users/README.md Co-authored-by: David John Coleman II <jcoleman@pagerduty.com> --------- Co-authored-by: Lewis Rafuse <lewisrafuse@gmail.com> Co-authored-by: David John Coleman II <jcoleman@pagerduty.com> Co-authored-by: Lewis Rafuse <47062950+rafusel@users.noreply.github.com>
1 parent 3d3d07b commit 54429ff

File tree

3 files changed

+283
-4
lines changed

3 files changed

+283
-4
lines changed

get_info_on_all_users/README.md

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ To execute the script, run this with your key:
2222

2323
## Get All Users With Certain Roles - `get_users_by_role.py`
2424

25-
This script will take a comma separated list of roles as a command line argument and fetches all the users in an account that match one of roles provided in the list. Roles will be fetched in the order that they are provided in the command line argument. Running with -v flag will show which role is being retrieved and then list the members who match it. After retrieving all the members for a given role, a tally will be shown for how many users have that role.
25+
This script will take a comma separated list of roles as a command line argument and fetches all the users in an account that match one of roles provided in the list. Roles will be fetched in the order that they are provided in the command line argument. Running with -v flag will show which role is being retrieved and then list the members who match it. After retrieving all the members for a given role, a tally will be shown for how many users have that role.
2626

2727
The script also creates a csv with names in the first column and users in the second. At the bottom of the CSV the totals for each role type are listed.
2828

@@ -48,7 +48,7 @@ You can also optionally turn on verbose logging in the console with the `-v` opt
4848

4949
## Get Team Roles of All Users - `team_roles.py`
5050

51-
This script will retrieves team roles for all users in a PagerDuty account that are members of any team. The default output is by user in the console, however you can optionally have the console output in comma-separated format for easier processing by using the `-c` option.
51+
This script will retrieves team roles for all users in a PagerDuty account that are members of any team. The default output is by user in the console, however you can optionally have the console output in comma-separated format for easier processing by using the `-c` option.
5252

5353
### Input Format
5454

@@ -57,12 +57,98 @@ Running the script requires the provision of just one argument: a global REST AP
5757
To execute the script, run this with your key:
5858

5959
```
60-
./team_roles.py -k API-KEY-HERE
60+
./team_roles.py -k API-KEY-HERE
6161
```
6262

6363
You can also optionally turn on comma-separated formatting in the console with the `-c` option.
6464

6565
### Options
6666

6767
- `-k`/`--api-key`: _(required)_ REST API key (should be a global key)
68-
- `-c`/`--comma-separated`: _(optional)_ Format console output separated by commas
68+
- `-c`/`--comma-separated`: _(optional)_ Format console output separated by commas
69+
70+
## Get User Role Changes
71+
72+
This script retrieves user role changes for all users in a PagerDuty account. By default the script will output
73+
a text formatted table to the console for user role changes in the past 24 hours. The script can optionally be configured to get role tier changes, can filter by a particular user ID, can filter by different date ranges,
74+
and can write results to a CSV file.
75+
76+
The following fields are returned by the script:
77+
- Date: ISO datetime string when the role change occurred
78+
- User ID: User ID of the user that the role change occurred on
79+
- User Name (not required): The user's nane that the role change occurred on
80+
- Role/Tier Before: The role/tier of the user before the change occurred
81+
- Role/Tier After: The role/tier of the user after the change occurred
82+
- Actor ID: The ID of the most specific actor that made the role change
83+
- Actor Type: The type of the most specific actor that made the role change
84+
- Actor Summary (not required): The display name of the most specific actor that made the role change
85+
86+
### Usage
87+
88+
Running the script requires one argument: a global REST API key
89+
90+
```
91+
./get_user_role_changes.py -k API-KEY-HERE
92+
```
93+
94+
#### Custom Date Range
95+
96+
```
97+
./get_user_role_changes.py -k API-KEY-HERE --since 2023-05-08T05:15:00Z --until 2024-05-08T05:15:00Z
98+
```
99+
100+
#### Filter by User ID
101+
102+
```
103+
./get_user_role_changes.py -k API-KEY-HERE --user-id PABC123
104+
```
105+
106+
#### Exclude user create and deletes
107+
108+
By default this script will include role changes from user creates and deletes
109+
(None -> New Role, Old Role -> None). To exclude these from reported results use the `--only-updates`
110+
options.
111+
112+
```
113+
./get_user_role_changes.py -k API-KEY-HERE --only-updates
114+
```
115+
116+
#### Role Tier Changes
117+
118+
Instead of getting all role changes, get all role tier changes in table format.
119+
120+
```
121+
./get_user_role_changes.py -k API-KEY-HERE --tier-changes
122+
```
123+
124+
#### Write Results to CSV
125+
126+
```
127+
./get_user_role_changes.py -k API-KEY-HERE --filename user_role_changes.csv
128+
```
129+
130+
#### Show all user change audit records
131+
132+
In some cases you may want to see all user changes or the complete role change audit record.
133+
This option provides a basic implementation to print the records in JSON format.
134+
135+
```
136+
./get_user_role_changes.py -k API-KEY-HERE --show-all
137+
```
138+
139+
### Options
140+
141+
To view all of the options available run the script with the help flag:
142+
143+
```
144+
./get_user_role_changes.py --help
145+
146+
-k API_KEY, --api-key API_KEY REST API key
147+
-s SINCE, --since SINCE Start of date range to search
148+
-u UNTIL, --until UNTIL End of date range to search
149+
-i USER_ID, --user-id USER_ID Filter results to a single user ID
150+
-o, --only-updates Exclude user creates and deletes from role change results
151+
-t, --tier-changes Get user role tier changes
152+
-a, --show-all Prints all fetched user records in JSON format
153+
-f FILENAME, --filename FILENAME Write results to a CSV file
154+
```
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python3
2+
3+
# this script retrieves all user role audit records for a given date range
4+
# the endpoint for audit records is https://api.pagerduty.com/audit/records
5+
6+
import argparse
7+
import csv
8+
import json
9+
import pdpyras
10+
from datetime import datetime, timezone
11+
from dateutil import parser, relativedelta
12+
from tabulate import tabulate
13+
14+
user_roles = {
15+
'owner': 'Owner',
16+
'admin': 'Global Admin',
17+
'user': 'Manager',
18+
'limited_user': 'Responder',
19+
'observer': 'Observer',
20+
'restricted_access': 'Restricted Access',
21+
'read_only_limited_user': 'Limited Stakeholder',
22+
'read_only_user': 'Stakeholder',
23+
'none': 'None'
24+
}
25+
26+
user_role_to_tier = {
27+
user_roles['owner']: 'Full User',
28+
user_roles['admin']: 'Full User',
29+
user_roles['user']: 'Full User',
30+
user_roles['limited_user']: 'Full User',
31+
user_roles['observer']: 'Full User',
32+
user_roles['restricted_access']: 'Full User',
33+
user_roles['read_only_limited_user']: 'Stakeholder',
34+
user_roles['read_only_user']: 'Stakeholder',
35+
user_roles['none']: 'None'
36+
}
37+
38+
actor_types = {
39+
'user_reference': 'User',
40+
'app_reference': 'App',
41+
'api_key_reference': 'API Key',
42+
}
43+
44+
def get_api_path(user_id):
45+
return f'users/{user_id}/audit/records' if user_id else 'audit/records'
46+
47+
def get_api_params(since, until, user_id):
48+
params={'since': since, 'until': until}
49+
if not user_id:
50+
params['root_resource_types[]'] = 'users'
51+
return params
52+
53+
def print_changes(changes, tier_changes):
54+
header = header_row(tier_changes)
55+
print(tabulate(header + changes, tablefmt='grid'))
56+
57+
def write_changes_to_csv(changes, tier_changes, filename):
58+
header = header_row(tier_changes)
59+
with open(filename, 'w') as csvfile:
60+
writer = csv.DictWriter(csvfile, fieldnames=header[0].keys())
61+
writer.writerows(header + changes)
62+
63+
def header_row(tier_changes):
64+
header = {
65+
'date': 'Date',
66+
'id': 'User ID',
67+
'summary': 'User Name',
68+
'before_value': 'Role Before',
69+
'value': 'Role After',
70+
'actor_id': 'Actor ID',
71+
'actor_type': 'Actor Type',
72+
'actor_summary': 'Actor Summary'
73+
}
74+
75+
if tier_changes:
76+
header.update({'before_value': 'Role Tier Before', 'value': 'Role Tier After'})
77+
78+
return [header]
79+
80+
def get_record_actor(record):
81+
actors = record.get('actors', [])
82+
if not len(actors):
83+
return {
84+
'actor_type': '',
85+
'actor_id': '',
86+
'actor_summary': ''
87+
}
88+
89+
actor = actors[0]
90+
return {
91+
'actor_type': actor_types[actor['type']],
92+
'actor_id': actor['id'],
93+
'actor_summary': actor.get('summary', '')
94+
}
95+
96+
def get_role_changes(record, only_updates):
97+
if only_updates and record['action'] != 'update':
98+
return []
99+
100+
# `details` and `fields` can both be null according to the API docs.
101+
field_changes = record.get('details', {}).get('fields', [])
102+
role_changes = filter(lambda fc: fc['name'] == 'role', field_changes)
103+
104+
def format_role_change(role_change):
105+
role_change = {
106+
'id': record['root_resource']['id'],
107+
'summary': record['root_resource'].get('summary', ''),
108+
'value': user_roles[role_change.get('value', 'none')],
109+
'before_value': user_roles[role_change.get('before_value', 'none')],
110+
'date': record['execution_time']
111+
}
112+
role_change.update(get_record_actor(record))
113+
return role_change
114+
115+
return list(map(format_role_change, role_changes))
116+
117+
def get_role_tier_changes(role_changes):
118+
tier_changes = []
119+
for role_change in role_changes:
120+
role_change['value'] = user_role_to_tier[role_change['value']]
121+
role_change['before_value'] = user_role_to_tier[role_change['before_value']]
122+
if role_change['value'] != role_change['before_value']:
123+
tier_changes.append(role_change)
124+
125+
return tier_changes
126+
127+
def chunk_date_range(args):
128+
# Mirror the default of the audit APIs, get records for the last 24 hours
129+
since, until = args.since, args.until
130+
if not since or not until:
131+
now = datetime.now(timezone.utc)
132+
yesterday = now - relativedelta.relativedelta(hours=+24)
133+
yield (datetime.isoformat(yesterday), datetime.isoformat(now))
134+
return
135+
136+
since = parser.isoparse(since)
137+
until = parser.isoparse(until)
138+
139+
if since > until:
140+
raise 'Invalid date range'
141+
142+
# Audit API requests have a date range limit of 31 days, so we
143+
# split the requested date range into 30 day chunks
144+
while True:
145+
next_since = since + relativedelta.relativedelta(days=+30)
146+
if next_since < until:
147+
yield (datetime.isoformat(since), datetime.isoformat(next_since))
148+
since = next_since
149+
else:
150+
yield (datetime.isoformat(since), datetime.isoformat(until))
151+
return
152+
153+
def main(args, session):
154+
user_id, tier_changes = args.user_id, args.tier_changes
155+
try:
156+
role_changes = []
157+
chunked_date_range = chunk_date_range(args)
158+
for since, until in chunked_date_range:
159+
for record in session.iter_cursor(get_api_path(user_id), params=get_api_params(since, until, user_id)):
160+
if args.show_all:
161+
print(json.dumps(record))
162+
record_role_changes = get_role_changes(record, args.only_updates)
163+
role_changes += record_role_changes
164+
165+
changes = get_role_tier_changes(role_changes) if tier_changes else role_changes
166+
if len(changes):
167+
changes = sorted(changes, key=lambda rc: rc['date'])
168+
print_changes(changes, tier_changes)
169+
if args.filename:
170+
write_changes_to_csv(changes, tier_changes, args.filename)
171+
else:
172+
print(f'No {"tier" if tier_changes else "role"} changes found.')
173+
174+
except pdpyras.PDClientError as e:
175+
print('Could not get user role change audit records')
176+
raise e
177+
178+
if __name__ == '__main__':
179+
ap = argparse.ArgumentParser(description='Prints all user role or tier changes between the given dates')
180+
ap.add_argument('-k', '--api-key', required=True, help='REST API key')
181+
ap.add_argument('-s', '--since', required=False, help='Start of date range to search')
182+
ap.add_argument('-u', '--until', required=False, help='End of date range to search')
183+
ap.add_argument('-i', '--user-id', required=False, help='Filter results to a single user ID')
184+
ap.add_argument('-o', '--only-updates', action='store_true', help='Exclude user creates and deletes from role change results')
185+
ap.add_argument('-t', '--tier-changes', action='store_true', help='Get user role tier changes')
186+
ap.add_argument('-a', '--show-all', action='store_true', help='Prints all fetched user records in JSON format')
187+
ap.add_argument('-f', '--filename', required=False, help='Write results to a CSV file')
188+
args = ap.parse_args()
189+
session = pdpyras.APISession(args.api_key)
190+
191+
main(args, session)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
pdpyras >= 2.0.2
2+
python-dateutil >= 2.8.2
3+
tabulate >= 0.9.0

0 commit comments

Comments
 (0)