From 7476c939c3b363d4033288f57e7e8dacaec2793b Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Fri, 27 Feb 2026 09:04:54 +0100 Subject: [PATCH] fixed issue failed to revoke grant access of the user --- csharp/App/Backend/Controller.cs | 32 ++ .../App/Backend/DataTypes/Methods/Session.cs | 26 +- .../dashboards/ManageAccess/UserAccess.tsx | 425 +++++++----------- .../src/content/dashboards/Tree/Folder.tsx | 4 +- 4 files changed, 216 insertions(+), 271 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index a6e38d3fd..46b4e3ad8 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -363,6 +363,38 @@ public class Controller : ControllerBase return user.AccessibleInstallations().ToList(); } + [HttpGet(nameof(GetDirectInstallationAccessForUser))] + public ActionResult> GetDirectInstallationAccessForUser(Int64 userId, Token authToken) + { + var sessionUser = Db.GetSession(authToken)?.User; + if (sessionUser == null) + return Unauthorized(); + + var user = Db.GetUserById(userId); + if (user == null) + return Unauthorized(); + + return user.DirectlyAccessibleInstallations() + .Select(i => new { i.Id, i.Name }) + .ToList(); + } + + [HttpGet(nameof(GetDirectFolderAccessForUser))] + public ActionResult> GetDirectFolderAccessForUser(Int64 userId, Token authToken) + { + var sessionUser = Db.GetSession(authToken)?.User; + if (sessionUser == null) + return Unauthorized(); + + var user = Db.GetUserById(userId); + if (user == null) + return Unauthorized(); + + return user.DirectlyAccessibleFolders() + .Select(f => new { f.Id, f.Name }) + .ToList(); + } + [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))] public ActionResult> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken) { diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index 58aed8b81..3b62cb6f3 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -473,13 +473,25 @@ public static class SessionMethods { var sessionUser = session?.User; - return sessionUser is not null - && installation is not null - && user is not null - && (user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2) - && sessionUser.HasAccessTo(installation) - && user.HasAccessTo(installation) - && Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0; + if (sessionUser is null || installation is null || user is null) + return false; + + if (!(user.IsDescendantOf(sessionUser) || sessionUser.UserType == 2)) + return false; + + if (!sessionUser.HasAccessTo(installation) || !user.HasAccessTo(installation)) + return false; + + // Try direct InstallationAccess record first + if (Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0) + return true; + + // No direct record — access is inherited via a folder; revoke that folder access + var accessFolder = installation.Ancestors() + .FirstOrDefault(f => user.HasDirectAccessTo(f)); + + return accessFolder is not null + && Db.FolderAccess.Delete(a => a.UserId == user.Id && a.FolderId == accessFolder.Id) > 0; } public static Boolean RevokeUserAccessTo(this Session? session, User? user, Folder? folder) diff --git a/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx b/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx index d0c471703..3f035d2ae 100644 --- a/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx @@ -15,9 +15,11 @@ import { IconButton, InputLabel, ListItem, + ListSubheader, MenuItem, Modal, Select, + Typography, useTheme } from '@mui/material'; import { TokenContext } from 'src/contexts/tokenContext'; @@ -26,6 +28,7 @@ import ListItemAvatar from '@mui/material/ListItemAvatar'; import Avatar from '@mui/material/Avatar'; import ListItemText from '@mui/material/ListItemText'; import PersonIcon from '@mui/icons-material/Person'; +import FolderIcon from '@mui/icons-material/Folder'; import Button from '@mui/material/Button'; import { Close as CloseIcon } from '@mui/icons-material'; import { AccessContext } from 'src/contexts/AccessContextProvider'; @@ -52,22 +55,24 @@ function UserAccess(props: UserAccessProps) { const tokencontext = useContext(TokenContext); const { removeToken } = tokencontext; const context = useContext(UserContext); - const { currentUser, setUser } = context; + const { currentUser } = context; const [openFolder, setOpenFolder] = useState(false); const [openInstallation, setOpenInstallation] = useState(false); const [openModal, setOpenModal] = useState(false); const [selectedFolderNames, setSelectedFolderNames] = useState([]); - const [selectedInstallationNames, setSelectedInstallationNames] = useState< - string[] - >([]); + const [selectedInstallationNames, setSelectedInstallationNames] = useState([]); + + // Available choices for grant modal + const [availableFolders, setAvailableFolders] = useState([]); + const [availableInstallations, setAvailableInstallations] = useState([]); + + // Direct grants for this user + const [directFolders, setDirectFolders] = useState<{ id: number; name: string }[]>([]); + const [directInstallations, setDirectInstallations] = useState<{ id: number; name: string }[]>([]); - const [folders, setFolders] = useState([]); - const [installations, setInstallations] = useState([]); const accessContext = useContext(AccessContext); const { - fetchInstallationsForUser, - accessibleInstallationsForUser, error, setError, updated, @@ -75,130 +80,118 @@ function UserAccess(props: UserAccessProps) { updatedmessage, errormessage, setErrorMessage, - setUpdatedMessage, - RevokeAccessFromResource + setUpdatedMessage } = accessContext; - const fetchFolders = useCallback(async () => { + const fetchDirectGrants = useCallback(async () => { + try { + const [foldersRes, installationsRes] = await Promise.all([ + axiosConfig.get(`/GetDirectFolderAccessForUser?userId=${props.current_user.id}`), + axiosConfig.get(`/GetDirectInstallationAccessForUser?userId=${props.current_user.id}`) + ]); + setDirectFolders(foldersRes.data); + setDirectInstallations(installationsRes.data); + } catch (err) { + if (err.response && err.response.status === 401) removeToken(); + } + }, [props.current_user.id]); + + const fetchAvailableFolders = useCallback(async () => { return axiosConfig .get('/GetAllFolders') - .then((res) => { - setFolders(res.data); - }) + .then((res) => setAvailableFolders(res.data)) .catch((err) => { - if (err.response && err.response.status == 401) { - removeToken(); - } + if (err.response && err.response.status == 401) removeToken(); }); - }, [setFolders]); + }, []); - const fetchInstallations = useCallback(async () => { + const fetchAvailableInstallations = useCallback(async () => { try { - // fetch product 0 - const res0 = await axiosConfig.get( - `/GetAllInstallationsFromProduct?product=0` - ); - const installations0 = res0.data; - - // fetch product 1 - const res1 = await axiosConfig.get( - `/GetAllInstallationsFromProduct?product=3` - ); - const installations1 = res1.data; - - // aggregate - const combined = [...installations0, ...installations1]; - - // update - setInstallations(combined); + const [res0, res1, res2, res3] = await Promise.all([ + axiosConfig.get(`/GetAllInstallationsFromProduct?product=0`), + axiosConfig.get(`/GetAllInstallationsFromProduct?product=1`), + axiosConfig.get(`/GetAllInstallationsFromProduct?product=2`), + axiosConfig.get(`/GetAllInstallationsFromProduct?product=3`) + ]); + setAvailableInstallations([...res0.data, ...res1.data, ...res2.data, ...res3.data]); } catch (err) { - if (err.response && err.response.status === 401) { - removeToken(); - } - } finally { + if (err.response && err.response.status === 401) removeToken(); } - }, [setInstallations]); + }, []); useEffect(() => { - fetchInstallationsForUser(props.current_user.id); + fetchDirectGrants(); }, [props.current_user]); const handleGrantAccess = () => { - fetchFolders(); - fetchInstallations(); + fetchAvailableFolders(); + fetchAvailableInstallations(); setOpenModal(true); setSelectedFolderNames([]); setSelectedInstallationNames([]); }; - const handleFolderChange = (event) => { - setSelectedFolderNames(event.target.value); + const handleRevokeFolder = async (folderId: number, folderName: string) => { + axiosConfig + .post(`/RevokeUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folderId}`) + .then(() => { + setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + props.current_user.name); + setUpdated(true); + setTimeout(() => setUpdated(false), 3000); + fetchDirectGrants(); + }) + .catch(() => { + setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' })); + setError(true); + }); }; - const handleInstallationChange = (event) => { - setSelectedInstallationNames(event.target.value); - }; - const handleOpenFolder = () => { - setOpenFolder(true); - }; - - const handleCloseFolder = () => { - setOpenFolder(false); - }; - const handleCancel = () => { - setOpenModal(false); - }; - const handleOpenInstallation = () => { - setOpenInstallation(true); - }; - - const handleCloseInstallation = () => { - setOpenInstallation(false); + const handleRevokeInstallation = async (installationId: number) => { + axiosConfig + .post(`/RevokeUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installationId}`) + .then(() => { + setUpdatedMessage(intl.formatMessage({ id: 'revokedAccessFromUser' }) + ' ' + props.current_user.name); + setUpdated(true); + setTimeout(() => setUpdated(false), 3000); + fetchDirectGrants(); + }) + .catch(() => { + setErrorMessage(intl.formatMessage({ id: 'unableToRevokeAccess' })); + setError(true); + }); }; const handleSubmit = async () => { for (const folderName of selectedFolderNames) { - const folder = folders.find((folder) => folder.name === folderName); - + const folder = availableFolders.find((f) => f.name === folderName); await axiosConfig - .post( - `/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}` - ) - .then((response) => { - if (response) { - setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name })); - setUpdated(true); - } + .post(`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}`) + .then(() => { + setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name })); + setUpdated(true); }) - .catch((err) => { + .catch(() => { setErrorMessage(intl.formatMessage({ id: 'errorOccured' })); setError(true); }); } for (const installationName of selectedInstallationNames) { - const installation = installations.find( - (installation) => installation.name === installationName - ); - + const installation = availableInstallations.find((i) => i.name === installationName); await axiosConfig - .post( - `/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}` - ) - .then((response) => { - if (response) { - setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name })); - setUpdated(true); - } + .post(`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`) + .then(() => { + setUpdatedMessage(intl.formatMessage({ id: 'grantedAccessToUser' }, { name: props.current_user.name })); + setUpdated(true); }) - .catch((err) => { + .catch(() => { setErrorMessage(intl.formatMessage({ id: 'errorOccured' })); setError(true); }); } setOpenModal(false); - fetchInstallationsForUser(props.current_user.id); + fetchDirectGrants(); }; return ( @@ -206,51 +199,25 @@ function UserAccess(props: UserAccessProps) { {updated && ( - + {updatedmessage} - setUpdated(false)} - > + setUpdated(false)}> )} {error && ( - + {errormessage} - setError(false)} - sx={{ - marginLeft: '10px' - }} - > + setError(false)} sx={{ marginLeft: '10px' }}> )} - {}} - aria-labelledby="error-modal" - aria-describedby="error-modal-description" - > + {/* Grant Access Modal */} + {}} aria-labelledby="grant-modal"> - +
- - + + setSelectedInstallationNames(e.target.value as string[])} open={openInstallation} - onClose={handleCloseInstallation} - onOpen={handleOpenInstallation} + onClose={() => setOpenInstallation(false)} + onOpen={() => setOpenInstallation(true)} renderValue={(selected) => ( -
- {selected.map((installation) => ( - {installation}, - ))} -
+
{selected.map((i) => {i}, )}
)} > - {installations.map((installation) => ( - - {installation.name} - + {availableInstallations.map((installation) => ( + {installation.name} ))} @@ -380,32 +291,15 @@ function UserAccess(props: UserAccessProps) {
@@ -416,43 +310,63 @@ function UserAccess(props: UserAccessProps) {
- - {accessibleInstallationsForUser.map((installation, index) => { - const isLast = index === accessibleInstallationsForUser.length - 1; + {/* Folder Access Section */} + + + + + {directFolders.map((folder, index) => { + const isLast = index === directFolders.length - 1; return ( - + { - RevokeAccessFromResource( - 'ToInstallation', - props.current_user.id, - 'InstallationId', - installation.id, - props.current_user.name - ); + handleRevokeFolder(folder.id, folder.name)} edge="end"> + + + ) + } + > + + + + + + + + + + ); + })} + {directFolders.length === 0 && ( + + + + )} + - fetchInstallationsForUser(props.current_user.id); - }} - edge="end" - > + {/* Direct Installation Access Section */} + + + + + {directInstallations.map((installation, index) => { + const isLast = index === directInstallations.length - 1; + return ( + + handleRevokeInstallation(installation.id)} edge="end"> ) @@ -469,22 +383,9 @@ function UserAccess(props: UserAccessProps) { ); })} - - {accessibleInstallationsForUser.length == 0 && ( - - - + {directInstallations.length === 0 && ( + + )} diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx index 6c73ab495..51d1087f5 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Folder.tsx @@ -44,12 +44,12 @@ function Folder(props: singleFolderProps) { value: 'information', label: }, - { + ...(currentUser.userType === UserType.admin ? [{ value: 'manage', label: ( ) - } + }] : []) ]; const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {