Skip to content

Commit 5127056

Browse files
authored
Merge pull request #454 from TheHive-Project/436-review-user-endpoints
#436 - Review user endpoints
2 parents 62a3f97 + 1f7c7e4 commit 5127056

File tree

4 files changed

+223
-30
lines changed

4 files changed

+223
-30
lines changed

docs/release-notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* [#425](https://github.com/TheHive-Project/TheHive4py/issues/425) - Use TheHive v5.5.2 in integration tests by [@Kamforka](https://github.com/Kamforka) in [#433](https://github.com/TheHive-Project/TheHive4py/pull/433)
3131
* [#438](https://github.com/TheHive-Project/TheHive4py/issues/438) - Add docstrings to the `task_log` endpoints by [@Kamforka](https://github.com/Kamforka) in [#451](https://github.com/TheHive-Project/TheHive4py/pull/451)
3232

33-
## New Contributors
33+
### New Contributors
3434
* [@3lina](https://github.com/3lina) made their first contribution in [#430](https://github.com/TheHive-Project/TheHive4py/pull/430)
3535

3636
**Full Changelog**: [2.0.0b10...2.0.0b11](https://github.com/TheHive-Project/TheHive4py/compare/2.0.0b10...2.0.0b11)

tests/test_user_endpoint.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111

1212
class TestUserEndpoint:
13-
def test_get_current(self, thehive: TheHiveApi):
14-
# TODO: implement a better control for user checking
13+
14+
def test_get_current(self, thehive: TheHiveApi, test_config: TestConfig):
1515
current_user = thehive.user.get_current()
16-
assert current_user["login"] == "admin@thehive.local"
16+
assert current_user["login"] == test_config.user
1717

1818
def test_create_and_get(self, thehive: TheHiveApi):
1919
created_user = thehive.user.create(
@@ -47,11 +47,13 @@ def test_update(
4747
def test_lock_and_unlock(self, thehive: TheHiveApi, test_user: OutputUser):
4848
user_id = test_user["_id"]
4949

50-
thehive.user.lock(user_id=user_id)
50+
with pytest.deprecated_call():
51+
thehive.user.lock(user_id=user_id)
5152
locked_user = thehive.user.get(user_id=user_id)
5253
assert locked_user["locked"] is True
5354

54-
thehive.user.unlock(user_id=user_id)
55+
with pytest.deprecated_call():
56+
thehive.user.unlock(user_id=user_id)
5557
unlocked_user = thehive.user.get(user_id=user_id)
5658
assert unlocked_user["locked"] is False
5759

@@ -61,7 +63,6 @@ def test_delete(self, thehive: TheHiveApi, test_user: OutputUser):
6163
with pytest.raises(TheHiveError):
6264
thehive.user.get(user_id=user_id)
6365

64-
@pytest.mark.skip(reason="integrator container only supports a single org ")
6566
def test_set_organisations(
6667
self, test_config: TestConfig, thehive: TheHiveApi, test_user: OutputUser
6768
):
@@ -73,24 +74,41 @@ def test_set_organisations(
7374
},
7475
{
7576
"default": False,
76-
"organisation": test_config.share_org,
77-
"profile": "read-only",
77+
"organisation": test_config.admin_org,
78+
"profile": "admin",
7879
},
7980
]
8081
user_organisations = thehive.user.set_organisations(
8182
user_id=test_user["_id"], organisations=organisations
8283
)
8384
assert organisations == user_organisations
8485

85-
def test_set_password(self, thehive: TheHiveApi, test_user: OutputUser):
86+
def test_set_and_change_password(self, thehive: TheHiveApi, test_user: OutputUser):
87+
8688
assert test_user["hasPassword"] is False
8789
user_id = test_user["_id"]
8890

8991
password = "super-secruht!"
9092
thehive.user.set_password(user_id=user_id, password=password)
9193

92-
user_with_password = thehive.user.get(user_id=user_id)
93-
assert user_with_password["hasPassword"] is True
94+
# apparently the change_password endpoint only works for the current user
95+
# meaning no other user can invoke a password change for other users
96+
thehive_test_user = TheHiveApi(
97+
url=thehive.session.hive_url, username=test_user["login"], password=password
98+
)
99+
100+
user_with_password = thehive_test_user.user.get_current()
101+
assert user_with_password
102+
103+
new_password = "l4m0u|2t0uj0u|25"
104+
thehive_test_user.user.change_password(
105+
user_id=user_with_password["login"],
106+
password=new_password,
107+
current_password=password,
108+
)
109+
110+
with pytest.raises(TheHiveError, match="AuthenticationError"):
111+
thehive_test_user.user.get_current()
94112

95113
def test_renew_get_and_remove_apikey(
96114
self, thehive: TheHiveApi, test_user: OutputUser

thehive4py/endpoints/user.py

Lines changed: 172 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from typing import List, Optional
23

34
from thehive4py.endpoints._base import EndpointBase
@@ -15,70 +16,218 @@
1516

1617

1718
class UserEndpoint(EndpointBase):
19+
def get_current(self) -> OutputUser:
20+
"""Get the current session's user.
21+
22+
Returns:
23+
The current session user.
24+
"""
25+
return self._session.make_request("GET", path="/api/v1/user/current")
26+
1827
def create(self, user: InputUser) -> OutputUser:
28+
"""Create a user.
29+
30+
Args:
31+
user: The body of the user.
32+
33+
Returns:
34+
The created user.
35+
"""
1936
return self._session.make_request("POST", path="/api/v1/user", json=user)
2037

2138
def get(self, user_id: str) -> OutputUser:
39+
"""Get a user by id.
40+
41+
Args:
42+
user_id: The id of the user.
43+
44+
Returns:
45+
The user specified by the id.
46+
"""
2247
return self._session.make_request("GET", path=f"/api/v1/user/{user_id}")
2348

24-
def get_current(self) -> OutputUser:
25-
return self._session.make_request("GET", path="/api/v1/user/current")
49+
def lock(self, user_id: str) -> None:
50+
"""Lock a user.
2651
27-
def delete(self, user_id: str, organisation: Optional[str] = None) -> None:
28-
return self._session.make_request(
29-
"DELETE",
30-
path=f"/api/v1/user/{user_id}/force",
31-
params={"organisation": organisation},
52+
!!! warning
53+
Deprecated: use the generic [user.update]
54+
[thehive4py.endpoints.user.UserEndpoint.update] method
55+
to set the `locked` field to `True`
56+
57+
Args:
58+
user_id: The id of the user.
59+
60+
Returns:
61+
N/A
62+
"""
63+
warnings.warn(
64+
message=(
65+
"Deprecated: use the generic user.update method to "
66+
"set the `locked` field to True"
67+
),
68+
category=DeprecationWarning,
69+
stacklevel=2,
3270
)
71+
return self.update(user_id=user_id, fields={"locked": True})
72+
73+
def unlock(self, user_id: str) -> None:
74+
"""Unlock a user.
75+
76+
!!! warning
77+
Deprecated: use the generic [user.update]
78+
[thehive4py.endpoints.user.UserEndpoint.update] method
79+
to set the `locked` field to `False`
80+
81+
Args:
82+
user_id: The id of the user.
83+
84+
Returns:
85+
N/A
86+
"""
87+
warnings.warn(
88+
message=(
89+
"Deprecated: use the generic user.update method to "
90+
"set the `locked` field to False"
91+
),
92+
category=DeprecationWarning,
93+
stacklevel=2,
94+
)
95+
return self.update(user_id=user_id, fields={"locked": False})
3396

3497
def update(self, user_id: str, fields: InputUpdateUser) -> None:
98+
"""Update a user.
99+
100+
Args:
101+
user_id: The id of the user.
102+
fields: The fields of the user to update.
103+
104+
Returns:
105+
N/A
106+
"""
35107
return self._session.make_request(
36108
"PATCH", path=f"/api/v1/user/{user_id}", json=fields
37109
)
38110

39-
def lock(self, user_id: str) -> None:
40-
return self.update(user_id=user_id, fields={"locked": True})
111+
def delete(self, user_id: str, organisation: Optional[str] = None) -> None:
112+
"""Delete a user.
41113
42-
def unlock(self, user_id: str) -> None:
43-
return self.update(user_id=user_id, fields={"locked": False})
114+
Args:
115+
user_id: The id of the user.
116+
organisation: The organisation from which to delete the user from. Optional.
117+
118+
Returns:
119+
N/A
120+
"""
121+
return self._session.make_request(
122+
"DELETE",
123+
path=f"/api/v1/user/{user_id}/force",
124+
params={"organisation": organisation},
125+
)
44126

45127
def set_organisations(
46128
self, user_id: str, organisations: List[InputUserOrganisation]
47129
) -> List[OutputUserOrganisation]:
130+
"""Set the organisations of a user.
131+
132+
Args:
133+
user_id: The id of the user.
134+
organisations: The list of organisations to set to the user.
135+
136+
Returns:
137+
The list of the set user organisations.
138+
"""
48139
return self._session.make_request(
49140
"PUT",
50141
path=f"/api/v1/user/{user_id}/organisations",
51142
json={"organisations": organisations},
52143
)["organisations"]
53144

54145
def set_password(self, user_id: str, password: str) -> None:
146+
"""Set the password of a user.
147+
148+
Args:
149+
user_id: The id of the user.
150+
password: The new password of the user.
151+
152+
Returns:
153+
N/A
154+
"""
55155
return self._session.make_request(
56156
"POST",
57157
path=f"/api/v1/user/{user_id}/password/set",
58158
json={"password": password},
59159
)
60160

161+
def change_password(
162+
self, user_id: str, password: str, current_password: str
163+
) -> None:
164+
"""Change the password of a user.
165+
166+
Args:
167+
user_id: The id of the user.
168+
password: The new password of the user.
169+
current_password: The old password of the user.
170+
171+
Returns:
172+
N/A
173+
"""
174+
return self._session.make_request(
175+
"POST",
176+
path=f"/api/v1/user/{user_id}/password/change",
177+
json={"password": password, "currentPassword": current_password},
178+
)
179+
61180
def get_apikey(self, user_id: str) -> str:
181+
"""Get the apikey of a user.
182+
183+
Args:
184+
user_id: The id of the user.
185+
186+
Returns:
187+
The apikey of the user.
188+
"""
62189
return self._session.make_request("GET", path=f"/api/v1/user/{user_id}/key")
63190

64191
def remove_apikey(self, user_id: str) -> None:
192+
"""Remove the apikey of a user.
193+
194+
Args:
195+
user_id: The id of the user.
196+
197+
Returns:
198+
N/A
199+
"""
65200
return self._session.make_request("DELETE", path=f"/api/v1/user/{user_id}/key")
66201

67202
def renew_apikey(self, user_id: str) -> str:
203+
"""Renew the apikey of a user.
204+
205+
Args:
206+
user_id: The id of the user.
207+
208+
Returns:
209+
The renewed apikey of the user.
210+
"""
68211
return self._session.make_request(
69212
"POST", path=f"/api/v1/user/{user_id}/key/renew"
70213
)
71214

72-
def get_avatar(self, user_id: str):
73-
# TODO: implement the avatar download
74-
raise NotImplementedError()
75-
76215
def find(
77216
self,
78217
filters: Optional[FilterExpr] = None,
79218
sortby: Optional[SortExpr] = None,
80219
paginate: Optional[Paginate] = None,
81220
) -> List[OutputUser]:
221+
"""Find multiple users.
222+
223+
Args:
224+
filters: The filter expressions to apply in the query.
225+
sortby: The sort expressions to apply in the query.
226+
paginate: The pagination experssion to apply in the query.
227+
228+
Returns:
229+
The list of users matched by the query or an empty list.
230+
"""
82231
query: QueryExpr = [
83232
{"_name": "listUser"},
84233
*self._build_subquery(filters=filters, sortby=sortby, paginate=paginate),
@@ -92,6 +241,14 @@ def find(
92241
)
93242

94243
def count(self, filters: Optional[FilterExpr] = None) -> int:
244+
"""Count users.
245+
246+
Args:
247+
filters: The filter expressions to apply in the query.
248+
249+
Returns:
250+
The count of users matched by the query.
251+
"""
95252
query: QueryExpr = [
96253
{"_name": "listUser"},
97254
*self._build_subquery(filters=filters),

thehive4py/types/user.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from typing import List, TypedDict
1+
from typing import List, Literal, TypedDict
2+
3+
InputUserType = Literal["Normal", "Service"]
24

35

46
class InputUserRequired(TypedDict):
@@ -11,15 +13,30 @@ class InputUser(InputUserRequired, total=False):
1113
email: str
1214
password: str
1315
organisation: str
14-
type: str
16+
type: InputUserType
17+
18+
19+
class OrganisationLinkRequired(TypedDict):
20+
toOrganisation: str
21+
linkType: str
22+
otherLinkType: str
23+
24+
25+
class OrganisationLink(OrganisationLinkRequired, total=False):
26+
avatar: str
1527

1628

17-
class OutputOrganisationProfile(TypedDict):
29+
class OutputOrganisationProfileRequired(TypedDict):
1830
organisationId: str
1931
organisation: str
2032
profile: str
2133

2234

35+
class OutputOrganisationProfile(OutputOrganisationProfileRequired, total=False):
36+
avatar: str
37+
links: List[OrganisationLink]
38+
39+
2340
class OutputUserRequired(TypedDict):
2441
_id: str
2542
_createdBy: str
@@ -54,6 +71,7 @@ class InputUpdateUser(TypedDict, total=False):
5471
avatar: str
5572
email: str
5673
defaultOrganisation: str
74+
type: InputUserType
5775

5876

5977
class InputUserOrganisationRequired(TypedDict):

0 commit comments

Comments
 (0)