Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit 7e599eb

Browse files
authored
feat: TT-138 add scopes (#221)
* feat(permision): add user permissions to the application * feat(scopes): add ceatedBy with task cancellability condition * fix(scopes): New task disabled if no access * fix(scopes): remove package manager line * fix(scopes): quick fixed * fix(refactor): change userScope functions
1 parent 2831e0d commit 7e599eb

File tree

15 files changed

+133
-59
lines changed

15 files changed

+133
-59
lines changed

aqueductcore/frontend/src/API/graphql/queries/tasks/getAllTasks.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export const GET_ALL_TASKS = gql`
1414
stdOut
1515
stdErr
1616
taskId
17+
createdBy
1718
experiment {
18-
createdBy
1919
uuid
2020
title
2121
eid

aqueductcore/frontend/src/API/graphql/queries/tasks/getTask.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export const GET_TASK = gql`
1414
stdOut
1515
stdErr
1616
taskId
17+
createdBy
1718
experiment {
18-
createdBy
1919
uuid
2020
title
2121
eid

aqueductcore/frontend/src/__mocks__/TasksDataMock.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const TasksDataMock: TaskType[] = [
77
"extensionName": "Mock extension name-0",
88
"actionName": "Mock action name-0",
99
"taskStatus": TaskStatus.Failure,
10-
// "username": "Tom-0",
10+
"createdBy": "Tom-0",
1111
"receivedAt": "2023-12-01T23:59:00.000999",
1212
"resultCode": 1,
1313
"stdOut": "Some text 2",
@@ -23,7 +23,7 @@ export const TasksDataMock: TaskType[] = [
2323
"extensionName": "Mock extension name-0",
2424
"actionName": "Mock action name-1",
2525
"taskStatus": TaskStatus.Pending,
26-
// "username": "Tom-0",
26+
"createdBy": "Tom-0",
2727
"receivedAt": "2023-12-02T23:59:01.000999",
2828
"resultCode": null,
2929
"stdOut": null,
@@ -39,7 +39,7 @@ export const TasksDataMock: TaskType[] = [
3939
"extensionName": "Mock extension name-0",
4040
"actionName": "Mock action name-2",
4141
"taskStatus": TaskStatus.Received,
42-
// "username": "Tom-0",
42+
"createdBy": "Tom-0",
4343
"receivedAt": "2023-12-03T23:59:02.000999",
4444
"resultCode": null,
4545
"stdOut": null,

aqueductcore/frontend/src/__mocks__/mutations/extension/cancelTask.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export const cancelTask_mock = {
1414
result: {
1515
data: {
1616
"cancelTask": {
17-
"resultCode": TaskStatus.Revoked,
17+
"taskId": "id-0",
18+
"taskStatus": TaskStatus.Revoked,
19+
"resultCode": 0,
1820
"__typename": "CancelTaskResult"
1921
}
2022
}
@@ -30,7 +32,9 @@ export const cancelTask_mock = {
3032
result: {
3133
data: {
3234
"cancelTask": {
33-
"resultCode": TaskStatus.Revoked,
35+
"taskId": "id-1",
36+
"taskStatus": TaskStatus.Revoked,
37+
"resultCode": 0,
3438
"__typename": "CancelTaskResult"
3539
}
3640
}

aqueductcore/frontend/src/__tests__/Extensions.test.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ test("render page extensions with no error", async () => {
2828
});
2929

3030
test("list of extension in the dropdown when clicked", async () => {
31-
const { findByTitle, findByText } = render(<ExtensionIncludedComponent />)
32-
const extensionOpenModalButton = await findByTitle("extensions")
31+
const { findByText } = render(<ExtensionIncludedComponent />)
32+
const extensionOpenModalButton = await findByText("New Task")
3333
await userEvent.click(extensionOpenModalButton)
3434

3535
// Check extensions to be listed in the dropdown
@@ -41,8 +41,8 @@ test("list of extension in the dropdown when clicked", async () => {
4141

4242
test("author's name in the modal", async () => {
4343

44-
const { findByTitle, findByText } = render(<ExtensionIncludedComponent />)
45-
const extensionOpenModalButton = await findByTitle("extensions")
44+
const { findByText } = render(<ExtensionIncludedComponent />)
45+
const extensionOpenModalButton = await findByText("New Task")
4646
await userEvent.click(extensionOpenModalButton)
4747

4848
const first_extension = await findByText(ExtensionsDataMock[0].name);
@@ -54,8 +54,8 @@ test("author's name in the modal", async () => {
5454

5555
test("list of actions in the extension", async () => {
5656

57-
const { findByTitle, findByText } = render(<ExtensionIncludedComponent />)
58-
const extensionOpenModalButton = await findByTitle("extensions")
57+
const { findByText } = render(<ExtensionIncludedComponent />)
58+
const extensionOpenModalButton = await findByText("New Task")
5959
await userEvent.click(extensionOpenModalButton)
6060

6161
const first_extension = await findByText(ExtensionsDataMock[0].name);
@@ -70,8 +70,8 @@ test("list of actions in the extension", async () => {
7070

7171
test("click the other action and params should be updated", async () => {
7272

73-
const { findByTitle, findByText } = render(<ExtensionIncludedComponent />)
74-
const extensionOpenModalButton = await findByTitle("extensions")
73+
const { findByText } = render(<ExtensionIncludedComponent />)
74+
const extensionOpenModalButton = await findByText("New Task")
7575
await userEvent.click(extensionOpenModalButton)
7676

7777
const first_extension = await findByText(ExtensionsDataMock[0].name);
@@ -91,8 +91,8 @@ test("click the other action and params should be updated", async () => {
9191

9292
test("data is persistence when switching between functions", async () => {
9393

94-
const { findByTitle, findByText } = render(<ExtensionIncludedComponent />)
95-
const extensionOpenModalButton = await findByTitle("extensions")
94+
const { findByText } = render(<ExtensionIncludedComponent />)
95+
const extensionOpenModalButton = await findByText("New Task")
9696
await userEvent.click(extensionOpenModalButton)
9797

9898
const firstActionName = ExtensionsDataMock[0].actions[0].name
@@ -128,7 +128,7 @@ test("data is persistence when switching between functions", async () => {
128128
test.skip("submit the form and success modal", async () => {
129129

130130
const { findByTitle, findByText } = render(<ExtensionIncludedComponent />)
131-
const extensionOpenModalButton = await findByTitle("extensions")
131+
const extensionOpenModalButton = await findByText("New Task")
132132
await userEvent.click(extensionOpenModalButton)
133133

134134
const first_extension = await findByText(ExtensionsDataMock[0].name);

aqueductcore/frontend/src/components/organisms/ExtensionsList/ExtensionsList.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ test("renders ExtensionsList", () => {
1313
});
1414

1515
test("open extension list", async () => {
16-
const { getByTitle, findByText } = render(
16+
const { getByText, findByText } = render(
1717
<AppContextAQDMock>
1818
<ExtensionsList />
1919
</AppContextAQDMock>);
2020

21-
const extensions_button = getByTitle('extensions')
21+
const extensions_button = getByText('New Task')
2222
await userEvent.click(extensions_button)
2323
const extension_1 = await findByText(ExtensionsDataMock[0].name)
2424
const extension_2 = await findByText(ExtensionsDataMock[1].name)

aqueductcore/frontend/src/components/organisms/ExtensionsList/index.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import MenuItem from '@mui/material/MenuItem';
66
import Popper from '@mui/material/Popper';
77
import { useRef, useState } from 'react';
88
import Paper from '@mui/material/Paper';
9+
import { Tooltip } from '@mui/material';
910
import Grow from '@mui/material/Grow';
1011
import toast from 'react-hot-toast';
1112

1213
import { BorderedButtonWithIcon } from 'components/atoms/sharedStyledComponents/BorderedButtonWithIcon';
1314
import { useGetAllExtensions } from 'API/graphql/queries/extension/getAllExtensions';
15+
import { useGetCurrentUserInfo } from 'API/graphql/queries/user/getUserInformation';
1416
import ExtensionModal from 'components/organisms/ExtensionModal';
17+
import { isUserAbleToCreateTask } from 'helper/auth/userScope';
1518

1619
function ExtensionsList() {
1720

@@ -21,6 +24,7 @@ function ExtensionsList() {
2124
const handleCloseExtensionModal = () => setIsExtensionOpen(false);
2225
const [open, setOpen] = useState(false);
2326
const anchorRef = useRef<HTMLDivElement>(null);
27+
const { data: userInfo } = useGetCurrentUserInfo()
2428

2529
const { data, error } = useGetAllExtensions()
2630
const extensions = data?.extensions
@@ -55,26 +59,32 @@ function ExtensionsList() {
5559
setOpen(false);
5660
};
5761

62+
const isTaskExecutable = Boolean(userInfo && isUserAbleToCreateTask(userInfo.getCurrentUserInfo))
63+
5864
return (
5965
<>
6066
<div
6167
ref={anchorRef}
6268
>
63-
<BorderedButtonWithIcon
64-
size="small"
65-
aria-controls={open ? 'split-button-menu' : undefined}
66-
aria-expanded={open ? 'true' : undefined}
67-
aria-label="select merge strategy"
68-
aria-haspopup="menu"
69-
onClick={handleToggle}
70-
color="neutral"
71-
variant="outlined"
72-
startIcon={<AutoAwesomeIcon />}
73-
endIcon={<ArrowDropDownIcon />}
74-
title='extensions'
75-
>
76-
New Job
77-
</BorderedButtonWithIcon>
69+
<Tooltip title={!isTaskExecutable ? "User has no permission to run a task in this experiment." : "extensions"}>
70+
<span>
71+
<BorderedButtonWithIcon
72+
size="small"
73+
aria-controls={open ? 'split-button-menu' : undefined}
74+
aria-expanded={open ? 'true' : undefined}
75+
aria-label="select merge strategy"
76+
aria-haspopup="menu"
77+
onClick={handleToggle}
78+
color="neutral"
79+
variant="outlined"
80+
startIcon={<AutoAwesomeIcon />}
81+
endIcon={<ArrowDropDownIcon />}
82+
disabled={!isTaskExecutable}
83+
>
84+
New Task
85+
</BorderedButtonWithIcon>
86+
</span>
87+
</Tooltip>
7888
</div>
7989
{extensions?.length ? <Popper
8090
sx={{

aqueductcore/frontend/src/components/organisms/JobDetailsModal/index.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { ReactNode, useState } from "react";
55
import toast from "react-hot-toast";
66

77
import JobExtensionStatus from "components/molecules/JobListTableCells/JobExtensionStatus";
8+
import { useGetCurrentUserInfo } from "API/graphql/queries/user/getUserInformation";
89
import { useCancelTask } from "API/graphql/mutations/extension/cancelTask";
910
import ConfirmActionModal from "components/organisms/ConfirmActionModal";
1011
import ActionParameters from "components/molecules/ActionParameters";
1112
import { TaskStatus } from "types/graphql/__GENERATED__/graphql";
1213
import { useGetTask } from "API/graphql/queries/tasks/getTask";
14+
import { isUserAbleToCancelTask } from "helper/auth/userScope";
1315
import LogViewer from "components/molecules/LogViewer";
1416
import { Loading } from "components/atoms/Loading";
1517
import { dateFormatter } from "helper/formatters";
@@ -143,7 +145,8 @@ function JobDetailsModal({ isOpen, handleClose, taskId }: JobDetailsModalProps)
143145
const [value, setValue] = useState(0);
144146
const { mutate: mutateCancelTask, loading: loadingCancelTask } = useCancelTask();
145147
const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState<boolean>(false);
146-
148+
const { data: userInfo } = useGetCurrentUserInfo()
149+
147150
const closeConfirmationModal = () => {
148151
setIsConfirmationModalOpen(false);
149152
}
@@ -173,6 +176,8 @@ function JobDetailsModal({ isOpen, handleClose, taskId }: JobDetailsModalProps)
173176
toast.success("Task cancelled successfully", {
174177
id: "task_cancelled",
175178
});
179+
// TODO: if service workers are down, it shows success! which is wrong, and it should be:
180+
// TODO: Task is not cancelled successfully, it might because celery workers are not up and running
176181
await client.refetchQueries({
177182
include: "active",
178183
});
@@ -185,8 +190,9 @@ function JobDetailsModal({ isOpen, handleClose, taskId }: JobDetailsModalProps)
185190
});
186191
}
187192
const task = data?.task
188-
const isTaskCancelleable = task?.taskStatus == TaskStatus.Pending || task?.taskStatus == TaskStatus.Received || task?.taskStatus == TaskStatus.Started;
189-
193+
const isTaskCancellableByUser = Boolean(userInfo && task && isUserAbleToCancelTask(userInfo.getCurrentUserInfo, task.createdBy))
194+
const isTaskInCancellableState = task && [TaskStatus.Pending, TaskStatus.Received, TaskStatus.Started].includes(task?.taskStatus)
195+
const isTaskCancellable = isTaskCancellableByUser && isTaskInCancellableState
190196
if (loading) return <Loading isGlobal />
191197
if (!task) return <></>
192198

@@ -283,7 +289,7 @@ function JobDetailsModal({ isOpen, handleClose, taskId }: JobDetailsModalProps)
283289
</List>
284290
</Grid>
285291
<Grid item>
286-
{isTaskCancelleable ? loadingCancelTask ? <div>loding</div>: <CancelTaskButton
292+
{isTaskCancellable ? loadingCancelTask ? <div>loading</div> : <CancelTaskButton
287293
variant="outlined"
288294
size="small"
289295
color="error"

aqueductcore/frontend/src/components/organisms/JobListInExperimentDetails/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export const JobsListColumns: readonly JobsListColumnsType[] = [
4040
/>
4141
),
4242
},
43-
// {
44-
// id: "experiment",
45-
// label: "User",
46-
// },
43+
{
44+
id: "createdBy",
45+
label: "User",
46+
},
4747
{
4848
id: "receivedAt",
4949
label: "submission Time",

aqueductcore/frontend/src/helper/auth/userScope.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ const defined_scopes = {
1616
}
1717

1818
export function isUserAbleToEditExperiment(userInfo: UserInfo, createdBy: ExperimentData['createdBy']) {
19-
for (const item of userInfo.scopes) {
19+
const isExperimentOwner = userInfo.username === createdBy
20+
for (const scope of userInfo.scopes) {
2021
if (
21-
item === defined_scopes.EXPERIMENT_EDIT_ALL ||
22-
item === defined_scopes.EXPERIMENT_EDIT_OWN && userInfo.username === createdBy
22+
scope === defined_scopes.EXPERIMENT_EDIT_ALL ||
23+
(scope === defined_scopes.EXPERIMENT_EDIT_OWN && isExperimentOwner)
2324
) {
2425
return true
2526
}
@@ -28,10 +29,33 @@ export function isUserAbleToEditExperiment(userInfo: UserInfo, createdBy: Experi
2829
}
2930

3031
export function isUserAbleToDeleteExperiment(userInfo: UserInfo, createdBy: ExperimentData['createdBy']) {
31-
for (const item of userInfo.scopes) {
32+
const isExperimentOwner = userInfo.username === createdBy
33+
for (const scope of userInfo.scopes) {
3234
if (
33-
item === defined_scopes.EXPERIMENT_DELETE_ALL ||
34-
item === defined_scopes.EXPERIMENT_DELETE_OWN && userInfo.username === createdBy
35+
scope === defined_scopes.EXPERIMENT_DELETE_ALL ||
36+
(scope === defined_scopes.EXPERIMENT_DELETE_OWN && isExperimentOwner)
37+
) {
38+
return true
39+
}
40+
}
41+
return false
42+
}
43+
44+
export function isUserAbleToCreateTask(userInfo: UserInfo) {
45+
for (const scope of userInfo.scopes) {
46+
if (scope === defined_scopes.JOB_CREATE) {
47+
return true
48+
}
49+
}
50+
return false
51+
}
52+
53+
export function isUserAbleToCancelTask(userInfo: UserInfo, createdBy: ExperimentData['createdBy']) {
54+
const isExperimentOwner = userInfo.username === createdBy
55+
for (const scope of userInfo.scopes) {
56+
if (
57+
scope === defined_scopes.JOB_CANCEL_OWN ||
58+
(scope === defined_scopes.JOB_CANCEL_ALL && isExperimentOwner)
3559
) {
3660
return true
3761
}

0 commit comments

Comments
 (0)