Skip to content

Commit

Permalink
EF-4826 - audit user role changes (#114)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Add only updates option.

* Update get_info_on_all_users/README.md

Co-authored-by: David John Coleman II <[email protected]>

* Update get_info_on_all_users/README.md

Co-authored-by: David John Coleman II <[email protected]>

---------

Co-authored-by: Lewis Rafuse <[email protected]>
Co-authored-by: David John Coleman II <[email protected]>
Co-authored-by: Lewis Rafuse <[email protected]>
  • Loading branch information
4 people authored May 15, 2024
1 parent 3d3d07b commit 54429ff
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 4 deletions.
94 changes: 90 additions & 4 deletions get_info_on_all_users/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ To execute the script, run this with your key:

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

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.
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.

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.

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

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

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.
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.

### Input Format

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

```
./team_roles.py -k API-KEY-HERE
./team_roles.py -k API-KEY-HERE
```

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

### Options

- `-k`/`--api-key`: _(required)_ REST API key (should be a global key)
- `-c`/`--comma-separated`: _(optional)_ Format console output separated by commas
- `-c`/`--comma-separated`: _(optional)_ Format console output separated by commas

## Get User Role Changes

This script retrieves user role changes for all users in a PagerDuty account. By default the script will output
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,
and can write results to a CSV file.

The following fields are returned by the script:
- Date: ISO datetime string when the role change occurred
- User ID: User ID of the user that the role change occurred on
- User Name (not required): The user's nane that the role change occurred on
- Role/Tier Before: The role/tier of the user before the change occurred
- Role/Tier After: The role/tier of the user after the change occurred
- Actor ID: The ID of the most specific actor that made the role change
- Actor Type: The type of the most specific actor that made the role change
- Actor Summary (not required): The display name of the most specific actor that made the role change

### Usage

Running the script requires one argument: a global REST API key

```
./get_user_role_changes.py -k API-KEY-HERE
```

#### Custom Date Range

```
./get_user_role_changes.py -k API-KEY-HERE --since 2023-05-08T05:15:00Z --until 2024-05-08T05:15:00Z
```

#### Filter by User ID

```
./get_user_role_changes.py -k API-KEY-HERE --user-id PABC123
```

#### Exclude user create and deletes

By default this script will include role changes from user creates and deletes
(None -> New Role, Old Role -> None). To exclude these from reported results use the `--only-updates`
options.

```
./get_user_role_changes.py -k API-KEY-HERE --only-updates
```

#### Role Tier Changes

Instead of getting all role changes, get all role tier changes in table format.

```
./get_user_role_changes.py -k API-KEY-HERE --tier-changes
```

#### Write Results to CSV

```
./get_user_role_changes.py -k API-KEY-HERE --filename user_role_changes.csv
```

#### Show all user change audit records

In some cases you may want to see all user changes or the complete role change audit record.
This option provides a basic implementation to print the records in JSON format.

```
./get_user_role_changes.py -k API-KEY-HERE --show-all
```

### Options

To view all of the options available run the script with the help flag:

```
./get_user_role_changes.py --help
-k API_KEY, --api-key API_KEY REST API key
-s SINCE, --since SINCE Start of date range to search
-u UNTIL, --until UNTIL End of date range to search
-i USER_ID, --user-id USER_ID Filter results to a single user ID
-o, --only-updates Exclude user creates and deletes from role change results
-t, --tier-changes Get user role tier changes
-a, --show-all Prints all fetched user records in JSON format
-f FILENAME, --filename FILENAME Write results to a CSV file
```
191 changes: 191 additions & 0 deletions get_info_on_all_users/get_user_role_changes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#!/usr/bin/env python3

# this script retrieves all user role audit records for a given date range
# the endpoint for audit records is https://api.pagerduty.com/audit/records

import argparse
import csv
import json
import pdpyras
from datetime import datetime, timezone
from dateutil import parser, relativedelta
from tabulate import tabulate

user_roles = {
'owner': 'Owner',
'admin': 'Global Admin',
'user': 'Manager',
'limited_user': 'Responder',
'observer': 'Observer',
'restricted_access': 'Restricted Access',
'read_only_limited_user': 'Limited Stakeholder',
'read_only_user': 'Stakeholder',
'none': 'None'
}

user_role_to_tier = {
user_roles['owner']: 'Full User',
user_roles['admin']: 'Full User',
user_roles['user']: 'Full User',
user_roles['limited_user']: 'Full User',
user_roles['observer']: 'Full User',
user_roles['restricted_access']: 'Full User',
user_roles['read_only_limited_user']: 'Stakeholder',
user_roles['read_only_user']: 'Stakeholder',
user_roles['none']: 'None'
}

actor_types = {
'user_reference': 'User',
'app_reference': 'App',
'api_key_reference': 'API Key',
}

def get_api_path(user_id):
return f'users/{user_id}/audit/records' if user_id else 'audit/records'

def get_api_params(since, until, user_id):
params={'since': since, 'until': until}
if not user_id:
params['root_resource_types[]'] = 'users'
return params

def print_changes(changes, tier_changes):
header = header_row(tier_changes)
print(tabulate(header + changes, tablefmt='grid'))

def write_changes_to_csv(changes, tier_changes, filename):
header = header_row(tier_changes)
with open(filename, 'w') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=header[0].keys())
writer.writerows(header + changes)

def header_row(tier_changes):
header = {
'date': 'Date',
'id': 'User ID',
'summary': 'User Name',
'before_value': 'Role Before',
'value': 'Role After',
'actor_id': 'Actor ID',
'actor_type': 'Actor Type',
'actor_summary': 'Actor Summary'
}

if tier_changes:
header.update({'before_value': 'Role Tier Before', 'value': 'Role Tier After'})

return [header]

def get_record_actor(record):
actors = record.get('actors', [])
if not len(actors):
return {
'actor_type': '',
'actor_id': '',
'actor_summary': ''
}

actor = actors[0]
return {
'actor_type': actor_types[actor['type']],
'actor_id': actor['id'],
'actor_summary': actor.get('summary', '')
}

def get_role_changes(record, only_updates):
if only_updates and record['action'] != 'update':
return []

# `details` and `fields` can both be null according to the API docs.
field_changes = record.get('details', {}).get('fields', [])
role_changes = filter(lambda fc: fc['name'] == 'role', field_changes)

def format_role_change(role_change):
role_change = {
'id': record['root_resource']['id'],
'summary': record['root_resource'].get('summary', ''),
'value': user_roles[role_change.get('value', 'none')],
'before_value': user_roles[role_change.get('before_value', 'none')],
'date': record['execution_time']
}
role_change.update(get_record_actor(record))
return role_change

return list(map(format_role_change, role_changes))

def get_role_tier_changes(role_changes):
tier_changes = []
for role_change in role_changes:
role_change['value'] = user_role_to_tier[role_change['value']]
role_change['before_value'] = user_role_to_tier[role_change['before_value']]
if role_change['value'] != role_change['before_value']:
tier_changes.append(role_change)

return tier_changes

def chunk_date_range(args):
# Mirror the default of the audit APIs, get records for the last 24 hours
since, until = args.since, args.until
if not since or not until:
now = datetime.now(timezone.utc)
yesterday = now - relativedelta.relativedelta(hours=+24)
yield (datetime.isoformat(yesterday), datetime.isoformat(now))
return

since = parser.isoparse(since)
until = parser.isoparse(until)

if since > until:
raise 'Invalid date range'

# Audit API requests have a date range limit of 31 days, so we
# split the requested date range into 30 day chunks
while True:
next_since = since + relativedelta.relativedelta(days=+30)
if next_since < until:
yield (datetime.isoformat(since), datetime.isoformat(next_since))
since = next_since
else:
yield (datetime.isoformat(since), datetime.isoformat(until))
return

def main(args, session):
user_id, tier_changes = args.user_id, args.tier_changes
try:
role_changes = []
chunked_date_range = chunk_date_range(args)
for since, until in chunked_date_range:
for record in session.iter_cursor(get_api_path(user_id), params=get_api_params(since, until, user_id)):
if args.show_all:
print(json.dumps(record))
record_role_changes = get_role_changes(record, args.only_updates)
role_changes += record_role_changes

changes = get_role_tier_changes(role_changes) if tier_changes else role_changes
if len(changes):
changes = sorted(changes, key=lambda rc: rc['date'])
print_changes(changes, tier_changes)
if args.filename:
write_changes_to_csv(changes, tier_changes, args.filename)
else:
print(f'No {"tier" if tier_changes else "role"} changes found.')

except pdpyras.PDClientError as e:
print('Could not get user role change audit records')
raise e

if __name__ == '__main__':
ap = argparse.ArgumentParser(description='Prints all user role or tier changes between the given dates')
ap.add_argument('-k', '--api-key', required=True, help='REST API key')
ap.add_argument('-s', '--since', required=False, help='Start of date range to search')
ap.add_argument('-u', '--until', required=False, help='End of date range to search')
ap.add_argument('-i', '--user-id', required=False, help='Filter results to a single user ID')
ap.add_argument('-o', '--only-updates', action='store_true', help='Exclude user creates and deletes from role change results')
ap.add_argument('-t', '--tier-changes', action='store_true', help='Get user role tier changes')
ap.add_argument('-a', '--show-all', action='store_true', help='Prints all fetched user records in JSON format')
ap.add_argument('-f', '--filename', required=False, help='Write results to a CSV file')
args = ap.parse_args()
session = pdpyras.APISession(args.api_key)

main(args, session)
2 changes: 2 additions & 0 deletions get_info_on_all_users/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pdpyras >= 2.0.2
python-dateutil >= 2.8.2
tabulate >= 0.9.0

0 comments on commit 54429ff

Please sign in to comment.