Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ jobs:
--env VK_CLIENT_ID='${{ secrets.VK_CLIENT_ID }}' \
--env VK_CLIENT_ACCESS_TOKEN='${{ secrets.VK_CLIENT_ACCESS_TOKEN }}' \
--env VK_CLIENT_SECRET='${{ secrets.VK_CLIENT_SECRET }}' \
--env AIRFLOW_AUTH_BASE_URL='${{ vars.AIRFLOW_AUTH_BASE_URL }}' \
--env AIRFLOW_AUTH_ADMIN_USERNAME='${{ secrets.AIRFLOW_AUTH_ADMIN_USERNAME }}' \
--env AIRFLOW_AUTH_ADMIN_PASSWORD='${{ secrets.AIRFLOW_AUTH_ADMIN_PASSWORD }}' \
--env CODER_AUTH_BASE_URL='${{ vars.CODER_AUTH_BASE_URL }}' \
--env CODER_AUTH_ADMIN_TOKEN='${{ secrets.CODER_AUTH_ADMIN_TOKEN }}' \
--env MAILU_AUTH_BASE_URL='${{ vars.MAILU_AUTH_BASE_URL }}' \
--env MAILU_AUTH_API_KEY='${{ secrets.MAILU_AUTH_API_KEY }}' \
--env POSTGRES_AUTH_DB_DSN='${{ secrets.POSTGRES_AUTH_DB_DSN }}' \
--env ENCRYPTION_KEY='${{ secrets.ENCRYPTION_KEY }}' \
--env KAFKA_DSN='${{ secrets.KAFKA_DSN }}' \
--env KAFKA_LOGIN='${{ secrets.KAFKA_LOGIN }}' \
Expand Down Expand Up @@ -190,6 +198,14 @@ jobs:
--env VK_CLIENT_ID='${{ secrets.VK_CLIENT_ID }}' \
--env VK_CLIENT_ACCESS_TOKEN='${{ secrets.VK_CLIENT_ACCESS_TOKEN }}' \
--env VK_CLIENT_SECRET='${{ secrets.VK_CLIENT_SECRET }}' \
--env AIRFLOW_AUTH_BASE_URL='${{ vars.AIRFLOW_AUTH_BASE_URL }}' \
--env AIRFLOW_AUTH_ADMIN_USERNAME='${{ secrets.AIRFLOW_AUTH_ADMIN_USERNAME }}' \
--env AIRFLOW_AUTH_ADMIN_PASSWORD='${{ secrets.AIRFLOW_AUTH_ADMIN_PASSWORD }}' \
--env CODER_AUTH_BASE_URL='${{ vars.CODER_AUTH_BASE_URL }}' \
--env CODER_AUTH_ADMIN_TOKEN='${{ secrets.CODER_AUTH_ADMIN_TOKEN }}' \
--env MAILU_AUTH_BASE_URL='${{ vars.MAILU_AUTH_BASE_URL }}' \
--env MAILU_AUTH_API_KEY='${{ secrets.MAILU_AUTH_API_KEY }}' \
--env POSTGRES_AUTH_DB_DSN='${{ secrets.POSTGRES_AUTH_DB_DSN }}' \
--env ENCRYPTION_KEY='${{ secrets.ENCRYPTION_KEY }}' \
--env KAFKA_DSN='${{ secrets.KAFKA_DSN }}' \
--env KAFKA_LOGIN='${{ secrets.KAFKA_LOGIN }}' \
Expand Down
35 changes: 23 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,29 @@ create-user:

create-admin:
source ./venv/bin/activate && python -m auth_backend user create --email test-admin@profcomff.com --password string
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.create --comment auth.group.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.delete --comment auth.group.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.read --comment auth.group.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.update --comment auth.group.update --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.create --comment auth.scope.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.delete --comment auth.scope.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.read --comment auth.scope.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.update --comment auth.scope.update --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.user.delete --comment auth.user.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.user.read --comment auth.user.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.user.update --comment auth.user.update --creator 1
source ./venv/bin/activate && python -m auth_backend group create --name root --scopes 1 2 3 4 5 6 7 8 9 10 11
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.create --comment auth.group.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.delete --comment auth.group.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.read --comment auth.group.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.group.update --comment auth.group.update --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.create --comment auth.scope.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.delete --comment auth.scope.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.read --comment auth.scope.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.scope.update --comment auth.scope.update --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.user.delete --comment auth.user.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.user.read --comment auth.user.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.user.update --comment auth.user.update --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.airflow_outer_auth.link.read --comment auth.airflow_outer_auth.link.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.airflow_outer_auth.link.create --comment auth.airflow_outer_auth.link.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.airflow_outer_auth.link.delete --comment auth.airflow_outer_auth.link.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.coder_outer_auth.link.read --comment auth.coder_outer_auth.link.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.coder_outer_auth.link.create --comment auth.coder_outer_auth.link.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.coder_outer_auth.link.delete --comment auth.coder_outer_auth.link.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.mailu_outer_auth.link.read --comment auth.mailu_outer_auth.link.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.mailu_outer_auth.link.create --comment auth.mailu_outer_auth.link.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.mailu_outer_auth.link.delete --comment auth.mailu_outer_auth.link.delete --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.postgres_outer_auth.link.read --comment auth.postgres_outer_auth.link.read --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.postgres_outer_auth.link.create --comment auth.postgres_outer_auth.link.create --creator 1
source ./venv/bin/activate && python -m auth_backend scope create --name auth.postgres_outer_auth.link.delete --comment auth.postgres_outer_auth.link.delete --creator 1
source ./venv/bin/activate && python -m auth_backend user_group create --user_id 1 --group_id 1

login-user:
Expand Down
4 changes: 2 additions & 2 deletions auth_backend/auth_method/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ async def user_updated(
*[m.on_user_update(new_user, old_user) for m in AuthPluginMeta.active_auth_methods()],
return_exceptions=True,
)
exceptions = [exc for exc in exceptions if exc]
if len(exceptions) > 0:
logger.error("Following errors occurred during on_user_update: ")
for exc in exceptions:
if exc:
logger.error(exc)
logger.error(exc, exc_info=exc)

@classmethod
async def on_user_update(cls, new_user: dict[str, Any], old_user: dict[str, Any] | None = None):
Expand Down
38 changes: 31 additions & 7 deletions auth_backend/auth_method/outer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from fastapi import Depends
from fastapi.exceptions import HTTPException
from fastapi_sqlalchemy import db
from starlette.status import HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
from starlette.status import HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT, HTTP_424_FAILED_DEPENDENCY

from auth_backend.auth_method.base import AuthPluginMeta
from auth_backend.base import Base
Expand Down Expand Up @@ -34,6 +34,13 @@ def __init__(self, user_id):
super().__init__(status_code=HTTP_404_NOT_FOUND, detail=f"User id={user_id} not linked")


class ConnectionIssue(HTTPException, OuterAuthException):
"""Ошибка запроса к внешнему сервису"""

def __init__(self, user_id):
super().__init__(status_code=HTTP_424_FAILED_DEPENDENCY, detail=f"User id={user_id} not linked")


class UserLinkingForbidden(HTTPException, OuterAuthException):
"""У пользователя недостаточно прав для привязки аккаунта к внешнему сервису"""

Expand All @@ -52,29 +59,38 @@ class LinkOuterAccount(Base):
class OuterAuthMeta(AuthPluginMeta, metaclass=ABCMeta):
"""Позволяет подключить внешний сервис для синхронизации пароля"""

__BASE_SCOPE: str
_BASE_SCOPE: str

def __new__(cls, *args, **kwargs):
cls._BASE_SCOPE = f"auth.{cls.get_name()}.link"
logger.info(
f"Init authmethod {cls.get_name()}, scopes: %s, %s, %s",
cls.get_scope(),
cls.post_scope(),
cls.delete_scope(),
)
return super().__new__(cls)

def __init__(self):
super().__init__()
self.router.add_api_route("/{user_id}/link", self._get_link, methods=["GET"])
self.router.add_api_route("/{user_id}/link", self._link, methods=["POST"])
self.router.add_api_route("/{user_id}/unlink", self._unlink, methods=["DELETE"])
self.__BASE_SCOPE = f"auth.{self.get_name()}.link"
self.router.add_api_route("/{user_id}/link", self._unlink, methods=["DELETE"])

@classmethod
def get_scope(cls):
"""Права, необходимые пользователю для получения данных о внешнем аккаунте"""
return cls.__BASE_SCOPE + ".read"
return cls._BASE_SCOPE + ".read"

@classmethod
def post_scope(cls):
"""Права, необходимые пользователю для создания данных о внешнем аккаунте"""
return cls.__BASE_SCOPE + ".create"
return cls._BASE_SCOPE + ".create"

@classmethod
def delete_scope(cls):
"""Права, необходимые пользователю для удаления данных о внешнем аккаунте"""
return cls.__BASE_SCOPE + ".delete"
return cls._BASE_SCOPE + ".delete"

@classmethod
@abstractmethod
Expand Down Expand Up @@ -103,34 +119,41 @@ async def on_user_update(cls, new_user: dict[str, Any], old_user: dict[str, Any]

Описания входных параметров соответствует параметрам `AuthMethodMeta.user_updated`.
"""
# logger.debug("on_user_update class=%s started, new_user=%s, old_user=%s", cls.get_name(), new_user, old_user)
if not new_user or not old_user:
# Пользователь был только что создан или удален
# Тут не будет дополнительных методов
logger.debug("%s not new_user or not old_user, closing", cls.get_name())
return

user_id = new_user.get("user_id")
password = new_user.get("email", {}).get("password")
if not password:
# В этом событии пароль не обновлялся, ничего не делаем
logger.debug("%s not password, closing", cls.get_name())
return

username = await cls.__get_username(user_id)
if not username:
# У пользователя нет имени во внешнем сервисе
logger.debug("%s not username, closing", cls.get_name())
return

if await cls._is_outer_user_exists(username.value):
logger.debug("%s user exists, changing password", cls.get_name())
await cls._update_outer_user_password(username.value, password)
else:
# Мы не нашли этого пользователя во внешнем сервисе
# Разорвем связку и кинем лог
logger.debug("%s user not exists, unlinking", cls.get_name())
username.is_deleted = True
logger.error(
"User id=%d has username %s, which can't be found in %s",
user_id,
username.value,
cls.get_name(),
)
logger.debug("on_user_update class=%s finished", cls.get_name())

@classmethod
async def _get_link(
Expand Down Expand Up @@ -184,3 +207,4 @@ async def _unlink(
if not username:
raise UserNotLinked(user_id)
username.is_deleted = True
db.session.commit()
11 changes: 11 additions & 0 deletions auth_backend/auth_plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from auth_backend.auth_method import AUTH_METHODS, AuthPluginMeta

from .airflow import AirflowOuterAuth
from .coder import CoderOuterAuth
from .email import Email
from .github import GithubAuth
from .google import GoogleAuth
from .keycloak import KeycloakAuth
from .lkmsu import LkmsuAuth
from .mailu import MailuOuterAuth
from .mymsu import MyMsuAuth
from .physics import PhysicsAuth
from .postgres import PostgresOuterAuth
from .telegram import TelegramAuth
from .vk import VkAuth
from .yandex import YandexAuth
Expand All @@ -15,7 +19,9 @@
__all__ = [
"AUTH_METHODS",
"AuthPluginMeta",
# Основной провайдер
"Email",
# Oauth провайдеры
"GoogleAuth",
"PhysicsAuth",
"LkmsuAuth",
Expand All @@ -25,4 +31,9 @@
"VkAuth",
"GithubAuth",
"KeycloakAuth",
# Провайдеры синхронизации паролей
"PostgresOuterAuth",
"CoderOuterAuth",
"AirflowOuterAuth",
"MailuOuterAuth",
]
55 changes: 55 additions & 0 deletions auth_backend/auth_plugins/airflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import logging

import aiohttp
from pydantic import AnyUrl

from auth_backend.auth_method.outer import ConnectionIssue, OuterAuthMeta
from auth_backend.settings import Settings


logger = logging.getLogger(__name__)


class AirflowOuterAuthSettings(Settings):
AIRFLOW_AUTH_BASE_URL: AnyUrl | None = None
AIRFLOW_AUTH_ADMIN_USERNAME: str | None = None
AIRFLOW_AUTH_ADMIN_PASSWORD: str | None = None


class AirflowOuterAuth(OuterAuthMeta):
prefix = '/airflow'
settings = AirflowOuterAuthSettings()

@classmethod
async def _is_outer_user_exists(cls, username: str) -> bool:
"""Проверяет наличие пользователя в Airflow"""
logger.debug("_is_outer_user_exists class=%s started", cls.get_name())
async with aiohttp.ClientSession() as session:
async with session.get(
str(cls.settings.AIRFLOW_AUTH_BASE_URL).removesuffix('/') + '/auth/fab/v1/users/' + username,
auth=aiohttp.BasicAuth(
cls.settings.AIRFLOW_AUTH_ADMIN_USERNAME, cls.settings.AIRFLOW_AUTH_ADMIN_PASSWORD
),
) as response:
if not response.ok:
raise ConnectionIssue(response.text)
res: dict[str] = await response.json()
Comment thread
dyakovri marked this conversation as resolved.
return res.get('username') == username

@classmethod
async def _update_outer_user_password(cls, username: str, password: str):
"""Устанавливает пользователю новый пароль в Airflow"""
logger.debug("_update_outer_user_password class=%s started", cls.get_name())
res = False
Comment thread
dyakovri marked this conversation as resolved.
async with aiohttp.ClientSession() as session:
async with session.patch(
str(cls.settings.AIRFLOW_AUTH_BASE_URL).removesuffix('/') + '/auth/fab/v1/users' + username,
auth=(cls.settings.AIRFLOW_AUTH_ADMIN_USERNAME, cls.settings.AIRFLOW_AUTH_ADMIN_PASSWORD),
json={'password': password},
) as response:
res = response.ok
logger.debug("_update_outer_user_password class=%s response %s", cls.get_name(), str(response.status))
if res:
logger.info("User %s updated in Airflow", username)
else:
logger.error("User %s can't be updated in Airflow. Error: %s", username, res)
52 changes: 52 additions & 0 deletions auth_backend/auth_plugins/coder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import logging

import aiohttp
from pydantic import AnyUrl

from auth_backend.auth_method.outer import ConnectionIssue, OuterAuthMeta
from auth_backend.settings import Settings


logger = logging.getLogger(__name__)


class CoderOuterAuthSettings(Settings):
CODER_AUTH_BASE_URL: AnyUrl | None = None
CODER_AUTH_ADMIN_TOKEN: str | None = None


class CoderOuterAuth(OuterAuthMeta):
prefix = '/coder'
settings = CoderOuterAuthSettings()

@classmethod
async def _is_outer_user_exists(cls, username: str) -> bool:
"""Проверяет наличие пользователя в Coder"""
logger.debug("_is_outer_user_exists class=%s started", cls.get_name())
async with aiohttp.ClientSession() as session:
async with session.get(
str(cls.settings.CODER_AUTH_BASE_URL).removesuffix('/') + '/api/v2/users/' + username,
headers={'Coder-Session-Token': cls.settings.CODER_AUTH_ADMIN_TOKEN, 'Accept': 'application/json'},
) as response:
if not response.ok:
raise ConnectionIssue(response.text)
res: dict[str] = await response.json()
return res.get('username') == username

@classmethod
async def _update_outer_user_password(cls, username: str, password: str):
"""Устанавливает пользователю новый пароль в Coder"""
logger.debug("_update_outer_user_password class=%s started", cls.get_name())
res = False
async with aiohttp.ClientSession() as session:
async with session.put(
str(cls.settings.CODER_AUTH_BASE_URL).removesuffix('/') + '/api/v2/users/' + username + '/password',
headers={'Coder-Session-Token': cls.settings.CODER_AUTH_ADMIN_TOKEN, 'Accept': 'application/json'},
json={'password': password},
) as response:
res: dict[str] = response.ok
logger.debug("_update_outer_user_password class=%s response %s", cls.get_name(), str(response.status))
if res:
logger.info("User %s updated in Coder", username)
else:
logger.error("User %s can't be updated in Coder. Error: %s", username, res)
Loading