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

Merge branch 'master' into 'production'

New auth behaviour

See merge request !26
parents 1eff506c a76f3477
No related branches found
No related tags found
1 merge request!26New auth behaviour
Pipeline #1151 passed
......@@ -20,39 +20,39 @@ before_script:
- echo "password ${CI_JOB_TOKEN}" >> ~/.netrc
stages:
# - build
# - test
- build
- test
- deploy
#build-job:
# stage: build
# image: python:3.10.9
# script:
# - python -V
# - pip install virtualenv
# - virtualenv venv
# - source venv/bin/activate
# - pwd
# - ls -lisah
# - git config --global url."https://gitlab.pik-potsdam.de/".insteadOf "ssh://git@gitlab.pik-potsdam.de/"
# - pip install -r requirements.txt
# - pip install -r requirements_dev.txt
#
#test-job1:
# stage: test
# image: python:3.10.9
# script:
# - source venv/bin/activate
# - flake8 --config .flake8
#
#test-job2:
# stage: test
# image: python:3.10.9
# script:
# - source venv/bin/activate
# - which python
# - pip freeze
# - python -m mypy --config-file=pyproject.toml server
build-job:
stage: build
image: python:3.10.9
script:
- python -V
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate
- pwd
- ls -lisah
- git config --global url."https://gitlab.pik-potsdam.de/".insteadOf "ssh://git@gitlab.pik-potsdam.de/"
- pip install -r requirements.txt
- pip install -r requirements_dev.txt
test-job1:
stage: test
image: python:3.10.9
script:
- source venv/bin/activate
- flake8 --config .flake8
test-job2:
stage: test
image: python:3.10.9
script:
- source venv/bin/activate
- which python
- pip freeze
- python -m mypy --config-file=pyproject.toml server
deploy-to-production:
stage: deploy
......
......@@ -11,5 +11,5 @@ NACSOS_DB__USER="root"
NACSOS_DB__PASSWORD="root"
NACSOS_DB__DATABASE="nacsos_core"
NACSOS_USERS__DEFAULT_USER="user1"
#NACSOS_USERS__DEFAULT_USER
\ No newline at end of file
#NACSOS_USERS__DEFAULT_USER="user1"
NACSOS_USERS__DEFAULT_USER
\ No newline at end of file
......@@ -199,7 +199,7 @@ async def get_assignment(assignment_id: str,
permissions=Depends(UserPermissionChecker('annotations_read'))):
assignment = await read_assignment(assignment_id=assignment_id, db_engine=db_engine)
if assignment.user_id != permissions.user.user_id:
if (assignment is None) or (assignment.user_id != permissions.user.user_id):
raise HTTPException(status_code=http_status.HTTP_401_UNAUTHORIZED,
detail='You do not have permission to handle this assignment, as it is not yours!')
......@@ -299,6 +299,9 @@ async def save_annotation(annotated_item: AnnotatedItem,
assignment_db = await read_assignment(assignment_id=annotated_item.assignment.assignment_id, db_engine=db_engine)
if assignment_db is None:
raise MissingInformationError('No assignment found!')
if permissions.user.user_id == assignment_db.user_id \
and str(assignment_db.assignment_scope_id) == annotated_item.assignment.assignment_scope_id \
and str(assignment_db.item_id) == annotated_item.assignment.item_id \
......
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from nacsos_data.models.users import UserModel
from nacsos_data.db.crud.users import authenticate_user_by_name
from nacsos_data.models.users import UserModel, AuthTokenModel
from server.util.security import Token, get_current_active_user, create_access_token
from server.util.config import settings
from server.util.security import get_current_active_user, auth_helper, InvalidCredentialsError
from server.util.logging import get_logger
from server.data import db_engine
logger = get_logger('nacsos.api.route.login')
router = APIRouter()
......@@ -17,104 +12,39 @@ router = APIRouter()
logger.info('Setting up login route')
@router.post('/token', response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = await authenticate_user_by_name(form_data.username, form_data.password, engine=db_engine)
if not user or not user.username:
@router.post('/token', response_model=AuthTokenModel)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()) -> AuthTokenModel:
try:
user = await auth_helper.check_username_password(username=form_data.username,
plain_password=form_data.password)
token = await auth_helper.refresh_or_create_token(username=user.username)
return token
except InvalidCredentialsError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Incorrect username or password',
detail=repr(e),
headers={'WWW-Authenticate': 'Bearer'},
)
access_token_expires = timedelta(minutes=settings.SERVER.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={'sub': user.username}, expires_delta=access_token_expires
)
return {'access_token': access_token, 'token_type': 'bearer'}
@router.get('/me', response_model=UserModel)
async def read_users_me(current_user: UserModel = Depends(get_current_active_user)):
return current_user
@router.get('/logout')
async def logout(current_user: UserModel = Depends(get_current_active_user)):
username = current_user.username
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='RuntimeError(empty username)',
headers={'WWW-Authenticate': 'Bearer'},
)
await auth_helper.clear_tokens_by_user(username=username)
# TODO forgot password route
# TODO update user info (separate route for password updates?) /
# only the non-admin stuff, e.g. what users can do themselves
# TODO (optional) create permanent auth token
# @router.post('/login/access-token", response_model=Token)
# def login_access_token(
# form_data: OAuth2PasswordRequestForm = Depends()
# ) -> Any:
# """
# OAuth2 compatible token login, get an access token for future requests
# """
# user = crud.user.authenticate(
# db, email=form_data.username, password=form_data.password
# )
# if not user:
# raise HTTPException(status_code=400, detail="Incorrect email or password")
# elif not crud.user.is_active(user):
# raise HTTPException(status_code=400, detail="Inactive user")
# access_token_expires = timedelta(minutes=settings.SERVER.ACCESS_TOKEN_EXPIRE_MINUTES)
# return {
# "access_token": security.create_access_token(
# user.id, expires_delta=access_token_expires
# ),
# "token_type": "bearer",
# }
#
#
# @router.post("/login/test-token", response_model=schemas.User)
# def test_token(current_user: models.User = Depends(deps.get_current_user)) -> Any:
# """
# Test access token
# """
# return current_user
#
#
# @router.post("/password-recovery/{email}", response_model=schemas.Msg)
# def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any:
# """
# Password Recovery
# """
# user = crud.user.get_by_email(db, email=email)
#
# if not user:
# raise HTTPException(
# status_code=404,
# detail="The user with this username does not exist in the system.",
# )
# password_reset_token = generate_password_reset_token(email=email)
# send_reset_password_email(
# email_to=user.email, email=email, token=password_reset_token
# )
# return {"msg": "Password recovery email sent"}
#
#
# @router.post("/reset-password/", response_model=schemas.Msg)
# def reset_password(
# token: str = Body(...),
# new_password: str = Body(...),
# db: Session = Depends(deps.get_db),
# ) -> Any:
# """
# Reset password
# """
# email = verify_password_reset_token(token)
# if not email:
# raise HTTPException(status_code=400, detail="Invalid token")
# user = crud.user.get_by_email(db, email=email)
# if not user:
# raise HTTPException(
# status_code=404,
# detail="The user with this username does not exist in the system.",
# )
# elif not crud.user.is_active(user):
# raise HTTPException(status_code=400, detail="Inactive user")
# hashed_password = get_password_hash(new_password)
# user.hashed_password = hashed_password
# db.add(user)
# db.commit()
# return {"msg": "Password updated successfully"}
from fastapi import APIRouter, Depends, Query, HTTPException, status as http_status
from nacsos_data.models.users import UserModel, UserInDBModel, UserBaseModel
from nacsos_data.util.auth import UserPermissions
from nacsos_data.db.crud.users import \
read_users, \
read_user_by_id, \
......@@ -10,7 +11,7 @@ from nacsos_data.db.crud.users import \
from server.data import db_engine
from server.api.errors import DataNotFoundWarning, UserNotFoundError
from server.util.logging import get_logger
from server.util.security import UserPermissionChecker, UserPermissions, get_current_active_user
from server.util.security import UserPermissionChecker, get_current_active_user
logger = get_logger('nacsos.api.route.admin.users')
router = APIRouter()
......
from typing import Optional
from datetime import timedelta, datetime
from pydantic import BaseModel
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status as http_status, Header
from fastapi.security import OAuth2PasswordBearer
from nacsos_data.models.users import UserModel
from nacsos_data.models.projects import ProjectPermissionsModel, ProjectPermission
from nacsos_data.db.crud.users import read_user_by_name as crud_get_user_by_name, read_user_by_name
from nacsos_data.db.crud.projects import read_project_permissions_for_user as crud_get_project_permissions_for_user
from nacsos_data.models.users import UserModel, UserInDBModel
from nacsos_data.models.projects import ProjectPermission
from nacsos_data.util.auth import Authentication, InsufficientPermissionError, InvalidCredentialsError, UserPermissions
from server.api.errors import MissingInformationError
from server.data import db_engine
from server.util.config import settings
......@@ -23,63 +17,21 @@ class InsufficientPermissions(Exception):
status = http_status.HTTP_403_FORBIDDEN
class UserPermissions(BaseModel):
user: UserModel
permissions: ProjectPermissionsModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
auth_helper = Authentication(engine=db_engine,
token_lifetime_minutes=settings.SERVER.ACCESS_TOKEN_EXPIRE_MINUTES,
default_user=settings.USERS.DEFAULT_USER)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='api/login/token', auto_error=False)
def create_access_token(data: dict[str, str | datetime], expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({'exp': expire})
encoded_jwt = jwt.encode(to_encode, settings.SERVER.SECRET_KEY, algorithm=settings.SERVER.HASH_ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=http_status.HTTP_401_UNAUTHORIZED,
detail='Could not validate credentials',
headers={'WWW-Authenticate': 'Bearer'},
)
user = None
if settings.USERS.DEFAULT_USER is None:
try:
if token is None:
raise credentials_exception
payload = jwt.decode(token, settings.SERVER.SECRET_KEY, algorithms=[settings.SERVER.HASH_ALGORITHM])
username: str = payload.get('sub')
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
token_user = token_data.username
if token_user is not None:
user = await crud_get_user_by_name(username=token_user, engine=db_engine)
else:
user = await read_user_by_name(username=settings.USERS.DEFAULT_USER, engine=db_engine)
logger.warning('Authentication using fake user!')
if user is None:
raise credentials_exception
logger.debug(f'Current user: user_id: {user.user_id} {user.username}')
return user
async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDBModel:
try:
return await auth_helper.get_current_user(token_id=token)
except (InvalidCredentialsError, InsufficientPermissionError) as e:
raise HTTPException(
status_code=http_status.HTTP_401_UNAUTHORIZED,
detail=repr(e),
headers={'WWW-Authenticate': 'Bearer'},
)
async def get_current_active_user(current_user: UserModel = Depends(get_current_user)) -> UserModel:
......@@ -96,19 +48,6 @@ def get_current_active_superuser(current_user: UserModel = Depends(get_current_a
return current_user
async def get_project_permissions_for_user(project_id: str, current_user: UserModel) -> ProjectPermissionsModel | None:
if current_user.user_id is None:
raise MissingInformationError('The `current_user` is missing the (here) required `user_id` field.')
if current_user.is_superuser:
# admin gets to do anything always, so return with simulated full permissions
return ProjectPermissionsModel.get_virtual_admin(project_id=project_id,
user_id=str(current_user.user_id))
return await crud_get_project_permissions_for_user(user_id=current_user.user_id,
project_id=project_id,
engine=db_engine)
class UserPermissionChecker:
def __init__(self,
permissions: list[ProjectPermission] | ProjectPermission | None = None,
......@@ -135,34 +74,16 @@ class UserPermissionChecker:
:return: `ProjectPermissions` if permissions are fulfilled, exception otherwise
:raises HTTPException if permissions are not fulfilled
"""
project_permissions = await get_project_permissions_for_user(project_id=x_project_id,
current_user=current_user)
user_permissions = UserPermissions(user=current_user, permissions=project_permissions)
if project_permissions is not None:
# no specific permissions were required (only basic access to the project) -> permitted!
if self.permissions is None:
return user_permissions
any_permission_fulfilled = False
# check that each required permission is fulfilled
for permission in self.permissions:
p_permission = getattr(project_permissions, permission, False)
if self.fulfill_all and not p_permission:
raise InsufficientPermissions(
f'User does not have permission "{permission}" for project "{x_project_id}".'
)
any_permission_fulfilled = any_permission_fulfilled or p_permission
if not any_permission_fulfilled and not self.fulfill_all:
raise InsufficientPermissions(
f'User does not have any of the required permissions ({self.permissions}) '
f'for project "{x_project_id}".'
)
return user_permissions
try:
return await auth_helper.check_permissions(project_id=x_project_id,
user=current_user,
required_permissions=self.permissions,
fulfill_all=self.fulfill_all)
raise HTTPException(
status_code=http_status.HTTP_403_FORBIDDEN,
detail=f'User does not have permission to access project "{x_project_id}".',
)
except (InvalidCredentialsError, InsufficientPermissionError) as e:
raise InsufficientPermissions(repr(e))
__all__ = ['InsufficientPermissionError', 'InvalidCredentialsError', 'InsufficientPermissions',
'auth_helper', 'oauth2_scheme', 'UserPermissionChecker', 'UserPermissions',
'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