Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b9f490e
Basic implementation of admin in user collection in mongodb
ddey2 Oct 25, 2023
2938834
black formatting
ddey2 Oct 25, 2023
5957c59
adding codegen files
ddey2 Oct 26, 2023
977eadc
minor fix to boolean logic
ddey2 Oct 26, 2023
795c825
Adding get_admin dependency
ddey2 Oct 30, 2023
dac6ee2
fixed pytest failure
ddey2 Nov 1, 2023
a377851
Adding admin dependency in authorization of dataset,files, metadata, …
ddey2 Nov 1, 2023
dde0481
allowing admin to view all datasets
ddey2 Nov 1, 2023
9055a50
adding admin in get_roles functions
ddey2 Nov 2, 2023
801d698
adding codegen files
ddey2 Nov 2, 2023
34364d2
modifying test
ddey2 Nov 6, 2023
fa8a351
Merge branch 'main' into 817-add-admin-dependency
ddey2 Nov 8, 2023
1059183
addressing commnets
ddey2 Nov 13, 2023
9d780da
adding the test back
ddey2 Nov 13, 2023
4441f9d
Admin mode implementation
ddey2 Nov 17, 2023
2918156
removing redundant things
ddey2 Nov 17, 2023
9938427
small fix. the name of the method for file was off so there was an er…
tcnichol Nov 22, 2023
a2b9004
fix to visualization rendering
ddey2 Nov 22, 2023
e3f9fd6
Merge branch '830-create-superadmin-mode' into refactor-create-supera…
longshuicy Dec 11, 2023
444bc1b
backend change to depends
longshuicy Dec 11, 2023
8e87b12
codegen
longshuicy Dec 11, 2023
7a32a1b
remove all the adminMode parameter
longshuicy Dec 11, 2023
6a86cac
fix bug
longshuicy Dec 11, 2023
84e78a4
black
longshuicy Dec 11, 2023
369454c
admin can be a part of the profile; no need for additional endpoint
longshuicy Dec 11, 2023
67645a0
fix drop/enable admin toggle
longshuicy Dec 11, 2023
957e9aa
simplify admin logic
longshuicy Dec 11, 2023
e95a34f
rewrite backend
longshuicy Dec 12, 2023
d40f419
codegen
longshuicy Dec 12, 2023
e4448ac
add endpoint to set admin
longshuicy Dec 12, 2023
179ea8a
fix bug
longshuicy Dec 12, 2023
59cfcad
pytest, black format and codegen
longshuicy Dec 12, 2023
0002c07
wire in the frontend toggle action
longshuicy Dec 12, 2023
8d87f45
setting is working but other bugs
longshuicy Dec 12, 2023
a7c6ff0
fix admin toggle
longshuicy Dec 12, 2023
0e625b1
add admin mode toggle to every listing page
longshuicy Dec 12, 2023
9375cc2
fix typo and bug in groups
longshuicy Dec 12, 2023
78cc77d
only check admin in authorization dependency
longshuicy Dec 13, 2023
7d22319
remove unnecessary admin_mode
longshuicy Dec 13, 2023
93395cb
codegen/black and remove redandunt admin_mode flag
longshuicy Dec 13, 2023
69f28aa
fix bug in dataset
longshuicy Dec 13, 2023
48ae259
fix search clause
longshuicy Dec 13, 2023
e050586
add more trigger on adminMode
longshuicy Dec 13, 2023
f6546ef
fix two bugs
longshuicy Dec 13, 2023
2b2a238
fix bug on error
longshuicy Dec 13, 2023
3a79c0e
Merge branch 'main' into refactor-create-superadmin-mode
longshuicy Dec 13, 2023
9ed0fa9
Merge branch 'main' into refactor-create-superadmin-mode
longshuicy Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions backend/app/deps/authorization_deps.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
from beanie import PydanticObjectId
from beanie.operators import Or
from bson import ObjectId
from fastapi import Depends, HTTPException

from app.keycloak_auth import get_current_username
from app.models.authorization import RoleType, AuthorizationDB
from app.models.datasets import DatasetDB, DatasetStatus
from app.models.files import FileOut, FileDB
from app.models.groups import GroupOut, GroupDB
from app.models.files import FileDB
from app.models.groups import GroupDB
from app.models.metadata import MetadataDB
from app.models.pyobjectid import PyObjectId
from app.routers.authentication import get_admin
from app.routers.authentication import get_admin_mode


async def get_role(
dataset_id: str,
current_user=Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin=Depends(get_admin),
) -> RoleType:
"""Returns the role a specific user has on a dataset. If the user is a creator (owner), they are not listed in
the user_ids list."""
if admin and admin_mode:
return RoleType.OWNER

authorization = await AuthorizationDB.find_one(
AuthorizationDB.dataset_id == PyObjectId(dataset_id),
Or(
Expand All @@ -32,7 +37,12 @@ async def get_role(
async def get_role_by_file(
file_id: str,
current_user=Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin=Depends(get_admin),
) -> RoleType:
if admin and admin_mode:
return RoleType.OWNER

if (file := await FileDB.get(PydanticObjectId(file_id))) is not None:
authorization = await AuthorizationDB.find_one(
AuthorizationDB.dataset_id == file.dataset_id,
Expand Down Expand Up @@ -66,7 +76,12 @@ async def get_role_by_file(
async def get_role_by_metadata(
metadata_id: str,
current_user=Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin=Depends(get_admin),
) -> RoleType:
if admin and admin_mode:
return RoleType.OWNER

if (md_out := await MetadataDB.get(PydanticObjectId(metadata_id))) is not None:
resource_type = md_out.resource.collection
resource_id = md_out.resource.resource_id
Expand Down Expand Up @@ -97,7 +112,12 @@ async def get_role_by_metadata(
async def get_role_by_group(
group_id: str,
current_user=Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin=Depends(get_admin),
) -> RoleType:
if admin and admin_mode:
return RoleType.OWNER

if (group := await GroupDB.get(group_id)) is not None:
if group.creator == current_user:
# Creator can do everything
Expand Down Expand Up @@ -148,12 +168,13 @@ async def __call__(
self,
dataset_id: str,
current_user: str = Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin: bool = Depends(get_admin),
):
# TODO: Make sure we enforce only one role per user per dataset, or find_one could yield wrong answer here.

# If the current user is admin, user has access irrespective of any role assigned
if admin:
# If the current user is admin and has turned on admin_mode, user has access irrespective of any role assigned
if admin and admin_mode:
return True

# Else check role assigned to the user
Expand Down Expand Up @@ -204,10 +225,11 @@ async def __call__(
self,
file_id: str,
current_user: str = Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin: bool = Depends(get_admin),
):
# If the current user is admin, user has access irrespective of any role assigned
if admin:
# If the current user is admin and has turned on admin_mode, user has access irrespective of any role assigned
if admin and admin_mode:
return True

# Else check role assigned to the user
Expand Down Expand Up @@ -241,10 +263,11 @@ async def __call__(
self,
metadata_id: str,
current_user: str = Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin: bool = Depends(get_admin),
):
# If the current user is admin, user has access irrespective of any role assigned
if admin:
# If the current user is admin and has turned on admin_mode, user has access irrespective of any role assigned
if admin and admin_mode:
return True

# Else check role assigned to the user
Expand Down Expand Up @@ -307,10 +330,11 @@ async def __call__(
self,
group_id: str,
current_user: str = Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin: bool = Depends(get_admin),
):
# If the current user is admin, user has access irrespective of any role assigned
if admin:
# If the current user is admin and has turned on admin_mode, user has access irrespective of any role assigned
if admin and admin_mode:
return True

# Else check role assigned to the user
Expand Down Expand Up @@ -377,9 +401,14 @@ async def __call__(
return False


def access(user_role: RoleType, role_required: RoleType) -> bool:
"""Enforce implied role hierarchy OWNER > EDITOR > UPLOADER > VIEWER"""
if user_role == RoleType.OWNER:
def access(
user_role: RoleType,
role_required: RoleType,
admin_mode: bool = Depends(get_admin_mode),
admin: bool = Depends(get_admin),
) -> bool:
"""Enforce implied role hierarchy ADMIN = OWNER > EDITOR > UPLOADER > VIEWER"""
if user_role == RoleType.OWNER or (admin and admin_mode):
return True
elif user_role == RoleType.EDITOR and role_required in [
RoleType.EDITOR,
Expand Down
1 change: 1 addition & 0 deletions backend/app/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class UserLogin(BaseModel):

class UserDoc(Document, UserBase):
admin: bool
admin_mode: bool = False

class Settings:
name = "users"
Expand Down
53 changes: 52 additions & 1 deletion backend/app/routers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ async def authenticate_user(email: str, password: str):
return user


async def get_admin(dataset_id: str = None, current_username=Depends(get_current_user)):
@router.get("/users/me/is_admin", response_model=bool)
async def get_admin(
dataset_id: str = None, current_username=Depends(get_current_user)
) -> bool:
if (
current_user := await UserDB.find_one(UserDB.email == current_username.email)
) is not None:
Expand All @@ -103,11 +106,59 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current
and (dataset_db := await DatasetDB.get(PydanticObjectId(dataset_id)))
is not None
):
# TODO: question regarding resource creator is considered as admin of the resource?
return dataset_db.creator.email == current_username.email
else:
return False


@router.get("/users/me/admin_mode")
async def get_admin_mode(current_username=Depends(get_current_user)) -> bool:
"""Get Admin mode from User Object."""
if (
current_user := await UserDB.find_one(UserDB.email == current_username.email)
) is not None:
if current_user.admin_mode is not None:
return current_user.admin_mode
else:
return False
else:
raise HTTPException(
status_code=404,
detail="User doesn't exist.",
headers={"WWW-Authenticate": "Bearer"},
)


@router.post("/users/me/admin_mode", response_model=bool)
async def set_admin_mode(
admin_mode_on: bool,
admin=Depends(get_admin),
current_username=Depends(get_current_user),
) -> bool:
"""Set Admin mode from User Object."""
if (
current_user := await UserDB.find_one(UserDB.email == current_username.email)
) is not None:
# only admin can set admin mode
if admin:
current_user.admin_mode = admin_mode_on
await current_user.replace()
return current_user.admin_mode
else:
raise HTTPException(
status_code=403,
detail="You are not admin yet. Only admin can set admin mode.",
headers={"WWW-Authenticate": "Bearer"},
)
else:
raise HTTPException(
status_code=404,
detail="User doesn't exist.",
headers={"WWW-Authenticate": "Bearer"},
)


@router.post("/users/set_admin/{useremail}", response_model=UserOut)
async def set_admin(
useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin)
Expand Down
47 changes: 20 additions & 27 deletions backend/app/routers/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from bson import ObjectId
from fastapi import APIRouter, Depends
from fastapi.exceptions import HTTPException

from app.dependencies import get_elasticsearchclient
from app.deps.authorization_deps import (
Authorization,
Expand All @@ -29,7 +30,7 @@
from app.models.groups import GroupDB
from app.models.pyobjectid import PyObjectId
from app.models.users import UserDB
from app.routers.authentication import get_admin
from app.routers.authentication import get_admin, get_admin_mode
from app.search.index import index_dataset

router = APIRouter()
Expand Down Expand Up @@ -69,22 +70,24 @@ async def save_authorization(
async def get_dataset_role(
dataset_id: str,
current_user=Depends(get_current_username),
admin_mode: bool = Depends(get_admin_mode),
admin=Depends(get_admin),
):
"""Retrieve role of user for a specific dataset."""
# Get group id and the associated users from authorization
if admin:
auth_db = await AuthorizationDB.find_one(
AuthorizationDB.dataset_id == PyObjectId(dataset_id)
)
else:
auth_db = await AuthorizationDB.find_one(
AuthorizationDB.dataset_id == PyObjectId(dataset_id),
criteria = []
if not admin or not admin_mode:
criteria.append(
Or(
AuthorizationDB.creator == current_user,
AuthorizationDB.user_ids == current_user,
),
)
)

auth_db = await AuthorizationDB.find_one(
AuthorizationDB.dataset_id == PyObjectId(dataset_id),
*criteria,
)
if auth_db is None:
if (
current_dataset := await DatasetDB.get(PydanticObjectId(dataset_id))
Expand All @@ -107,18 +110,20 @@ async def get_dataset_role(
return auth_db.dict()


@router.get("/datasets/{dataset_id}/role/viewer")
@router.get("/datasets/{dataset_id}/role/viewer}")
async def get_dataset_role_viewer(
dataset_id: str, allow: bool = Depends(Authorization("viewer"))
dataset_id: str,
allow: bool = Depends(Authorization("viewer")),
):
"""Used for testing only. Returns true if user has viewer permission on dataset, otherwise throws a 403 Forbidden HTTP exception.
See `routers/authorization.py` for more info."""
return {"dataset_id": dataset_id, "allow": allow}


@router.get("/datasets/{dataset_id}/role/owner")
@router.get("/datasets/{dataset_id}/role/owner}")
async def get_dataset_role_owner(
dataset_id: str, allow: bool = Depends(Authorization("owner"))
dataset_id: str,
allow: bool = Depends(Authorization("owner")),
):
"""Used for testing only. Returns true if user has owner permission on dataset, otherwise throws a 403 Forbidden HTTP exception.
See `routers/authorization.py` for more info."""
Expand All @@ -130,25 +135,17 @@ async def get_file_role(
file_id: str,
current_user=Depends(get_current_username),
role: RoleType = Depends(get_role_by_file),
admin=Depends(get_admin),
):
# admin is a superuser and has all the privileges
if admin:
return RoleType.OWNER
"""Retrieve role of user for an individual file. Role cannot change between file versions."""
return role


@router.get("/metadata/{metadata_id}/role", response_model=AuthorizationMetadata)
@router.get("/metadata/{metadata_id}/role}", response_model=AuthorizationMetadata)
async def get_metadata_role(
metadata_id: str,
current_user=Depends(get_current_username),
role: RoleType = Depends(get_role_by_metadata),
admin=Depends(get_admin),
):
# admin is a superuser and has all the privileges
if admin:
return RoleType.OWNER
"""Retrieve role of user for group. Group roles can be OWNER, EDITOR, or VIEWER (for regular Members)."""
return role

Expand All @@ -158,11 +155,7 @@ async def get_group_role(
group_id: str,
current_user=Depends(get_current_username),
role: RoleType = Depends(get_role_by_group),
admin=Depends(get_admin),
):
# admin is a superuser and has all the privileges
if admin:
return RoleType.OWNER
"""Retrieve role of user on a particular group (i.e. whether they can change group memberships)."""
return role

Expand Down Expand Up @@ -342,7 +335,7 @@ async def remove_dataset_user_role(
raise HTTPException(status_code=404, detail=f"Dataset {dataset_id} not found")


@router.get("/datasets/{dataset_id}/roles", response_model=DatasetRoles)
@router.get("/datasets/{dataset_id}/roles}", response_model=DatasetRoles)
async def get_dataset_roles(
dataset_id: str,
allow: bool = Depends(Authorization("editor")),
Expand Down
10 changes: 6 additions & 4 deletions backend/app/routers/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
from app.models.users import UserOut
from app.rabbitmq.listeners import submit_dataset_job
from app.routers.authentication import get_admin
from app.routers.files import add_file_entry, add_local_file_entry, remove_file_entry
from app.routers.authentication import get_admin_mode
from app.routers.files import add_file_entry, remove_file_entry, add_local_file_entry
from app.search.connect import (
delete_document_by_id,
)
Expand Down Expand Up @@ -212,8 +213,9 @@ async def get_datasets(
limit: int = 10,
mine: bool = False,
admin=Depends(get_admin),
admin_mode: bool = Depends(get_admin_mode),
):
if admin:
if admin and admin_mode:
datasets = await DatasetDBViewList.find(
sort=(-DatasetDBViewList.created),
skip=skip,
Expand Down Expand Up @@ -257,10 +259,10 @@ async def get_dataset_files(
dataset_id: str,
folder_id: Optional[str] = None,
authenticated: bool = Depends(CheckStatus("AUTHENTICATED")),
allow: bool = Depends(Authorization("viewer")),
user_id=Depends(get_user),
skip: int = 0,
limit: int = 10,
allow: bool = Depends(Authorization("viewer")),
):
if authenticated:
query = [
Expand Down Expand Up @@ -380,10 +382,10 @@ async def get_dataset_folders(
dataset_id: str,
parent_folder: Optional[str] = None,
user_id=Depends(get_user),
allow: bool = Depends(Authorization("viewer")),
authenticated: bool = Depends(CheckStatus("authenticated")),
skip: int = 0,
limit: int = 10,
allow: bool = Depends(Authorization("viewer")),
):
if (await DatasetDB.get(PydanticObjectId(dataset_id))) is not None:
if authenticated:
Expand Down
Loading