Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user rights in token payload #68

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ repos:
additional_dependencies: [flake8-isort]

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.309
rev: v1.1.329
hooks:
- id: pyright
exclude: "tests/"
Expand Down
28 changes: 0 additions & 28 deletions pyright_errors
Original file line number Diff line number Diff line change
@@ -1,28 +0,0 @@
No configuration file found.
pyproject.toml file found at /home/sbkubric/workspace/movix/projects/movix-auth.
Loading pyproject.toml file at /home/sbkubric/workspace/movix/projects/movix-auth/pyproject.toml
Assuming Python version 3.10
Assuming Python platform Linux
Auto-excluding **/node_modules
Auto-excluding **/__pycache__
Auto-excluding **/.*
stubPath /home/sbkubric/workspace/movix/projects/movix-auth/typings is not a valid directory.
Searching for source files
File or directory "/home/sbkubric/workspace/movix/projects/movix-auth/2" does not exist.
Found 15 source files
pyright 1.1.281
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/oauth.py
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/oauth.py:147:43 - error: Argument of type "UOAP@get_oauth_router" cannot be assigned to parameter "user" of type "UP@get_oauth_router" in function "on_after_login"
  Type "UOAP@get_oauth_router" cannot be assigned to type "UP@get_oauth_router" (reportGeneralTypeIssues)
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/roles.py
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/roles.py:55:13 - error: Expected 1 positional argument (reportGeneralTypeIssues)
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:1:1 - error: Expected expression
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:2:1 - error: Expected expression
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:4:1 - error: Expected expression
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:21:1 - error: Expected expression
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:29:1 - error: Expected parameter name
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:22:21 - error: "(" was not closed
/home/sbkubric/workspace/movix/projects/movix-auth/src/api/v1/roles.py:33:1 - error: Expected expression
9 errors, 0 warnings, 0 informations
Completed in 3.29sec
14 changes: 14 additions & 0 deletions src/api/auth_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from authentication import AuthenticationBackend, Authenticator
from core.jwt_utils import SecretType
from db import models_protocol
from managers.rights import AccessRightManagerDependency
from managers.role import RoleManagerDependency
from managers.user import UserManagerDependency


Expand All @@ -39,6 +41,12 @@ def __init__(
models_protocol.OAP,
models_protocol.UOAP,
],
get_role_manager: RoleManagerDependency[
models_protocol.UP, models_protocol.RP, models_protocol.URP,
],
get_access_rights_manager: AccessRightManagerDependency[
models_protocol.ARP, models_protocol.RARP,
],
access_backends: Sequence[
AuthenticationBackend[models_protocol.UP, models_protocol.SIHE]
],
Expand All @@ -51,6 +59,8 @@ def __init__(
self.access_authenticator = Authenticator(access_backends, get_user_manager)
self.refresh_authenticator = Authenticator(refresh_backends, get_user_manager)
self.get_user_manager = get_user_manager
self.get_role_manager = get_role_manager
self.get_access_right_manager = get_access_rights_manager
self.current_user = self.access_authenticator.current_user
self.auth_current_user = self.refresh_authenticator.current_user
self.user_channels_schema = user_channels_schema
Expand Down Expand Up @@ -92,6 +102,8 @@ def return_auth_router(
access_backend,
refresh_backend,
self.get_user_manager,
self.get_role_manager,
self.get_access_right_manager,
self.access_authenticator,
self.refresh_authenticator,
requires_verification,
Expand Down Expand Up @@ -162,6 +174,8 @@ def return_oauth_router(
oauth_client,
backend,
self.get_user_manager,
self.get_role_manager,
self.get_access_right_manager,
state_secret,
redirect_url,
associate_by_email,
Expand Down
2 changes: 2 additions & 0 deletions src/api/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

api_users = APIUsers[models.UserRead, models.EventRead](
get_user_manager,
get_role_manager,
get_access_right_manager,
[access_backend],
[refresh_backend],
schemas.UserChannels,
Expand Down
24 changes: 22 additions & 2 deletions src/api/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
from httpx_oauth.oauth2 import BaseOAuth2, OAuth2Token
from pydantic import BaseModel

from api.v1.common import ErrorCode, ErrorModel
from api.v1.common import ErrorCode, ErrorModel, _get_user_rigths
from authentication import AuthenticationBackend, Strategy
from core.exceptions import UserAlreadyExists
from core.jwt_utils import SecretType, decode_jwt, generate_jwt
from db import models_protocol as models
from managers.rights import (
AccessRightManagerDependency,
BaseAccessRightManager,
get_access_right_manager,
)
from managers.role import BaseRoleManager, RoleManagerDependency
from managers.user import BaseUserManager, UserManagerDependency

STATE_TOKEN_AUDIENCE = "movix:oauth-state"
Expand All @@ -33,6 +39,8 @@ def get_oauth_router(
get_user_manager: UserManagerDependency[
models.UP, models.SIHE, models.OAP, models.UOAP
],
get_role_manager: RoleManagerDependency[models.UP, models.RP, models.URP],
get_access_rights_manager: AccessRightManagerDependency[models.ARP, models.RARP],
state_secret: SecretType,
redirect_url: str | None = None,
associate_by_email: bool = False,
Expand Down Expand Up @@ -103,6 +111,12 @@ async def callback( # type: ignore
user_manager: BaseUserManager[
models.UP, models.SIHE, models.OAP, models.UOAP
] = Depends(get_user_manager),
role_manager: BaseRoleManager[models.UP, models.RP, models.URP] = Depends(
get_role_manager
),
access_right_manager: BaseAccessRightManager[models.ARP, models.RARP] = Depends(
get_access_right_manager
),
strategy: Strategy[models.UP, models.SIHE] = Depends(backend.get_strategy),
):
token, state = access_token_state
Expand Down Expand Up @@ -144,8 +158,14 @@ async def callback( # type: ignore
detail=ErrorCode.LOGIN_BAD_CREDENTIALS,
)
user = cast(models.UP, user)
access_rights_ids = [
right.id
for right in await _get_user_rigths(
user.id, role_manager, access_right_manager
)
]
# Authenticate
response = await backend.login(strategy, user)
response = await backend.login(strategy, user, access_rights_ids)
await user_manager.on_after_login(user, request, response)
return response

Expand Down
27 changes: 24 additions & 3 deletions src/api/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm

from api.v1.common import ErrorCode, ErrorModel
from api.v1.common import ErrorCode, ErrorModel, _get_user_rigths
from authentication import AuthenticationBackend, Authenticator, Strategy
from core.logger import logger
from db import models_protocol
from managers.rights import AccessRightManagerDependency, BaseAccessRightManager
from managers.role import BaseRoleManager, RoleManagerDependency
from managers.user import BaseUserManager, UserManagerDependency
from openapi import OpenAPIResponseType
from rate_limiter import RateLimiter, RateLimitTime
Expand All @@ -23,6 +25,12 @@ def get_auth_router( # noqa: C901
models_protocol.OAP,
models_protocol.UOAP,
],
get_role_manager: RoleManagerDependency[
models_protocol.UP, models_protocol.RP, models_protocol.URP
],
get_access_right_manager: AccessRightManagerDependency[
models_protocol.ARP, models_protocol.RARP
],
access_authenticator: Authenticator[models_protocol.UP, models_protocol.SIHE],
refresh_authenticator: Authenticator[models_protocol.UP, models_protocol.SIHE],
requires_verification: bool = False,
Expand Down Expand Up @@ -78,7 +86,8 @@ async def login( # pyright: ignore
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorCode.LOGIN_BAD_CREDENTIALS,
)
response = await refresh_backend.login(strategy, user)

response = await refresh_backend.login(strategy, user, [])
await user_manager.on_after_login(user, request, response)
logging.info("success:%s" % user.id)
return response
Expand All @@ -100,6 +109,12 @@ async def refresh( # pyright: ignore
strategy: Strategy[models_protocol.UP, models_protocol.SIHE] = Depends(
access_backend.get_strategy
),
role_manager: BaseRoleManager[
models_protocol.UP, models_protocol.RP, models_protocol.URP
] = Depends(get_role_manager),
access_right_manager: BaseAccessRightManager[
models_protocol.ARP, models_protocol.RARP
] = Depends(get_access_right_manager),
):
if not user_token:
logging.exception("BAD_TOKEN:%s" % user_token)
Expand All @@ -108,8 +123,14 @@ async def refresh( # pyright: ignore
detail=ErrorCode.REFRESH_BAD_TOKEN,
)
user, _ = user_token
access_rights_ids = [
right.id
for right in await _get_user_rigths(
user.id, role_manager, access_right_manager
)
]

response = await access_backend.login(strategy, user)
response = await access_backend.login(strategy, user, access_rights_ids)
logging.info("success:%s" % user.id)
return response

Expand Down
15 changes: 14 additions & 1 deletion src/api/v1/common.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from enum import Enum
from typing import Union
from typing import Iterable, Union
from uuid import UUID

from pydantic import BaseModel

from db import models_protocol


class ErrorModel(BaseModel):
detail: Union[str, dict[str, str]]
Expand Down Expand Up @@ -40,3 +43,13 @@ class ErrorCode(str, Enum):
# Access rights
UPDATE_ACCESS_NAME_ALREADY_EXISTS = "UPDATE_ACCESS_NAME_ALREADY_EXISTS"
ACCESS_IS_NOT_EXISTS = "ACCESS_DOES_NOT_EXIST"


async def _get_user_rigths(
user_id: UUID, role_manager, access_right_manager
) -> Iterable[models_protocol.AccessRightProtocol]:
roles_list = await role_manager.get_user_roles(user_id)
role_ids = [role.role_id for role in roles_list]
user_rights = await access_right_manager.get_roles_access_rights(role_ids)

return user_rights
8 changes: 4 additions & 4 deletions src/api/v1/rights.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,8 @@ async def remove_role_access( # pyright: ignore
)

@router.get(
"/roles/{roles_id}/rights",
response_model=list[schemas.AR],
"/roles/{role_id}/rights",
response_model=list[role_access_right_schema],
summary="List the role's access right",
description="Get list the role's access right",
response_description="Message entity",
Expand All @@ -351,14 +351,14 @@ async def get_role_rights( # pyright: ignore
role_manager: BaseRoleManager[
models_protocol.UP, models_protocol.RP, models_protocol.URP
] = Depends(get_role_manager),
) -> list[schemas.AR]:
) -> list[role_access_right_schema]:
try:
role = await role_manager.get(role_id)

rights = await access_right_manager.get_role_access_rights(role.id)

logging.info("success:%s" % role_id)
return list(access_right_schema.from_orm(right) for right in rights)
return list(role_access_right_schema.from_orm(right) for right in rights)

except exceptions.RoleNotExists:
logging.exception("RoleNotExists:%s" % role_id)
Expand Down
2 changes: 1 addition & 1 deletion src/api/v1/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ async def remove_user_role( # pyright: ignore

@router.get(
"/users/{user_id}/roles",
response_model=list[UUID],
response_model=list[user_role_schema],
summary="List the user's roles",
description="Get list the user's roles",
dependencies=[
Expand Down
6 changes: 4 additions & 2 deletions src/authentication/backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Generic
from typing import Generic, Iterable
from uuid import UUID

from fastapi import Response, status

Expand Down Expand Up @@ -39,8 +40,9 @@ async def login(
self,
strategy: Strategy[models_protocol.UP, models_protocol.SIHE],
user: models_protocol.UP,
access_right_ids: Iterable[UUID],
) -> Response:
token = await strategy.write_token(user)
token = await strategy.write_token(user, access_right_ids)
return await self.transport.get_login_response(token)

async def logout(
Expand Down
7 changes: 5 additions & 2 deletions src/authentication/strategy/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Protocol
from typing import Iterable, Protocol
from uuid import UUID

from db import models_protocol
from managers.user import BaseUserManager
Expand All @@ -21,7 +22,9 @@ async def read_token(
) -> models_protocol.UP | None:
...

async def write_token(self, user: models_protocol.UP) -> str:
async def write_token(
self, user: models_protocol.UP, access_right_ids: Iterable[UUID]
) -> str:
...

async def destroy_token(self, token: str, user: models_protocol.UP) -> None:
Expand Down
19 changes: 17 additions & 2 deletions src/authentication/strategy/jwt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from typing import Generic
from typing import Generic, Iterable
from uuid import UUID

import jwt

Expand Down Expand Up @@ -67,7 +68,9 @@ async def read_token(
except (exceptions.UserNotExists, exceptions.InvalidID):
return None

async def write_token(self, user: models_protocol.UP) -> str:
async def write_token(
self, user: models_protocol.UP, access_right_ids: Iterable[UUID]
) -> str:
data = {"sub": str(user.id), "aud": self.token_audience}
return generate_jwt(
data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm
Expand Down Expand Up @@ -97,6 +100,18 @@ def __init__(
secret, lifetime_seconds, token_audience, algorithm, public_key
)

async def write_token(
self, user: models_protocol.UP, access_right_ids: Iterable[UUID]
) -> str:
data = {
"sub": str(user.id),
"aud": self.token_audience,
"acr": [str(id) for id in access_right_ids],
}
return generate_jwt(
data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm
)

async def read_token(
self,
token: str | None,
Expand Down
2 changes: 1 addition & 1 deletion src/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Settings(BaseSettings):

# Настройки PSQL
pghost: str = "localhost"
pgport: str = "5434"
pgport: str = "5432"
pgdb: str = "yamp_movies_db"
pguser: str = "yamp_dummy"
pgpassword: str = "qweasd123"
Expand Down
Loading