Skip to content

Commit 0f00d18

Browse files
feat: implement workspace authorization checks for folder and sidebar (#8805)
* feat: implement workspace authorization checks for folder and sidebar components * fix: correct button type assignment in Button component * fix: drag-and-drop functionality with access restrictions * feat: hide context menu for non-editors * feat: add workspace auth check for creating new folders in the dashboard header * fix: admins can perform editor and viewer actions and editors can perform viewer actions * feat: add team editor check for creating new devboxes in empty folders * fix: add missing semicolon in ContextMenu component
1 parent aa99921 commit 0f00d18

File tree

12 files changed

+80
-44
lines changed

12 files changed

+80
-44
lines changed

packages/app/src/app/hooks/useWorkspaceAuthorization.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ export const useWorkspaceAuthorization = (): WorkspaceAuthorizationReturn => {
5353

5454
const isTeamAdmin = isAdmin;
5555

56-
const isTeamEditor = authorization === TeamMemberAuthorization.Write;
56+
const isTeamEditor = authorization === TeamMemberAuthorization.Write || isTeamAdmin;
5757

58-
const isTeamViewer = authorization === TeamMemberAuthorization.Read;
58+
const isTeamViewer = authorization === TeamMemberAuthorization.Read || isTeamEditor || isTeamAdmin;
5959

6060
return {
6161
isBillingManager: Boolean(teamManager) || isAdmin,

packages/app/src/app/pages/Dashboard/Components/Folder/FolderCard.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
IconButton,
88
InteractiveOverlay,
99
} from '@codesandbox/components';
10+
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
1011
import { FolderItemComponentProps } from './types';
1112
import { StyledCard } from '../shared/StyledCard';
1213

@@ -35,7 +36,10 @@ export const FolderCard: React.FC<FolderItemComponentProps> = ({
3536

3637
'data-selection-id': dataSelectionId,
3738
...props
38-
}) => (
39+
}) => {
40+
const { isTeamEditor } = useWorkspaceAuthorization();
41+
42+
return (
3943
<InteractiveOverlay>
4044
<StyledCard
4145
data-selection-id={dataSelectionId}
@@ -44,7 +48,7 @@ export const FolderCard: React.FC<FolderItemComponentProps> = ({
4448
>
4549
<Stack justify="space-between">
4650
<Icon size={20} name="folder" color="#E3FF73" />
47-
{!isNewFolder ? (
51+
{!isNewFolder && isTeamEditor ? (
4852
<IconButton
4953
css={{
5054
marginRight: '-4px',
@@ -99,4 +103,4 @@ export const FolderCard: React.FC<FolderItemComponentProps> = ({
99103
</Stack>
100104
</StyledCard>
101105
</InteractiveOverlay>
102-
);
106+
)};

packages/app/src/app/pages/Dashboard/Components/Folder/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import track from '@codesandbox/common/lib/utils/analytics';
77
import { ESC } from '@codesandbox/common/lib/utils/keycodes';
88
import { dashboard as dashboardUrls } from '@codesandbox/common/lib/utils/url-generator';
99
import { useAppState, useActions } from 'app/overmind';
10+
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
1011
import { FolderCard } from './FolderCard';
1112
import { FolderListItem } from './FolderListItem';
1213
import { useSelection } from '../Selection';
@@ -18,6 +19,7 @@ export const Folder = (folderItem: DashboardFolder) => {
1819
const {
1920
dashboard: { renameFolder },
2021
} = useActions();
22+
const { isTeamEditor } = useWorkspaceAuthorization();
2123

2224
const {
2325
name = '',
@@ -93,7 +95,7 @@ export const Folder = (folderItem: DashboardFolder) => {
9395

9496
/* Drop target logic */
9597

96-
const accepts = ['sandbox'];
98+
const accepts = isTeamEditor ? ['sandbox'] : [];
9799

98100
const [{ isOver, canDrop }, dropRef] = useDrop({
99101
accept: accepts,
@@ -112,6 +114,7 @@ export const Folder = (folderItem: DashboardFolder) => {
112114

113115
const [, dragRef, preview] = useDrag({
114116
item: folderItem,
117+
canDrag: isTeamEditor,
115118
end: (item, monitor) => {
116119
const dropResult = monitor.getDropResult();
117120

@@ -124,7 +127,7 @@ export const Folder = (folderItem: DashboardFolder) => {
124127

125128
const dragProps = {
126129
ref: dragRef,
127-
onDragStart: event => onDragStart(event, path, 'folder'),
130+
onDragStart: isTeamEditor ? (event => onDragStart(event, path, 'folder')) : undefined,
128131
};
129132

130133
React.useEffect(() => {
@@ -185,7 +188,7 @@ export const Folder = (folderItem: DashboardFolder) => {
185188
onClick,
186189
onDoubleClick,
187190
// edit mode
188-
editing: isRenaming && selected,
191+
editing: isRenaming && selected && isTeamEditor,
189192
isNewFolder: false,
190193
isDragging,
191194
newName,

packages/app/src/app/pages/Dashboard/Components/Header/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { useLocation } from 'react-router-dom';
33
import { useAppState, useActions } from 'app/overmind';
44
import { Stack, Text, Button, Icon } from '@codesandbox/components';
5+
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
56
import { Breadcrumbs, BreadcrumbProps } from '../Breadcrumbs';
67
import { ViewOptions } from '../Filters/ViewOptions';
78
import { SortOptions } from '../Filters/SortOptions';
@@ -35,6 +36,7 @@ export const Header = ({
3536
const location = useLocation();
3637
const { modalOpened, dashboard: dashboardActions } = useActions();
3738
const { dashboard } = useAppState();
39+
const { isTeamEditor } = useWorkspaceAuthorization();
3840

3941
const repositoriesListPage =
4042
location.pathname.includes('/repositories') &&
@@ -68,7 +70,7 @@ export const Header = ({
6870
)}
6971
</Stack>
7072
<Stack gap={1} align="center">
71-
{location.pathname.includes('/sandboxes') && (
73+
{location.pathname.includes('/sandboxes') && isTeamEditor && (
7274
<Button onClick={createNewFolder} variant="ghost" autoWidth>
7375
<Icon
7476
name="folder"

packages/app/src/app/pages/Dashboard/Components/Sandbox/index.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,18 @@ const GenericSandbox = ({ isScrolling, item, page }: GenericSandboxProps) => {
9393
screenshotUrl = '/static/img/default-sandbox-thumbnail.png';
9494
}
9595

96+
/** Access restrictions */
97+
let { noDrag } = item;
98+
99+
if (activeWorkspaceAuthorization === 'READ') {
100+
noDrag = true;
101+
}
102+
96103
/* Drag logic */
97104

98105
const [, dragRef, preview] = useDrag({
99106
item,
107+
canDrag: !noDrag,
100108
end: (_item, monitor) => {
101109
const dropResult = monitor.getDropResult();
102110

@@ -120,13 +128,6 @@ const GenericSandbox = ({ isScrolling, item, page }: GenericSandboxProps) => {
120128
const Component: React.FC<SandboxItemComponentProps> =
121129
viewMode === 'list' ? SandboxListItem : SandboxCard;
122130

123-
/** Access restrictions */
124-
let { noDrag } = item;
125-
126-
if (activeWorkspaceAuthorization === 'READ') {
127-
noDrag = true;
128-
}
129-
130131
// interactions
131132
const {
132133
selectedIds,

packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/FolderMenu.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { useActions } from 'app/overmind';
33
import { Menu } from '@codesandbox/components';
44
import track from '@codesandbox/common/lib/utils/analytics';
5+
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
56
import { Context, MenuItem } from '../ContextMenu';
67
import { DashboardBaseFolder } from '../../../types';
78

@@ -14,8 +15,13 @@ export const FolderMenu = ({ folder, setRenaming }: FolderMenuProps) => {
1415
const {
1516
dashboard: { deleteFolder },
1617
} = useActions();
18+
const { isTeamEditor } = useWorkspaceAuthorization();
1719
const { visible, setVisibility, position } = React.useContext(Context);
1820

21+
if (!isTeamEditor) {
22+
return null;
23+
}
24+
1925
return (
2026
<Menu.ContextMenu
2127
visible={visible}

packages/app/src/app/pages/Dashboard/Content/routes/Sandboxes/index.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SelectionProvider } from 'app/pages/Dashboard/Components/Selection';
99
import { VariableGrid } from 'app/pages/Dashboard/Components/VariableGrid';
1010
import { DashboardGridItem, PageTypes } from 'app/pages/Dashboard/types';
1111
import { useWorkspaceLimits } from 'app/hooks/useWorkspaceLimits';
12+
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
1213
import { ActionCard } from 'app/pages/Dashboard/Components/shared/ActionCard';
1314
import { useFilteredItems } from './useFilteredItems';
1415

@@ -21,6 +22,7 @@ export const SandboxesPage = () => {
2122
const items = useFilteredItems(currentPath, cleanParam, level);
2223
const actions = useActions();
2324
const { isFrozen } = useWorkspaceLimits();
25+
const { isTeamEditor } = useWorkspaceAuthorization();
2426
const {
2527
dashboard: { allCollections },
2628
activeTeam,
@@ -87,23 +89,25 @@ export const SandboxesPage = () => {
8789
{isEmpty ? (
8890
<EmptyPage.StyledWrapper>
8991
<EmptyPage.StyledGrid>
90-
<ActionCard
91-
icon="plus"
92-
disabled={isFrozen}
93-
onClick={() => {
94-
track('Empty Folder - Create devbox', {
95-
codesandbox: 'V1',
96-
event_source: 'UI',
97-
});
92+
{(isTeamEditor) && (
93+
<ActionCard
94+
icon="plus"
95+
disabled={isFrozen}
96+
onClick={() => {
97+
track('Empty Folder - Create devbox', {
98+
codesandbox: 'V1',
99+
event_source: 'UI',
100+
});
98101

99-
actions.modalOpened({
100-
modal: 'create',
101-
itemId: currentCollection.id,
102-
});
103-
}}
104-
>
105-
Create
106-
</ActionCard>
102+
actions.modalOpened({
103+
modal: 'create',
104+
itemId: currentCollection.id,
105+
});
106+
}}
107+
>
108+
Create
109+
</ActionCard>
110+
)}
107111
</EmptyPage.StyledGrid>
108112
</EmptyPage.StyledWrapper>
109113
) : (

packages/app/src/app/pages/Dashboard/Sidebar/ContextMenu.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useHistory, useLocation } from 'react-router-dom';
44
import { Stack, Menu, Icon, Text } from '@codesandbox/components';
55
import track from '@codesandbox/common/lib/utils/analytics';
66
import { dashboard as dashboardUrls } from '@codesandbox/common/lib/utils/url-generator';
7+
import { useWorkspaceAuthorization } from 'app/hooks/useWorkspaceAuthorization';
78
import { Position } from '../Components/Selection';
89
import { DashboardBaseFolder } from '../types';
910
import { NEW_FOLDER_ID } from './constants';
@@ -14,7 +15,6 @@ const Context = React.createContext({
1415

1516
interface ContextMenuProps {
1617
activeTeam: string | null;
17-
authorization: string | null;
1818
visible: boolean;
1919
setVisibility: (val: boolean) => void;
2020
position: Position;
@@ -25,7 +25,6 @@ interface ContextMenuProps {
2525

2626
export const ContextMenu: React.FC<ContextMenuProps> = ({
2727
activeTeam,
28-
authorization,
2928
visible,
3029
position,
3130
setVisibility,
@@ -34,15 +33,18 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({
3433
setNewFolderPath,
3534
}) => {
3635
const { deleteFolder } = useActions().dashboard;
36+
const { isTeamEditor } = useWorkspaceAuthorization();
3737

3838
const history = useHistory();
3939
const location = useLocation();
4040

41-
if (!visible || !folder) return null;
41+
if (!visible || !folder || !isTeamEditor) {
42+
return null;
43+
};
4244

4345
let menuOptions;
4446

45-
if (folder.name === 'Drafts' || authorization === 'READ') {
47+
if (folder.name === 'Drafts') {
4648
menuOptions = (
4749
<MenuItem onSelect={() => {}}>
4850
<Stack gap={1}>

packages/app/src/app/pages/Dashboard/Sidebar/NestableRowItem.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface NestableRowItemProps {
3535
path: string;
3636
page: PageTypes;
3737
folders: DashboardBaseFolder[];
38+
canEdit?: boolean;
3839
}
3940

4041
export const NestableRowItem: React.FC<NestableRowItemProps> = ({
@@ -43,6 +44,7 @@ export const NestableRowItem: React.FC<NestableRowItemProps> = ({
4344
page,
4445
folderPath,
4546
folders,
47+
canEdit = true,
4648
}) => {
4749
const actions = useActions();
4850
const state = useAppState();
@@ -96,6 +98,11 @@ export const NestableRowItem: React.FC<NestableRowItemProps> = ({
9698

9799
const onContextMenu = event => {
98100
event.preventDefault();
101+
102+
if (!canEdit) {
103+
return;
104+
};
105+
99106
setMenuVisibility(true);
100107
setMenuFolder({ name, path: folderPath });
101108
setMenuPosition({ x: event.clientX, y: event.clientY });
@@ -206,6 +213,7 @@ export const NestableRowItem: React.FC<NestableRowItemProps> = ({
206213
icon="folder"
207214
nestingLevel={nestingLevel}
208215
setFoldersVisibility={setFoldersVisibility}
216+
canEdit={canEdit}
209217
>
210218
<Link
211219
to={folderUrl}
@@ -315,6 +323,7 @@ export const NestableRowItem: React.FC<NestableRowItemProps> = ({
315323
path={dashboardUrls.sandboxes(folder.path, state.activeTeam)}
316324
folderPath={folder.path}
317325
folders={folders}
326+
canEdit={canEdit}
318327
/>
319328
))}
320329
</motion.ul>

packages/app/src/app/pages/Dashboard/Sidebar/RowItem.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ interface RowItemProps {
6060
folderPath?: string;
6161
nestingLevel?: number;
6262
style?: CSSProperties;
63+
canEdit?: boolean;
6364
}
6465

6566
export const RowItem: React.FC<RowItemProps> = ({
@@ -71,14 +72,18 @@ export const RowItem: React.FC<RowItemProps> = ({
7172
icon,
7273
setFoldersVisibility = null,
7374
style = {},
75+
canEdit = true,
7476
...props
7577
}) => {
7678
const accepts: Array<'sandbox' | 'folder' | 'template'> = [];
77-
if (!canNotAcceptSandboxes.includes(page)) {
78-
accepts.push('template');
79-
accepts.push('sandbox');
79+
80+
if (canEdit && !canNotAcceptSandboxes.includes(page)) {
81+
accepts.push('template', 'sandbox');
82+
}
83+
84+
if (canEdit && !canNotAcceptFolders.includes(page)) {
85+
accepts.push('folder');
8086
}
81-
if (!canNotAcceptFolders.includes(page)) accepts.push('folder');
8287

8388
const usedPath = folderPath || path;
8489
const [{ canDrop, isOver, isDragging }, dropRef] = useDrop({

0 commit comments

Comments
 (0)