Skip to content

Commit

Permalink
Add file permissions to details view
Browse files Browse the repository at this point in the history
fixes: cockpit-project#565

Adds permissions column to details view with the ability to sort by
permissions. Sorting is done by comparing permission octal values.
  • Loading branch information
tomasmatus committed Jul 22, 2024
1 parent 0f8f6f6 commit 3cb440d
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 2 deletions.
20 changes: 18 additions & 2 deletions src/files-card-body.scss
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,14 @@
text-align: end;
}

.col-perms, .item-perms {
inline-size: 17ch;
text-align: end;
}

// Remove the extra padding on the end of the column and button, as the icon has padding already
.col-size, .col-date {
.col-size, .col-date,
.col-perms {
&, .pf-v5-c-table__button {
padding-inline-end: 0;
}
Expand Down Expand Up @@ -279,7 +285,7 @@
color: var(--pf-v5-global--Color--200);
}

.item-date {
.item-date, .item-perms {
display: none;
}

Expand All @@ -299,3 +305,13 @@
}
}
}

.pf-v5-c-content.permissions-tooltip-text {
color: var(--pf-v5-u-color-light-200);
text-align: start;
font-size: var(--pf-v5-u-font-size-md);

li + li {
margin-block-start: var(--pf-v5-u-mt-xs);
}
}
79 changes: 79 additions & 0 deletions src/files-card-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from "react"
import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
import { MenuItem, MenuList } from "@patternfly/react-core/dist/esm/components/Menu";
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner";
import {
Text, TextContent, TextList,
TextListItem, TextVariants
} from "@patternfly/react-core/dist/esm/components/Text";
import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex";
import { FolderIcon, SearchIcon } from '@patternfly/react-icons';
import { SortByDirection, Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
Expand All @@ -33,6 +38,7 @@ import { useDialogs } from "dialogs";
import * as timeformat from "timeformat";

import { FolderFileInfo, useFilesContext } from "./app";
import { get_permissions } from "./common";
import { confirm_delete } from "./dialogs/delete";
import { Sort, filterColumnMapping, filterColumns } from "./header";
import { get_menu_items } from "./menu";
Expand All @@ -48,6 +54,8 @@ function compare(sortBy: Sort): (a: FolderFileInfo, b: FolderFileInfo) => number
// treat non-regular files and infos with missing 'size' field as having size of zero
const size = (a: FolderFileInfo) => (a.type === "reg" && a.size) || 0;
const mtime = (a: FolderFileInfo) => a.mtime || 0; // fallbak for missing .mtime field
// mask special bits when sorting
const perms = (a: FolderFileInfo) => a.mode ? (a.mode & (~(0b111 << 9))) : 0;

switch (sortBy) {
case Sort.az:
Expand All @@ -62,6 +70,10 @@ function compare(sortBy: Sort): (a: FolderFileInfo, b: FolderFileInfo) => number
return (a, b) => dir_sort(a, b) || (size(b) - size(a)) || name_sort(a, b);
case Sort.smallest_size:
return (a, b) => dir_sort(a, b) || (size(a) - size(b)) || name_sort(a, b);
case Sort.largest_permissions:
return (a, b) => dir_sort(a, b) || (perms(b) - perms(a)) || name_sort(a, b);
case Sort.smallest_permissions:
return (a, b) => dir_sort(a, b) || (perms(a) - perms(b)) || name_sort(a, b);
}
}

Expand Down Expand Up @@ -409,6 +421,7 @@ export const FilesCardBody = ({
modifier="nowrap"
>{_("Modified")}
</Th>
<Th sort={sortColumn(3)} className="col-perms">{_("Permissions")}</Th>
</Tr>
</Thead>
<Tbody>
Expand All @@ -434,6 +447,66 @@ const getFileType = (file: FolderFileInfo) => {
}
};

const FilePermissions = ({ file } : {
file: FolderFileInfo,
}) => {
function permissionShortStr(mode: number) {
const specialBits = (mode >> 9) & 0b111;
const permsStr = [];
for (let i = 2; i >= 0; i--) {
const offset = i * 3;
let shortStr = "";
shortStr += (mode & (0b1 << (offset + 2))) ? "r" : "-";
shortStr += (mode & (0b1 << (offset + 1))) ? "w" : "-";

if (mode & (1 << offset)) {
if (specialBits & (0b1 << i)) {
shortStr += (i === 0) ? "t" : "s";
} else {
shortStr += "x";
}
} else {
if (specialBits & (0b1 << i)) {
shortStr += (i === 0) ? "T" : "S";
} else {
shortStr += "-";
}
}

permsStr.push(shortStr);
}

return permsStr;
}

const { mode } = file;
if (mode === undefined) {
return null;
}
const permsShortStr = permissionShortStr(mode);
const permsGroups = [_("Owner"), _("Group"), _("Others")];

const tooltip = (
<TextContent className="permissions-tooltip-text">
<TextList isPlain>
{permsGroups.map((permGroup, i) => {
return (
<TextListItem key={file.name + "-perms-" + permGroup}>
{permGroup + ": " + get_permissions(mode >> (6 - 3 * i))}
</TextListItem>
);
})}
</TextList>
</TextContent>
);

return (
<Tooltip content={tooltip}>
<Text component={TextVariants.pre}>{permsShortStr.join(" ")}</Text>
</Tooltip>
);
};

// Memoize the Item component as rendering thousands of them on each render of parent component is costly.
const Row = React.memo(function Item({ file, isSelected } : {
file: FolderFileInfo,
Expand Down Expand Up @@ -470,6 +543,12 @@ const Row = React.memo(function Item({ file, isSelected } : {
>
{file.mtime ? timeformat.dateTime(file.mtime * 1000) : null}
</Td>
<Td
className="item-perms"
modifier="nowrap"
>
<FilePermissions file={file} />
</Td>
</Tr>
);
});
13 changes: 13 additions & 0 deletions src/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export enum Sort {
smallest_size = 'smallest_size',
first_modified = 'first_modified',
last_modified = 'last_modified',
smallest_permissions = 'smallest_permissions',
largest_permissions = 'largest_permissions',
}

export function is_sort(x: unknown): x is Sort {
Expand Down Expand Up @@ -86,6 +88,17 @@ export const filterColumns = [
label: _("Last modified"),
},
},
{
title: _("Permissions"),
[SortByDirection.asc]: {
itemId: Sort.largest_permissions,
label: _("Largest permissions"),
},
[SortByDirection.desc]: {
itemId: Sort.smallest_permissions,
label: _("Smallest permissions"),
},
},
] as const;

// { itemId: [index, sortdirection] }
Expand Down
115 changes: 115 additions & 0 deletions test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,68 @@ class TestFiles(testlib.MachineCase):
b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa")
b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc")

# Test sorting by permissions
basedir = "/home/admin"
m.execute(f"""
chmod 0754 {basedir}/eee
chmod 0755 {basedir}/Eee
chmod 0500 {basedir}/BBB
chmod 0477 {basedir}/aaa
chmod 0511 {basedir}/ccc
""")

b.click("th button:contains(Permissions)")
b.wait_visible("th[aria-sort='ascending'].pf-m-selected button:contains(Permissions)")
b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd")
b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "Eee")
b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "eee")
b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "ccc")
b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB")
b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "aaa")

# Directories are sorted first
m.execute(f"""
chmod 0755 {basedir}/eee
chmod 0123 {basedir}/Eee
chmod 0777 {basedir}/BBB
chmod 0640 {basedir}/aaa
chmod 0600 {basedir}/ccc
""")

b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd")
b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee")
b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee")
b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "BBB")
b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa")
b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc")

# Clicking again inverts the list
# Directories are still sorted first
b.click("th button:contains(Permissions)")
b.wait_visible("th[aria-sort='descending'].pf-m-selected button:contains(Permissions)")
b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee")
b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee")
b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd")
b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "ccc")
b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa")
b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "BBB")

# Special bits are not used for sorting
m.execute(f"""
chmod 6755 {basedir}/eee
chmod 0755 {basedir}/Eee
chmod 1700 {basedir}/BBB
chmod 3700 {basedir}/aaa
chmod 0701 {basedir}/ccc
""")

b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee")
b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee")
b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd")
b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa")
b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB")
b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc")

def testDelete(self) -> None:
b = self.browser
m = self.machine
Expand Down Expand Up @@ -1131,6 +1193,59 @@ class TestFiles(testlib.MachineCase):
b.click("button.pf-m-link")
b.wait_not_present(".pf-v5-c-modal-box")

# Test permissions in details view
b.click("button[aria-label='Display as a list']")
b.click(".breadcrumb-button-edit")
b.set_input_text("#new-path-input", "/home/admin")
b.click(".breadcrumb-button-edit-apply")
basedir = "/home/admin"
for i in range(8):
m.execute(f"runuser -u admin touch {basedir}/file{i}")

def check_perms_match(file: str, basedir: str) -> None:
ls = m.execute(f"ls -l {basedir}/{file}")[:10]
ui = b.text(f"[data-item='{file}'] td:nth-child(4) pre").replace(" ", "")
self.assertEqual(ls[1:], ui)

# Simple permissions
m.execute(f"chmod 000 {basedir}/file0")
m.execute(f"chmod 111 {basedir}/file1")
m.execute(f"chmod 222 {basedir}/file2")
m.execute(f"chmod 333 {basedir}/file3")
m.execute(f"chmod 444 {basedir}/file4")
m.execute(f"chmod 555 {basedir}/file5")
m.execute(f"chmod 666 {basedir}/file6")
m.execute(f"chmod 777 {basedir}/file7")

for i in range(8):
check_perms_match(f"file{i}", basedir)

# Test different permissions for owner/grou/others
m.execute(f"chmod 411 {basedir}/file0")
m.execute(f"chmod 546 {basedir}/file1")
m.execute(f"chmod 337 {basedir}/file2")
m.execute(f"chmod 755 {basedir}/file3")
m.execute(f"chmod 613 {basedir}/file4")
m.execute(f"chmod 711 {basedir}/file5")
m.execute(f"chmod 531 {basedir}/file6")
m.execute(f"chmod 740 {basedir}/file7")

for i in range(8):
check_perms_match(f"file{i}", basedir)

# Test permissions with setuid, setgid, sticky
m.execute(f"chmod 1000 {basedir}/file0")
m.execute(f"chmod 2111 {basedir}/file1")
m.execute(f"chmod 3222 {basedir}/file2")
m.execute(f"chmod 4333 {basedir}/file3")
m.execute(f"chmod 5444 {basedir}/file4")
m.execute(f"chmod 6555 {basedir}/file5")
m.execute(f"chmod 7666 {basedir}/file6")
m.execute(f"chmod 3777 {basedir}/file7")

for i in range(8):
check_perms_match(f"file{i}", basedir)

def testErrors(self) -> None:
b = self.browser
m = self.machine
Expand Down

0 comments on commit 3cb440d

Please sign in to comment.