Skip to content

Commit 37c7f27

Browse files
alanag13timabrmsnJuliya Smith
authored
Legal hold implementation (#82) (#90)
* Legal hold implementation (#82) Co-authored-by: Alan Grgic <[email protected]> * print "No active matter members." instead of "Active matter members:\nNone" * add comment to clarify None/True state for `active` arg * changelog periods * add additionally * with => will * move matter_id accessible check to remove_user * print "No inactive matter members." fix tests * Remove extra space Co-authored-by: Tim Abramson <[email protected]> Co-authored-by: tim.abramson <[email protected]> Co-authored-by: Juliya Smith <[email protected]>
1 parent c089404 commit 37c7f27

27 files changed

+748
-93
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ubuntu-latest
99
strategy:
1010
matrix:
11-
python: [2.7, 3.5, 3.6, 3.7, 3.8]
11+
python: [3.5, 3.6, 3.7, 3.8]
1212

1313
steps:
1414
- uses: actions/checkout@v2

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
4545
- `path`
4646
- `remove`: that takes a csv file with rule IDs and usernames.
4747

48+
- `code42 legal-hold` commands:
49+
- `add-user` with parameters `--matter-id/-m` and `--username/-u`.
50+
- `remove-user` with parameters `--matter-id/-m` and `--username/-u`.
51+
- `list` prints out existing active legal hold matters.
52+
- `show` takes a `matter_id` and prints details of the matter.
53+
- optional argument `--include-inactive` additionally prints matter memberships that are no longer active.
54+
- optional argument `--include-policy` additionally prints out the matter's backup preservation policy in json form.
55+
- `bulk` with subcommands:
56+
- `add-user`: that takes a csv file with matter IDs and usernames.
57+
- `remove-user`: that takes a csv file with matter IDs and usernames.
58+
- `generate-template`: that creates the file templates.
59+
- `cmd`: with options `add` and `remove`.
60+
- `path`
61+
4862
- Success messages for `profile delete` and `profile update`.
4963

5064
- Additional information in the error log file:

setup.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
long_description_content_type="text/markdown",
2020
packages=find_packages("src"),
2121
package_dir={"": "src"},
22-
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4",
22+
python_requires=">3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4",
2323
install_requires=[
24-
"c42eventextractor==0.3.0b1",
24+
"c42eventextractor==0.3.1",
2525
"keyring==18.0.1",
2626
"keyrings.alt==3.2.0",
2727
"py42>=1.2.0",
@@ -46,8 +46,6 @@
4646
"Natural Language :: English",
4747
"License :: OSI Approved :: MIT License",
4848
"Programming Language :: Python",
49-
"Programming Language :: Python :: 2",
50-
"Programming Language :: Python :: 2.7",
5149
"Programming Language :: Python :: 3",
5250
"Programming Language :: Python :: 3.5",
5351
"Programming Language :: Python :: 3.6",

src/code42cli/bulk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def generate_template(handler, path=None):
2323
`handler` only has one parameter that is not `sdk` or `profile`, it will create a blank file.
2424
This is useful for commands such as `remove` which only require a list of users.
2525
"""
26-
path = path or u"{0}/{1}.csv".format(os.getcwd(), str(handler.__name__))
26+
path = path or os.path.join(os.getcwd(), u"{}.csv".format(str(handler.__name__)))
2727
args = [
2828
arg
2929
for arg in inspect.getargspec(handler).args

src/code42cli/cmds/alerts/rules/commands.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
3+
from code42cli.commands import Command
14
from code42cli import MAIN_COMMAND
25
from code42cli.commands import Command, SubcommandLoader
36
from code42cli.bulk import generate_template, BulkCommandType
@@ -52,17 +55,21 @@ def _generate_template_file(cmd, path=None):
5255
"""Generates a template file a user would need to fill-in for bulk operating.
5356
5457
Args:
55-
cmd (str or unicode): An option from the `BulkCommandType` enum specifying which type of file to
56-
generate.
57-
path (str or unicode, optional): A path to put the file after it's generated. If None, will use
58-
the current working directory. Defaults to None.
58+
cmd (str or unicode): An option from the `BulkCommandType` enum specifying which type of
59+
file to generate.
60+
path (str or unicode, optional): A path to put the file after it's generated. If None, will
61+
use the current working directory. Defaults to None.
5962
"""
6063
handler = None
64+
filename = u"alert_rule.csv"
6165
if cmd == BulkCommandType.ADD:
6266
handler = add_user
67+
filename = u"add_users_to_{}".format(filename)
6368
elif cmd == BulkCommandType.REMOVE:
6469
handler = remove_user
65-
70+
filename = u"remove_users_from_{}".format(filename)
71+
if not path:
72+
path = os.path.join(os.getcwd(), filename)
6673
generate_template(handler, path)
6774

6875

src/code42cli/cmds/alerts/rules/user_rule.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1+
from collections import OrderedDict
2+
13
from py42.exceptions import Py42InternalServerError
24
from py42.util import format_json
35

46

57
from code42cli.errors import InvalidRuleTypeError
6-
from code42cli.util import format_to_table, find_format_width
8+
from code42cli.util import format_to_table, find_format_width, get_user_id
79
from code42cli.bulk import run_bulk_process
810
from code42cli.file_readers import create_csv_reader
911
from code42cli.logger import get_main_cli_logger
10-
from code42cli.cmds.detectionlists import get_user_id
1112
from code42cli.cmds.alerts.rules.enums import AlertRuleTypes
1213

1314

14-
_HEADER_KEYS_MAP = {
15-
u"observerRuleId": u"RuleId",
16-
u"name": u"Name",
17-
u"severity": u"Severity",
18-
u"type": u"Type",
19-
u"ruleSource": u"Source",
20-
u"isEnabled": u"Enabled",
21-
}
15+
_HEADER_KEYS_MAP = OrderedDict()
16+
_HEADER_KEYS_MAP[u"observerRuleId"] = u"RuleId"
17+
_HEADER_KEYS_MAP[u"name"] = u"Name"
18+
_HEADER_KEYS_MAP[u"severity"] = u"Severity"
19+
_HEADER_KEYS_MAP[u"type"] = u"Type"
20+
_HEADER_KEYS_MAP[u"ruleSource"] = u"Source"
21+
_HEADER_KEYS_MAP[u"isEnabled"] = u"Enabled"
2222

2323

2424
def add_user(sdk, profile, rule_id, username):

src/code42cli/cmds/detectionlists/__init__.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from code42cli.cmds.detectionlists.commands import DetectionListSubcommandLoader
44
from code42cli.bulk import generate_template, run_bulk_process
55
from code42cli.file_readers import create_csv_reader, create_flat_file_reader
6-
from code42cli.errors import UserAlreadyAddedError, UserDoesNotExistError, UnknownRiskTagError
6+
from code42cli.errors import UserAlreadyAddedError, UnknownRiskTagError
77
from code42cli.cmds.detectionlists.enums import DetectionLists, DetectionListUserKeys, RiskTags
88
from code42cli.cmds.detectionlists.bulk import BulkDetectionList, BulkHighRiskEmployee
9+
from code42cli.util import get_user_id
910

1011

1112
def try_handle_user_already_added_error(bad_request_err, username_tried_adding, list_name):
@@ -205,23 +206,6 @@ def load_user_descriptions(argument_collection):
205206
notes.set_help(u"Notes about the employee.")
206207

207208

208-
def get_user_id(sdk, username):
209-
"""Returns the user's UID (referred to by `user_id` in detection lists). If the user does not
210-
exist, it prints an error and exits.
211-
212-
Args:
213-
sdk (py42.sdk.SDKClient): The py42 sdk.
214-
username (str or unicode): The username of the user to get an ID for.
215-
216-
Returns:
217-
str: The user ID for the user with the given username.
218-
"""
219-
users = sdk.users.get_by_username(username)[u"users"]
220-
if not users:
221-
raise UserDoesNotExistError(username)
222-
return users[0][u"userUid"]
223-
224-
225209
def update_user(sdk, user_id, cloud_alias=None, risk_tag=None, notes=None):
226210
"""Updates a detection list user.
227211

src/code42cli/cmds/detectionlists/departing_employee.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
DetectionList,
33
DetectionListHandlers,
44
load_user_descriptions,
5-
get_user_id,
65
update_user,
76
try_handle_user_already_added_error,
87
DetectionListSubcommandLoader,
98
)
9+
from code42cli.util import get_user_id
1010
from code42cli.cmds.detectionlists.enums import DetectionLists
1111

1212
from py42.exceptions import Py42BadRequestError
@@ -48,7 +48,7 @@ def add_departing_employee(
4848
sdk.detectionlists.departing_employee.add(user_id, departure_date)
4949
update_user(sdk, user_id, cloud_alias, notes=notes)
5050
except Py42BadRequestError as err:
51-
list_name = DetectionLists.DEPARTING_EMPLOYEE
51+
list_name = u"{} list".format(DetectionLists.DEPARTING_EMPLOYEE)
5252
try_handle_user_already_added_error(err, username, list_name)
5353
raise
5454

src/code42cli/cmds/detectionlists/high_risk_employee.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
DetectionList,
77
DetectionListHandlers,
88
load_user_descriptions,
9-
get_user_id,
109
update_user,
1110
try_handle_user_already_added_error,
1211
add_risk_tags,
1312
remove_risk_tags,
1413
load_username_description,
1514
handle_list_args,
1615
)
16+
from code42cli.util import get_user_id
1717
from code42cli.cmds.detectionlists.enums import DetectionLists, DetectionListUserKeys, RiskTags
1818

1919

@@ -71,7 +71,7 @@ def add_high_risk_employee(sdk, profile, username, cloud_alias=None, risk_tag=No
7171
sdk.detectionlists.high_risk_employee.add(user_id)
7272
update_user(sdk, user_id, cloud_alias, risk_tag, notes)
7373
except Py42BadRequestError as err:
74-
list_name = DetectionLists.HIGH_RISK_EMPLOYEE
74+
list_name = u"{} list".format(DetectionLists.HIGH_RISK_EMPLOYEE)
7575
try_handle_user_already_added_error(err, username, list_name)
7676
raise
7777

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from collections import OrderedDict
2+
from functools import lru_cache
3+
from pprint import pprint
4+
5+
from py42.exceptions import Py42ForbiddenError, Py42BadRequestError
6+
7+
8+
from code42cli.errors import (
9+
UserAlreadyAddedError,
10+
UserNotInLegalHoldError,
11+
LegalHoldNotFoundOrPermissionDeniedError,
12+
)
13+
from code42cli.util import (
14+
format_to_table,
15+
find_format_width,
16+
format_string_list_to_columns,
17+
get_user_id,
18+
)
19+
from code42cli.bulk import run_bulk_process
20+
from code42cli.file_readers import create_csv_reader
21+
from code42cli.logger import get_main_cli_logger
22+
23+
_MATTER_KEYS_MAP = OrderedDict()
24+
_MATTER_KEYS_MAP[u"legalHoldUid"] = u"Matter ID"
25+
_MATTER_KEYS_MAP[u"name"] = u"Name"
26+
_MATTER_KEYS_MAP[u"description"] = u"Description"
27+
_MATTER_KEYS_MAP[u"creator_username"] = u"Creator"
28+
_MATTER_KEYS_MAP[u"creationDate"] = u"Creation Date"
29+
30+
logger = get_main_cli_logger()
31+
32+
33+
def add_user(sdk, matter_id, username):
34+
user_id = get_user_id(sdk, username)
35+
matter = _check_matter_is_accessible(sdk, matter_id)
36+
try:
37+
sdk.legalhold.add_to_matter(user_id, matter_id)
38+
except Py42BadRequestError as e:
39+
if u"USER_ALREADY_IN_HOLD" in e.response.text:
40+
matter_id_and_name_text = u"legal hold matter id={}, name={}".format(
41+
matter_id, matter[u"name"]
42+
)
43+
raise UserAlreadyAddedError(username, matter_id_and_name_text)
44+
raise
45+
46+
47+
def remove_user(sdk, matter_id, username):
48+
_check_matter_is_accessible(sdk, matter_id)
49+
membership_id = _get_legal_hold_membership_id_for_user_and_matter(sdk, username, matter_id)
50+
sdk.legalhold.remove_from_matter(membership_id)
51+
52+
53+
def get_matters(sdk):
54+
matters = _get_all_active_matters(sdk)
55+
if matters:
56+
rows, column_size = find_format_width(matters, _MATTER_KEYS_MAP)
57+
format_to_table(rows, column_size)
58+
59+
60+
def add_bulk_users(sdk, file_name):
61+
reader = create_csv_reader(file_name)
62+
run_bulk_process(
63+
lambda matter_id, username: add_user(sdk, matter_id, username), reader,
64+
)
65+
66+
67+
def remove_bulk_users(sdk, file_name):
68+
reader = create_csv_reader(file_name)
69+
run_bulk_process(
70+
lambda matter_id, username: remove_user(sdk, matter_id, username), reader,
71+
)
72+
73+
74+
def show_matter(sdk, matter_id, include_inactive=False, include_policy=False):
75+
matter = _check_matter_is_accessible(sdk, matter_id)
76+
matter[u"creator_username"] = matter[u"creator"][u"username"]
77+
78+
# if `active` is None then all matters (whether active or inactive) are returned. True returns
79+
# only those that are active.
80+
active = None if include_inactive else True
81+
memberships = _get_legal_hold_memberships_for_matter(sdk, matter_id, active=active)
82+
active_usernames = [member[u"user"][u"username"] for member in memberships if member[u"active"]]
83+
inactive_usernames = [
84+
member[u"user"][u"username"] for member in memberships if not member[u"active"]
85+
]
86+
87+
rows, column_size = find_format_width([matter], _MATTER_KEYS_MAP)
88+
89+
print(u"")
90+
format_to_table(rows, column_size)
91+
if active_usernames:
92+
print(u"\nActive matter members:\n")
93+
format_string_list_to_columns(active_usernames)
94+
else:
95+
print("\nNo active matter members.\n")
96+
97+
if include_inactive:
98+
if inactive_usernames:
99+
print(u"\nInactive matter members:\n")
100+
format_string_list_to_columns(inactive_usernames)
101+
else:
102+
print("No inactive matter members.\n")
103+
104+
if include_policy:
105+
_get_and_print_preservation_policy(sdk, matter[u"holdPolicyUid"])
106+
print(u"")
107+
108+
109+
def _get_and_print_preservation_policy(sdk, policy_uid):
110+
preservation_policy = sdk.legalhold.get_policy_by_uid(policy_uid)
111+
print(u"\nPreservation Policy:\n")
112+
pprint(preservation_policy._data_root)
113+
114+
115+
def _get_legal_hold_membership_id_for_user_and_matter(sdk, username, matter_id):
116+
user_id = get_user_id(sdk, username)
117+
memberships = _get_legal_hold_memberships_for_matter(sdk, matter_id, active=True)
118+
for member in memberships:
119+
if member[u"user"][u"userUid"] == user_id:
120+
return member[u"legalHoldMembershipUid"]
121+
raise UserNotInLegalHoldError(username, matter_id)
122+
123+
124+
def _get_legal_hold_memberships_for_matter(sdk, matter_id, active=True):
125+
memberships_generator = sdk.legalhold.get_all_matter_custodians(
126+
legal_hold_uid=matter_id, active=active
127+
)
128+
memberships = [
129+
member for page in memberships_generator for member in page[u"legalHoldMemberships"]
130+
]
131+
return memberships
132+
133+
134+
def _get_all_active_matters(sdk):
135+
matters_generator = sdk.legalhold.get_all_matters()
136+
matters = [
137+
matter for page in matters_generator for matter in page[u"legalHolds"] if matter[u"active"]
138+
]
139+
for matter in matters:
140+
matter[u"creator_username"] = matter[u"creator"][u"username"]
141+
return matters
142+
143+
144+
@lru_cache(maxsize=None)
145+
def _check_matter_is_accessible(sdk, matter_id):
146+
try:
147+
matter = sdk.legalhold.get_matter_by_uid(matter_id)
148+
return matter
149+
except (Py42BadRequestError, Py42ForbiddenError):
150+
raise LegalHoldNotFoundOrPermissionDeniedError(matter_id)

0 commit comments

Comments
 (0)