diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py index cb32d894b..ac5997637 100644 --- a/backend/app/deps/authorization_deps.py +++ b/backend/app/deps/authorization_deps.py @@ -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( @@ -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, @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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, diff --git a/backend/app/models/users.py b/backend/app/models/users.py index fd168d69f..23a5e570b 100644 --- a/backend/app/models/users.py +++ b/backend/app/models/users.py @@ -25,6 +25,7 @@ class UserLogin(BaseModel): class UserDoc(Document, UserBase): admin: bool + admin_mode: bool = False class Settings: name = "users" diff --git a/backend/app/routers/authentication.py b/backend/app/routers/authentication.py index f3e82736d..a0643a729 100644 --- a/backend/app/routers/authentication.py +++ b/backend/app/routers/authentication.py @@ -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: @@ -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) diff --git a/backend/app/routers/authorization.py b/backend/app/routers/authorization.py index ca66d607f..ef9ddf930 100644 --- a/backend/app/routers/authorization.py +++ b/backend/app/routers/authorization.py @@ -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, @@ -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() @@ -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)) @@ -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.""" @@ -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 @@ -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 @@ -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")), diff --git a/backend/app/routers/datasets.py b/backend/app/routers/datasets.py index 10bad0c93..5caa22537 100644 --- a/backend/app/routers/datasets.py +++ b/backend/app/routers/datasets.py @@ -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, ) @@ -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, @@ -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 = [ @@ -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: diff --git a/backend/app/routers/elasticsearch.py b/backend/app/routers/elasticsearch.py index 4c0c63057..6c463b8b0 100644 --- a/backend/app/routers/elasticsearch.py +++ b/backend/app/routers/elasticsearch.py @@ -6,15 +6,20 @@ from app.config import settings from app.keycloak_auth import get_current_username from app.routers.authentication import get_admin +from app.routers.authentication import get_admin_mode from app.search.connect import connect_elasticsearch, search_index router = APIRouter() -def _add_permissions_clause(query, username: str, admin: bool = Depends(get_admin)): +def _add_permissions_clause( + query, + username: str, + admin_mode: bool, + admin: bool, +): """Append filter to Elasticsearch object that restricts permissions based on the requesting user.""" # TODO: Add public filter once added - user_clause = { "bool": { "should": [ @@ -31,7 +36,7 @@ def _add_permissions_clause(query, username: str, admin: bool = Depends(get_admi continue # last line json_content = json.loads(content) if "query" in json_content: - if admin: + if admin_mode and admin: json_content["query"] = {"bool": {"must": [json_content["query"]]}} else: json_content["query"] = { @@ -42,9 +47,15 @@ def _add_permissions_clause(query, username: str, admin: bool = Depends(get_admi @router.put("/search", response_model=str) -async def search(index_name: str, query: str, username=Depends(get_current_username)): +async def search( + index_name: str, + query: str, + username=Depends(get_current_username), + admin=Depends(get_admin), + admin_mode: bool = Depends(get_admin_mode), +): es = await connect_elasticsearch() - query = _add_permissions_clause(query, username) + query = _add_permissions_clause(query, username, admin, admin_mode) return search_index(es, index_name, query) @@ -52,9 +63,11 @@ async def search(index_name: str, query: str, username=Depends(get_current_usern async def msearch( request: Request, username=Depends(get_current_username), + admin=Depends(get_admin), + admin_mode: bool = Depends(get_admin_mode), ): es = await connect_elasticsearch() query = await request.body() - query = _add_permissions_clause(query, username) + query = _add_permissions_clause(query, username, admin, admin_mode) r = search_index(es, [settings.elasticsearch_index], query) return r diff --git a/backend/app/routers/groups.py b/backend/app/routers/groups.py index 5c6115ccc..2c2af59d9 100644 --- a/backend/app/routers/groups.py +++ b/backend/app/routers/groups.py @@ -11,6 +11,7 @@ from app.models.authorization import RoleType from app.models.groups import GroupOut, GroupIn, GroupDB, GroupBase, Member from app.models.users import UserOut, UserDB +from app.routers.authentication import get_admin_mode, get_admin router = APIRouter() @@ -33,6 +34,8 @@ async def get_groups( user_id=Depends(get_user), skip: int = 0, limit: int = 10, + admin_mode: bool = Depends(get_admin_mode), + admin=Depends(get_admin), ): """Get a list of all Groups in the db the user is a member/owner of. @@ -42,11 +45,17 @@ async def get_groups( """ + criteria_list = [] + if not admin or not admin_mode: + criteria_list.append( + Or( + GroupDB.creator == user_id, + GroupDB.users.user.email == user_id, + ) + ) + groups = await GroupDB.find( - Or( - GroupDB.creator == user_id, - GroupDB.users.user.email == user_id, - ), + *criteria_list, sort=(-GroupDB.created), skip=skip, limit=limit, @@ -60,6 +69,8 @@ async def search_group( user_id=Depends(get_user), skip: int = 0, limit: int = 10, + admin_mode: bool = Depends(get_admin_mode), + admin=Depends(get_admin), ): """Search all groups in the db based on text. @@ -69,13 +80,20 @@ async def search_group( limit -- restrict number of records to be returned (i.e. for pagination) """ - # user has to be the creator or member first; then apply search - groups = await GroupDB.find( - Or(GroupDB.creator == user_id, GroupDB.users.user.email == user_id), + criteria_list = [ Or( RegEx(field=GroupDB.name, pattern=search_term), RegEx(field=GroupDB.description, pattern=search_term), ), + ] + if not admin or not admin_mode: + criteria_list.append( + Or(GroupDB.creator == user_id, GroupDB.users.user.email == user_id) + ) + + # user has to be the creator or member first; then apply search + groups = await GroupDB.find( + *criteria_list, skip=skip, limit=limit, ).to_list() diff --git a/backend/app/routers/metadata_datasets.py b/backend/app/routers/metadata_datasets.py index e665c9d68..08598498b 100644 --- a/backend/app/routers/metadata_datasets.py +++ b/backend/app/routers/metadata_datasets.py @@ -12,7 +12,7 @@ from app.deps.authorization_deps import Authorization from app.keycloak_auth import get_current_user, UserOut from app.models.datasets import DatasetOut, DatasetDB -from app.models.listeners import LegacyEventListenerIn, EventListenerDB +from app.models.listeners import EventListenerDB from app.models.metadata import ( MongoDBRef, MetadataAgent, diff --git a/backend/app/tests/test_authorization.py b/backend/app/tests/test_authorization.py index d2501e706..f6e473c71 100644 --- a/backend/app/tests/test_authorization.py +++ b/backend/app/tests/test_authorization.py @@ -1,6 +1,7 @@ from fastapi.testclient import TestClient + from app.config import settings -from app.tests.utils import create_dataset, create_group +from app.tests.utils import create_dataset def test_create(client: TestClient, headers: dict): @@ -12,3 +13,35 @@ def test_create(client: TestClient, headers: dict): headers=headers, ) assert response.status_code == 200 + + +def test_get_admin_info(client: TestClient, headers: dict): + response = client.get( + f"{settings.API_V2_STR}/users/me/is_admin", + headers=headers, + ) + assert response.status_code == 200 + assert response.json() == True + + response = client.get( + f"{settings.API_V2_STR}/users/me/admin_mode", + headers=headers, + ) + assert response.status_code == 200 + assert response.json() == False + + +def test_set_admin_mode(client: TestClient, headers: dict): + response = client.post( + f"{settings.API_V2_STR}/users/me/admin_mode?admin_mode_on=True", + headers=headers, + ) + assert response.status_code == 200 + assert response.json() == True + + response = client.post( + f"{settings.API_V2_STR}/users/me/admin_mode?admin_mode_on=False", + headers=headers, + ) + assert response.status_code == 200 + assert response.json() == False diff --git a/frontend/src/actions/dataset.js b/frontend/src/actions/dataset.js index aa970045d..2024eefca 100644 --- a/frontend/src/actions/dataset.js +++ b/frontend/src/actions/dataset.js @@ -121,7 +121,10 @@ export function fetchFilesInDataset(datasetId, folderId, skip, limit) { }) .catch((reason) => { dispatch( - handleErrors(reason, fetchFilesInDataset(datasetId, folderId, skip, limit)) + handleErrors( + reason, + fetchFilesInDataset(datasetId, folderId, skip, limit) + ) ); }); }; @@ -146,7 +149,10 @@ export function fetchFoldersInDataset(datasetId, parentFolder, skip, limit) { }) .catch((reason) => { dispatch( - handleErrors(reason, fetchFoldersInDataset(datasetId, parentFolder, skip, limit)) + handleErrors( + reason, + fetchFoldersInDataset(datasetId, parentFolder, skip, limit) + ) ); }); }; diff --git a/frontend/src/actions/folder.js b/frontend/src/actions/folder.js index 348305f38..35ce92d21 100644 --- a/frontend/src/actions/folder.js +++ b/frontend/src/actions/folder.js @@ -1,37 +1,46 @@ -import {V2} from "../openapi"; -import {handleErrors} from "./common"; +import { V2 } from "../openapi"; +import { handleErrors } from "./common"; export const FOLDER_ADDED = "FOLDER_ADDED"; -export function folderAdded(datasetId, folderName, parentFolder = null){ + +export function folderAdded(datasetId, folderName, parentFolder = null) { return (dispatch) => { - const folder = {"name": folderName, "parent_folder": parentFolder} - return V2.DatasetsService.addFolderApiV2DatasetsDatasetIdFoldersPost(datasetId, folder) - .then(json => { + const folder = { name: folderName, parent_folder: parentFolder }; + return V2.DatasetsService.addFolderApiV2DatasetsDatasetIdFoldersPost( + datasetId, + folder + ) + .then((json) => { dispatch({ type: FOLDER_ADDED, folder: json, receivedAt: Date.now(), }); }) - .catch(reason => { - dispatch(handleErrors(reason, folderAdded(datasetId, folderName, parentFolder))); + .catch((reason) => { + dispatch( + handleErrors(reason, folderAdded(datasetId, folderName, parentFolder)) + ); }); }; } export const GET_FOLDER_PATH = "GET_FOLDER_PATH"; -export function fetchFolderPath(folderId){ + +export function fetchFolderPath(folderId) { return (dispatch) => { if (folderId != null) { - return V2.FoldersService.downloadFolderApiV2FoldersFolderIdPathGet(folderId) - .then(json => { + return V2.FoldersService.downloadFolderApiV2FoldersFolderIdPathGet( + folderId + ) + .then((json) => { dispatch({ type: GET_FOLDER_PATH, folderPath: json, receivedAt: Date.now(), }); }) - .catch(reason => { + .catch((reason) => { dispatch(handleErrors(reason, fetchFolderPath(folderId))); }); } else { @@ -45,17 +54,21 @@ export function fetchFolderPath(folderId){ } export const FOLDER_DELETED = "FOLDER_DELETED"; -export function folderDeleted(datasetId, folderId){ + +export function folderDeleted(datasetId, folderId) { return (dispatch) => { - return V2.DatasetsService.deleteFolderApiV2DatasetsDatasetIdFoldersFolderIdDelete(datasetId, folderId) - .then(json => { + return V2.DatasetsService.deleteFolderApiV2DatasetsDatasetIdFoldersFolderIdDelete( + datasetId, + folderId + ) + .then((json) => { dispatch({ type: FOLDER_DELETED, - folder: {"id":folderId}, + folder: { id: folderId }, receivedAt: Date.now(), }); }) - .catch(reason => { + .catch((reason) => { dispatch(handleErrors(reason, folderDeleted(datasetId, folderId))); }); }; diff --git a/frontend/src/actions/group.js b/frontend/src/actions/group.js index 7f17818db..01070b691 100644 --- a/frontend/src/actions/group.js +++ b/frontend/src/actions/group.js @@ -168,10 +168,7 @@ export const UPDATE_GROUP = "UPDATE_GROUP"; export function updateGroup(groupId, formData) { return (dispatch) => { - return V2.GroupsService.editGroupApiV2GroupsGroupIdPut( - groupId, - formData - ) + return V2.GroupsService.editGroupApiV2GroupsGroupIdPut(groupId, formData) .then((json) => { dispatch({ type: UPDATE_GROUP, diff --git a/frontend/src/actions/metadata.js b/frontend/src/actions/metadata.js index 9597fb2dc..8314de07d 100644 --- a/frontend/src/actions/metadata.js +++ b/frontend/src/actions/metadata.js @@ -94,11 +94,11 @@ export function deleteMetadataDefinition(metadataDefinitionId) { export const SEARCH_METADATA_DEFINITIONS = "SEARCH_METADATA_DEFINITIONS"; export function searchMetadataDefinitions(searchTerm, skip, limit) { - if (searchTerm.trim() === '') { - // Search term is empty. - console.log('Please enter a search term'); - return; - } + if (searchTerm.trim() === "") { + // Search term is empty. + console.log("Please enter a search term"); + return; + } return (dispatch) => { return V2.MetadataService.searchMetadataDefinitionApiV2MetadataDefinitionSearchSearchTermGet( searchTerm, diff --git a/frontend/src/actions/user.js b/frontend/src/actions/user.js index a4eca2fa6..145e70e5e 100644 --- a/frontend/src/actions/user.js +++ b/frontend/src/actions/user.js @@ -127,6 +127,42 @@ export function fetchAllUsers(skip = 0, limit = 101) { }; } +export const TOGGLE_ADMIN_MODE = "TOGGLE_ADMIN_MODE"; + +export function toggleAdminMode(adminModeOn) { + return (dispatch) => { + return V2.LoginService.setAdminModeApiV2UsersMeAdminModePost(adminModeOn) + .then((json) => { + dispatch({ + type: TOGGLE_ADMIN_MODE, + adminMode: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(toggleAdminMode(adminModeOn)); + }); + }; +} + +export const GET_ADMIN_MODE_STATUS = "GET_ADMIN_MODE_STATUS"; + +export function getAdminModeStatus() { + return (dispatch) => { + return V2.LoginService.getAdminModeApiV2UsersMeAdminModeGet() + .then((json) => { + dispatch({ + type: GET_ADMIN_MODE_STATUS, + adminMode: json, + receivedAt: Date.now(), + }); + }) + .catch((reason) => { + dispatch(getAdminModeStatus()); + }); + }; +} + export const PREFIX_SEARCH_USERS = "PREFIX_SEARCH_USERS"; export function prefixSearchAllUsers(text = "", skip = 0, limit = 101) { diff --git a/frontend/src/components/Explore.tsx b/frontend/src/components/Explore.tsx index d28b67021..0cfb6fea5 100644 --- a/frontend/src/components/Explore.tsx +++ b/frontend/src/components/Explore.tsx @@ -28,6 +28,7 @@ export const Explore = (): JSX.Element => { mine: boolean | undefined ) => dispatch(fetchDatasets(skip, limit, mine)); const datasets = useSelector((state: RootState) => state.dataset.datasets); + const adminMode = useSelector((state: RootState) => state.user.adminMode); // TODO add option to determine limit number; default show 5 datasets each time const [currPageNum, setCurrPageNum] = useState(0); @@ -45,6 +46,11 @@ export const Explore = (): JSX.Element => { listDatasets(0, limit, mine); }, []); + // Admin mode will fetch all datasets + useEffect(() => { + listDatasets(0, limit, mine); + }, [adminMode]); + // fetch thumbnails from each individual dataset/id calls useEffect(() => { // disable flipping if reaches the last page @@ -85,7 +91,6 @@ export const Explore = (): JSX.Element => { {/*Error Message dialogue*/} - diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 448c6f670..32c89dd2b 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -30,7 +30,12 @@ import { getCurrEmail } from "../utils/common"; import VpnKeyIcon from "@mui/icons-material/VpnKey"; import LogoutIcon from "@mui/icons-material/Logout"; import { EmbeddedSearch } from "./search/EmbeddedSearch"; -import { fetchUserProfile } from "../actions/user"; +import { + fetchUserProfile, + getAdminModeStatus as getAdminModeStatusAction, + toggleAdminMode as toggleAdminModeAction, +} from "../actions/user"; +import { AdminPanelSettings } from "@mui/icons-material"; import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; const drawerWidth = 240; @@ -99,15 +104,25 @@ const link = { }; export default function PersistentDrawerLeft(props) { + const dispatch = useDispatch(); const { children } = props; const theme = useTheme(); const [open, setOpen] = React.useState(false); const [embeddedSearchHidden, setEmbeddedSearchHidden] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const isMenuOpen = Boolean(anchorEl); - const profile = useSelector((state: RootState) => state.user.profile); - const dispatch = useDispatch(); - const fetchProfile = () => dispatch(fetchUserProfile()); + const currUserProfile = useSelector((state: RootState) => state.user.profile); + const adminMode = useSelector((state: RootState) => state.user.adminMode); + + const fetchCurrUserProfile = () => dispatch(fetchUserProfile()); + const toggleAdminMode = (adminModeOn: boolean) => + dispatch(toggleAdminModeAction(adminModeOn)); + const getAdminModeStatus = () => dispatch(getAdminModeStatusAction()); + + useEffect(() => { + fetchCurrUserProfile(); + getAdminModeStatus(); + }, []); const handleDrawerOpen = () => { setOpen(true); @@ -133,7 +148,7 @@ export default function PersistentDrawerLeft(props) { } else { setEmbeddedSearchHidden(false); } - fetchProfile(); + fetchCurrUserProfile(); }, [location]); const loggedOut = useSelector((state: RootState) => state.error.loggedOut); @@ -223,6 +238,32 @@ export default function PersistentDrawerLeft(props) { User Profile + {currUserProfile.admin ? ( + <> + toggleAdminMode(!adminMode)}> + {adminMode ? ( + <> + + + + Drop Admin Mode + + ) : ( + <> + + + + Enable Admin Mode + + )} + + + + + ) : ( + <> + )} + @@ -284,7 +325,7 @@ export default function PersistentDrawerLeft(props) { - {profile.admin ? ( + {currUserProfile.admin ? ( <> diff --git a/frontend/src/components/datasets/ChangeDatasetRoleModal.tsx b/frontend/src/components/datasets/ChangeDatasetRoleModal.tsx index 4f9d7363d..7009cd9bc 100644 --- a/frontend/src/components/datasets/ChangeDatasetRoleModal.tsx +++ b/frontend/src/components/datasets/ChangeDatasetRoleModal.tsx @@ -1,29 +1,45 @@ import React, { useEffect, useState } from "react"; -import { Alert, Autocomplete, Button, Collapse, Container, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, IconButton, InputLabel, MenuItem, Select, TextField, Typography } from "@mui/material"; -import {useParams} from "react-router-dom"; +import { + Alert, + Button, + Collapse, + Container, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + Typography, +} from "@mui/material"; +import { useParams } from "react-router-dom"; import { setDatasetUserRole } from "../../actions/dataset"; import { useDispatch } from "react-redux"; import CloseIcon from "@mui/icons-material/Close"; - type ChangeDatasetRoleProps = { - open: boolean, - handleClose: any, - datasetName: string, - currentRole: string, + open: boolean; + handleClose: any; + datasetName: string; + currentRole: string; currentUser: string; -} +}; export default function ChangeDatasetRoleModal(props: ChangeDatasetRoleProps) { const dispatch = useDispatch(); - const { open, handleClose, datasetName, currentRole , currentUser} = props; - const {datasetId} = useParams<{ datasetId?: string }>(); + const { open, handleClose, datasetName, currentRole, currentUser } = props; + const { datasetId } = useParams<{ datasetId?: string }>(); const [email, setEmail] = useState(currentUser); const [role, setRole] = useState(currentRole); const [showSuccessAlert, setShowSuccessAlert] = useState(false); - const setUserRole = (datasetId: string, username: string, role: string) => dispatch(setDatasetUserRole(datasetId, username, role)); + const setUserRole = (datasetId: string, username: string, role: string) => + dispatch(setDatasetUserRole(datasetId, username, role)); // component did mount useEffect(() => { @@ -39,20 +55,27 @@ export default function ChangeDatasetRoleModal(props: ChangeDatasetRoleProps) { return ( - + }} + > Share dataset '{datasetName}' Change role for user {currentUser} -
+
Status Role + - - : - member.editor !== undefined && member.editor ? - "Editor" : "Member" - } - {/*only owner or editor are allowed to modify roles of the member*/} - - { - editRoleOn ? + + + ) : member.editor !== undefined && member.editor ? ( + "Editor" + ) : ( + "Member" + )} + {/*only owner or editor are allowed to modify roles of the member*/} + + {editRoleOn ? ( - - + + - - + + - : - { - setEditRoleOn(true); - }}> + ) : ( + { + setEditRoleOn(true); + }} + > - } - - - } - {member.user.email == creatorEmail && - + )} + - } - {member.user.email != creatorEmail && + )} + {member.user.email == creatorEmail && ( + + )} + {member.user.email != creatorEmail && ( - {/*only owner or editor are allowed to delete*/} - - { - setSelectMemberUsername(member.user.email) - setDeleteMemberConfirmOpen(true); - }}> - - - - - } + {/*only owner or editor are allowed to delete*/} + + { + setSelectMemberUsername(member.user.email); + setDeleteMemberConfirmOpen(true); + }} + > + + + + + )} - ) + ); } diff --git a/frontend/src/components/listeners/ExtractionHistory.tsx b/frontend/src/components/listeners/ExtractionHistory.tsx index 2c0c6fe49..fac3191e7 100644 --- a/frontend/src/components/listeners/ExtractionHistory.tsx +++ b/frontend/src/components/listeners/ExtractionHistory.tsx @@ -116,6 +116,7 @@ export const ExtractionHistory = (): JSX.Element => { const listeners = useSelector((state: RootState) => state.listener.listeners); const jobs = useSelector((state: RootState) => state.listener.jobs); + const adminMode = useSelector((state: RootState) => state.user.adminMode); const [errorOpen, setErrorOpen] = useState(false); const [currPageNum, setCurrPageNum] = useState(0); @@ -133,6 +134,11 @@ export const ExtractionHistory = (): JSX.Element => { listListenerJobs(null, null, null, null, null, null, 0, 100); }, []); + useEffect(() => { + listListeners(skip, limit, 0, null, null); + listListenerJobs(null, null, null, null, null, null, 0, 100); + }, [adminMode]); + useEffect(() => { if (selectedExtractor) { listListenerJobs( diff --git a/frontend/src/components/metadata/DisplayListenerMetadata.tsx b/frontend/src/components/metadata/DisplayListenerMetadata.tsx index b1d94a8c8..e899157f1 100644 --- a/frontend/src/components/metadata/DisplayListenerMetadata.tsx +++ b/frontend/src/components/metadata/DisplayListenerMetadata.tsx @@ -1,39 +1,54 @@ -import React, {useEffect, useState} from "react"; -import {Box, Grid, Typography} from "@mui/material"; -import {metadataConfig} from "../../metadata.config"; -import {useSelector, useDispatch} from "react-redux"; -import {RootState} from "../../types/data"; -import {fetchDatasetMetadata, fetchFileMetadata, fetchMetadataDefinitions} from "../../actions/metadata"; -import {Agent} from "./Agent"; -import {MetadataDeleteButton} from "./widgets/MetadataDeleteButton"; -import {ListenerMetadataEntry} from "../metadata/ListenerMetadataEntry"; +import React, { useEffect } from "react"; +import { Grid } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "../../types/data"; +import { + fetchDatasetMetadata, + fetchFileMetadata, + fetchMetadataDefinitions, +} from "../../actions/metadata"; +import { ListenerMetadataEntry } from "../metadata/ListenerMetadataEntry"; import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; type MetadataType = { - updateMetadata: any, - deleteMetadata: any, - resourceType: string | undefined, - resourceId: string | undefined, - version: number | undefined, -} + updateMetadata: any; + deleteMetadata: any; + resourceType: string | undefined; + resourceId: string | undefined; + version: number | undefined; +}; /* This is the interface displayed already created metadata and allow eidts Uses only the list of metadata */ export const DisplayListenerMetadata = (props: MetadataType) => { - - const {updateMetadata, deleteMetadata, resourceType, resourceId, version} = props; + const { updateMetadata, deleteMetadata, resourceType, resourceId, version } = + props; const dispatch = useDispatch(); - const getMetadatDefinitions = (name: string | null, skip: number, limit: number) => dispatch(fetchMetadataDefinitions(name, skip, limit)); - const metadataDefinitionList = useSelector((state: RootState) => state.metadata.metadataDefinitionList); - const listDatasetMetadata = (datasetId: string | undefined) => dispatch(fetchDatasetMetadata(datasetId)); - const listFileMetadata = (fileId: string | undefined, version: number | undefined) => dispatch(fetchFileMetadata(fileId, version)); - const datasetMetadataList = useSelector((state: RootState) => state.metadata.datasetMetadataList); - const fileMetadataList = useSelector((state: RootState) => state.metadata.fileMetadataList); + const getMetadatDefinitions = ( + name: string | null, + skip: number, + limit: number + ) => dispatch(fetchMetadataDefinitions(name, skip, limit)); + const metadataDefinitionList = useSelector( + (state: RootState) => state.metadata.metadataDefinitionList + ); + const listDatasetMetadata = (datasetId: string | undefined) => + dispatch(fetchDatasetMetadata(datasetId)); + const listFileMetadata = ( + fileId: string | undefined, + version: number | undefined + ) => dispatch(fetchFileMetadata(fileId, version)); + const datasetMetadataList = useSelector( + (state: RootState) => state.metadata.datasetMetadataList + ); + const fileMetadataList = useSelector( + (state: RootState) => state.metadata.fileMetadataList + ); useEffect(() => { getMetadatDefinitions(null, 0, 100); @@ -50,32 +65,37 @@ export const DisplayListenerMetadata = (props: MetadataType) => { return ( <> - { - (() => { - let metadataList = []; - if (resourceType === "dataset") metadataList = datasetMetadataList; - else if (resourceType === "file") metadataList = fileMetadataList; - let listenerMetadataList = []; - let listenerMetadataContent = []; + {(() => { + let metadataList = []; + if (resourceType === "dataset") metadataList = datasetMetadataList; + else if (resourceType === "file") metadataList = fileMetadataList; + let listenerMetadataList = []; + let listenerMetadataContent = []; - return ( + return ( + {metadataList.map((metadata, idx) => { - if (metadata.agent.listener !== null) { - return ( - - - - ); - } + if (metadata.agent.listener !== null) { + return ( + + + + + + + + ); + } })} - ); - })() - } + + ); + })()} - ) -} + ); +}; diff --git a/frontend/src/components/metadata/DisplayMetadata.tsx b/frontend/src/components/metadata/DisplayMetadata.tsx index a223344a3..32ad33c70 100644 --- a/frontend/src/components/metadata/DisplayMetadata.tsx +++ b/frontend/src/components/metadata/DisplayMetadata.tsx @@ -1,110 +1,130 @@ -import React, {useEffect} from "react"; -import {Box, Grid, Typography} from "@mui/material"; -import {metadataConfig} from "../../metadata.config"; -import {useSelector, useDispatch} from "react-redux"; -import {RootState} from "../../types/data"; -import {fetchDatasetMetadata, fetchFileMetadata, fetchMetadataDefinitions} from "../../actions/metadata"; -import {Agent} from "./Agent"; -import {MetadataDeleteButton} from "./widgets/MetadataDeleteButton"; +import React, { useEffect } from "react"; +import { Box, Grid, Typography } from "@mui/material"; +import { metadataConfig } from "../../metadata.config"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "../../types/data"; +import { + fetchDatasetMetadata, + fetchFileMetadata, + fetchMetadataDefinitions, +} from "../../actions/metadata"; +import { Agent } from "./Agent"; +import { MetadataDeleteButton } from "./widgets/MetadataDeleteButton"; type MetadataType = { - updateMetadata: any, - deleteMetadata: any, - resourceType:string|undefined, - resourceId:string|undefined, -} + updateMetadata: any; + deleteMetadata: any; + resourceType: string | undefined; + resourceId: string | undefined; +}; /* This is the interface displayed already created metadata and allow eidts Uses only the list of metadata */ export const DisplayMetadata = (props: MetadataType) => { - - const {updateMetadata, deleteMetadata, resourceType, resourceId} = props; + const { updateMetadata, deleteMetadata, resourceType, resourceId } = props; const dispatch = useDispatch(); - const getMetadatDefinitions = (name:string|null, skip:number, limit:number) => dispatch(fetchMetadataDefinitions(name, skip,limit)); - const metadataDefinitionList = useSelector((state: RootState) => state.metadata.metadataDefinitionList); - const listDatasetMetadata = (datasetId: string | undefined) => dispatch(fetchDatasetMetadata(datasetId)); - const listFileMetadata = (fileId: string | undefined) => dispatch(fetchFileMetadata(fileId)); - const datasetMetadataList = useSelector((state: RootState) => state.metadata.datasetMetadataList); - const fileMetadataList = useSelector((state: RootState) => state.metadata.fileMetadataList); + const getMetadatDefinitions = ( + name: string | null, + skip: number, + limit: number + ) => dispatch(fetchMetadataDefinitions(name, skip, limit)); + const metadataDefinitionList = useSelector( + (state: RootState) => state.metadata.metadataDefinitionList + ); + const listDatasetMetadata = (datasetId: string | undefined) => + dispatch(fetchDatasetMetadata(datasetId)); + const listFileMetadata = (fileId: string | undefined) => + dispatch(fetchFileMetadata(fileId)); + const datasetMetadataList = useSelector( + (state: RootState) => state.metadata.datasetMetadataList + ); + const fileMetadataList = useSelector( + (state: RootState) => state.metadata.fileMetadataList + ); const datasetRole = useSelector( (state: RootState) => state.dataset.datasetRole ); - console.log(updateMetadata, 'updateMetadataDisplay'); + console.log(updateMetadata, "updateMetadataDisplay"); useEffect(() => { getMetadatDefinitions(null, 0, 100); }, []); // complete metadata list with both definition and values useEffect(() => { - if (resourceType === "dataset"){ + if (resourceType === "dataset") { listDatasetMetadata(resourceId); - } - else if (resourceType === "file"){ + } else if (resourceType === "file") { listFileMetadata(resourceId); } }, [resourceType, resourceId]); return ( <> - { - (() => { - let metadataList = []; - if (resourceType === "dataset") metadataList = datasetMetadataList; - else if (resourceType === "file") metadataList = fileMetadataList; + {(() => { + let metadataList = []; + if (resourceType === "dataset") metadataList = datasetMetadataList; + else if (resourceType === "file") metadataList = fileMetadataList; - return metadataDefinitionList.map((metadataDef) => { - return metadataList.map((metadata,idx) => { - if (metadataDef.name === metadata.definition) { - return ( - - {metadata.definition} - {metadata.description} - { - // construct metadata using its definition - metadataDef.fields.map((field,idxx) => { - return React.cloneElement( - metadataConfig[field.widgetType ?? "NA"] ?? metadataConfig["NA"], - { - widgetName: metadataDef.name, - fieldName: field.name, - options: field.config.options ?? [], - updateMetadata: updateMetadata, - initialReadOnly: true, - resourceId: resourceId, - content: metadata.content ?? null, - metadataId: metadata.id ?? null, - isRequired: field.required, - key:idxx, - datasetRole: datasetRole - } - ); - }) - } - - - - {datasetRole.role !== undefined && datasetRole.role !== "viewer" ? - : - <> + return metadataDefinitionList.map((metadataDef) => { + return metadataList.map((metadata, idx) => { + if (metadataDef.name === metadata.definition) { + return ( + + {metadata.definition} + + {metadata.description} + + { + // construct metadata using its definition + metadataDef.fields.map((field, idxx) => { + return React.cloneElement( + metadataConfig[field.widgetType ?? "NA"] ?? + metadataConfig["NA"], + { + widgetName: metadataDef.name, + fieldName: field.name, + options: field.config.options ?? [], + updateMetadata: updateMetadata, + initialReadOnly: true, + resourceId: resourceId, + content: metadata.content ?? null, + metadataId: metadata.id ?? null, + isRequired: field.required, + key: idxx, + datasetRole: datasetRole, } - + ); + }) + } + + + + {datasetRole.role !== undefined && + datasetRole.role !== "viewer" ? ( + + ) : ( + <> + )} - - ); - } - }); - + + + ); + } }); - })() - } + }); + })()} ); }; diff --git a/frontend/src/components/metadata/EditMetadata.tsx b/frontend/src/components/metadata/EditMetadata.tsx index c9377cd63..f17d576fa 100644 --- a/frontend/src/components/metadata/EditMetadata.tsx +++ b/frontend/src/components/metadata/EditMetadata.tsx @@ -1,135 +1,153 @@ -import React, {useEffect} from "react"; -import {Box, Typography} from "@mui/material"; -import {metadataConfig} from "../../metadata.config"; -import {useSelector, useDispatch} from "react-redux"; -import {RootState} from "../../types/data"; -import {fetchDatasetMetadata, fetchFileMetadata, fetchMetadataDefinitions} from "../../actions/metadata"; -import {Agent} from "./Agent"; +import React, { useEffect } from "react"; +import { Box, Typography } from "@mui/material"; +import { metadataConfig } from "../../metadata.config"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "../../types/data"; +import { + fetchDatasetMetadata, + fetchFileMetadata, + fetchMetadataDefinitions, +} from "../../actions/metadata"; +import { Agent } from "./Agent"; type MetadataType = { - setMetadata: any, - resourceType: string, - resourceId: string|undefined, -} + setMetadata: any; + resourceType: string; + resourceId: string | undefined; +}; /* This is the interface add more metadata on a existing resource Uses metadata definition as well as created metadata */ export const EditMetadata = (props: MetadataType) => { - - const {setMetadata, resourceType, resourceId} = props; + const { setMetadata, resourceType, resourceId } = props; const dispatch = useDispatch(); - const getMetadatDefinitions = (name:string|null, skip:number, limit:number) => dispatch(fetchMetadataDefinitions(name, skip,limit)); - const metadataDefinitionList = useSelector((state: RootState) => state.metadata.metadataDefinitionList); - const listDatasetMetadata = (datasetId: string | undefined) => dispatch(fetchDatasetMetadata(datasetId)); - const listFileMetadata = (fileId: string | undefined) => dispatch(fetchFileMetadata(fileId)); - const datasetMetadataList = useSelector((state: RootState) => state.metadata.datasetMetadataList); - const fileMetadataList = useSelector((state: RootState) => state.metadata.fileMetadataList); + const getMetadatDefinitions = ( + name: string | null, + skip: number, + limit: number + ) => dispatch(fetchMetadataDefinitions(name, skip, limit)); + const metadataDefinitionList = useSelector( + (state: RootState) => state.metadata.metadataDefinitionList + ); + const listDatasetMetadata = (datasetId: string | undefined) => + dispatch(fetchDatasetMetadata(datasetId)); + const listFileMetadata = (fileId: string | undefined) => + dispatch(fetchFileMetadata(fileId)); + const datasetMetadataList = useSelector( + (state: RootState) => state.metadata.datasetMetadataList + ); + const fileMetadataList = useSelector( + (state: RootState) => state.metadata.fileMetadataList + ); const datasetRole = useSelector( (state: RootState) => state.dataset.datasetRole ); - useEffect(() => { getMetadatDefinitions(null, 0, 100); }, []); // complete metadata list with both definition and values useEffect(() => { - if (resourceType === "dataset"){ + if (resourceType === "dataset") { listDatasetMetadata(resourceId); - } - else if (resourceType === "file"){ + } else if (resourceType === "file") { listFileMetadata(resourceId); } }, [resourceType, resourceId]); return ( <> - { - (() => { - let metadataList = []; - let metadataNameList = []; - if (resourceType === "dataset"){ - metadataList = datasetMetadataList; - metadataNameList = datasetMetadataList.reduce((list:string[], item) => { + {(() => { + let metadataList = []; + let metadataNameList = []; + if (resourceType === "dataset") { + metadataList = datasetMetadataList; + metadataNameList = datasetMetadataList.reduce( + (list: string[], item) => { return [...list, item.definition]; - }, []); - } - else if (resourceType === "file") { - metadataList = fileMetadataList; - metadataNameList = fileMetadataList.reduce((list:string[], item) => { - return [...list, item.definition]; - }, []); - } + }, + [] + ); + } else if (resourceType === "file") { + metadataList = fileMetadataList; + metadataNameList = fileMetadataList.reduce((list: string[], item) => { + return [...list, item.definition]; + }, []); + } - return metadataDefinitionList.map((metadataDef) => { - // filter and only show those do not already created - if (!metadataNameList.includes(metadataDef.name)) { - return ( - - {metadataDef.name} - {metadataDef.description} - - { - // construct metadata using its definition - metadataDef.fields.map(field => { - return React.cloneElement( - metadataConfig[field.widgetType ?? "NA"] ?? metadataConfig["NA"], - { - widgetName: metadataDef.name, - fieldName: field.name, - options: field.config.options ?? [], - setMetadata: setMetadata, - initialReadOnly: false, - isRequired: field.required, - datasetRole: datasetRole - } - ); - }) - } - - ); - } - else{ - return metadataList.map((metadata, idx) => { - if (metadataDef.name === metadata.definition) { - return ( - - {metadata.definition} - {metadata.description} + return metadataDefinitionList.map((metadataDef) => { + // filter and only show those do not already created + if (!metadataNameList.includes(metadataDef.name)) { + return ( + + {metadataDef.name} + + {metadataDef.description} + + { + // construct metadata using its definition + metadataDef.fields.map((field) => { + return React.cloneElement( + metadataConfig[field.widgetType ?? "NA"] ?? + metadataConfig["NA"], { - // construct metadata using its definition - metadataDef.fields.map((field, idxx) => { - return React.cloneElement( - metadataConfig[field.widgetType ?? "NA"] ?? metadataConfig["NA"], - { - widgetName: metadataDef.name, - fieldName: field.name, - options: field.config.options ?? [], - setMetadata: setMetadata, - initialReadOnly: false, - resourceId: resourceId, - content: metadata.content ?? null, - metadataId: metadata.id ?? null, - isRequired: field.required, - key:idxx - } - ); - }) + widgetName: metadataDef.name, + fieldName: field.name, + options: field.config.options ?? [], + setMetadata: setMetadata, + initialReadOnly: false, + isRequired: field.required, + datasetRole: datasetRole, } - - - ); + ); + }) } - }); - } - }); - })() - } + + ); + } else { + return metadataList.map((metadata, idx) => { + if (metadataDef.name === metadata.definition) { + return ( + + {metadata.definition} + + {metadata.description} + + + { + // construct metadata using its definition + metadataDef.fields.map((field, idxx) => { + return React.cloneElement( + metadataConfig[field.widgetType ?? "NA"] ?? + metadataConfig["NA"], + { + widgetName: metadataDef.name, + fieldName: field.name, + options: field.config.options ?? [], + setMetadata: setMetadata, + initialReadOnly: false, + resourceId: resourceId, + content: metadata.content ?? null, + metadataId: metadata.id ?? null, + isRequired: field.required, + key: idxx, + } + ); + }) + } + + + ); + } + }); + } + }); + })()} ); }; diff --git a/frontend/src/components/metadata/MetadataDefinitions.tsx b/frontend/src/components/metadata/MetadataDefinitions.tsx index 5db91cab4..0d91025e1 100644 --- a/frontend/src/components/metadata/MetadataDefinitions.tsx +++ b/frontend/src/components/metadata/MetadataDefinitions.tsx @@ -49,6 +49,7 @@ export function MetadataDefinitions() { const metadataDefinitions = useSelector( (state: RootState) => state.metadata.metadataDefinitionList ); + const adminMode = useSelector((state: RootState) => state.user.adminMode); // TODO add option to determine limit number; default show 5 metadata definitions each time const [currPageNum, setCurrPageNum] = useState(0); @@ -83,6 +84,11 @@ export function MetadataDefinitions() { listMetadataDefinitions(null, skip, limit); }, []); + // Admin mode will fetch all datasets + useEffect(() => { + listMetadataDefinitions(null, skip, limit); + }, [adminMode]); + useEffect(() => { // disable flipping if reaches the last page if (metadataDefinitions.length < limit) setNextDisabled(true); diff --git a/frontend/src/components/search/Search.tsx b/frontend/src/components/search/Search.tsx index 91383354c..f24ce91d3 100644 --- a/frontend/src/components/search/Search.tsx +++ b/frontend/src/components/search/Search.tsx @@ -13,9 +13,12 @@ import { SearchResult } from "./SearchResult"; import { searchTheme, theme } from "../../theme"; import config from "../../app.config"; import Cookies from "universal-cookie"; +import { useSelector } from "react-redux"; +import { RootState } from "../../types/data"; export function Search() { const [luceneOn, setLuceneOn] = useState(false); + const cookies = new Cookies(); const [authorizationHeader, setAuthorizationHeader] = useState({ Authorization: cookies.get("Authorization"), @@ -34,6 +37,9 @@ export function Search() { return () => clearInterval(intervalId); }, []); + // toggle super admin + const adminMode = useSelector((state: RootState) => state.user.adminMode); + // @ts-ignore return ( diff --git a/frontend/src/components/visualizations/Visualization.tsx b/frontend/src/components/visualizations/Visualization.tsx index 6d886e4a7..8a96fcb74 100644 --- a/frontend/src/components/visualizations/Visualization.tsx +++ b/frontend/src/components/visualizations/Visualization.tsx @@ -63,11 +63,13 @@ export const Visualization = (props: previewProps) => { // if raw type supported if ( fileSummary && - ((fileSummary.content_type && fileSummary.content_type.content_type !== undefined && - // @ts-ignore + ((fileSummary.content_type && + fileSummary.content_type.content_type !== undefined && + // @ts-ignore supportedMimeType.includes(fileSummary.content_type.content_type)) || - (fileSummary.content_type && fileSummary.content_type.main_type !== undefined && - // @ts-ignore + (fileSummary.content_type && + fileSummary.content_type.main_type !== undefined && + // @ts-ignore supportedMimeType.includes(fileSummary.content_type.main_type))) ) { setIsRawDataSupported(true); @@ -75,9 +77,12 @@ export const Visualization = (props: previewProps) => { setIsRawDataSupported(false); } - if (fileSummary && - fileSummary.bytes && fileSummary.bytes >= config["rawDataVisualizationThreshold"]) { - setIsVisDataGreaterThanMaxSize(true); + if ( + fileSummary && + fileSummary.bytes && + fileSummary.bytes >= config["rawDataVisualizationThreshold"] + ) { + setIsVisDataGreaterThanMaxSize(true); } else { setIsVisDataGreaterThanMaxSize(false); } @@ -87,7 +92,7 @@ export const Visualization = (props: previewProps) => { return ( - {isEmptyVisData && !isRawDataSupported? ( + {isEmptyVisData && !isRawDataSupported ? (
No visualization data or parameters available. Incomplete visualization configuration. diff --git a/frontend/src/openapi/v2/models/UserOut.ts b/frontend/src/openapi/v2/models/UserOut.ts index f624b5122..d10ffda13 100644 --- a/frontend/src/openapi/v2/models/UserOut.ts +++ b/frontend/src/openapi/v2/models/UserOut.ts @@ -21,4 +21,5 @@ export type UserOut = { last_name: string; id?: string; admin: boolean; + admin_mode?: boolean; } diff --git a/frontend/src/openapi/v2/services/AuthorizationService.ts b/frontend/src/openapi/v2/services/AuthorizationService.ts index a44faa63a..d38af4eb7 100644 --- a/frontend/src/openapi/v2/services/AuthorizationService.ts +++ b/frontend/src/openapi/v2/services/AuthorizationService.ts @@ -66,7 +66,7 @@ export class AuthorizationService { ): CancelablePromise { return __request({ method: 'GET', - path: `/api/v2/authorizations/datasets/${datasetId}/role/viewer`, + path: `/api/v2/authorizations/datasets/${datasetId}/role/viewer}`, errors: { 422: `Validation Error`, }, @@ -86,7 +86,7 @@ export class AuthorizationService { ): CancelablePromise { return __request({ method: 'GET', - path: `/api/v2/authorizations/datasets/${datasetId}/role/owner`, + path: `/api/v2/authorizations/datasets/${datasetId}/role/owner}`, errors: { 422: `Validation Error`, }, @@ -95,6 +95,7 @@ export class AuthorizationService { /** * Get File Role + * Retrieve role of user for an individual file. Role cannot change between file versions. * @param fileId * @param datasetId * @returns RoleType Successful Response @@ -118,6 +119,7 @@ export class AuthorizationService { /** * Get Metadata Role + * Retrieve role of user for group. Group roles can be OWNER, EDITOR, or VIEWER (for regular Members). * @param metadataId * @param datasetId * @returns AuthorizationMetadata Successful Response @@ -129,7 +131,7 @@ export class AuthorizationService { ): CancelablePromise { return __request({ method: 'GET', - path: `/api/v2/authorizations/metadata/${metadataId}/role`, + path: `/api/v2/authorizations/metadata/${metadataId}/role}`, query: { 'dataset_id': datasetId, }, @@ -141,6 +143,7 @@ export class AuthorizationService { /** * Get Group Role + * Retrieve role of user on a particular group (i.e. whether they can change group memberships). * @param groupId * @param datasetId * @returns RoleType Successful Response @@ -262,7 +265,7 @@ export class AuthorizationService { ): CancelablePromise { return __request({ method: 'GET', - path: `/api/v2/authorizations/datasets/${datasetId}/roles`, + path: `/api/v2/authorizations/datasets/${datasetId}/roles}`, errors: { 422: `Validation Error`, }, diff --git a/frontend/src/openapi/v2/services/ElasticsearchService.ts b/frontend/src/openapi/v2/services/ElasticsearchService.ts index be50e0964..b0342e284 100644 --- a/frontend/src/openapi/v2/services/ElasticsearchService.ts +++ b/frontend/src/openapi/v2/services/ElasticsearchService.ts @@ -10,12 +10,14 @@ export class ElasticsearchService { * Search * @param indexName * @param query + * @param datasetId * @returns string Successful Response * @throws ApiError */ public static searchApiV2ElasticsearchSearchPut( indexName: string, query: string, + datasetId?: string, ): CancelablePromise { return __request({ method: 'PUT', @@ -23,6 +25,7 @@ export class ElasticsearchService { query: { 'index_name': indexName, 'query': query, + 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, @@ -32,13 +35,22 @@ export class ElasticsearchService { /** * Msearch + * @param datasetId * @returns any Successful Response * @throws ApiError */ - public static msearchApiV2ElasticsearchAllMsearchPost(): CancelablePromise { + public static msearchApiV2ElasticsearchAllMsearchPost( + datasetId?: string, + ): CancelablePromise { return __request({ method: 'POST', path: `/api/v2/elasticsearch/all/_msearch`, + query: { + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, }); } diff --git a/frontend/src/openapi/v2/services/GroupsService.ts b/frontend/src/openapi/v2/services/GroupsService.ts index 6c20074ce..404a84348 100644 --- a/frontend/src/openapi/v2/services/GroupsService.ts +++ b/frontend/src/openapi/v2/services/GroupsService.ts @@ -18,12 +18,14 @@ export class GroupsService { * limit -- restrict number of records to be returned (i.e. for pagination) * @param skip * @param limit + * @param datasetId * @returns GroupOut Successful Response * @throws ApiError */ public static getGroupsApiV2GroupsGet( skip?: number, limit: number = 10, + datasetId?: string, ): CancelablePromise> { return __request({ method: 'GET', @@ -31,6 +33,7 @@ export class GroupsService { query: { 'skip': skip, 'limit': limit, + 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, @@ -69,6 +72,7 @@ export class GroupsService { * @param searchTerm * @param skip * @param limit + * @param datasetId * @returns GroupOut Successful Response * @throws ApiError */ @@ -76,6 +80,7 @@ export class GroupsService { searchTerm: string, skip?: number, limit: number = 10, + datasetId?: string, ): CancelablePromise> { return __request({ method: 'GET', @@ -83,6 +88,7 @@ export class GroupsService { query: { 'skip': skip, 'limit': limit, + 'dataset_id': datasetId, }, errors: { 422: `Validation Error`, diff --git a/frontend/src/openapi/v2/services/LoginService.ts b/frontend/src/openapi/v2/services/LoginService.ts index 58d601260..5c7aaab57 100644 --- a/frontend/src/openapi/v2/services/LoginService.ts +++ b/frontend/src/openapi/v2/services/LoginService.ts @@ -49,6 +49,65 @@ export class LoginService { }); } + /** + * Get Admin + * @param datasetId + * @returns boolean Successful Response + * @throws ApiError + */ + public static getAdminApiV2UsersMeIsAdminGet( + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'GET', + path: `/api/v2/users/me/is_admin`, + query: { + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Get Admin Mode + * Get Admin mode from User Object. + * @returns boolean Successful Response + * @throws ApiError + */ + public static getAdminModeApiV2UsersMeAdminModeGet(): CancelablePromise { + return __request({ + method: 'GET', + path: `/api/v2/users/me/admin_mode`, + }); + } + + /** + * Set Admin Mode + * Set Admin mode from User Object. + * @param adminModeOn + * @param datasetId + * @returns boolean Successful Response + * @throws ApiError + */ + public static setAdminModeApiV2UsersMeAdminModePost( + adminModeOn: boolean, + datasetId?: string, + ): CancelablePromise { + return __request({ + method: 'POST', + path: `/api/v2/users/me/admin_mode`, + query: { + 'admin_mode_on': adminModeOn, + 'dataset_id': datasetId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** * Set Admin * @param useremail diff --git a/frontend/src/reducers/user.ts b/frontend/src/reducers/user.ts index badc97424..5d81b9583 100644 --- a/frontend/src/reducers/user.ts +++ b/frontend/src/reducers/user.ts @@ -1,6 +1,7 @@ import { DELETE_API_KEY, GENERATE_API_KEY, + GET_ADMIN_MODE_STATUS, LIST_API_KEYS, LOGIN_ERROR, RECEIVE_USER_PROFILE, @@ -8,6 +9,7 @@ import { REGISTER_USER, RESET_API_KEY, SET_USER, + TOGGLE_ADMIN_MODE, } from "../actions/user"; import { UserState } from "../types/data"; import { DataAction } from "../types/action"; @@ -16,6 +18,7 @@ import { UserOut } from "../openapi/v2"; const defaultState: UserState = { Authorization: null, loginError: false, + adminMode: false, registerSucceeded: false, errorMsg: "", hashedKey: "", @@ -25,6 +28,14 @@ const defaultState: UserState = { const user = (state = defaultState, action: DataAction) => { switch (action.type) { + case TOGGLE_ADMIN_MODE: + return Object.assign({}, state, { + adminMode: action.adminMode, + }); + case GET_ADMIN_MODE_STATUS: + return Object.assign({}, state, { + adminMode: action.adminMode, + }); case SET_USER: return Object.assign({}, state, { Authorization: action.Authorization, diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index ee1bb9d45..ba3798c44 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -21,7 +21,12 @@ import { Search } from "./components/search/Search"; import { isAuthorized } from "./utils/common"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "./types/data"; -import { refreshToken, resetLogout } from "./actions/common"; +import { + refreshToken, + resetFailedReason, + resetFailedReasonInline, + resetLogout, +} from "./actions/common"; import { Explore } from "./components/Explore"; import { ExtractionHistory } from "./components/listeners/ExtractionHistory"; import { fetchDatasetRole, fetchFileRole } from "./actions/authorization"; @@ -74,11 +79,17 @@ const PrivateRoute = (props): JSX.Element => { } }, [loggedOut]); - // not found or unauthorized + // not found or unauthorized redirect useEffect(() => { if (reason == "Forbidden") { + // if redirect to new page, reset error so the error modal/message doesn't stuck in "Forbidden" state + dispatch(resetFailedReason()); + dispatch(resetFailedReasonInline()); history("/forbidden"); } else if (reason == "Not Found") { + // if redirect to new page, reset error so the error modal/message doesn't stuck in "Forbidden" state + dispatch(resetFailedReason()); + dispatch(resetFailedReasonInline()); history("/not-found"); } }, [reason]); diff --git a/frontend/src/types/action.ts b/frontend/src/types/action.ts index 038314f51..912aeccc8 100644 --- a/frontend/src/types/action.ts +++ b/frontend/src/types/action.ts @@ -22,14 +22,6 @@ import { VisualizationConfigOut, VisualizationDataOut, } from "../openapi/v2"; -import { - LIST_USERS, - PREFIX_SEARCH_USERS, - RECEIVE_USER_PROFILE, -} from "../actions/user"; -import { CREATE_GROUP, DELETE_GROUP } from "../actions/group"; -import { RECEIVE_FILE_PRESIGNED_URL } from "../actions/file"; -import { GET_VIS_DATA_PRESIGNED_URL } from "../actions/visualization"; interface RECEIVE_FILES_IN_DATASET { type: "RECEIVE_FILES_IN_DATASET"; @@ -101,6 +93,16 @@ interface SET_USER { Authorization: string; } +interface TOGGLE_ADMIN_MODE { + adminMode: boolean; + type: "TOGGLE_ADMIN_MODE"; +} + +interface GET_ADMIN_MODE_STATUS { + adminMode: boolean; + type: "GET_ADMIN_MODE_STATUS"; +} + interface LOGIN_ERROR { errorMsg: string; type: "LOGIN_ERROR"; @@ -456,6 +458,8 @@ interface RESET_VIS_DATA_PRESIGNED_URL { } export type DataAction = + | GET_ADMIN_MODE_STATUS + | TOGGLE_ADMIN_MODE | RECEIVE_FILES_IN_DATASET | RECEIVE_FOLDERS_IN_DATASET | DELETE_FILE diff --git a/frontend/src/types/data.ts b/frontend/src/types/data.ts index 914925bb5..39bd8ef1d 100644 --- a/frontend/src/types/data.ts +++ b/frontend/src/types/data.ts @@ -186,6 +186,7 @@ export interface UserState { hashedKey: string; apiKeys: UserAPIKeyOut[]; profile: UserOut; + adminMode: boolean; } export interface ErrorState {