Skip to content

Commit 337ebba

Browse files
committed
Merge branch 'main' of github.com:descope/python-sdk into return-user-response
2 parents 3189d71 + 393ac69 commit 337ebba

File tree

10 files changed

+230
-38
lines changed

10 files changed

+230
-38
lines changed

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,10 @@
44
"flake8.importStrategy": "fromEnvironment",
55
"mypy-type-checker.importStrategy": "fromEnvironment",
66
"isort.importStrategy": "fromEnvironment",
7-
"black-formatter.importStrategy": "fromEnvironment"
7+
"black-formatter.importStrategy": "fromEnvironment",
8+
"workbench.colorCustomizations": {
9+
"activityBar.background": "#4D1C3B",
10+
"titleBar.activeBackground": "#6B2752",
11+
"titleBar.activeForeground": "#FDF8FB"
12+
}
813
}

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ refresh_token = jwt_response[REFRESH_SESSION_TOKEN_NAME].get("jwt")
282282

283283
The session and refresh JWTs should be returned to the caller, and passed with every request in the session. Read more on [session validation](#session-validation)
284284

285+
#### Deleting the TOTP Seed
286+
287+
Pass the loginId to the function to remove the user's TOTP seed.
288+
289+
```python
290+
response = descope_client.mgmt.user.remove_totp_seed(login_id=login_id)
291+
```
292+
285293
### Passwords
286294

287295
The user can also authenticate with a password, though it's recommended to

descope/auth.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,34 @@ def __init__(
105105
self.public_keys = {kid: (pub_key, alg)}
106106

107107
def _raise_rate_limit_exception(self, response):
108-
resp = response.json()
109-
raise RateLimitException(
110-
resp.get("errorCode", HTTPStatus.TOO_MANY_REQUESTS),
111-
ERROR_TYPE_API_RATE_LIMIT,
112-
resp.get("errorDescription", ""),
113-
resp.get("errorMessage", ""),
114-
rate_limit_parameters={
115-
API_RATE_LIMIT_RETRY_AFTER_HEADER: int(
116-
response.headers.get(API_RATE_LIMIT_RETRY_AFTER_HEADER, 0)
117-
)
118-
},
119-
)
108+
try:
109+
resp = response.json()
110+
raise RateLimitException(
111+
resp.get("errorCode", HTTPStatus.TOO_MANY_REQUESTS),
112+
ERROR_TYPE_API_RATE_LIMIT,
113+
resp.get("errorDescription", ""),
114+
resp.get("errorMessage", ""),
115+
rate_limit_parameters={
116+
API_RATE_LIMIT_RETRY_AFTER_HEADER: self._parse_retry_after(
117+
response.headers
118+
)
119+
},
120+
)
121+
except RateLimitException:
122+
raise
123+
except Exception as e:
124+
raise RateLimitException(
125+
status_code=HTTPStatus.TOO_MANY_REQUESTS,
126+
error_type=ERROR_TYPE_API_RATE_LIMIT,
127+
error_message=ERROR_TYPE_API_RATE_LIMIT,
128+
error_description=ERROR_TYPE_API_RATE_LIMIT,
129+
)
130+
131+
def _parse_retry_after(self, headers):
132+
try:
133+
return int(headers.get(API_RATE_LIMIT_RETRY_AFTER_HEADER, 0))
134+
except (ValueError, TypeError):
135+
return 0
120136

121137
def do_get(
122138
self,

descope/management/common.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ class MgmtV1:
8080
# jwt
8181
update_jwt_path = "/v1/mgmt/jwt/update"
8282
impersonate_path = "/v1/mgmt/impersonate"
83-
mgmt_sign_in = "/v1/mgmt/auth/signin"
84-
mgmt_sign_up = "/v1/mgmt/auth/signup"
85-
mgmt_sign_up_or_in = "/v1/mgmt/auth/signup-in"
83+
mgmt_sign_in_path = "/v1/mgmt/auth/signin"
84+
mgmt_sign_up_path = "/v1/mgmt/auth/signup"
85+
mgmt_sign_up_or_in_path = "/v1/mgmt/auth/signup-in"
86+
anonymous_path = "/v1/mgmt/auth/anonymous"
8687

8788
# permission
8889
permission_create_path = "/v1/mgmt/permission/create"

descope/management/jwt.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
class JWT(AuthBase):
1515
def update_jwt(
16-
self, jwt: str, custom_claims: dict, refresh_duration: Optional[int]
16+
self, jwt: str, custom_claims: dict, refresh_duration: int = 0
1717
) -> str:
1818
"""
1919
Given a valid JWT, update it with custom claims, and update its authz claims as well
@@ -108,7 +108,7 @@ def sign_in(
108108
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "JWT is required")
109109

110110
response = self._auth.do_post(
111-
MgmtV1.mgmt_sign_in,
111+
MgmtV1.mgmt_sign_in_path,
112112
{
113113
"loginId": login_id,
114114
"stepup": login_options.stepup,
@@ -139,7 +139,7 @@ def sign_up(
139139
"""
140140

141141
return self._sign_up_internal(
142-
login_id, MgmtV1.mgmt_sign_up, user, signup_options
142+
login_id, MgmtV1.mgmt_sign_up_path, user, signup_options
143143
)
144144

145145
def sign_up_or_in(
@@ -157,7 +157,7 @@ def sign_up_or_in(
157157
signup_options (MgmtSignUpOptions): signup options.
158158
"""
159159
return self._sign_up_internal(
160-
login_id, MgmtV1.mgmt_sign_up_or_in, user, signup_options
160+
login_id, MgmtV1.mgmt_sign_up_or_in_path, user, signup_options
161161
)
162162

163163
def _sign_up_internal(
@@ -193,3 +193,30 @@ def _sign_up_internal(
193193
resp = response.json()
194194
jwt_response = self._auth.generate_jwt_response(resp, None, None)
195195
return jwt_response
196+
197+
def anonymous(
198+
self,
199+
custom_claims: Optional[dict] = None,
200+
tenant_id: Optional[str] = None,
201+
) -> dict:
202+
"""
203+
Generate a JWT for an anonymous user.
204+
205+
Args:
206+
custom_claims dict: Custom claims to add to JWT
207+
tenant_id (str): tenant id to set on DCT claim.
208+
"""
209+
210+
response = self._auth.do_post(
211+
MgmtV1.anonymous_path,
212+
{
213+
"customClaims": custom_claims,
214+
"selectedTenant": tenant_id,
215+
},
216+
pswd=self._auth.management_key,
217+
)
218+
resp = response.json()
219+
jwt_response = self._auth.generate_jwt_response(resp, None, None)
220+
del jwt_response["firstSeen"]
221+
del jwt_response["user"]
222+
return jwt_response

descope/management/user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,7 @@ def search_all(
611611
to_created_time: Optional[int] = None,
612612
from_modified_time: Optional[int] = None,
613613
to_modified_time: Optional[int] = None,
614+
user_ids: Optional[List[str]] = None,
614615
) -> dict:
615616
"""
616617
Search all users.
@@ -634,6 +635,7 @@ def search_all(
634635
to_created_time (int): Optional int, only include users who were created on or before this time (in Unix epoch milliseconds)
635636
from_modified_time (int): Optional int, only include users whose last modification/update occurred on or after this time (in Unix epoch milliseconds)
636637
to_modified_time (int): Optional int, only include users whose last modification/update occurred on or before this time (in Unix epoch milliseconds)
638+
user_ids (List[str]): Optional list of user IDs to filter by
637639
638640
Return value (dict):
639641
Return dict in the format
@@ -681,6 +683,9 @@ def search_all(
681683
if login_ids is not None:
682684
body["loginIds"] = login_ids
683685

686+
if user_ids is not None:
687+
body["userIds"] = user_ids
688+
684689
if text is not None:
685690
body["text"] = text
686691

poetry.lock

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ importlib-metadata==8.5.0 ; python_full_version >= "3.8.1" and python_version <
234234
itsdangerous==2.2.0 ; python_full_version >= "3.8.1" and python_version < "4.0" \
235235
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
236236
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
237-
Jinja2==3.1.5; python_full_version >= "3.8.1" and python_version < "4.0" \
238-
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
239-
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
237+
Jinja2==3.1.6; python_full_version >= "3.8.1" and python_version < "4.0" \
238+
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
239+
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
240240
liccheck==0.9.2 ; python_full_version >= "3.8.1" and python_version < "4.0" \
241241
--hash=sha256:15cbedd042515945fe9d58b62e0a5af2f2a7795def216f163bb35b3016a16637 \
242242
--hash=sha256:bdc2190f8e95af3c8f9c19edb784ba7d41ecb2bf9189422eae6112bf84c08cd5

tests/management/test_jwt.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ def test_update_jwt(self):
6969
timeout=DEFAULT_TIMEOUT_SECONDS,
7070
)
7171

72+
resp = client.mgmt.jwt.update_jwt("test", {"k1": "v1"})
73+
self.assertEqual(resp, "response")
74+
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.update_jwt_path}"
75+
mock_post.assert_called_with(
76+
expected_uri,
77+
headers={
78+
**common.default_headers,
79+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
80+
},
81+
json={
82+
"jwt": "test",
83+
"customClaims": {"k1": "v1"},
84+
"refreshDuration": 0,
85+
},
86+
allow_redirects=False,
87+
verify=True,
88+
params=None,
89+
timeout=DEFAULT_TIMEOUT_SECONDS,
90+
)
91+
7292
def test_impersonate(self):
7393
client = DescopeClient(
7494
self.dummy_project_id,
@@ -145,7 +165,7 @@ def test_sign_in(self):
145165
network_resp.json.return_value = json.loads("""{"jwt": "response"}""")
146166
mock_post.return_value = network_resp
147167
client.mgmt.jwt.sign_in("loginId")
148-
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.mgmt_sign_in}"
168+
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.mgmt_sign_in_path}"
149169
mock_post.assert_called_with(
150170
expected_uri,
151171
headers={
@@ -184,7 +204,7 @@ def test_sign_up(self):
184204
network_resp.json.return_value = json.loads("""{"jwt": "response"}""")
185205
mock_post.return_value = network_resp
186206
client.mgmt.jwt.sign_up("loginId")
187-
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.mgmt_sign_up}"
207+
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.mgmt_sign_up_path}"
188208
mock_post.assert_called_with(
189209
expected_uri,
190210
headers={
@@ -233,7 +253,7 @@ def test_sign_up_or_in(self):
233253
network_resp.json.return_value = json.loads("""{"jwt": "response"}""")
234254
mock_post.return_value = network_resp
235255
client.mgmt.jwt.sign_up_or_in("loginId")
236-
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.mgmt_sign_up_or_in}"
256+
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.mgmt_sign_up_or_in_path}"
237257
mock_post.assert_called_with(
238258
expected_uri,
239259
headers={
@@ -263,3 +283,32 @@ def test_sign_up_or_in(self):
263283
params=None,
264284
timeout=DEFAULT_TIMEOUT_SECONDS,
265285
)
286+
287+
def test_anonymous(self):
288+
client = DescopeClient(
289+
self.dummy_project_id,
290+
self.public_key_dict,
291+
False,
292+
self.dummy_management_key,
293+
)
294+
295+
# Test success flow
296+
with patch("requests.post") as mock_post:
297+
network_resp = mock.Mock()
298+
network_resp.ok = True
299+
network_resp.json.return_value = json.loads("""{"jwt": "response"}""")
300+
mock_post.return_value = network_resp
301+
client.mgmt.jwt.anonymous({"k1": "v1"}, "id")
302+
expected_uri = f"{common.DEFAULT_BASE_URL}{MgmtV1.anonymous_path}"
303+
mock_post.assert_called_with(
304+
expected_uri,
305+
headers={
306+
**common.default_headers,
307+
"Authorization": f"Bearer {self.dummy_project_id}:{self.dummy_management_key}",
308+
},
309+
json={"customClaims": {"k1": "v1"}, "selectedTenant": "id"},
310+
allow_redirects=False,
311+
verify=True,
312+
params=None,
313+
timeout=DEFAULT_TIMEOUT_SECONDS,
314+
)

0 commit comments

Comments
 (0)