-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #259 from Tauffer-Consulting/feat/improve-permissions
refactor/improve permissions
- Loading branch information
Showing
16 changed files
with
387 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
from fastapi import HTTPException, Security | ||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer | ||
from datetime import datetime, timedelta | ||
from passlib.context import CryptContext | ||
import jwt | ||
from schemas.errors.base import ForbiddenError, ResourceNotFoundError | ||
from core.settings import settings | ||
from schemas.context.auth_context import AuthorizationContextData, WorkspaceAuthorizerData | ||
from repository.user_repository import UserRepository | ||
from repository.workspace_repository import WorkspaceRepository | ||
from repository.piece_repository_repository import PieceRepositoryRepository | ||
from database.models.enums import Permission, UserWorkspaceStatus | ||
import functools | ||
from typing import Optional, Dict | ||
from cryptography.fernet import Fernet | ||
from math import floor | ||
|
||
|
||
class BaseAuthorizer(): | ||
security = HTTPBearer() | ||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | ||
secret = settings.AUTH_SECRET_KEY | ||
expire = settings.AUTH_ACCESS_TOKEN_EXPIRE_MINUTES | ||
algorithm = settings.AUTH_ALGORITHM | ||
user_repository = UserRepository() | ||
workspace_repository = WorkspaceRepository() | ||
piece_repository_repository = PieceRepositoryRepository() | ||
github_token_fernet = Fernet(settings.GITHUB_TOKEN_SECRET_KEY) | ||
|
||
@classmethod | ||
def get_password_hash(cls, password): | ||
return cls.pwd_context.hash(password) | ||
|
||
@classmethod | ||
def verify_password(cls, plain_password, hashed_password): | ||
return cls.pwd_context.verify(plain_password, hashed_password) | ||
|
||
@classmethod | ||
def encode_token(cls, user_id): | ||
exp = datetime.utcnow() + timedelta(days=0, minutes=cls.expire) | ||
current_date = datetime.utcnow() | ||
expires_in = floor((exp - current_date).total_seconds()) | ||
if expires_in >= 120: | ||
expires_in = expires_in - 120 | ||
|
||
payload = { | ||
'exp': datetime.utcnow() + timedelta(days=0, minutes=cls.expire), | ||
'iat': datetime.utcnow(), | ||
'sub': user_id | ||
} | ||
return { | ||
"token": jwt.encode( | ||
payload, | ||
settings.AUTH_SECRET_KEY, | ||
algorithm=cls.algorithm | ||
), | ||
"expires_in": expires_in | ||
} | ||
|
||
@classmethod | ||
def decode_token(cls, token): | ||
try: | ||
payload = jwt.decode(token, cls.secret, algorithms=[cls.algorithm]) | ||
return payload['sub'] | ||
except jwt.ExpiredSignatureError: | ||
raise HTTPException(status_code=401, detail='Signature has expired') | ||
except jwt.InvalidTokenError as e: | ||
raise HTTPException(status_code=401, detail='Invalid token') | ||
|
||
@classmethod | ||
def auth_wrapper(cls, auth: HTTPAuthorizationCredentials = Security(security)): | ||
user_id = cls.decode_token(auth.credentials) | ||
return AuthorizationContextData( | ||
user_id=user_id | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
from fastapi import HTTPException, Security | ||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer | ||
import jwt | ||
from schemas.errors.base import ForbiddenError, ResourceNotFoundError | ||
from schemas.context.auth_context import AuthorizationContextData, WorkspaceAuthorizerData | ||
from database.models.enums import Permission, UserWorkspaceStatus | ||
from typing import Optional, Dict | ||
from auth.base_authorizer import BaseAuthorizer | ||
|
||
|
||
|
||
class Authorizer(BaseAuthorizer): | ||
security = HTTPBearer() | ||
# Permission level map is used to determine what permission can access each level | ||
# Ex: owners can access everything, admin can access everything except owner | ||
permission_level_map = { | ||
Permission.owner.value: [Permission.owner], | ||
Permission.admin.value: [Permission.admin, Permission.owner], | ||
Permission.write.value: [Permission.write, Permission.admin, Permission.owner], | ||
Permission.read.value: [Permission.read, Permission.write, Permission.admin, Permission.owner] | ||
} | ||
def __init__(self, permission_level: Permission = Permission.read.value): | ||
super().__init__() | ||
self.permission = permission_level | ||
self.permission_level = self.permission_level_map[permission_level] | ||
|
||
def authorize( | ||
self, | ||
workspace_id: Optional[int], | ||
auth: HTTPAuthorizationCredentials = Security(security), | ||
): | ||
auth_context = self.auth_wrapper(auth) | ||
if not workspace_id: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
workspace_associative_data = self.workspace_repository.find_by_id_and_user_id( | ||
id=workspace_id, | ||
user_id=auth_context.user_id | ||
) | ||
if not workspace_associative_data: | ||
raise HTTPException(status_code=ResourceNotFoundError().status_code, detail=ResourceNotFoundError().message) | ||
|
||
if workspace_associative_data and not workspace_associative_data.permission: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
|
||
if workspace_associative_data and workspace_associative_data.status != UserWorkspaceStatus.accepted.value: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
|
||
if workspace_associative_data.permission not in self.permission_level: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
|
||
decoded_github_token = None if not workspace_associative_data.github_access_token else self.github_token_fernet.decrypt(workspace_associative_data.github_access_token.encode('utf-8')).decode('utf-8') | ||
auth_context.workspace = WorkspaceAuthorizerData( | ||
id=workspace_associative_data.workspace_id, | ||
name=workspace_associative_data.name, | ||
github_access_token=decoded_github_token, | ||
user_permission=workspace_associative_data.permission | ||
) | ||
return auth_context | ||
|
||
def authorize_with_body( | ||
self, | ||
body: Optional[Dict] = None, | ||
auth: HTTPAuthorizationCredentials = Security(security), | ||
): | ||
workspace_id = body.get('workspace_id') | ||
return self.authorize(workspace_id=workspace_id, auth=auth) | ||
|
||
def authorize_piece_repository( | ||
self, | ||
piece_repository_id: Optional[int], | ||
body: Optional[Dict] = None, | ||
auth: HTTPAuthorizationCredentials = Security(security), | ||
): | ||
if body is None: | ||
body = {} | ||
auth_context = self.auth_wrapper(auth) | ||
repository = self.piece_repository_repository.find_by_id(id=piece_repository_id) | ||
if not repository: | ||
raise HTTPException(status_code=ResourceNotFoundError().status_code, detail=ResourceNotFoundError().message) | ||
workspace_associative_data = self.workspace_repository.find_by_id_and_user_id(id=repository.workspace_id, user_id=auth_context.user_id) | ||
|
||
if not workspace_associative_data: | ||
raise HTTPException(status_code=ResourceNotFoundError().status_code, detail=ResourceNotFoundError().message) | ||
|
||
if workspace_associative_data and not workspace_associative_data.permission: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
|
||
if workspace_associative_data and workspace_associative_data.status != UserWorkspaceStatus.accepted.value: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
|
||
if workspace_associative_data.permission not in self.permission_level: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
|
||
if not body or not getattr(body, "workspace_id", None): | ||
return auth_context | ||
|
||
if body.workspace_id != repository.workspace_id: | ||
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message) | ||
return auth_context | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
""" | ||
Update permission enum values | ||
Revision ID: a9f4cd2e4f57 | ||
Revises: ab54cfed2bdc | ||
Create Date: 2024-03-22 10:29:23.445775 | ||
""" | ||
from alembic import op | ||
import sqlalchemy as sa | ||
|
||
|
||
# revision identifiers, used by Alembic. | ||
revision = 'a9f4cd2e4f57' | ||
down_revision = 'ab54cfed2bdc' | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
from alembic import op | ||
|
||
def upgrade(): | ||
op.execute("ALTER TABLE user_workspace_associative ALTER COLUMN permission DROP DEFAULT") | ||
op.execute("CREATE TYPE permission_new AS ENUM ('owner', 'admin', 'write', 'read')") | ||
op.execute("ALTER TABLE user_workspace_associative ALTER COLUMN permission TYPE permission_new USING permission::text::permission_new") | ||
op.execute("DROP TYPE permission") | ||
op.execute("ALTER TYPE permission_new RENAME TO permission") | ||
op.execute("ALTER TABLE user_workspace_associative ALTER COLUMN permission SET DEFAULT 'owner'") | ||
|
||
def downgrade(): | ||
op.execute("ALTER TABLE user_workspace_associative ALTER COLUMN permission DROP DEFAULT") | ||
op.execute("CREATE TYPE permission_new AS ENUM ('owner', 'read', 'Config')") | ||
op.execute("ALTER TABLE user_workspace_associative ALTER COLUMN permission TYPE permission_new USING permission::text::permission_new") | ||
op.execute("DROP TYPE permission") | ||
op.execute("ALTER TYPE permission_new RENAME TO permission") | ||
op.execute("ALTER TABLE user_workspace_associative ALTER COLUMN permission SET DEFAULT 'owner'") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.