diff --git a/fileglancer/filestore.py b/fileglancer/filestore.py index ce3417d6..553ad20f 100644 --- a/fileglancer/filestore.py +++ b/fileglancer/filestore.py @@ -748,17 +748,23 @@ def change_file_permissions(self, path: str, permissions: str): raise ValueError("Permissions must be a string of length 10") full_path = self._check_path_in_root(path) # Convert permission string (like '-rw-r--r--') to octal mode + # Execute positions (3, 6, 9) can contain special characters: + # 'x' = execute, 's'/'S' = setuid/setgid, 't'/'T' = sticky bit + # Lowercase = execute set, uppercase = execute not set mode = 0 # Owner permissions (positions 1-3) if permissions[1] == 'r': mode |= stat.S_IRUSR if permissions[2] == 'w': mode |= stat.S_IWUSR - if permissions[3] == 'x': mode |= stat.S_IXUSR + if permissions[3] in ('x', 's'): mode |= stat.S_IXUSR + if permissions[3] in ('s', 'S'): mode |= stat.S_ISUID # Group permissions (positions 4-6) if permissions[4] == 'r': mode |= stat.S_IRGRP if permissions[5] == 'w': mode |= stat.S_IWGRP - if permissions[6] == 'x': mode |= stat.S_IXGRP + if permissions[6] in ('x', 's'): mode |= stat.S_IXGRP + if permissions[6] in ('s', 'S'): mode |= stat.S_ISGID # Other permissions (positions 7-9) if permissions[7] == 'r': mode |= stat.S_IROTH if permissions[8] == 'w': mode |= stat.S_IWOTH - if permissions[9] == 'x': mode |= stat.S_IXOTH + if permissions[9] in ('x', 't'): mode |= stat.S_IXOTH + if permissions[9] in ('t', 'T'): mode |= stat.S_ISVTX os.chmod(full_path, mode) diff --git a/frontend/src/__tests__/componentTests/ChangePermissions.test.tsx b/frontend/src/__tests__/componentTests/ChangePermissions.test.tsx index 04830fa6..66b7f89f 100644 --- a/frontend/src/__tests__/componentTests/ChangePermissions.test.tsx +++ b/frontend/src/__tests__/componentTests/ChangePermissions.test.tsx @@ -73,7 +73,7 @@ describe('Change Permissions dialog', () => { expect(submitButton).toBeDisabled(); }); - it('should update local permissions when input is checked', async () => { + it('should update local permissions when write input is checked', async () => { const user = userEvent.setup(); const checkbox = screen.getByRole('checkbox', { name: 'w_8' }); @@ -85,6 +85,60 @@ describe('Change Permissions dialog', () => { expect(checkbox).toBeChecked(); }); + it('should update local permissions when execute input is toggled', async () => { + const user = userEvent.setup(); + // Owner execute is at position 3 - initial 'drwxrwxr-x' has 'x' at position 3 + const checkbox = screen.getByRole('checkbox', { name: 'x_3' }); + + expect(checkbox).toBeChecked(); + await user.click(checkbox); + expect(checkbox).not.toBeChecked(); + }); + + it('should show sticky bit checkbox for directories', () => { + const stickyCheckbox = screen.getByRole('checkbox', { name: 't_9' }); + expect(stickyCheckbox).toBeInTheDocument(); + // Initial 'drwxrwxr-x' does not have sticky bit + expect(stickyCheckbox).not.toBeChecked(); + }); + + it('should toggle sticky bit while preserving execute', async () => { + const user = userEvent.setup(); + const stickyCheckbox = screen.getByRole('checkbox', { name: 't_9' }); + const executeCheckbox = screen.getByRole('checkbox', { name: 'x_9' }); + + // Initial 'drwxrwxr-x' has execute at position 9 + expect(executeCheckbox).toBeChecked(); + expect(stickyCheckbox).not.toBeChecked(); + + // Enable sticky bit - should change 'x' to 't' (sticky + execute) + await user.click(stickyCheckbox); + expect(stickyCheckbox).toBeChecked(); + expect(executeCheckbox).toBeChecked(); // execute preserved + }); + + it('should show setgid checkbox for directories', () => { + const setgidCheckbox = screen.getByRole('checkbox', { name: 's_6' }); + expect(setgidCheckbox).toBeInTheDocument(); + // Initial 'drwxrwxr-x' does not have setgid + expect(setgidCheckbox).not.toBeChecked(); + }); + + it('should toggle setgid while preserving group execute', async () => { + const user = userEvent.setup(); + const setgidCheckbox = screen.getByRole('checkbox', { name: 's_6' }); + const groupExecuteCheckbox = screen.getByRole('checkbox', { name: 'x_6' }); + + // Initial 'drwxrwxr-x' has group execute at position 6 + expect(groupExecuteCheckbox).toBeChecked(); + expect(setgidCheckbox).not.toBeChecked(); + + // Enable setgid - should change 'x' to 's' (setgid + execute) + await user.click(setgidCheckbox); + expect(setgidCheckbox).toBeChecked(); + expect(groupExecuteCheckbox).toBeChecked(); // execute preserved + }); + it('calls toast.success for an ok HTTP response', async () => { const user = userEvent.setup(); const checkbox = screen.getByRole('checkbox', { name: 'w_8' }); diff --git a/frontend/src/components/ui/Dialogs/ChangePermissions.tsx b/frontend/src/components/ui/Dialogs/ChangePermissions.tsx index 6994366d..63573cb4 100644 --- a/frontend/src/components/ui/Dialogs/ChangePermissions.tsx +++ b/frontend/src/components/ui/Dialogs/ChangePermissions.tsx @@ -64,6 +64,7 @@ export default function ChangePermissions({ Read Write + Execute @@ -73,7 +74,7 @@ export default function ChangePermissions({ Owner: {fileBrowserState.propertiesTarget.owner} - {/* Owner read/write */} + {/* Owner read/write/execute */} + + handleLocalPermissionChange(event)} + type="checkbox" + /> + Group: {fileBrowserState.propertiesTarget.group} - {/* Group read/write */} + {/* Group read/write/execute */} + + handleLocalPermissionChange(event)} + type="checkbox" + /> + Everyone else - {/* Everyone else read/write */} + {/* Everyone else read/write/execute */} + + handleLocalPermissionChange(event)} + type="checkbox" + /> + ) : null} + + {fileBrowserState.propertiesTarget.is_dir && localPermissions ? ( + <> + + + + ) : null}