diff --git a/frontend/src/features/workspaces/components/workspaceSettings/UsersCard.tsx b/frontend/src/features/workspaces/components/workspaceSettings/UsersCard.tsx
index fd2c33f8..f67b7433 100644
--- a/frontend/src/features/workspaces/components/workspaceSettings/UsersCard.tsx
+++ b/frontend/src/features/workspaces/components/workspaceSettings/UsersCard.tsx
@@ -88,8 +88,9 @@ export const UsersCard: FC = () => {
setPermission(e.target.value);
}}
>
+
+
-
diff --git a/rest/auth/__init__.py b/rest/auth/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/rest/auth/base_authorizer.py b/rest/auth/base_authorizer.py
new file mode 100644
index 00000000..e37202f2
--- /dev/null
+++ b/rest/auth/base_authorizer.py
@@ -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
+ )
diff --git a/rest/auth/permission_authorizer.py b/rest/auth/permission_authorizer.py
new file mode 100644
index 00000000..03f78ab4
--- /dev/null
+++ b/rest/auth/permission_authorizer.py
@@ -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
\ No newline at end of file
diff --git a/rest/database/alembic/versions/a9f4cd2e4f57_.py b/rest/database/alembic/versions/a9f4cd2e4f57_.py
new file mode 100644
index 00000000..f29705dd
--- /dev/null
+++ b/rest/database/alembic/versions/a9f4cd2e4f57_.py
@@ -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'")
\ No newline at end of file
diff --git a/rest/database/models/enums.py b/rest/database/models/enums.py
index ed4ad701..e5f91de5 100644
--- a/rest/database/models/enums.py
+++ b/rest/database/models/enums.py
@@ -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'
diff --git a/rest/repository/workflow_repository.py b/rest/repository/workflow_repository.py
index 1d766977..4d7ccabf 100644
--- a/rest/repository/workflow_repository.py
+++ b/rest/repository/workflow_repository.py
@@ -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
):
@@ -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:
@@ -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:
@@ -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)
@@ -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:
@@ -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
\ No newline at end of file
diff --git a/rest/repository/workspace_repository.py b/rest/repository/workspace_repository.py
index ce496ca4..47ceb9f6 100644
--- a/rest/repository/workspace_repository.py
+++ b/rest/repository/workspace_repository.py
@@ -1,5 +1,5 @@
from database.interface import session_scope
-from database.models import Workspace, UserWorkspaceAssociative, User
+from database.models import Workspace, UserWorkspaceAssociative, User, Workflow
from database.models.enums import UserWorkspaceStatus, Permission
from typing import Tuple, List
from sqlalchemy import and_, func
@@ -44,7 +44,7 @@ def find_by_id(self, id: int) -> Workspace:
if result:
session.expunge_all()
return result
-
+
def find_workspace_users(self, workspace_id: int, page: int, page_size: int):
"""
SELECT user_workspace_associative.*, "user".*, total_count.count AS count
@@ -75,7 +75,7 @@ def find_workspace_users(self, workspace_id: int, page: int, page_size: int):
session.expunge_all()
return results
-
+
def find_pending_workspace_invite(self, user_id: int, workspace_id: int):
with session_scope() as session:
result = session.query(Workspace, UserWorkspaceAssociative)\
@@ -85,7 +85,7 @@ def find_pending_workspace_invite(self, user_id: int, workspace_id: int):
if result:
session.expunge_all()
return result
-
+
def update_user_workspace_associative_by_ids(self, associative: UserWorkspaceAssociative):
with session_scope() as session:
saved_associative = session.query(UserWorkspaceAssociative)\
@@ -105,14 +105,14 @@ def find_by_id_and_user(self, id: int, user_id: int) -> Workspace:
"""
SELECT workspace.id, workspace.name, workspace.github_access_token, user_workspace_associative.permission
FROM workspace
- INNER JOIN user_workspace_associative
+ INNER JOIN user_workspace_associative
ON user_workspace_associative.workspace_id = workspace.id and user_id=1
WHERE workspace_id=9;
"""
query = session.query(
- Workspace.id.label('id'),
- Workspace.name,
- Workspace.github_access_token,
+ Workspace.id.label('id'),
+ Workspace.name,
+ Workspace.github_access_token,
UserWorkspaceAssociative.permission.label('permission'),
UserWorkspaceAssociative.status.label('status')
)\
@@ -157,52 +157,74 @@ def remove_user_from_workspaces(self, user_id: int, workspaces_ids: List[int]):
session.query(UserWorkspaceAssociative)\
.filter(and_(UserWorkspaceAssociative.user_id==user_id, UserWorkspaceAssociative.workspace_id.in_(workspaces_ids)))\
.delete(synchronize_session=False)
-
+
def find_user_workspaces_members_owners_count(self, user_id: int, workspaces_ids: List[int]) -> List:
"""
- SELECT * from user_workspace_associative as t1
+ SELECT t1.*, t2.members_count, t3.owners_count, COUNT(wf.id) AS total_workflows
+ FROM user_workspace_associative AS t1
INNER JOIN (
- SELECT workspace_id, COUNT(*) as user_count from user_workspace_associative
- WHERE user_workspace_associative.workspace_id in (1) GROUP BY workspace_id
- ) as t2
- INNER JOIN (
- SELECT workspace_id, COUNT(*) as owners_count from user_workspace_associative
- WHERE user_workspace_associative.workspace_id in (ids) and user_workspace_associative.permission = 'owner'
- GROUP BY workspace_id
- ) as t3
- ON t2.workspace_id=t2.workspace_id
- ON t1.workspace_id=t2.workspace_id
- WHERE t1.user_id=2;
+ SELECT workspace_id, COUNT(*) AS members_count
+ FROM user_workspace_associative
+ WHERE user_workspace_associative.workspace_id IN (2)
+ GROUP BY workspace_id
+ ) AS t2 ON t1.workspace_id = t2.workspace_id
+ INNER JOIN (
+ SELECT workspace_id, COUNT(*) AS owners_count
+ FROM user_workspace_associative
+ WHERE user_workspace_associative.workspace_id IN (2)
+ AND user_workspace_associative.permission = 'owner'
+ GROUP BY workspace_id
+ ) AS t3 ON t1.workspace_id = t3.workspace_id
+ LEFT JOIN workflow AS wf ON t1.workspace_id = wf.workspace_id
+ WHERE t1.user_id = 2
+ AND wf.workspace_id = 2
+ GROUP BY t1.user_id, t1.workspace_id, t2.members_count, t3.owners_count;
"""
with session_scope() as session:
- # create a subquery
- subquery_owners = session.query(
- UserWorkspaceAssociative.workspace_id,
- func.count(UserWorkspaceAssociative.workspace_id).label('owners_count')
- ).filter(UserWorkspaceAssociative.workspace_id.in_(workspaces_ids))\
- .filter(UserWorkspaceAssociative.permission == Permission.owner.value)\
- .group_by(UserWorkspaceAssociative.workspace_id).subquery()
+ subquery_users = (
+ session.query(
+ UserWorkspaceAssociative.workspace_id,
+ func.count('*').label('members_count')
+ )
+ .filter(UserWorkspaceAssociative.workspace_id.in_(workspaces_ids))
+ .group_by(UserWorkspaceAssociative.workspace_id)
+ .subquery()
+ )
- subquery = (
+ # Subquery for counting owners in each workspace
+ subquery_owners = (
session.query(
UserWorkspaceAssociative.workspace_id,
- func.count(UserWorkspaceAssociative.workspace_id).label('members_count'),
- subquery_owners.c.owners_count
+ func.count('*').label('owners_count')
)
.filter(UserWorkspaceAssociative.workspace_id.in_(workspaces_ids))
- .group_by(UserWorkspaceAssociative.workspace_id, subquery_owners.c.owners_count)
- .join(subquery_owners, UserWorkspaceAssociative.workspace_id == subquery_owners.c.workspace_id)
+ .filter(UserWorkspaceAssociative.permission == 'owner')
+ .group_by(UserWorkspaceAssociative.workspace_id)
.subquery()
)
- query = session.query(
- UserWorkspaceAssociative.user_id,
- UserWorkspaceAssociative.workspace_id,
- UserWorkspaceAssociative.permission,
- subquery.c.members_count,
- subquery.c.owners_count
- ).join(subquery, UserWorkspaceAssociative.workspace_id == subquery.c.workspace_id)\
+ # Main query
+ query = (
+ session.query(
+ UserWorkspaceAssociative.user_id,
+ UserWorkspaceAssociative.workspace_id,
+ UserWorkspaceAssociative.permission,
+ UserWorkspaceAssociative.status,
+ subquery_users.c.members_count,
+ subquery_owners.c.owners_count,
+ func.count(Workflow.id).label('total_workflows')
+ )
+ .join(subquery_users, UserWorkspaceAssociative.workspace_id == subquery_users.c.workspace_id)
+ .join(subquery_owners, UserWorkspaceAssociative.workspace_id == subquery_owners.c.workspace_id)
+ .outerjoin(Workflow, UserWorkspaceAssociative.workspace_id == Workflow.workspace_id)
.filter(UserWorkspaceAssociative.user_id == user_id)
+ .group_by(
+ UserWorkspaceAssociative.user_id,
+ UserWorkspaceAssociative.workspace_id,
+ subquery_users.c.members_count,
+ subquery_owners.c.owners_count
+ )
+ )
result = query.all()
if result:
session.expunge_all()
@@ -213,7 +235,7 @@ def find_user_workspaces_members_count(self, user_id: int, workspaces_ids: List[
SQL Query:
SELECT * from user_workspace_associative as t1
INNER JOIN (
- SELECT workspace_id, COUNT(*) as user_count from user_workspace_associative
+ SELECT workspace_id, COUNT(*) as members_count from user_workspace_associative
WHERE user_workspace_associative.workspace_id in (ids) GROUP BY workspace_id
) as t2
ON t1.workspace_id=t2.workspace_id
@@ -222,14 +244,14 @@ def find_user_workspaces_members_count(self, user_id: int, workspaces_ids: List[
with session_scope() as session:
# create a subquery
subquery = session.query(
- UserWorkspaceAssociative.workspace_id,
+ UserWorkspaceAssociative.workspace_id,
func.count(UserWorkspaceAssociative.workspace_id).label('members_count')
).filter(UserWorkspaceAssociative.workspace_id.in_(workspaces_ids))\
.group_by(UserWorkspaceAssociative.workspace_id).subquery()
query = session.query(
UserWorkspaceAssociative.user_id,
- UserWorkspaceAssociative.workspace_id,
+ UserWorkspaceAssociative.workspace_id,
UserWorkspaceAssociative.permission,
subquery.c.members_count
).join(subquery, UserWorkspaceAssociative.workspace_id == subquery.c.workspace_id)\
@@ -238,7 +260,7 @@ def find_user_workspaces_members_count(self, user_id: int, workspaces_ids: List[
if result:
session.expunge_all()
return result
-
+
def find_by_name_and_user_id(self, name: str, user_id: int):
with session_scope() as session:
result = session.query(Workspace)\
diff --git a/rest/routers/piece_repository_router.py b/rest/routers/piece_repository_router.py
index 056ded72..35ba295e 100644
--- a/rest/routers/piece_repository_router.py
+++ b/rest/routers/piece_repository_router.py
@@ -1,11 +1,9 @@
from fastapi import APIRouter, HTTPException, status, Depends, Response
-from services.auth_service import AuthService
from services.piece_repository_service import PieceRepositoryService
from schemas.context.auth_context import AuthorizationContextData
from schemas.requests.piece_repository import CreateRepositoryRequest, PatchRepositoryRequest, ListRepositoryFilters
from schemas.responses.piece_repository import (
CreateRepositoryReponse,
- PatchRepositoryResponse,
GetRepositoryReleasesResponse,
GetRepositoryReleaseDataResponse,
GetWorkspaceRepositoriesResponse,
@@ -15,12 +13,18 @@
from schemas.exceptions.base import BaseException, ConflictException, ForbiddenException, ResourceNotFoundException, UnauthorizedException
from schemas.errors.base import ConflictError, ForbiddenError, ResourceNotFoundError, SomethingWrongError, UnauthorizedError
from typing import List, Optional
+from auth.permission_authorizer import Authorizer
+from database.models.enums import Permission
+
router = APIRouter(prefix="/pieces-repositories")
-auth_service = AuthService()
+
piece_repository_service = PieceRepositoryService()
+admin_authorizer = Authorizer(permission_level=Permission.admin.value)
+read_authorizer = Authorizer(permission_level=Permission.read.value)
+
@router.post(
path="",
@@ -36,7 +40,7 @@
)
def create_piece_repository(
body: CreateRepositoryRequest,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_owner_access_authorizer_body)
+ auth_context: AuthorizationContextData = Depends(admin_authorizer.authorize_with_body)
) -> CreateRepositoryReponse:
"""
Create piece repository for workspace.
@@ -67,7 +71,7 @@ def get_piece_repository_releases(
source: RepositorySource,
path: str,
workspace_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> List[GetRepositoryReleasesResponse]:
"""Get piece repository releases"""
try:
@@ -97,7 +101,7 @@ def get_piece_repository_release_data(
source: RepositorySource,
path: str,
workspace_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetRepositoryReleaseDataResponse:
"""Get piece repository release data"""
try:
@@ -120,7 +124,7 @@ def get_piece_repository_release_data(
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': SomethingWrongError},
status.HTTP_403_FORBIDDEN: {'model': ForbiddenError},
},
- dependencies=[Depends(auth_service.workspace_access_authorizer)]
+ dependencies=[Depends(read_authorizer.authorize)]
)
def get_pieces_repositories(
workspace_id: int,
@@ -188,7 +192,7 @@ def get_pieces_repositories_worker(
)
def delete_repository(
piece_repository_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.piece_repository_workspace_owner_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(admin_authorizer.authorize_piece_repository)
):
try:
response = piece_repository_service.delete_repository(
@@ -209,10 +213,9 @@ def delete_repository(
},
)
-@auth_service.authorize_repository_workspace_access
def get_piece_repository(
piece_repository_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize_piece_repository)
) -> GetRepositoryResponse:
"""Get piece repository info by id"""
try:
diff --git a/rest/routers/piece_router.py b/rest/routers/piece_router.py
index 9e45e1df..05b9a173 100644
--- a/rest/routers/piece_router.py
+++ b/rest/routers/piece_router.py
@@ -1,18 +1,18 @@
from fastapi import APIRouter, HTTPException, status, Depends
-from services.auth_service import AuthService
from services.piece_service import PieceService
from schemas.context.auth_context import AuthorizationContextData
from schemas.requests.piece import ListPiecesFilters
from schemas.responses.piece import GetPiecesResponse
-from schemas.exceptions.base import BaseException, ForbiddenException, ResourceNotFoundException
+from schemas.exceptions.base import BaseException, ForbiddenException
from schemas.errors.base import SomethingWrongError, ForbiddenError
from typing import List
-
+from auth.permission_authorizer import Authorizer
+from database.models.enums import Permission
router = APIRouter(prefix="/pieces-repositories/{piece_repository_id}/pieces")
piece_service = PieceService()
-auth_service = AuthService()
+read_authorizer = Authorizer(permission_level=Permission.read.value)
@router.get(
@@ -24,13 +24,12 @@
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': SomethingWrongError}
}
)
-@auth_service.authorize_repository_workspace_access
def get_pieces(
piece_repository_id: int,
page: int = 0,
page_size: int = 100,
filters: ListPiecesFilters = Depends(),
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize_piece_repository)
):
"""List pieces from a piece repository"""
try:
diff --git a/rest/routers/secret_router.py b/rest/routers/secret_router.py
index f0a4c535..34d33720 100644
--- a/rest/routers/secret_router.py
+++ b/rest/routers/secret_router.py
@@ -1,5 +1,4 @@
from fastapi import APIRouter, HTTPException, Depends, status
-from services.auth_service import AuthService
from services.secret_service import SecretService
from schemas.context.auth_context import AuthorizationContextData
from schemas.requests.secret import PatchSecretValueRequest
@@ -7,13 +6,19 @@
from schemas.exceptions.base import BaseException, ForbiddenException, ResourceNotFoundException
from schemas.errors.base import ResourceNotFoundError, SomethingWrongError, ForbiddenError
from typing import List
+from auth.permission_authorizer import Authorizer
+from database.models.enums import Permission
router = APIRouter(prefix="/pieces-repositories/{piece_repository_id}/secrets")
-auth_service = AuthService()
secret_service = SecretService()
+read_authorizer = Authorizer(permission_level=Permission.read.value)
+admin_authorizer = Authorizer(permission_level=Permission.admin.value)
+
+
+
@router.get(
'',
status_code=200,
@@ -23,10 +28,9 @@
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': SomethingWrongError}
}
)
-@auth_service.authorize_repository_workspace_access
def get_repository_secrets(
piece_repository_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize_piece_repository)
) -> List[ListRepositorySecretsResponse]:
"""
Get the list of piece repository secrets.
@@ -49,12 +53,11 @@ def get_repository_secrets(
status.HTTP_404_NOT_FOUND: {'model': ResourceNotFoundError}
}
)
-@auth_service.authorize_repository_workspace_owner_access # To update a secret user must have owner access to workspace
def update_repository_secret(
piece_repository_id: int,
secret_id: int,
body: PatchSecretValueRequest,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(admin_authorizer.authorize_piece_repository)
):
"""
Update an piece repository secret value.
diff --git a/rest/routers/user_router.py b/rest/routers/user_router.py
index 5f808d08..a94c80a9 100644
--- a/rest/routers/user_router.py
+++ b/rest/routers/user_router.py
@@ -4,12 +4,12 @@
from schemas.exceptions.base import BaseException, ForbiddenException, UnauthorizedException
from schemas.errors.base import SomethingWrongError, UnauthorizedError, ForbiddenError
from schemas.context.auth_context import AuthorizationContextData
-from services.auth_service import AuthService
+from auth.permission_authorizer import Authorizer
router = APIRouter(prefix="/users")
user_service = UserService()
-auth_service = AuthService()
+authorizer = Authorizer()
@router.delete(
"/{user_id}",
@@ -21,8 +21,8 @@
}
)
async def delete_user(
- user_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ user_id: int,
+ auth_context: AuthorizationContextData = Depends(authorizer.auth_wrapper)
):
"""
Delete user by id.
diff --git a/rest/routers/workflow_router.py b/rest/routers/workflow_router.py
index e8625987..6fa9b443 100644
--- a/rest/routers/workflow_router.py
+++ b/rest/routers/workflow_router.py
@@ -2,7 +2,6 @@
from schemas.context.auth_context import AuthorizationContextData
from typing import List
from services.workflow_service import WorkflowService
-from services.auth_service import AuthService
from schemas.requests.workflow import CreateWorkflowRequest, ListWorkflowsFilters
from schemas.responses.workflow import (
GetWorkflowsResponse,
@@ -28,11 +27,16 @@
ResourceNotFoundError,
SomethingWrongError,
)
+from auth.permission_authorizer import Authorizer
+from database.models.enums import Permission
router = APIRouter(prefix="/workspaces/{workspace_id}/workflows")
-auth_service = AuthService()
+
workflow_service = WorkflowService()
+read_authorizer = Authorizer(permission_level=Permission.read.value)
+write_authorizer = Authorizer(permission_level=Permission.write.value)
+
@router.post(
@@ -48,7 +52,7 @@
def create_workflow(
workspace_id: int,
body: CreateWorkflowRequest,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(write_authorizer.authorize)
) -> CreateWorkflowResponse:
"""Create a new workflow"""
try:
@@ -69,7 +73,7 @@ def create_workflow(
status.HTTP_500_INTERNAL_SERVER_ERROR: {"model": SomethingWrongError},
status.HTTP_403_FORBIDDEN: {"model": ForbiddenError},
},
- dependencies=[Depends(auth_service.workspace_access_authorizer)]
+ dependencies=[Depends(read_authorizer.authorize)]
)
async def list_workflows(
workspace_id: int,
@@ -99,11 +103,10 @@ async def list_workflows(
},
status_code=200,
)
-@auth_service.authorize_workspace_access
def get_workflow(
workspace_id: int,
workflow_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetWorkflowResponse:
"""Get a workflow information"""
try:
@@ -125,7 +128,7 @@ def get_workflow(
status.HTTP_403_FORBIDDEN: {"model": ForbiddenError},
status.HTTP_404_NOT_FOUND: {"model": ResourceNotFoundError}
},
- dependencies=[Depends(auth_service.workspace_owner_access_authorizer)]
+ dependencies=[Depends(write_authorizer.authorize)]
)
async def delete_workflow(
workspace_id: int,
@@ -154,7 +157,7 @@ async def delete_workflow(
def run_workflow(
workspace_id: int,
workflow_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(write_authorizer.authorize)
):
try:
return workflow_service.run_workflow(
@@ -178,7 +181,7 @@ def list_workflow_runs(
workflow_id: int,
page: int = 0,
page_size: int = 5,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetWorkflowRunsResponse:
try:
return workflow_service.list_workflow_runs(
@@ -205,7 +208,7 @@ def list_run_tasks(
workflow_run_id: str,
page: int = 0,
page_size: int = 5,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetWorkflowRunTasksResponse:
try:
return workflow_service.list_run_tasks(
@@ -231,7 +234,7 @@ def generate_report(
workspace_id: int,
workflow_id: int,
workflow_run_id: str,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetWorkflowResultReportResponse:
try:
return workflow_service.generate_report(
@@ -257,7 +260,7 @@ def get_task_logs(
workflow_run_id: str,
task_id: str,
task_try_number: int,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetWorkflowRunTaskLogsResponse:
"""
@@ -290,7 +293,7 @@ def get_task_result(
workflow_run_id: str,
task_id: str,
task_try_number: int,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> GetWorkflowRunTaskResultResponse:
"""
diff --git a/rest/routers/workspace_router.py b/rest/routers/workspace_router.py
index 3137e82d..73ccc746 100644
--- a/rest/routers/workspace_router.py
+++ b/rest/routers/workspace_router.py
@@ -1,24 +1,26 @@
from fastapi import APIRouter, HTTPException, status, Depends, Response
-from services.auth_service import AuthService
-
from services.workspace_service import WorkspaceService
from schemas.context.auth_context import AuthorizationContextData
from schemas.requests.workspace import CreateWorkspaceRequest, AssignWorkspaceRequest, PatchWorkspaceRequest
from schemas.responses.workspace import (
- CreateWorkspaceResponse,
- ListUserWorkspacesResponse,
- GetWorkspaceResponse,
- PatchWorkspaceResponse,
+ CreateWorkspaceResponse,
+ ListUserWorkspacesResponse,
+ GetWorkspaceResponse,
+ PatchWorkspaceResponse,
ListWorkspaceUsersResponse
)
from schemas.exceptions.base import BaseException, ConflictException, ResourceNotFoundException, ForbiddenException, UnauthorizedException
from schemas.errors.base import ConflictError, ForbiddenError, SomethingWrongError, ResourceNotFoundError, UnauthorizedError
from database.models.enums import UserWorkspaceStatus
from typing import List
+from auth.permission_authorizer import Authorizer
router = APIRouter(prefix="/workspaces")
-auth_service = AuthService()
+
+owner_authorizer = Authorizer(permission_level='owner')
+admin_authorizer = Authorizer(permission_level='admin')
+read_authorizer = Authorizer(permission_level='read')
workspace_service = WorkspaceService()
@@ -36,7 +38,7 @@
)
def create_workspace(
body: CreateWorkspaceRequest,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.auth_wrapper)
) -> CreateWorkspaceResponse:
"""Create workspace"""
try:
@@ -61,7 +63,7 @@ def create_workspace(
def list_user_workspaces(
page: int = 0,
page_size: int = 10,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.auth_wrapper)
) -> List[ListUserWorkspacesResponse]:
"""List user workspaces summary"""
try:
@@ -84,7 +86,7 @@ def list_user_workspaces(
status.HTTP_404_NOT_FOUND: {'model': ResourceNotFoundError}
},
)
-def get_workspace(workspace_id: int, auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)) -> GetWorkspaceResponse:
+def get_workspace(workspace_id: int, auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)) -> GetWorkspaceResponse:
"""Get specific workspace data. Includes users, workflows and repositories"""
try:
response = workspace_service.get_workspace_data(workspace_id=workspace_id, auth_context=auth_context)
@@ -107,7 +109,7 @@ def get_workspace(workspace_id: int, auth_context: AuthorizationContextData = De
def add_user_to_workspace(
workspace_id: int,
body: AssignWorkspaceRequest,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_owner_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(admin_authorizer.authorize)
):
"""Assign workspace to user with permission"""
try:
@@ -117,7 +119,7 @@ def add_user_to_workspace(
)
except (BaseException, ResourceNotFoundException, ConflictException, ForbiddenException) as e:
raise HTTPException(status_code=e.status_code, detail=e.message)
-
+
@router.post(
'/{workspace_id}/invites/accept',
@@ -132,7 +134,7 @@ def add_user_to_workspace(
)
def accept_workspace_invite(
workspace_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.auth_wrapper)
) -> GetWorkspaceResponse:
"""
Accept workspace invite.
@@ -161,7 +163,7 @@ def accept_workspace_invite(
)
def reject_workspace_invite(
workspace_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.auth_wrapper)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.auth_wrapper)
) -> GetWorkspaceResponse:
"""
Reject workspace invite.
@@ -189,7 +191,7 @@ def reject_workspace_invite(
status.HTTP_403_FORBIDDEN: {'model': ForbiddenError},
status.HTTP_409_CONFLICT: {'model': ConflictError}
},
- dependencies=[Depends(auth_service.workspace_owner_access_authorizer)]
+ dependencies=[Depends(owner_authorizer.authorize)]
)
async def delete_workspace(
workspace_id: int,
@@ -201,7 +203,7 @@ async def delete_workspace(
return response
except (BaseException, ResourceNotFoundException, ConflictException, ForbiddenException) as e:
raise HTTPException(status_code=e.status_code, detail=e.message)
-
+
@router.patch(
path="/{workspace_id}",
@@ -216,7 +218,7 @@ async def delete_workspace(
def patch_workspace(
workspace_id: int,
body: PatchWorkspaceRequest,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_owner_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(owner_authorizer.authorize)
):
try:
response = workspace_service.patch_workspace(
@@ -227,7 +229,7 @@ def patch_workspace(
return response
except (BaseException, ResourceNotFoundException, ForbiddenException) as e:
raise HTTPException(status_code=e.status_code, detail=e.message)
-
+
@router.delete(
path="/{workspace_id}/users/{user_id}",
@@ -242,7 +244,7 @@ def patch_workspace(
async def remove_user_from_workspace(
workspace_id: int,
user_id: int,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(admin_authorizer.authorize)
):
try:
await workspace_service.remove_user_from_workspace(
@@ -267,7 +269,7 @@ def list_workspace_users(
workspace_id: int,
page: int = 0,
page_size: int = 10,
- auth_context: AuthorizationContextData = Depends(auth_service.workspace_access_authorizer)
+ auth_context: AuthorizationContextData = Depends(read_authorizer.authorize)
) -> ListWorkspaceUsersResponse:
try:
return workspace_service.list_workspace_users(
diff --git a/rest/schemas/requests/workspace.py b/rest/schemas/requests/workspace.py
index b59fcd2b..94644251 100644
--- a/rest/schemas/requests/workspace.py
+++ b/rest/schemas/requests/workspace.py
@@ -1,14 +1,14 @@
from pydantic import BaseModel, Field, SecretStr
from typing import Optional
-from database.models.enums import Permission
+from database.models.enums import MembersPermissions
class CreateWorkspaceRequest(BaseModel):
name: str = Field(..., description="Name of the workspace")
-
+
class PatchWorkspaceRequest(BaseModel):
github_access_token: Optional[str] = Field(description='Secret value', default=None)
class AssignWorkspaceRequest(BaseModel):
- permission: Permission
+ permission: MembersPermissions
user_email: str = Field(..., description="Email of the user to be assigned to the workspace")
\ No newline at end of file
diff --git a/rest/services/workspace_service.py b/rest/services/workspace_service.py
index 1c3fb1a9..4b9a5eed 100644
--- a/rest/services/workspace_service.py
+++ b/rest/services/workspace_service.py
@@ -33,7 +33,7 @@ def __init__(self) -> None:
self.logger = get_configured_logger(self.__class__.__name__)
self.workflow_service = WorkflowService()
self.github_token_fernet = Fernet(settings.GITHUB_TOKEN_SECRET_KEY)
-
+
def create_workspace(
self,
@@ -94,7 +94,7 @@ def create_workspace(
)
thread.start()
threads.append(thread)
-
+
for thread in threads:
thread.join()
@@ -189,6 +189,9 @@ def add_user_to_workspace(
if not user:
raise ResourceNotFoundException('User email not found.')
+ if body.permission.value == Permission.owner.value:
+ raise ConflictException('Cannot assign owner permission to user.')
+
for workspace_assoc in user.workspaces:
if workspace_assoc.workspace.id == workspace_id and workspace_assoc.status == UserWorkspaceStatus.pending.value:
raise ConflictException('User already invited to this workspace.')
@@ -255,31 +258,36 @@ def handle_invite_action(self, workspace_id: int, auth_context: AuthorizationCon
return response
async def remove_user_from_workspace(self, workspace_id: int, user_id: int, auth_context: AuthorizationContextData):
- # Can't remove other users if not owner
- if auth_context.user_id != user_id and auth_context.workspace.user_permission != Permission.owner.value:
- raise ForbiddenException()
workspace_infos = self.workspace_repository.find_user_workspaces_members_owners_count(
user_id=user_id,
workspaces_ids=[workspace_id]
)
+
if not workspace_infos:
raise ResourceNotFoundException('User not found in workspace.')
workspace_info = workspace_infos[0]
+ workflows_count = workspace_info.total_workflows
+
+ if workspace_info.permission == Permission.owner.value:
+ raise ForbiddenException('Cannot remove owner from workspace.')
+
+ if workspace_info.members_count == 1 and workflows_count > 0:
+ raise ForbiddenException('Cannot remove last user from workspace with workflows.')
+
# If the workspace has only one member (the user) delete the workspace (even the user not being the owner).
if workspace_info.members_count == 1:
await self.delete_workspace(workspace_id=workspace_id)
return
# If the user is owner and the workspace has only one owner (the user) but has more than one member, delete the workspace.
- if workspace_info.owners_count == 1 and auth_context.user_id == user_id and auth_context.workspace.user_permission == Permission.owner.value:
- await self.delete_workspace(workspace_id=workspace_id)
-
+ # DEPRECATED now we cant remove owners from workspace, owners can only delete workspaces (would be the same action as this one)
+ # if workspace_info.owners_count == 1 and auth_context.user_id == user_id and auth_context.workspace.user_permission == Permission.owner.value:
+ # await self.delete_workspace(workspace_id=workspace_id)
- # If the user is owner but is deleting another user, just remove the user from workspace
- # If user is read only and workspace has more than one member, just remove the user from workspace
- # Or if workspace has more than one owner just remove the user from workspace
+ # If the user is admin/owner but is deleting another user, just remove the user from workspace
+ # If user is read/write and workspace has more than one member, just remove the user from workspace
self.workspace_repository.remove_user_from_workspaces(
workspaces_ids=[workspace_id],
user_id=user_id