bazis-users 2.3.2__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bazis_users-2.3.2 → bazis_users-2.4.0}/PKG-INFO +2 -2
- bazis_users-2.4.0/bazis/contrib/users/AGENTS.md +61 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/_version.py +3 -3
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/apps.py +4 -0
- bazis_users-2.4.0/bazis/contrib/users/bazis_manifest.toml +45 -0
- bazis_users-2.4.0/bazis/contrib/users/checks.py +48 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/routes.py +47 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/PKG-INFO +2 -2
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/SOURCES.txt +6 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/requires.txt +1 -1
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/scm_file_list.json +6 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/scm_version.json +2 -2
- bazis_users-2.4.0/docs/releases/2.4.0.md +20 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/pyproject.toml +1 -1
- bazis_users-2.4.0/tests/test_manifest.py +35 -0
- bazis_users-2.4.0/tests/test_user_access.py +113 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/.dockerignore +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/.github/workflows/build-publish.yml +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/.github/workflows/release.yml +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/.github/workflows/tests.yml +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/.github/workflows/update-licenses.yml +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/.gitignore +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/CLAUDE.md +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/README.md +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/admin_abstract.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/conf.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/locale/en/LC_MESSAGES/django.mo +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/locale/en/LC_MESSAGES/django.po +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/locale/ru/LC_MESSAGES/django.mo +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/locale/ru/LC_MESSAGES/django.po +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/middleware.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/migrations/0001_initial.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/migrations/0002_user_dt_first_login.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/migrations/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/models.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/models_abstract.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/router.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/routes_abstract.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/schemas.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/service.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/dependency_links.txt +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/bazis_users.egg-info/top_level.txt +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/Makefile +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/README.md +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/_static/pydata-custom.css +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/_static/readthedocs-custom.css +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/_templates/custom-class-template.rst +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/_templates/custom-module-template.rst +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/conf.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/index.rst +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/make.bat +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/manage.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/project.env +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/releases/2.3.0.md +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/releases/2.3.1.md +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/releases/2.3.2.md +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/requirements.txt +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/settings.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/docs/structure.rst +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/pytest.ini +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/deploy/admin.sh +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/deploy/app.sh +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/deploy/config/admin.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/deploy/config/app.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/deploy/db.sh +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/deploy/init.sh +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/admin.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/migrations/0001_initial.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/migrations/0002_alter_childentity_dt_created_and_more.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/migrations/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/models.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/router.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/entity/routes.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/manage.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/project.env +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/pyproject.toml +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/sample/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/sample/main.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/sample/router.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/sample/settings.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/sample/urls.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/sample/wsgi.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/admin.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/apps.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/migrations/0001_initial.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/migrations/0002_user_dt_first_login.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/migrations/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/models.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/users/router.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/sample/uv.lock +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/setup.cfg +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/tests/__init__.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/tests/conftest.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/tests/factories.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/tests/test_protected.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/tests/test_tokens.py +0 -0
- {bazis_users-2.3.2 → bazis_users-2.4.0}/tests/test_users.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazis-users
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: User management module for Bazis framework.
|
|
5
5
|
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
6
|
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Framework :: Django
|
|
|
17
17
|
Classifier: Framework :: FastAPI
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
-
Requires-Dist: bazis>=2.
|
|
20
|
+
Requires-Dist: bazis>=2.4.0
|
|
21
21
|
Requires-Dist: python-multipart>=0.0.18
|
|
22
22
|
Requires-Dist: PyJWT>=2.10
|
|
23
23
|
Provides-Extra: test
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# bazis-users — guide for AI agents
|
|
2
|
+
|
|
3
|
+
Users and authentication by session JWT for Bazis: the user model, the token endpoint,
|
|
4
|
+
and the route base classes that know the current user. Use it whenever an API has users;
|
|
5
|
+
bazis-permit (permissions) and bazis-authing (login flows) build on it.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
1. A project app with the user model (the package does not create the table itself):
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
# users/models.py
|
|
13
|
+
from bazis.contrib.users.models_abstract import AnonymousUserAbstract, UserAbstract
|
|
14
|
+
from bazis.core.models_abstract import JsonApiMixin, UuidMixin
|
|
15
|
+
|
|
16
|
+
class User(JsonApiMixin, UuidMixin, UserAbstract):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class AnonymousUser(AnonymousUserAbstract):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# users/apps.py
|
|
23
|
+
from bazis.contrib.users.apps import UsersConfig as UsersConfigBase
|
|
24
|
+
|
|
25
|
+
class UsersConfig(UsersConfigBase):
|
|
26
|
+
name = 'users'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
2. `BS_INSTALLED_APPS='["users", ...]'`, `BS_AUTH_USER_MODEL=users.User`,
|
|
30
|
+
`BS_AUTH_ANONYMOUS_USER_MODEL=users.models.AnonymousUser`.
|
|
31
|
+
3. Optionally register the user routes: `router.register('bazis.contrib.users.router')`
|
|
32
|
+
(`/user/`). With them a user sees and changes only himself, staff create, list and
|
|
33
|
+
delete users, and only a superuser changes `is_staff`, `is_superuser`, `is_active`.
|
|
34
|
+
Projects with bazis-permit protect users with a `PermitRouteBase` route instead.
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
- `POST BAZIS_OPENAPI_TOKEN_URL` (default `/api/openapi-token/`, OAuth2 password form)
|
|
39
|
+
returns `{"access_token": ..., "token_type": "bearer"}`; Swagger uses it.
|
|
40
|
+
- The token is read from `Authorization: Bearer`, or the query parameter or the cookie
|
|
41
|
+
named `BAZIS_AUTH_COOKIE_NAME` (default `bazis_auth`). It is an HMAC JWT signed with
|
|
42
|
+
`SECRET_KEY` with `sub` (username), `iat` and `exp` (`BAZIS_JWT_SESSION_LIFETIME`
|
|
43
|
+
seconds). A token without
|
|
44
|
+
`exp` is anonymous; an expired or invalid token is 401.
|
|
45
|
+
- `user.jwt_build()` builds a token (e.g. after a custom login).
|
|
46
|
+
|
|
47
|
+
## Routes
|
|
48
|
+
|
|
49
|
+
- `UserRouteBase` (`bazis.contrib.users.routes_abstract`): `self.inject.user` is the user
|
|
50
|
+
or the anonymous user; the filter context gets `_user`. Base class for routes that need
|
|
51
|
+
the user (bazis-permit routes extend it).
|
|
52
|
+
- `UserRequiredRouteBase`: the same, for routes that require authentication.
|
|
53
|
+
- FastAPI dependencies in `bazis.contrib.users.service`: `get_user_optional`,
|
|
54
|
+
`get_user_required`, `get_token_data`.
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
|
|
58
|
+
- A user must never be able to change his own groups, permissions or staff flags: keep
|
|
59
|
+
those fields out of the update schemas of custom user routes, or check them in
|
|
60
|
+
`hook_after_update` as `UserRouteSet` does.
|
|
61
|
+
- Check `bazis_doctor` after changing the user model (`users.E001`).
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gd67874105'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# What the package is for and how to use it, for tools and AI agents (see AGENTS.md).
|
|
2
|
+
# `import` paths and `check` ids are verified by tests/test_manifest.py.
|
|
3
|
+
|
|
4
|
+
[package]
|
|
5
|
+
name = "bazis-users"
|
|
6
|
+
summary = "users, session JWT authentication and routes that know the current user"
|
|
7
|
+
solves = [
|
|
8
|
+
"user accounts and login by username and password",
|
|
9
|
+
"authentication by JWT (header, query parameter or cookie)",
|
|
10
|
+
"routes that need the current user or require authentication",
|
|
11
|
+
]
|
|
12
|
+
requires = ["bazis"]
|
|
13
|
+
pairs_well = ["bazis-permit", "bazis-authing", "bazis-author"]
|
|
14
|
+
|
|
15
|
+
[[extension_points]]
|
|
16
|
+
name = "UserAbstract"
|
|
17
|
+
import = "bazis.contrib.users.models_abstract.UserAbstract"
|
|
18
|
+
use_when = "the user model of the project (with JsonApiMixin, UuidMixin)"
|
|
19
|
+
|
|
20
|
+
[[extension_points]]
|
|
21
|
+
name = "UsersConfig"
|
|
22
|
+
import = "bazis.contrib.users.apps.UsersConfig"
|
|
23
|
+
use_when = "the AppConfig of the project app that holds the user model"
|
|
24
|
+
|
|
25
|
+
[[extension_points]]
|
|
26
|
+
name = "UserRouteBase"
|
|
27
|
+
import = "bazis.contrib.users.routes_abstract.UserRouteBase"
|
|
28
|
+
use_when = "a route that needs the current user (self.inject.user)"
|
|
29
|
+
|
|
30
|
+
[[extension_points]]
|
|
31
|
+
name = "UserRequiredRouteBase"
|
|
32
|
+
import = "bazis.contrib.users.routes_abstract.UserRequiredRouteBase"
|
|
33
|
+
use_when = "a route that requires authentication"
|
|
34
|
+
|
|
35
|
+
[[extension_points]]
|
|
36
|
+
name = "get_user_required"
|
|
37
|
+
import = "bazis.contrib.users.service.get_user_required"
|
|
38
|
+
use_when = "a FastAPI dependency for the authenticated user in a custom endpoint"
|
|
39
|
+
|
|
40
|
+
[[pitfalls]]
|
|
41
|
+
text = "the user model must inherit UserAbstract and be set in BS_AUTH_USER_MODEL"
|
|
42
|
+
check = "users.E001"
|
|
43
|
+
|
|
44
|
+
[[pitfalls]]
|
|
45
|
+
text = "groups, permissions and is_staff must not be writable by the user himself"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2026 EcoFuture Technology Services LLC and contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Django system checks of bazis-users (see `manage.py bazis_doctor`).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from django.contrib.auth import get_user_model
|
|
20
|
+
from django.core.checks import Error, register
|
|
21
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@register()
|
|
25
|
+
def check_user_model(app_configs, **kwargs):
|
|
26
|
+
"""
|
|
27
|
+
The session tokens and the user routes need the user model of bazis-users.
|
|
28
|
+
"""
|
|
29
|
+
from .models_abstract import UserAbstract
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
user_model = get_user_model()
|
|
33
|
+
except ImproperlyConfigured as err:
|
|
34
|
+
return [
|
|
35
|
+
Error(str(err), hint='Set BS_AUTH_USER_MODEL to an installed model.', id='users.E001')
|
|
36
|
+
]
|
|
37
|
+
if not issubclass(user_model, UserAbstract):
|
|
38
|
+
return [
|
|
39
|
+
Error(
|
|
40
|
+
f'The user model {user_model._meta.label} does not inherit UserAbstract.',
|
|
41
|
+
hint=(
|
|
42
|
+
'Inherit the user model from bazis.contrib.users.models_abstract.UserAbstract '
|
|
43
|
+
'and set BS_AUTH_USER_MODEL to it.'
|
|
44
|
+
),
|
|
45
|
+
id='users.E001',
|
|
46
|
+
)
|
|
47
|
+
]
|
|
48
|
+
return []
|
|
@@ -23,6 +23,7 @@ from fastapi.security import OAuth2PasswordRequestForm
|
|
|
23
23
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
|
24
24
|
|
|
25
25
|
from bazis.core.app import app
|
|
26
|
+
from bazis.core.errors import JsonApi403Exception
|
|
26
27
|
from bazis.core.routes_abstract.initial import inject_make
|
|
27
28
|
from bazis.core.routes_abstract.jsonapi import JsonapiRouteBase
|
|
28
29
|
from bazis.core.schemas import CrudApiAction, SchemaFields, SchemaInclusion, SchemaInclusions
|
|
@@ -81,6 +82,52 @@ class UserRouteSet(JsonapiRouteBase):
|
|
|
81
82
|
),
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
#: the fields only a superuser can set
|
|
86
|
+
privileged_fields = ('is_staff', 'is_superuser', 'is_active')
|
|
87
|
+
|
|
88
|
+
def get_queryset(self):
|
|
89
|
+
"""
|
|
90
|
+
A user who is not staff sees and changes only himself.
|
|
91
|
+
"""
|
|
92
|
+
queryset = super().get_queryset()
|
|
93
|
+
if not self.inject.user.is_staff:
|
|
94
|
+
queryset = queryset.filter(pk=self.inject.user.pk)
|
|
95
|
+
return queryset
|
|
96
|
+
|
|
97
|
+
def check_privileged_fields(self, item, before: dict):
|
|
98
|
+
if self.inject.user.is_superuser:
|
|
99
|
+
return
|
|
100
|
+
for field in self.privileged_fields:
|
|
101
|
+
if getattr(item, field) != before[field]:
|
|
102
|
+
raise JsonApi403Exception(detail=str(_('Only a superuser can change %s')) % field)
|
|
103
|
+
|
|
104
|
+
def hook_before_create(self, item):
|
|
105
|
+
if not self.inject.user.is_staff:
|
|
106
|
+
raise JsonApi403Exception()
|
|
107
|
+
super().hook_before_create(item)
|
|
108
|
+
|
|
109
|
+
def hook_after_create(self, item):
|
|
110
|
+
# inside the transaction: the error rolls the creation back
|
|
111
|
+
super().hook_after_create(item)
|
|
112
|
+
self.check_privileged_fields(
|
|
113
|
+
item, {'is_staff': False, 'is_superuser': False, 'is_active': True}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def hook_before_update(self, item):
|
|
117
|
+
super().hook_before_update(item)
|
|
118
|
+
# the data is not applied yet
|
|
119
|
+
self._privileged_before = {field: getattr(item, field) for field in self.privileged_fields}
|
|
120
|
+
|
|
121
|
+
def hook_after_update(self, item):
|
|
122
|
+
# inside the transaction: the error rolls the change back
|
|
123
|
+
super().hook_after_update(item)
|
|
124
|
+
self.check_privileged_fields(item, self._privileged_before)
|
|
125
|
+
|
|
126
|
+
def destroy(self, item_id: str):
|
|
127
|
+
if not self.inject.user.is_staff:
|
|
128
|
+
raise JsonApi403Exception()
|
|
129
|
+
return super().destroy(item_id)
|
|
130
|
+
|
|
84
131
|
inclusions = {
|
|
85
132
|
CrudApiAction.RETRIEVE: SchemaInclusions(
|
|
86
133
|
origin={
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazis-users
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: User management module for Bazis framework.
|
|
5
5
|
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
6
|
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Framework :: Django
|
|
|
17
17
|
Classifier: Framework :: FastAPI
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
-
Requires-Dist: bazis>=2.
|
|
20
|
+
Requires-Dist: bazis>=2.4.0
|
|
21
21
|
Requires-Dist: python-multipart>=0.0.18
|
|
22
22
|
Requires-Dist: PyJWT>=2.10
|
|
23
23
|
Provides-Extra: test
|
|
@@ -8,10 +8,13 @@ pytest.ini
|
|
|
8
8
|
.github/workflows/release.yml
|
|
9
9
|
.github/workflows/tests.yml
|
|
10
10
|
.github/workflows/update-licenses.yml
|
|
11
|
+
bazis/contrib/users/AGENTS.md
|
|
11
12
|
bazis/contrib/users/__init__.py
|
|
12
13
|
bazis/contrib/users/_version.py
|
|
13
14
|
bazis/contrib/users/admin_abstract.py
|
|
14
15
|
bazis/contrib/users/apps.py
|
|
16
|
+
bazis/contrib/users/bazis_manifest.toml
|
|
17
|
+
bazis/contrib/users/checks.py
|
|
15
18
|
bazis/contrib/users/conf.py
|
|
16
19
|
bazis/contrib/users/middleware.py
|
|
17
20
|
bazis/contrib/users/models.py
|
|
@@ -52,6 +55,7 @@ docs/_templates/custom-module-template.rst
|
|
|
52
55
|
docs/releases/2.3.0.md
|
|
53
56
|
docs/releases/2.3.1.md
|
|
54
57
|
docs/releases/2.3.2.md
|
|
58
|
+
docs/releases/2.4.0.md
|
|
55
59
|
sample/manage.py
|
|
56
60
|
sample/project.env
|
|
57
61
|
sample/pyproject.toml
|
|
@@ -87,6 +91,8 @@ sample/users/migrations/__init__.py
|
|
|
87
91
|
tests/__init__.py
|
|
88
92
|
tests/conftest.py
|
|
89
93
|
tests/factories.py
|
|
94
|
+
tests/test_manifest.py
|
|
90
95
|
tests/test_protected.py
|
|
91
96
|
tests/test_tokens.py
|
|
97
|
+
tests/test_user_access.py
|
|
92
98
|
tests/test_users.py
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
".gitignore",
|
|
9
9
|
"CLAUDE.md",
|
|
10
10
|
"README.md",
|
|
11
|
+
"bazis/contrib/users/AGENTS.md",
|
|
11
12
|
"bazis/contrib/users/__init__.py",
|
|
12
13
|
"bazis/contrib/users/admin_abstract.py",
|
|
13
14
|
"bazis/contrib/users/apps.py",
|
|
15
|
+
"bazis/contrib/users/bazis_manifest.toml",
|
|
16
|
+
"bazis/contrib/users/checks.py",
|
|
14
17
|
"bazis/contrib/users/conf.py",
|
|
15
18
|
"bazis/contrib/users/locale/en/LC_MESSAGES/django.mo",
|
|
16
19
|
"bazis/contrib/users/locale/en/LC_MESSAGES/django.po",
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
"docs/releases/2.3.0.md",
|
|
42
45
|
"docs/releases/2.3.1.md",
|
|
43
46
|
"docs/releases/2.3.2.md",
|
|
47
|
+
"docs/releases/2.4.0.md",
|
|
44
48
|
"docs/requirements.txt",
|
|
45
49
|
"docs/settings.py",
|
|
46
50
|
"docs/structure.rst",
|
|
@@ -81,8 +85,10 @@
|
|
|
81
85
|
"tests/__init__.py",
|
|
82
86
|
"tests/conftest.py",
|
|
83
87
|
"tests/factories.py",
|
|
88
|
+
"tests/test_manifest.py",
|
|
84
89
|
"tests/test_protected.py",
|
|
85
90
|
"tests/test_tokens.py",
|
|
91
|
+
"tests/test_user_access.py",
|
|
86
92
|
"tests/test_users.py"
|
|
87
93
|
]
|
|
88
94
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# bazis-users 2.4.0
|
|
2
|
+
|
|
3
|
+
Requires Bazis 2.4.
|
|
4
|
+
|
|
5
|
+
## Security
|
|
6
|
+
|
|
7
|
+
- **Any authenticated user could take over any account through the user routes**
|
|
8
|
+
(`bazis.contrib.users.router`, `UserRouteSet`): list all users, change any user,
|
|
9
|
+
including his password (`raw_password`) and `is_staff` / `is_superuser`, and delete
|
|
10
|
+
users. Now a user who is not staff sees and changes only himself; only staff create and
|
|
11
|
+
delete users; only a superuser changes `is_staff`, `is_superuser` and `is_active`
|
|
12
|
+
(403 otherwise). Projects that let users manage other users through these routes must
|
|
13
|
+
give them staff status.
|
|
14
|
+
|
|
15
|
+
## Tools and AI agents
|
|
16
|
+
|
|
17
|
+
- The package ships `AGENTS.md` (how to set up users and authentication) and
|
|
18
|
+
`bazis_manifest.toml` for tools and AI agents (see `docs/agents.md` of Bazis).
|
|
19
|
+
- New system check `users.E001`: the user model does not inherit `UserAbstract`
|
|
20
|
+
(`manage.py bazis_doctor`).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 EcoFuture Technology Services LLC and contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from bazis.contrib.users.checks import check_user_model
|
|
16
|
+
from bazis.core.introspect import packages, validate_manifest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_manifest_is_valid():
|
|
20
|
+
assert validate_manifest('bazis.contrib.users') == []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_manifest_is_found():
|
|
24
|
+
users = {it['name']: it for it in packages()}['bazis-users']
|
|
25
|
+
assert users['module'] == 'bazis.contrib.users'
|
|
26
|
+
assert users['manifest']['package']['name'] == 'bazis-users'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_user_model_check(monkeypatch):
|
|
30
|
+
assert check_user_model(None) == []
|
|
31
|
+
|
|
32
|
+
from django.contrib.auth.models import Group
|
|
33
|
+
|
|
34
|
+
monkeypatch.setattr('bazis.contrib.users.checks.get_user_model', lambda: Group)
|
|
35
|
+
assert [it.id for it in check_user_model(None)] == ['users.E001']
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Copyright 2026 EcoFuture Technology Services LLC and contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from bazis_test_utils.utils import get_api_client
|
|
17
|
+
|
|
18
|
+
from bazis.contrib.users import get_user_model
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
User = get_user_model()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _patch(user, **attributes) -> dict:
|
|
25
|
+
return {
|
|
26
|
+
'data': {
|
|
27
|
+
'id': str(user.id),
|
|
28
|
+
'type': 'users.user',
|
|
29
|
+
'bs:action': 'change',
|
|
30
|
+
'attributes': attributes,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def people():
|
|
37
|
+
superuser = User.objects.create_superuser('root', password='weak_password_1')
|
|
38
|
+
staff = User.objects.create_user('staff', password='weak_password_2', is_staff=True)
|
|
39
|
+
user = User.objects.create_user('user', password='weak_password_3')
|
|
40
|
+
other = User.objects.create_user('other', password='weak_password_4')
|
|
41
|
+
return superuser, staff, user, other
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.django_db(transaction=True)
|
|
45
|
+
def test_user_sees_and_changes_only_himself(sample_app, people):
|
|
46
|
+
"""
|
|
47
|
+
Any authenticated user could list, change (including the password) and delete any
|
|
48
|
+
user through the user routes.
|
|
49
|
+
"""
|
|
50
|
+
_, _, user, other = people
|
|
51
|
+
client = get_api_client(sample_app, user.jwt_build())
|
|
52
|
+
|
|
53
|
+
response = client.get('/api/v1/users/user/')
|
|
54
|
+
assert [it['id'] for it in response.json()['data']] == [str(user.id)]
|
|
55
|
+
assert client.get(f'/api/v1/users/user/{other.id}/').status_code == 404
|
|
56
|
+
|
|
57
|
+
response = client.patch(
|
|
58
|
+
f'/api/v1/users/user/{other.id}/', json_data=_patch(other, raw_password='hijacked1')
|
|
59
|
+
)
|
|
60
|
+
assert response.status_code == 404
|
|
61
|
+
other.refresh_from_db()
|
|
62
|
+
assert other.check_password('weak_password_4')
|
|
63
|
+
|
|
64
|
+
response = client.patch(f'/api/v1/users/user/{user.id}/', json_data=_patch(user, first_name='Me'))
|
|
65
|
+
assert response.status_code == 200
|
|
66
|
+
|
|
67
|
+
assert client.delete(f'/api/v1/users/user/{user.id}/').status_code == 403
|
|
68
|
+
assert User.objects.filter(id=user.id).exists()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.django_db(transaction=True)
|
|
72
|
+
@pytest.mark.parametrize('field', ['is_staff', 'is_superuser', 'is_active'])
|
|
73
|
+
def test_only_superuser_changes_privileges(sample_app, people, field):
|
|
74
|
+
superuser, staff, user, _ = people
|
|
75
|
+
value = field != 'is_active'
|
|
76
|
+
|
|
77
|
+
for actor in (user, staff):
|
|
78
|
+
response = get_api_client(sample_app, actor.jwt_build()).patch(
|
|
79
|
+
f'/api/v1/users/user/{user.id}/', json_data=_patch(user, **{field: value})
|
|
80
|
+
)
|
|
81
|
+
assert response.status_code == 403
|
|
82
|
+
user.refresh_from_db()
|
|
83
|
+
assert getattr(user, field) is not value
|
|
84
|
+
|
|
85
|
+
response = get_api_client(sample_app, superuser.jwt_build()).patch(
|
|
86
|
+
f'/api/v1/users/user/{user.id}/', json_data=_patch(user, **{field: value})
|
|
87
|
+
)
|
|
88
|
+
assert response.status_code == 200
|
|
89
|
+
user.refresh_from_db()
|
|
90
|
+
assert getattr(user, field) is value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.django_db(transaction=True)
|
|
94
|
+
def test_only_staff_creates_users(sample_app, people):
|
|
95
|
+
_, staff, user, _ = people
|
|
96
|
+
payload = {
|
|
97
|
+
'data': {
|
|
98
|
+
'type': 'users.user',
|
|
99
|
+
'bs:action': 'add',
|
|
100
|
+
'attributes': {'username': 'new', 'raw_password': 'weak_password_5'},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
response = get_api_client(sample_app, user.jwt_build()).post('/api/v1/users/user/', json_data=payload)
|
|
104
|
+
assert response.status_code == 403
|
|
105
|
+
|
|
106
|
+
payload['data']['attributes']['is_superuser'] = True
|
|
107
|
+
response = get_api_client(sample_app, staff.jwt_build()).post('/api/v1/users/user/', json_data=payload)
|
|
108
|
+
assert response.status_code == 403
|
|
109
|
+
assert not User.objects.filter(username='new').exists()
|
|
110
|
+
|
|
111
|
+
del payload['data']['attributes']['is_superuser']
|
|
112
|
+
response = get_api_client(sample_app, staff.jwt_build()).post('/api/v1/users/user/', json_data=payload)
|
|
113
|
+
assert response.status_code == 201
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazis_users-2.3.2 → bazis_users-2.4.0}/bazis/contrib/users/migrations/0002_user_dt_first_login.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|