The login cache is shared by every mdrs process, but the refresh was guarded by a lock that only reaches inside one. Concurrent runs each sent the same refresh token, and a provider that rotates them accepts the first and refuses the rest. - hold a lock that spans processes across the whole read-refresh- write, checking cheaply first so ordinary requests never take it - write the cache through a temporary file: opening it for writing truncates it, and a reader landing in that window found it empty and threw the session away - take the lock for every write, not just the refresh, so a login running beside one cannot be silently reverted - wait for a busy lock on Windows rather than giving up after the ten attempts msvcrt allows, and retry the rename it refuses while a reader still holds the file open - bound the refresh request on its own, so a provider that goes quiet cannot hold the lock indefinitely - accept bare hostnames such as localhost, store URLs without the trailing slash, and join download paths through one helper
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
from typing import Final
|
|
|
|
import requests
|
|
from pydantic import TypeAdapter
|
|
from pydantic.dataclasses import dataclass
|
|
|
|
from mdrsclient.api.base import BaseApi
|
|
from mdrsclient.exceptions import UnauthorizedException
|
|
from mdrsclient.models import Token, User
|
|
|
|
# (connect, read) seconds for the token refresh.
|
|
TOKEN_REFRESH_TIMEOUT: Final[tuple[float, float]] = (5.0, 30.0)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UsersCurrentResponseLaboratory:
|
|
id: int
|
|
name: str
|
|
role: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UsersApiCurrentResponse:
|
|
id: int
|
|
username: str
|
|
first_name: str
|
|
last_name: str
|
|
email: str
|
|
orcid_id: str
|
|
laboratories: list[UsersCurrentResponseLaboratory]
|
|
is_staff: bool
|
|
is_active: bool
|
|
is_superuser: bool
|
|
is_reviewer: bool
|
|
last_login: str # ISO8601
|
|
date_joined: str # ISO8601
|
|
|
|
|
|
class UsersApi(BaseApi):
|
|
ENTRYPOINT: Final[str] = "v3/users/"
|
|
|
|
def current(self) -> User:
|
|
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
|
|
url = self.ENTRYPOINT + "current/"
|
|
response = self.connection.get(url)
|
|
self._raise_response_error(response)
|
|
obj = TypeAdapter(UsersApiCurrentResponse).validate_python(response.json())
|
|
laboratory_ids = list(map(lambda x: x.id, obj.laboratories))
|
|
user = User(id=obj.id, username=obj.username, laboratory_ids=laboratory_ids, is_reviewer=obj.is_reviewer)
|
|
return user
|
|
|
|
def token(self, username: str, password: str) -> Token:
|
|
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
|
|
url = self.ENTRYPOINT + "token/"
|
|
data: dict[str, str | int] = {"username": username, "password": password}
|
|
response = self.connection.post(url, data=data)
|
|
if response.status_code == requests.codes.unauthorized:
|
|
raise UnauthorizedException("Invalid username or password.")
|
|
self._raise_response_error(response)
|
|
token = TypeAdapter(Token).validate_python(response.json())
|
|
return token
|
|
|
|
def tokenRefresh(self, token: Token) -> Token:
|
|
# print(self.__class__.__name__ + "::" + sys._getframe().f_code.co_name)
|
|
url = self.ENTRYPOINT + "token/refresh/"
|
|
data: dict[str, str | int] = {"refresh": token.refresh}
|
|
# Bounded on its own: the caller holds a lock that spans processes while this runs,
|
|
# so a provider that accepts the connection and then goes quiet would otherwise
|
|
# stall every other request on this machine rather than just this one.
|
|
response = self.connection.post(url, data=data, timeout=TOKEN_REFRESH_TIMEOUT)
|
|
if response.status_code == requests.codes.unauthorized:
|
|
raise UnauthorizedException("Token is invalid or expired.")
|
|
self._raise_response_error(response)
|
|
token = TypeAdapter(Token).validate_python(response.json())
|
|
return token
|