Skip to content

Commit

Permalink
Release 5.11.13
Browse files Browse the repository at this point in the history
### Changelog:
- Feature(backend): Add support methods for test cases for login and logout.
- Feature(backend): Provide overriding ``UserWrapper`` class.
- Fix(backend): Default OID client id and other stuff from settings.
- Fix(backend): Performance improvement for bulk requests on JWT auth.
- Chore(deps): Bump cross-spawn from 7.0.3 to 7.0.6.
  • Loading branch information
onegreyonewhite committed Nov 19, 2024
1 parent 2820532 commit f788793
Show file tree
Hide file tree
Showing 13 changed files with 56 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ max-attributes=7
min-public-methods=2

# Maximum number of public methods for a class (see R0904).
max-public-methods=25
max-public-methods=30

# Maximum number of boolean expressions in a if statement
max-bool-expr=5
Expand Down
7 changes: 7 additions & 0 deletions test_src/test_proj/oauth_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from vstutils.oauth2.user import UserWrapper

class OAuthUser(UserWrapper):
def get_profile_claims(self) -> dict:
claims = super().get_profile_claims()
claims['test_value'] = 'test_value'
return claims
2 changes: 2 additions & 0 deletions test_src/test_proj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,5 @@
'allowed_redirect_uris': 'https://some-app.com/auth-callback',
'allowed_response_types': ['code'],
}

OAUTH_SERVER_USER_WRAPPER = 'test_proj.oauth_user.OAuthUser'
16 changes: 9 additions & 7 deletions test_src/test_proj/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ def test_users_api(self):
password2='12345'
)
user_get_request = {"method": "get", "path": ['user', 'profile']}
self._login()
self.login_user()
results = self.bulk([
{"method": "post", "path": ['user', 'profile', 'change_password'], "data": i}
for i in (invalid_old_password, not_identical_passwords, update_password)
Expand All @@ -951,7 +951,7 @@ def test_users_api(self):
self.assertEqual(results[0]['status'], 200)
self.assertEqual(results[0]['data']['username'], self.user.username)

self._logout(self.client)
self.logout_user()

self.change_identity(True)
data = [
Expand Down Expand Up @@ -1501,7 +1501,7 @@ def test_bulk(self):
headers={'accept-language': 'ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,es;q=0.6'},
relogin=False
)
self._logout(self.client)
self.logout_user()

self.assertEqual(result[0]['status'], 204)
self.assertEqual(result[1]['status'], 404)
Expand Down Expand Up @@ -1602,7 +1602,7 @@ def test_get_openapi(self):
del expected['some_barcode128']
self.assertDictEqual(expected, from_api)
# Test swagger ui
client = self._login()
client = self.login_user()
response = client.get('/api/endpoint/')
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'drf-yasg/swagger-ui.html')
Expand Down Expand Up @@ -2416,7 +2416,7 @@ def test_etag(self):
response3 = client.get('/api/endpoint/?format=openapi&version=v2', headers=headers)
self.assertEqual(response3.status_code, 200)

self._login()
self.login_user()
headers['Cookie'] = f'{settings.SESSION_COOKIE_NAME}={self.client.session.session_key}; lang=en'
response4 = client.get('/api/endpoint/?format=openapi', headers=headers)
self.assertEqual(response4.status_code, 200, response4.content)
Expand Down Expand Up @@ -3414,7 +3414,7 @@ def test_user_language_detection(self):
self.assertEqual(to_soup(response.content).html['lang'], 'en')

def test_server_translation(self):
self._logout(self.client)
self.logout_user()
bulk_data = [
dict(path=['_lang', 'ru'], method='get'),
]
Expand Down Expand Up @@ -5893,7 +5893,7 @@ class WebSocketTestCase(BaseTestCase):
def setUp(self):
super().setUp()

self.client = self._login()
self.client = self.login_user()
self.cookie = f"{self.client.cookies.output(header='', sep='; ').strip()};"
self.cookie += f"csrftoken={_get_new_csrf_token()}"

Expand Down Expand Up @@ -7087,6 +7087,7 @@ def test_anon_user_login(self):
self.assertDictEqual(response.json(), {
"sub": access_token_jwt['sub'],
"anon": True,
"test_value": "test_value",
})

def test_schema(self):
Expand Down Expand Up @@ -7319,6 +7320,7 @@ def test_userinfo(self):
"family_name": "Doe",
"preferred_username": "j.doe",
"email": "[email protected]",
"test_value": "test_value",
})

def test_auth_with_scope(self):
Expand Down
2 changes: 1 addition & 1 deletion vstutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# pylint: disable=django-not-available
__version__: str = '5.11.12'
__version__: str = '5.11.13'
7 changes: 6 additions & 1 deletion vstutils/api/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def request_handler(self, request: HttpRequest) -> HttpRequest:
request.user = request.META.pop('user')
# pylint: disable=protected-access
request._cached_user = request.user # type: ignore
request.auth_obj = request.META.pop('auth_obj')
if 'language' in request.META:
request.language = request.META.pop('language') # type: ignore
if 'session' in request.META:
Expand All @@ -175,6 +176,7 @@ class BulkClient(Client):
def __init__(self, enforce_csrf_checks=False, **defaults):
# pylint: disable=bad-super-call
self.user = defaults.pop('user', None)
self.auth_obj = defaults.pop('auth_obj', None)
self.language = defaults.pop('language', None)
self.session = defaults.pop('session', None)
self.notificator = defaults.pop('notificator', None)
Expand All @@ -184,6 +186,8 @@ def __init__(self, enforce_csrf_checks=False, **defaults):
def request(self, **request):
if self.user:
request['user'] = self.user
if self.auth_obj:
request['auth_obj'] = self.auth_obj
if self.language:
request['language'] = self.language
if self.session:
Expand Down Expand Up @@ -369,14 +373,15 @@ def get_client(self, request: BulkRequestType) -> BulkClient:

def original_environ_data(self, request: BulkRequestType, *args) -> _t.Dict:
get_environ = request.META.get
kwargs: _t.Dict[str, _t.Optional[_t.Any]] = {}
kwargs: _t.Dict[str, _t.Optional[_t.Any]] = {"auth_obj": None}
for env_var in tuple(self.client_environ_keys_copy) + args:
value = get_environ(env_var, None)
if value:
kwargs[env_var] = str(value)

if request.user.is_authenticated:
kwargs['user'] = request.user
kwargs['auth_obj'] = request.successful_authenticator

if cookies := get_environ('HTTP_COOKIE'):
kwargs['HTTP_COOKIE'] = str(cookies)
Expand Down
6 changes: 6 additions & 0 deletions vstutils/oauth2/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ def get_session(session_key):

class JWTBearerTokenAuthentication(BaseAuthentication):
def authenticate(self, request: "Request"):
# pylint: disable=protected-access
if getattr(request._request, "is_bulk", False) and getattr(request, "auth_obj", 0).__class__ is self.__class__:
# Hack for Bulk requests performance
return request._request.user, request.auth_obj.token

if token := _get_request_token(request):
self.token = token
request._request.session = get_session(token['jti']) # pylint: disable=protected-access
self.patch_vary(request)
return get_user(request._request), token # pylint: disable=protected-access
Expand Down
4 changes: 3 additions & 1 deletion vstutils/oauth2/authorization_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@
from django.conf import settings
from django.contrib.auth import load_backend
from django.core.cache import caches
from django.utils.module_loading import import_string

from .client import SimpleClient
from .requests import DjangoOAuth2Request
from .user import UserWrapper
from .jwk import jwk_set


_auth_code_cache = caches[settings.OAUTH_SERVER_AUTHORIZATION_CODE_CACHE_NAME]
_auth_code_ttl = 300

UserWrapper = import_string(settings.OAUTH_SERVER_USER_WRAPPER)


@dataclass
class AuthorizationCode(AuthorizationCodeMixin):
Expand Down
2 changes: 1 addition & 1 deletion vstutils/oauth2/authorization_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
from .client import query_simple_client, SimpleClient
from .jwk import jwk_set
from .requests import DjangoOAuth2Request, DjangoOAuthJsonRequest
from .user import UserWrapper

if TYPE_CHECKING: # nocv
from authlib.jose import KeySet
from django.contrib.auth.models import AbstractBaseUser


SESSION_STORE = get_session_store()
UserWrapper = import_string(settings.OAUTH_SERVER_USER_WRAPPER)
extra_claims_provider: 'Optional[Callable[[AbstractBaseUser], Optional[dict]]]' = (
import_string(settings.OAUTH_SERVER_JWT_EXTRA_CLAIMS_PROVIDER)
if settings.OAUTH_SERVER_JWT_EXTRA_CLAIMS_PROVIDER
Expand Down
2 changes: 1 addition & 1 deletion vstutils/oauth2/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
from vstutils.api.responses import HTTP_200_OK

from .authentication import JWTBearerTokenAuthentication
from .user import UserWrapper

if TYPE_CHECKING:
from authlib.oauth2.rfc6749 import AuthorizationServer # nocv

ServerClass = import_string(settings.OAUTH_SERVER_CLASS)
server: "AuthorizationServer" = ServerClass()
UserWrapper = import_string(settings.OAUTH_SERVER_USER_WRAPPER)


class Oauth2Throttle(AnonRateThrottle):
Expand Down
1 change: 1 addition & 0 deletions vstutils/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,7 @@ class OauthServerClientConfig(_t.TypedDict):
'default_redirect_uri': None,
}
}
OAUTH_SERVER_USER_WRAPPER = 'vstutils.oauth2.user.UserWrapper'

if OAUTH_SERVER_ENABLE:
if config['oauth']['server_allow_insecure']:
Expand Down
17 changes: 15 additions & 2 deletions vstutils/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ class BaseTestCase(TestCase):
server_class = import_string(settings.OAUTH_SERVER_CLASS)

#: oAuth2 client id
client_token_app_id = 'simple-client-id'
client_token_app_id = list(settings.OAUTH_SERVER_CLIENTS.keys())[0]

#: oAuth2 grant type
client_token_grant_type = 'password'
client_token_grant_type = settings.OAUTH_SERVER_CLIENTS[client_token_app_id]['allowed_grant_types'][0]

#: oAuth2 scopes
client_token_scopes = 'openid read write'
Expand Down Expand Up @@ -137,6 +137,19 @@ def generate_token_for_session(self, session: SessionBase):
}
return jwt.encode(header, payload, key=jwk_set).decode('utf-8')

def login_user(self, user=None, client=None):
client = client or self.client
client.force_login(user or self.user)
# Make OAuth2 auth over session auth
if self.client_oauth_session:
client.session.save()
client.defaults.setdefault('Sec-Fetch-Site', 'same-origin')
client.defaults['HTTP_AUTHORIZATION'] = f'Bearer {self.generate_token_for_session(client.session)}'
return client

def logout_user(self, client=None):
self._logout(client or self.client)

def _login(self):
client = self.client
client.force_login(self.user)
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1693,9 +1693,9 @@ cross-fetch@^4.0.0:
node-fetch "^2.6.12"

cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
Expand Down

0 comments on commit f788793

Please sign in to comment.