Skip to content

Commit 9606ac2

Browse files
authored
User on_update method (#182)
## Изменения Колбэк в включенные методы аутентификации об изменениях в пользователе ## Детали реализации - Добавил метод user_updated, который сообщает аутх методам об изменении пользователя - Добавил метод on_user_update, который вызывается в каждом аутх методе, когда хотя бы в одном пользователь поменялся
1 parent 57eedd0 commit 9606ac2

15 files changed

Lines changed: 287 additions & 75 deletions

File tree

auth_backend/auth_plugins/auth_method.py

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
import re
66
import string
77
from abc import ABCMeta, abstractmethod
8+
from asyncio import gather
89
from datetime import datetime
9-
from typing import Any, final
10+
from typing import Annotated, Any, Iterable, final
1011

12+
from annotated_types import MinLen
1113
from event_schema.auth import UserLogin, UserLoginKey
1214
from fastapi import APIRouter, Depends
1315
from fastapi_sqlalchemy import db
14-
from pydantic import constr
1516
from sqlalchemy.orm import Session as DbSession
1617

1718
from auth_backend.base import Base
18-
from auth_backend.exceptions import AlreadyExists, LastAuthMethodDelete
19+
from auth_backend.exceptions import LastAuthMethodDelete
1920
from auth_backend.models.db import AuthMethod, User, UserSession
2021
from auth_backend.schemas.types.scopes import Scope as TypeScope
2122
from auth_backend.settings import get_settings
@@ -32,7 +33,7 @@ def random_string(length: int = 32) -> str:
3233

3334

3435
class Session(Base):
35-
token: constr(min_length=1)
36+
token: Annotated[str, MinLen(1)]
3637
expires: datetime
3738
id: int
3839
user_id: int
@@ -128,6 +129,75 @@ async def _get_user(
128129
async def _convert_data_to_userdata_format(cls, data: Any) -> UserLogin:
129130
raise NotImplementedError()
130131

132+
@staticmethod
133+
async def user_updated(
134+
new_user: dict[str, Any] | None,
135+
old_user: dict[str, Any] | None = None,
136+
):
137+
"""Сообщить всем активированным провайдерам авторизации об обновлении пользователя
138+
139+
Каждый AuthMethod должен вызывать эту функцию при создании или изменении пользователя, но
140+
не более одного раза на один запрос пользователя на изменение. При вызове во всех
141+
активированных (включенных в настройках) AuthMethod выполняется функция on_user_update.
142+
143+
## Diff-пользователя
144+
`new_user` и `old_user` – словари, представляющие изменения в данных пользователя.
145+
146+
Если `new_user` равен `None`, то пользователь был удален. Если `old_user` равен `None`, то
147+
пользователь был создан. В остальных случаях словарь, в котором обязательно есть ключ
148+
`user_id`.
149+
150+
Словарь может содержать ключи с названиями AuthMethod, в которых данные изменились. В
151+
значениях будут находиться словари с ключами `param` и значениями `value` параметров
152+
AuthMethod.
153+
154+
### Примеры:
155+
156+
Пользователь id=1 был удален, вместе с ним были удалены параметры email метода Email и
157+
user_id метода GitHub.
158+
```python
159+
new_user = None
160+
old_user = {'user_id': 1, "email": {"email": "user@example.com"}, "github": {"user_id": "123"}}
161+
```
162+
163+
Пользователь id=2 сменил пароль.
164+
```python
165+
new_user = {
166+
"user_id": 2,
167+
"email": {"hashed_password": "somerandomshit", "salt": "blahblah"}
168+
}
169+
old_user = {
170+
"user_id": 2,
171+
"email": {"hashed_password": "tihsmodnaremos", "salt": "abracadabra", "password": "plain_password"}
172+
}
173+
```
174+
"""
175+
exceptions = await gather(
176+
*[m.on_user_update(new_user, old_user) for m in AuthMethodMeta.active_auth_methods()],
177+
return_exceptions=True,
178+
)
179+
if len(exceptions) > 0:
180+
logger.error("Following errors occurred during on_user_update: ")
181+
for exc in exceptions:
182+
logger.error(exc)
183+
184+
@staticmethod
185+
async def on_user_update(new_user: dict[str, Any], old_user: dict[str, Any] | None = None):
186+
"""Произвести действия на обновление пользователя, в т.ч. обновление в других провайдерах
187+
188+
Описания входных параметров соответствует параметрам `AuthMethodMeta.user_updated`.
189+
"""
190+
191+
@classmethod
192+
def is_active(cls):
193+
return settings.ENABLED_AUTH_METHODS is None or cls.get_name() in settings.ENABLED_AUTH_METHODS
194+
195+
@staticmethod
196+
def active_auth_methods() -> Iterable[type['AuthMethodMeta']]:
197+
for method in AUTH_METHODS.values():
198+
if method.is_active():
199+
yield method
200+
131201

132202
class OauthMeta(AuthMethodMeta):
133203
"""Абстрактная авторизация и аутентификация через OAuth"""
@@ -156,7 +226,11 @@ async def _auth_url(*args, **kwargs) -> UrlSchema:
156226
@classmethod
157227
async def _unregister(cls, user_session: UserSession = Depends(UnionAuth(scopes=[], auto_error=True))):
158228
"""Отключает для пользователя метод входа"""
159-
await cls._delete_auth_methods(user_session.user, db_session=db.session)
229+
old_user = {"user_id": user_session.user.id}
230+
new_user = {"user_id": user_session.user.id}
231+
old_user_params = await cls._delete_auth_methods(user_session.user, db_session=db.session)
232+
old_user[cls.get_name()] = old_user_params
233+
await AuthMethodMeta.user_updated(new_user, old_user)
160234
return None
161235

162236
@classmethod
@@ -175,9 +249,9 @@ async def _get_user(cls, key: str, value: str | int, *, db_session: DbSession) -
175249
return auth_method.user
176250

177251
@classmethod
178-
async def _register_auth_method(cls, key: str, value: str | int, user: User, *, db_session):
252+
async def _register_auth_method(cls, key: str, value: str | int, user: User, *, db_session) -> AuthMethod:
179253
"""Добавление пользователю новый AuthMethod"""
180-
AuthMethod.create(
254+
return AuthMethod.create(
181255
user_id=user.id,
182256
auth_method=cls.get_name(),
183257
param=key,
@@ -186,9 +260,9 @@ async def _register_auth_method(cls, key: str, value: str | int, user: User, *,
186260
)
187261

188262
@classmethod
189-
async def _delete_auth_methods(cls, user: User, *, db_session):
263+
async def _delete_auth_methods(cls, user: User, *, db_session) -> list[AuthMethod]:
190264
"""Удаляет пользователю все AuthMethod конкретной авторизации"""
191-
auth_methods = (
265+
auth_methods: list[AuthMethod] = (
192266
AuthMethod.query(session=db_session)
193267
.filter(
194268
AuthMethod.user_id == user.id,
@@ -203,6 +277,7 @@ async def _delete_auth_methods(cls, user: User, *, db_session):
203277
for method in auth_methods:
204278
method.is_deleted = True
205279
db_session.flush()
280+
return {m.param: m.value for m in auth_methods}
206281

207282
@classmethod
208283
def userdata_process_empty_strings(cls, userdata: UserLogin) -> UserLogin:

0 commit comments

Comments
 (0)