Skip to content

Commit 428c73b

Browse files
authored
refactor(app-workflows): permissions (#4787)
1 parent 20c2edc commit 428c73b

File tree

16 files changed

+264
-144
lines changed

16 files changed

+264
-144
lines changed

eslint.config.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,18 @@ export default defineConfig([
183183
}
184184
},
185185
globalIgnores([
186+
//".github/**/*",
186187
".idea/**/*",
187188
".nx/**/*",
188-
".yarn/**/*",
189+
".pulumi/**/*",
190+
".stormTests/**/*",
191+
".swc/**/*",
189192
".webiny/**/*",
193+
".yarn/**/*",
194+
"ai-context/**/*",
195+
"coverage/**/*",
196+
//"cypress-tests/**/*",
197+
"docs/**/*",
190198
"**/node_modules/",
191199
"**/dist/",
192200
"**/lib/",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export const WORKFLOW_MODEL_ID = "workflow";
22
export const WORKFLOW_STATE_MODEL_ID = "workflowState";
3+
export const WORKFLOWS_PERMISSION = "workflows";

packages/api-workflows/src/context/WorkflowsContext.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { Context } from "~/types.js";
1+
import {
2+
type Context,
3+
type IWorkflowsSecurityPermission,
4+
WorkflowsSecurityPermissionAccessLevel
5+
} from "~/types.js";
26
import type { CmsModel } from "@webiny/api-headless-cms/types/index.js";
37
import { NotFoundError } from "@webiny/handler-graphql";
48
import { createIdentifier } from "@webiny/utils";
@@ -14,6 +18,7 @@ import type { IWorkflowsTransformer } from "./transformer/abstractions/Workflows
1418
import type { IWorkflow } from "~/context/abstractions/Workflow.js";
1519
import { parseIdentifier } from "@webiny/utils/parseIdentifier.js";
1620
import { NotAuthorizedError } from "./errors.js";
21+
import { WORKFLOWS_PERMISSION } from "~/constants.js";
1722

1823
export interface IWorkflowsContextParams {
1924
context: Pick<Context, "cms" | "security">;
@@ -89,6 +94,7 @@ export class WorkflowsContext implements IWorkflowsContext {
8994
}
9095

9196
public async getWorkflow(params: IWorkflowsContextGetParams): Promise<IWorkflow | null> {
97+
this.ensureReadAccess();
9298
try {
9399
const id = createIdentifier({
94100
id: params.id,
@@ -113,6 +119,7 @@ export class WorkflowsContext implements IWorkflowsContext {
113119
public async listWorkflows(
114120
params: IWorkflowsContextListParams
115121
): Promise<IWorkflowsContextListResponse> {
122+
this.ensureReadAccess();
116123
return this.context.security.withoutAuthorization(async () => {
117124
const where = {
118125
...this.convertListWhere(params.where)
@@ -136,12 +143,27 @@ export class WorkflowsContext implements IWorkflowsContext {
136143
});
137144
}
138145

139-
private async ensureManageAccess(): Promise<void> {
140-
const permissions = await this.context.security.getPermissions("workflows");
141-
if (permissions?.length) {
146+
private ensureReadAccess(): void {
147+
const identity = this.context.security.getIdentity();
148+
if (identity?.id) {
142149
return;
143150
}
144-
throw new NotAuthorizedError("You have no access to workflows.");
151+
throw new NotAuthorizedError("You cannot read workflows.");
152+
}
153+
154+
private async ensureManageAccess(): Promise<void> {
155+
const permissions =
156+
await this.context.security.getPermissions<IWorkflowsSecurityPermission>(
157+
WORKFLOWS_PERMISSION
158+
);
159+
for (const permission of permissions) {
160+
if (permission.name === "*") {
161+
return;
162+
} else if (permission.editor === WorkflowsSecurityPermissionAccessLevel.YES) {
163+
return;
164+
}
165+
}
166+
throw new NotAuthorizedError("You cannot manage workflows.");
145167
}
146168

147169
private async createWorkflow(params: ICreateWorkflowParams): Promise<IWorkflow> {

packages/api-workflows/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@ import type { Context as TasksContext } from "@webiny/tasks/types.js";
33
import type { IWorkflowsContext } from "~/context/abstractions/WorkflowsContext.js";
44
import type { IWorkflowStateContext } from "~/context/abstractions/WorkflowStateContext.js";
55
import type { ApiCoreContext } from "@webiny/api-core/types/core.js";
6+
import type { SecurityPermission } from "@webiny/api-core/types/security.js";
67

78
export interface Context extends ApiCoreContext, CmsContext, TasksContext {
89
workflows: IWorkflowsContext;
910
workflowState: IWorkflowStateContext;
1011
}
12+
13+
export enum WorkflowsSecurityPermissionAccessLevel {
14+
NO = "no",
15+
YES = "yes"
16+
}
17+
18+
export interface IWorkflowsSecurityPermission extends SecurityPermission {
19+
editor: WorkflowsSecurityPermissionAccessLevel;
20+
}

packages/app-headless-cms-workflows/src/Components/CmsWorkflows/CmsWorkflowsEditorView.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import type { IconProp } from "@fortawesome/fontawesome-svg-core";
1111
import { createAppName } from "~/utils/appName.js";
1212

1313
const {
14-
Admin: { WorkflowsEditor }
14+
Admin: { WorkflowsEditor },
15+
Permissions: { HasWorkflowsEditorPermission }
1516
} = Components;
1617

1718
const { Menu } = AdminConfig;
@@ -25,14 +26,19 @@ export const CmsWorkflowsEditorMenu = () => {
2526
}
2627

2728
return (
28-
<Menu
29-
name={"headlessCMS.contentModels.workflows"}
30-
pinnable={true}
31-
parent={"headlessCMS"}
32-
element={
33-
<Menu.Link text={"Workflows"} to={router.getLink(Routes.ContentModels.Workflows)} />
34-
}
35-
/>
29+
<HasWorkflowsEditorPermission>
30+
<Menu
31+
name={"headlessCMS.contentModels.workflows"}
32+
pinnable={true}
33+
parent={"headlessCMS"}
34+
element={
35+
<Menu.Link
36+
text={"Workflows"}
37+
to={router.getLink(Routes.ContentModels.Workflows)}
38+
/>
39+
}
40+
/>
41+
</HasWorkflowsEditorPermission>
3642
);
3743
};
3844

@@ -72,6 +78,13 @@ export const CmsWorkflowsEditorView = () => {
7278
});
7379
}, [models, canEdit]);
7480

81+
const app = useMemo(() => {
82+
if (!route.params.app) {
83+
return apps.find(() => true);
84+
}
85+
return apps.find(a => a.id === route.params.app) || null;
86+
}, [route.params.app]);
87+
7588
const onAppClick = useCallback(
7689
(id: string) => {
7790
goToRoute(Routes.ContentModels.Workflows, {
@@ -87,5 +100,5 @@ export const CmsWorkflowsEditorView = () => {
87100
return <Loader size="lg" variant="accent" indeterminate={true} text="Loading..." />;
88101
}
89102

90-
return <WorkflowsEditor apps={apps} onAppClick={onAppClick} app={route.params.app} />;
103+
return <WorkflowsEditor apps={apps} onAppClick={onAppClick} app={app?.id} />;
91104
};

packages/app-workflows/src/Components/App/ContentReviews.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
WorkflowStatesOwnWidget,
1010
WorkflowStatesRequestedWidget
1111
} from "~/Components/WorkflowStatesWidget/index.js";
12-
import { HasPermission } from "@webiny/app-security/components/index.js";
1312

1413
const { Route } = AdminConfig;
1514

@@ -30,18 +29,16 @@ export const ContentReviews = () => {
3029
/>
3130
<WorkflowsMenu />
3231

33-
<HasPermission name={"workflows.contentReviews"}>
34-
<AdminConfig.Dashboard.Widget
35-
name="workflows.requested"
36-
column="right"
37-
element={<WorkflowStatesRequestedWidget client={client} />}
38-
/>
39-
<AdminConfig.Dashboard.Widget
40-
name="workflows.assigned"
41-
column="right"
42-
element={<WorkflowStatesOwnWidget client={client} />}
43-
/>
44-
</HasPermission>
32+
<AdminConfig.Dashboard.Widget
33+
name="workflows.requested"
34+
column="right"
35+
element={<WorkflowStatesRequestedWidget client={client} />}
36+
/>
37+
<AdminConfig.Dashboard.Widget
38+
name="workflows.own"
39+
column="right"
40+
element={<WorkflowStatesOwnWidget client={client} />}
41+
/>
4542
</AdminConfig>
4643
);
4744
};
Lines changed: 6 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,11 @@
1-
import React, { useMemo } from "react";
2-
import { Alert } from "@webiny/admin-ui";
3-
import { useCanUseWorkflows } from "~/hooks/canUseWorkflows.js";
4-
import {
5-
LeftPanel,
6-
RightPanel,
7-
SimpleForm,
8-
SimpleFormContent,
9-
SimpleFormFooter,
10-
SimpleFormHeader,
11-
SplitView
12-
} from "@webiny/app-admin";
13-
import { WorkflowsDataList } from "~/Components/WorkflowsEditor/DataList/WorkflowsDataList.js";
14-
import { WorkflowEditor } from "./Editor/WorkflowEditor.js";
15-
import type { IWorkflowApplication } from "~/types.js";
1+
import { HasWorkflowsEditorPermission } from "../WorkflowsPermissions/HasWorkflowsEditorPermission.js";
2+
import { type IWorkflowsEditorProps, WorkflowsEditorBase } from "./WorkflowsEditorBase.js";
3+
import React from "react";
164

17-
export interface IWorkflowsEditorProps {
18-
apps: IWorkflowApplication[];
19-
app: string | null | undefined;
20-
onAppClick: (id: string) => void;
21-
}
22-
/**
23-
* Main component which should get used to render Workflows Admin UI.
24-
*/
255
export const WorkflowsEditor = (props: IWorkflowsEditorProps) => {
26-
const { apps, onAppClick, app: initialApp } = props;
27-
28-
const canUseWorkflows = useCanUseWorkflows();
29-
30-
const app = useMemo(() => {
31-
if (!initialApp) {
32-
return null;
33-
}
34-
return apps.find(a => a.id === initialApp);
35-
}, [initialApp, apps]);
36-
37-
if (!canUseWorkflows) {
38-
return (
39-
<Alert type={"danger"} title={"You don't have access to Workflows."}>
40-
You do not have access to Workflows. Please contact your system administrator.
41-
</Alert>
42-
);
43-
}
44-
// return <WorkflowsAdminView apps={apps} onAppClick={onAppClick} app={app} />;
456
return (
46-
<SplitView>
47-
<LeftPanel>
48-
<WorkflowsDataList apps={apps} activeId={app?.id} onSelectApp={onAppClick} />
49-
</LeftPanel>
50-
<RightPanel>
51-
{app ? (
52-
<SimpleForm size={"lg"}>
53-
<SimpleFormHeader title={app.name} />
54-
<SimpleFormContent>
55-
<WorkflowEditor app={app} />
56-
</SimpleFormContent>
57-
<SimpleFormFooter>
58-
<></>
59-
</SimpleFormFooter>
60-
</SimpleForm>
61-
) : null}
62-
</RightPanel>
63-
</SplitView>
7+
<HasWorkflowsEditorPermission>
8+
<WorkflowsEditorBase {...props} />
9+
</HasWorkflowsEditorPermission>
6410
);
6511
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React, { useMemo } from "react";
2+
import { Alert } from "@webiny/admin-ui";
3+
import { useCanUseWorkflows } from "~/hooks/canUseWorkflows.js";
4+
import {
5+
LeftPanel,
6+
RightPanel,
7+
SimpleForm,
8+
SimpleFormContent,
9+
SimpleFormFooter,
10+
SimpleFormHeader,
11+
SplitView
12+
} from "@webiny/app-admin";
13+
import { WorkflowsDataList } from "~/Components/WorkflowsEditor/DataList/WorkflowsDataList.js";
14+
import { WorkflowEditor } from "./Editor/WorkflowEditor.js";
15+
import type { IWorkflowApplication } from "~/types.js";
16+
17+
export interface IWorkflowsEditorProps {
18+
apps: IWorkflowApplication[];
19+
app: string | null | undefined;
20+
onAppClick: (id: string) => void;
21+
}
22+
/**
23+
* Main component which should get used to render Workflows Admin UI.
24+
*/
25+
export const WorkflowsEditorBase = (props: IWorkflowsEditorProps) => {
26+
const { apps, onAppClick, app: initialApp } = props;
27+
28+
const canUseWorkflows = useCanUseWorkflows();
29+
30+
const app = useMemo(() => {
31+
if (!initialApp) {
32+
return null;
33+
}
34+
return apps.find(a => a.id === initialApp);
35+
}, [initialApp, apps]);
36+
37+
if (!canUseWorkflows) {
38+
return (
39+
<Alert type={"danger"} title={"You don't have access to Workflows."}>
40+
You do not have access to Workflows. Please contact your system administrator.
41+
</Alert>
42+
);
43+
}
44+
45+
return (
46+
<SplitView>
47+
<LeftPanel>
48+
<WorkflowsDataList apps={apps} activeId={app?.id} onSelectApp={onAppClick} />
49+
</LeftPanel>
50+
<RightPanel>
51+
{app ? (
52+
<SimpleForm size={"lg"}>
53+
<SimpleFormHeader title={app.name} />
54+
<SimpleFormContent>
55+
<WorkflowEditor app={app} />
56+
</SimpleFormContent>
57+
<SimpleFormFooter>
58+
<></>
59+
</SimpleFormFooter>
60+
</SimpleForm>
61+
) : null}
62+
</RightPanel>
63+
</SplitView>
64+
);
65+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
import { useWorkflowsPermission } from "~/Components/WorkflowsPermissions/useWorkflowsPermission.js";
3+
4+
export interface IHasWorkflowsSecurityPermissionProps {
5+
children: React.ReactElement | React.ReactElement[];
6+
}
7+
8+
export const HasWorkflowsEditorPermission = (props: IHasWorkflowsSecurityPermissionProps) => {
9+
const hasPermission = useWorkflowsPermission();
10+
11+
if (!hasPermission.editor) {
12+
return null;
13+
}
14+
return props.children;
15+
};

0 commit comments

Comments
 (0)