Skip to content
Snippets Groups Projects
Commit 8384a083 authored by Tim Repke's avatar Tim Repke
Browse files

improve security handling and add token handling options

parent e76d632b
No related branches found
No related tags found
1 merge request!41Master
from fastapi import APIRouter, Depends, HTTPException, status from typing import TYPE_CHECKING
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from nacsos_data.db.schemas.users import AuthToken
from sqlalchemy import select
from nacsos_data.models.users import UserModel, AuthTokenModel from nacsos_data.models.users import UserModel, AuthTokenModel
from server.util.security import get_current_active_user, auth_helper, InvalidCredentialsError from server.api.errors import NoDataForKeyError
from server.util.security import get_current_active_user, auth_helper, InvalidCredentialsError, NotAuthenticated
from server.util.logging import get_logger from server.util.logging import get_logger
from server import db_engine
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession # noqa F401
logger = get_logger('nacsos.api.route.login') logger = get_logger('nacsos.api.route.login')
router = APIRouter() router = APIRouter()
...@@ -20,11 +29,35 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( ...@@ -20,11 +29,35 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
token = await auth_helper.refresh_or_create_token(username=user.username) token = await auth_helper.refresh_or_create_token(username=user.username)
return token return token
except InvalidCredentialsError as e: except InvalidCredentialsError as e:
raise HTTPException( raise NotAuthenticated(repr(e))
status_code=status.HTTP_401_UNAUTHORIZED,
detail=repr(e),
headers={'WWW-Authenticate': 'Bearer'}, @router.put('/token/{token_id}', response_model=AuthTokenModel)
) async def refresh_token(token_id: str, current_user: UserModel = Depends(get_current_active_user)) -> AuthTokenModel:
try:
token = await auth_helper.refresh_or_create_token(token_id=token_id,
verify_username=current_user.username)
return token
except (InvalidCredentialsError, AssertionError) as e:
raise NotAuthenticated(repr(e))
@router.delete('/token/{token_id}')
async def refresh_token(token_id: str, current_user: UserModel = Depends(get_current_active_user)):
await auth_helper.clear_token_by_id(token_id=token_id,
verify_username=current_user.username)
@router.get('/my-tokens', response_model=list[AuthTokenModel])
async def read_tokens_me(current_user: UserModel = Depends(get_current_active_user)):
async with db_engine.session() as session: # type: AsyncSession
stmt = select(AuthToken) \
.where(AuthToken.username == current_user.username) \
.order_by(AuthToken.valid_till)
tokens = (await session.scalars(stmt)).all()
if tokens is None or len(tokens) == 0:
raise NoDataForKeyError('No auth token for this user (this error should not exist)')
return [AuthTokenModel.parse_obj(token.__dict__) for token in tokens]
@router.get('/me', response_model=UserModel) @router.get('/me', response_model=UserModel)
...@@ -37,11 +70,7 @@ async def logout(current_user: UserModel = Depends(get_current_active_user)): ...@@ -37,11 +70,7 @@ async def logout(current_user: UserModel = Depends(get_current_active_user)):
username = current_user.username username = current_user.username
if username is None: if username is None:
raise HTTPException( raise NotAuthenticated('RuntimeError(empty username)')
status_code=status.HTTP_401_UNAUTHORIZED,
detail='RuntimeError(empty username)',
headers={'WWW-Authenticate': 'Bearer'},
)
await auth_helper.clear_tokens_by_user(username=username) await auth_helper.clear_tokens_by_user(username=username)
......
from typing import TYPE_CHECKING
import uuid
from fastapi import APIRouter, Depends, Query, HTTPException, status as http_status from fastapi import APIRouter, Depends, Query, HTTPException, status as http_status
from nacsos_data.models.users import UserModel, UserInDBModel, UserBaseModel from nacsos_data.models.users import UserModel, UserInDBModel, UserBaseModel
from nacsos_data.util.auth import UserPermissions from nacsos_data.util.auth import UserPermissions
from nacsos_data.db.schemas import User
from nacsos_data.db.crud.users import \ from nacsos_data.db.crud.users import \
read_users, \ read_users, \
read_user_by_id, \ read_user_by_id, \
read_users_by_ids, \ read_users_by_ids, \
create_or_update_user create_or_update_user, get_password_hash
from sqlalchemy import select
from server.data import db_engine from server.data import db_engine
from server.api.errors import DataNotFoundWarning, UserNotFoundError from server.api.errors import DataNotFoundWarning, UserNotFoundError, UserPermissionError
from server.util.logging import get_logger from server.util.logging import get_logger
from server.util.security import UserPermissionChecker, get_current_active_user from server.util.security import UserPermissionChecker, get_current_active_user
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession # noqa F401
logger = get_logger('nacsos.api.route.admin.users') logger = get_logger('nacsos.api.route.admin.users')
router = APIRouter() router = APIRouter()
...@@ -62,9 +70,31 @@ async def get_users_by_ids(user_id: list[str] = Query(), ...@@ -62,9 +70,31 @@ async def get_users_by_ids(user_id: list[str] = Query(),
async def save_user(user: UserInDBModel | UserModel, current_user: UserModel = Depends(get_current_active_user)): async def save_user(user: UserInDBModel | UserModel, current_user: UserModel = Depends(get_current_active_user)):
# Users can only edit their own info, admins can edit all. # Users can only edit their own info, admins can edit all.
if user.user_id != current_user.user_id and not current_user.is_superuser: if user.user_id != current_user.user_id and not current_user.is_superuser:
raise HTTPException( raise UserPermissionError('You do not have permission to perform this action.')
status_code=http_status.HTTP_403_FORBIDDEN,
detail='You do not have permission to perform this action.',
)
return await create_or_update_user(user, engine=db_engine) return await create_or_update_user(user, engine=db_engine)
@router.put('/my-details', response_model=str)
async def save_user_self(user: UserInDBModel | UserModel,
current_user: UserModel = Depends(get_current_active_user)):
if current_user.user_id != user.user_id:
raise UserPermissionError('This is not you!')
async with db_engine.session() as session: # type: AsyncSession
user_db: User | None = (await session.scalars(select(User).where(User.user_id == user.user_id))).one_or_none()
password: str | None = getattr(user, 'password', None)
if password is not None:
user_db.password = get_password_hash(password)
user_db.email = user.email
user_db.full_name = user.full_name
user_db.affiliation = user.affiliation
user_id = str(user_db.user_id)
# save changes
await session.commit()
return user_id
...@@ -87,5 +87,5 @@ class UserPermissionChecker: ...@@ -87,5 +87,5 @@ class UserPermissionChecker:
__all__ = ['InsufficientPermissionError', 'InvalidCredentialsError', 'InsufficientPermissions', __all__ = ['InsufficientPermissionError', 'InvalidCredentialsError', 'InsufficientPermissions',
'auth_helper', 'oauth2_scheme', 'UserPermissionChecker', 'UserPermissions', 'auth_helper', 'oauth2_scheme', 'UserPermissionChecker', 'UserPermissions', 'NotAuthenticated',
'get_current_user', 'get_current_active_user', 'get_current_active_superuser'] 'get_current_user', 'get_current_active_user', 'get_current_active_superuser']
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment