Skip to content

Commit

Permalink
Merge pull request #262 from Tauffer-Consulting/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
vinicvaz authored Mar 27, 2024
2 parents 90f9b66 + 95f91f6 commit 6859cb0
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,9 @@ export const UsersCard: FC = () => {
setPermission(e.target.value);
}}
>
<MenuItem value={"admin"}>Admin</MenuItem>
<MenuItem value={"write"}>Write</MenuItem>
<MenuItem value={"read"}>Read</MenuItem>
<MenuItem value={"owner"}>Owner</MenuItem>
</Select>
</FormControl>
</Grid>
Expand Down
Empty file added rest/auth/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions rest/auth/base_authorizer.py
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
)
99 changes: 99 additions & 0 deletions rest/auth/permission_authorizer.py
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
36 changes: 36 additions & 0 deletions rest/database/alembic/versions/a9f4cd2e4f57_.py
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'")
7 changes: 7 additions & 0 deletions rest/database/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ class Config:

class Permission(str, enum.Enum):
owner = 'owner'
admin = 'admin'
write = 'write'
read = 'read'

class Config:
use_enum_values = True


class MembersPermissions(str, enum.Enum):
admin = 'admin'
write = 'write'
read = 'read'

class UserWorkspaceStatus(str, enum.Enum):
pending = 'pending'
accepted = 'accepted'
Expand Down
22 changes: 11 additions & 11 deletions rest/repository/workflow_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ def find_by_id(self, id: int):
return result

def find_by_workspace_id(
self,
workspace_id: int,
page: int = 0,
page_size: int = 100,
filters: dict = None,
paginate=True,
self,
workspace_id: int,
page: int = 0,
page_size: int = 100,
filters: dict = None,
paginate=True,
count=True,
descending=False
):
Expand All @@ -39,7 +39,7 @@ def find_by_workspace_id(

if filters:
query = query.magic_filters(filters)

if paginate:
results = query.paginate(page, page_size)
else:
Expand All @@ -65,7 +65,7 @@ def create(self, workflow: Workflow):
session.refresh(workflow)
session.expunge(workflow)
return workflow


def get_workflows_summary(self):
with session_scope() as session:
Expand All @@ -81,7 +81,7 @@ def delete(self, id):
session.flush()
session.expunge_all()
return result

def delete_by_workspace_id(self, workspace_id: int):
with session_scope() as session:
result = session.query(Workflow).filter(Workflow.workspace_id==workspace_id).delete(synchronize_session=False)
Expand All @@ -90,7 +90,7 @@ def delete_by_workspace_id(self, workspace_id: int):
return result

def create_workflow_piece_repositories_associations(
self,
self,
workflow_piece_repository_associative: list[WorkflowPieceRepositoryAssociative]
):
with session_scope() as session:
Expand Down Expand Up @@ -135,4 +135,4 @@ def update(self, workflow: Workflow):
saved_workflow.last_changed_by = workflow.last_changed_by
session.flush()
session.expunge(saved_workflow)
return workflow
return workflow
Loading

0 comments on commit 6859cb0

Please sign in to comment.