Skip to content

Commit

Permalink
Merge pull request #259 from Tauffer-Consulting/feat/improve-permissions
Browse files Browse the repository at this point in the history
refactor/improve permissions
  • Loading branch information
vinicvaz authored Mar 27, 2024
2 parents 99fe800 + f5acd84 commit c764ce3
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

Check warning on line 16 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L1-L16

Added lines #L1 - L16 were not covered by tests


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)

Check warning on line 28 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L19-L28

Added lines #L19 - L28 were not covered by tests

@classmethod
def get_password_hash(cls, password):
return cls.pwd_context.hash(password)

Check warning on line 32 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L30-L32

Added lines #L30 - L32 were not covered by tests

@classmethod
def verify_password(cls, plain_password, hashed_password):
return cls.pwd_context.verify(plain_password, hashed_password)

Check warning on line 36 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L34-L36

Added lines #L34 - L36 were not covered by tests

@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

Check warning on line 44 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L38-L44

Added lines #L38 - L44 were not covered by tests

payload = {

Check warning on line 46 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L46

Added line #L46 was not covered by tests
'exp': datetime.utcnow() + timedelta(days=0, minutes=cls.expire),
'iat': datetime.utcnow(),
'sub': user_id
}
return {

Check warning on line 51 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L51

Added line #L51 was not covered by tests
"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')

Check warning on line 68 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L60-L68

Added lines #L60 - L68 were not covered by tests

@classmethod
def auth_wrapper(cls, auth: HTTPAuthorizationCredentials = Security(security)):
user_id = cls.decode_token(auth.credentials)
return AuthorizationContextData(

Check warning on line 73 in rest/auth/base_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/base_authorizer.py#L70-L73

Added lines #L70 - L73 were not covered by tests
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

Check warning on line 8 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L1-L8

Added lines #L1 - L8 were not covered by tests



class Authorizer(BaseAuthorizer):
security = HTTPBearer()

Check warning on line 13 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L12-L13

Added lines #L12 - L13 were not covered by tests
# 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 = {

Check warning on line 16 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L16

Added line #L16 was not covered by tests
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]

Check warning on line 25 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L22-L25

Added lines #L22 - L25 were not covered by tests

def authorize(

Check warning on line 27 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L27

Added line #L27 was not covered by tests
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(

Check warning on line 35 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L32-L35

Added lines #L32 - L35 were not covered by tests
id=workspace_id,
user_id=auth_context.user_id
)
if not workspace_associative_data:
raise HTTPException(status_code=ResourceNotFoundError().status_code, detail=ResourceNotFoundError().message)

Check warning on line 40 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L39-L40

Added lines #L39 - L40 were not covered by tests

if workspace_associative_data and not workspace_associative_data.permission:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)

Check warning on line 43 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L42-L43

Added lines #L42 - L43 were not covered by tests

if workspace_associative_data and workspace_associative_data.status != UserWorkspaceStatus.accepted.value:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)

Check warning on line 46 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L45-L46

Added lines #L45 - L46 were not covered by tests

if workspace_associative_data.permission not in self.permission_level:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)

Check warning on line 49 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L48-L49

Added lines #L48 - L49 were not covered by tests

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(

Check warning on line 52 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L51-L52

Added lines #L51 - L52 were not covered by tests
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

Check warning on line 58 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L58

Added line #L58 was not covered by tests

def authorize_with_body(

Check warning on line 60 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L60

Added line #L60 was not covered by tests
self,
body: Optional[Dict] = None,
auth: HTTPAuthorizationCredentials = Security(security),
):
workspace_id = body.get('workspace_id')
return self.authorize(workspace_id=workspace_id, auth=auth)

Check warning on line 66 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L65-L66

Added lines #L65 - L66 were not covered by tests

def authorize_piece_repository(

Check warning on line 68 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L68

Added line #L68 was not covered by tests
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)

Check warning on line 80 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L74-L80

Added lines #L74 - L80 were not covered by tests

if not workspace_associative_data:
raise HTTPException(status_code=ResourceNotFoundError().status_code, detail=ResourceNotFoundError().message)

Check warning on line 83 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L82-L83

Added lines #L82 - L83 were not covered by tests

if workspace_associative_data and not workspace_associative_data.permission:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)

Check warning on line 86 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L85-L86

Added lines #L85 - L86 were not covered by tests

if workspace_associative_data and workspace_associative_data.status != UserWorkspaceStatus.accepted.value:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)

Check warning on line 89 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L88-L89

Added lines #L88 - L89 were not covered by tests

if workspace_associative_data.permission not in self.permission_level:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)

Check warning on line 92 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L91-L92

Added lines #L91 - L92 were not covered by tests

if not body or not getattr(body, "workspace_id", None):
return auth_context

Check warning on line 95 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L94-L95

Added lines #L94 - L95 were not covered by tests

if body.workspace_id != repository.workspace_id:
raise HTTPException(status_code=ForbiddenError().status_code, detail=ForbiddenError().message)
return auth_context

Check warning on line 99 in rest/auth/permission_authorizer.py

View check run for this annotation

Codecov / codecov/patch

rest/auth/permission_authorizer.py#L97-L99

Added lines #L97 - L99 were not covered by tests
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 c764ce3

Please sign in to comment.