Skip to content

Commit 8255958

Browse files
committed
#302 - Review organisation endpoint methods and types
1 parent 5835075 commit 8255958

File tree

4 files changed

+266
-5
lines changed

4 files changed

+266
-5
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from pathlib import Path
2+
3+
from tests.utils import TestConfig
4+
from thehive4py.client import TheHiveApi
5+
from thehive4py.query.filters import Eq
6+
7+
8+
class TestOrganisationEndpoint:
9+
10+
def test_add_and_download_attachment_to_main_org(
11+
self, thehive: TheHiveApi, tmp_path: Path
12+
):
13+
attachment_paths = [str(tmp_path / f"attachment-{i}.txt") for i in range(2)]
14+
download_attachment_paths = [
15+
str(tmp_path / f"dl-attachment-{i}.txt") for i in range(2)
16+
]
17+
18+
for path in attachment_paths:
19+
with open(path, "w") as attachment_fp:
20+
attachment_fp.write(f"content of {path}")
21+
22+
added_attachments = thehive.organisation.add_attachment(
23+
attachment_paths=attachment_paths
24+
)
25+
26+
for attachment, path in zip(added_attachments, download_attachment_paths):
27+
thehive.organisation.download_attachment(
28+
attachment_id=attachment["_id"],
29+
attachment_path=path,
30+
)
31+
32+
for original, downloaded in zip(attachment_paths, download_attachment_paths):
33+
with open(original) as original_fp, open(downloaded) as downloaded_fp:
34+
assert original_fp.read() == downloaded_fp.read()
35+
36+
def test_add_and_delete_attachment_to_main_org(
37+
self, thehive: TheHiveApi, tmp_path: Path, test_config: TestConfig
38+
):
39+
attachment_path = str(tmp_path / "my-attachment.txt")
40+
with open(attachment_path, "w") as attachment_fp:
41+
attachment_fp.write("some content...")
42+
43+
added_attachments = thehive.organisation.add_attachment(
44+
attachment_paths=[attachment_path]
45+
)
46+
47+
for attachment in added_attachments:
48+
thehive.organisation.delete_attachment(attachment_id=attachment["_id"])
49+
50+
assert thehive.organisation.find_attachments(org_id=test_config.main_org) == []
51+
52+
def test_get_organisation(self, thehive_admin: TheHiveApi, test_config: TestConfig):
53+
main_org = thehive_admin.organisation.get(org_id=test_config.main_org)
54+
55+
assert main_org["name"] == test_config.main_org
56+
57+
def test_find_organisations(
58+
self, thehive_admin: TheHiveApi, test_config: TestConfig
59+
):
60+
61+
organisations = thehive_admin.organisation.find(
62+
filters=Eq("name", test_config.main_org)
63+
)
64+
65+
assert len(organisations) == 1
66+
67+
# most of the other organisation endpoints cannot be tested due to constraints
68+
# as at the moment the community license allows up to maximum 1 organisation

tests/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,16 @@ def _reset_hive_org(hive_url: str, test_config: TestConfig, organisation: str) -
9595
)
9696

9797
alerts = client.alert.find()
98+
attachments = client.organisation.find_attachments(org_id=organisation)
9899
cases = client.case.find()
99100
case_templates = client.case_template.find()
100101

101102
with ThreadPoolExecutor() as executor:
102103
executor.map(client.alert.delete, [alert["_id"] for alert in alerts])
104+
executor.map(
105+
client.organisation.delete_attachment,
106+
[attachment["_id"] for attachment in attachments],
107+
)
103108
executor.map(client.case.delete, [case["_id"] for case in cases])
104109
executor.map(
105110
client.case_template.delete,

thehive4py/endpoints/organisation.py

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,216 @@
55
from thehive4py.query.filters import FilterExpr
66
from thehive4py.query.page import Paginate
77
from thehive4py.query.sort import SortExpr
8+
from thehive4py.types.attachment import OutputAttachment
89
from thehive4py.types.organisation import (
910
InputBulkOrganisationLink,
1011
InputOrganisation,
1112
InputOrganisationLink,
1213
InputUpdateOrganisation,
1314
OutputOrganisation,
15+
OutputOrganisationLink,
1416
OutputSharingProfile,
1517
)
1618

1719

1820
class OrganisationEndpoint(EndpointBase):
21+
def add_attachment(
22+
self, attachment_paths: List[str], can_rename: bool = True
23+
) -> List[OutputAttachment]:
24+
"""Add attachment to organisation.
25+
26+
Args:
27+
attachment_paths: List of paths to the attachments to create.
28+
can_rename: If set to True, the files can be renamed if they already exist
29+
with the same name
30+
31+
Returns:
32+
The created attachments.
33+
"""
34+
files = [
35+
("attachments", self._fileinfo_from_filepath(attachment_path))
36+
for attachment_path in attachment_paths
37+
]
38+
return self._session.make_request(
39+
"POST", "/api/v1/attachment", data={"canRename": can_rename}, files=files
40+
)["attachments"]
41+
42+
def delete_attachment(self, attachment_id: str) -> None:
43+
"""Delete an attachment.
44+
45+
Args:
46+
attachment_id: The id of the attachment.
47+
48+
Returns:
49+
N/A
50+
"""
51+
return self._session.make_request(
52+
"DELETE", path=f"/api/v1/attachment/{attachment_id}"
53+
)
54+
55+
def download_attachment(self, attachment_id: str, attachment_path: str) -> None:
56+
"""Download an attachment.
57+
58+
Args:
59+
attachment_id: The id of the attachment.
60+
attachment_path: The local path to download the attachment to.
61+
62+
Returns:
63+
N/A
64+
"""
65+
return self._session.make_request(
66+
"GET",
67+
path=f"/api/v1/attachment/{attachment_id}/download",
68+
download_path=attachment_path,
69+
)
70+
71+
def find_attachments(
72+
self,
73+
org_id: str,
74+
filters: Optional[FilterExpr] = None,
75+
sortby: Optional[SortExpr] = None,
76+
paginate: Optional[Paginate] = None,
77+
) -> List[OutputAttachment]:
78+
"""Find attachments related to an organisation.
79+
80+
Args:
81+
org_id: The id of the organisation.
82+
filters: The filter expressions to apply in the query.
83+
sortby: The sort expressions to apply in the query.
84+
paginate: The pagination experssion to apply in the query.
85+
86+
Returns:
87+
The list of case attachments matched by the query or an empty list.
88+
"""
89+
query: QueryExpr = [
90+
{"_name": "getOrganisation", "idOrName": org_id},
91+
{"_name": "attachments"},
92+
*self._build_subquery(filters=filters, sortby=sortby, paginate=paginate),
93+
]
94+
return self._session.make_request(
95+
"POST",
96+
path="/api/v1/query",
97+
params={"name": "organisation-attachments"},
98+
json={"query": query},
99+
)
100+
19101
def create(self, organisation: InputOrganisation) -> OutputOrganisation:
102+
"""Create an organisation.
103+
104+
Args:
105+
organisation: The body of the organisation.
106+
107+
Returns:
108+
The created organisation.
109+
"""
20110
return self._session.make_request(
21111
"POST", path="/api/v1/organisation", json=organisation
22112
)
23113

24114
def get(self, org_id: str) -> OutputOrganisation:
115+
"""Get an organisation.
116+
117+
Args:
118+
org_id: The id of the organisation.
119+
120+
Returns:
121+
The organisation specified by the id.
122+
"""
25123
return self._session.make_request("GET", path=f"/api/v1/organisation/{org_id}")
26124

27125
def update(self, org_id: str, fields: InputUpdateOrganisation) -> None:
126+
"""Get an organisation.
127+
128+
Args:
129+
org_id: The id of the organisation.
130+
fields: The fields of the organisation to update.
131+
132+
Returns:
133+
N/A
134+
"""
28135
return self._session.make_request(
29136
"PATCH", path=f"/api/v1/organisation/{org_id}", json=fields
30137
)
31138

32-
def delete(self, org_id: str) -> None:
139+
def get_avatar(self, org_id: str, file_hash: str, avatar_path: str) -> None:
140+
"""Get an organisaton avatar.
141+
142+
Args:
143+
org_id: The id of the organisation.
144+
file_hash: The hash of the organisation avatar.
145+
avatar_path: The local path to download the organisation avatar to.
146+
147+
Returns:
148+
N/A
149+
"""
33150
return self._session.make_request(
34-
"DELETE", path=f"/api/v1/organisation/{org_id}"
151+
"GET",
152+
path=f"/api/v1/organisation/{org_id}/avatar/{file_hash}",
153+
download_path=avatar_path,
35154
)
36155

37156
def link(self, org_id: str, other_org_id: str, link: InputOrganisationLink) -> None:
157+
"""Link two organisatons.
158+
159+
Args:
160+
org_id: The id of the organisation.
161+
other_org_id: The id of the other organisation.
162+
link: The type of organisation links.
163+
164+
Returns:
165+
N/A
166+
"""
38167
return self._session.make_request(
39168
"PUT", path=f"/api/v1/organisation/{org_id}/link/{other_org_id}", json=link
40169
)
41170

42171
def unlink(self, org_id: str, other_org_id: str) -> None:
172+
"""Unlink two organisatons.
173+
174+
Args:
175+
org_id: The id of the organisation.
176+
other_org_id: The id of the other organisation.
177+
178+
Returns:
179+
N/A
180+
"""
43181
return self._session.make_request(
44-
"GET", path=f"/api/v1/organisation/{org_id}/link/{other_org_id}"
182+
"DELETE", path=f"/api/v1/organisation/{org_id}/link/{other_org_id}"
45183
)
46184

47-
def list_links(self, org_id: str) -> List[OutputOrganisation]:
185+
def list_links(self, org_id: str) -> List[OutputOrganisationLink]:
186+
"""List links of an organisatons.
187+
188+
Args:
189+
org_id: The id of the organisation.
190+
191+
Returns:
192+
The list of organisation links.
193+
"""
48194
return self._session.make_request(
49195
"GET", path=f"/api/v1/organisation/{org_id}/links"
50196
)
51197

52198
def bulk_link(self, org_id: str, links: List[InputBulkOrganisationLink]) -> None:
199+
"""Bulk link organisations.
200+
201+
Args:
202+
org_id: The id of the organisation.
203+
links: The list of organisation links.
204+
205+
Returns:
206+
N/A
207+
"""
53208
return self._session.make_request(
54209
"PUT", path=f"/api/v1/organisation/{org_id}/links", json={"links": links}
55210
)
56211

57212
def list_sharing_profiles(self) -> List[OutputSharingProfile]:
213+
"""List all sharing profiles.
214+
215+
Returns:
216+
The list of sharing profiles.
217+
"""
58218
return self._session.make_request("GET", path="/api/v1/sharingProfile")
59219

60220
def find(
@@ -63,6 +223,16 @@ def find(
63223
sortby: Optional[SortExpr] = None,
64224
paginate: Optional[Paginate] = None,
65225
) -> List[OutputOrganisation]:
226+
"""Find multiple organisations.
227+
228+
Args:
229+
filters: The filter expressions to apply in the query.
230+
sortby: The sort expressions to apply in the query.
231+
paginate: The pagination experssion to apply in the query.
232+
233+
Returns:
234+
The list of organisations matched by the query or an empty list.
235+
"""
66236
query: QueryExpr = [
67237
{"_name": "listOrganisation"},
68238
*self._build_subquery(filters=filters, sortby=sortby, paginate=paginate),
@@ -76,6 +246,14 @@ def find(
76246
)
77247

78248
def count(self, filters: Optional[FilterExpr] = None) -> int:
249+
"""Count organisations.
250+
251+
Args:
252+
filters: The filter expressions to apply in the query.
253+
254+
Returns:
255+
The count of organisations matched by the query.
256+
"""
79257
query: QueryExpr = [
80258
{"_name": "listOrganisation"},
81259
*self._build_subquery(filters=filters),

thehive4py/types/organisation.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ class InputOrganisationLink(TypedDict, total=False):
66
otherLinkType: str
77

88

9-
class InputBulkOrganisationLink(TypedDict):
9+
class InputBulkOrganisationLinkRequired(TypedDict):
1010
toOrganisation: str
1111
linkType: str
1212
otherLinkType: str
1313

1414

15+
class InputBulkOrganisationLink(InputBulkOrganisationLinkRequired, total=False):
16+
avatar: str
17+
18+
1519
class OutputSharingProfile(TypedDict):
1620
name: str
1721
description: str
@@ -60,3 +64,9 @@ class InputUpdateOrganisation(TypedDict, total=False):
6064
observableRule: str
6165
locked: bool
6266
avatar: str
67+
68+
69+
class OutputOrganisationLink(TypedDict):
70+
linkType: str
71+
otherLinkType: str
72+
organisation: OutputOrganisation

0 commit comments

Comments
 (0)