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 24, 2024
1 parent e90539b commit bc8b245
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const permissions = [
/* 4 */ _("Read-only"),
/* 5 */ _("Read and execute"),
/* 6 */ _("Read and write"),
/* 7 */ _("Read, write and execute"),
/* 7 */ _("Read, write, and execute"),
];

export const inode_types = {
Expand Down
17 changes: 13 additions & 4 deletions src/files-card-body.scss
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,15 @@
}

.col-size, .item-size,
.col-date, .item-date {
.col-date, .item-date,
.col-perms, .item-perms {
inline-size: 12ch;
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 All @@ -218,7 +220,7 @@
padding-inline-start: var(--pf-v5-global--spacer--lg);
}

:last-child:is(td, th) {
:last-child:is(td, th):not(.pf-v5-c-table__sort) {
padding-inline-end: var(--pf-v5-global--spacer--lg);
}
}
Expand Down Expand Up @@ -279,7 +281,7 @@
color: var(--pf-v5-global--Color--200);
}

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

Expand All @@ -299,3 +301,10 @@
}
}
}

.pf-v5-c-tooltip .permissions-tooltip-text {
display: grid;
grid-template-columns: auto 1fr;
column-gap: var(--pf-v5-global--spacer--sm);
text-align: start;
}
77 changes: 77 additions & 0 deletions src/files-card-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ 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, 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 +35,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 +51,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 +67,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.most_permissive:
return (a, b) => dir_sort(a, b) || (perms(b) - perms(a)) || name_sort(a, b);
case Sort.least_permissive:
return (a, b) => dir_sort(a, b) || (perms(a) - perms(b)) || name_sort(a, b);
}
}

Expand Down Expand Up @@ -409,6 +418,11 @@ export const FilesCardBody = ({
modifier="nowrap"
>{_("Modified")}
</Th>
<Th
sort={sortColumn(3)} className="col-perms"
modifier="nowrap"
>{_("Permissions")}
</Th>
</Tr>
</Thead>
<Tbody>
Expand All @@ -434,6 +448,63 @@ 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.mode;
if (mode === undefined) {
return null;
}
const permsGroups = [_("Owner"), _("Group"), _("Others")];
const tooltip = (
<dl className="permissions-tooltip-text">
{permsGroups.map((permGroup, i) => {
return (
<React.Fragment key={file.name + "-" + permGroup}>
<dt>{permGroup + ":"}</dt>
<dd>{get_permissions(mode >> (6 - 3 * i)).toLowerCase()}</dd>
</React.Fragment>
);
})}
</dl>
);

return (
<Tooltip content={tooltip}>
<Text component={TextVariants.pre}>{permissionShortStr(mode).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 +541,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',
most_permissive = 'most_permissive',
least_permissive = 'least_permissive',
}

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.most_permissive,
label: _("Most permissive"),
},
[SortByDirection.desc]: {
itemId: Sort.least_permissive,
label: _("Least permissive"),
},
},
] as const;

// { itemId: [index, sortdirection] }
Expand Down
118 changes: 116 additions & 2 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 @@ -1058,7 +1120,7 @@ class TestFiles(testlib.MachineCase):
select_access("7")
b.click("button.pf-m-primary")
self.assertEqual(m.execute("ls -l /home/admin/newfile")[:10], "-rwxrwxrwx")
wait_permissions("Read, write and execute")
wait_permissions("Read, write, and execute")

# Test changing CWD permissions
test_dir = "/home/admin/testdir"
Expand Down Expand Up @@ -1116,7 +1178,7 @@ class TestFiles(testlib.MachineCase):
b.wait_not_in_text(".pf-v5-c-modal-box__body", "Ownership")
b.click("button.pf-m-primary")
self.assertEqual(m.execute("ls -l /home/admin/adminfile")[:10], "-rwxrwxrwx")
wait_permissions("Read, write and execute")
wait_permissions("Read, write, and execute")
# Does not change ownership
b.wait_text("#description-list-owner dd", "admin")
b.wait_text("#description-list-group dd", "admin")
Expand All @@ -1131,6 +1193,58 @@ 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.mouse("[data-item='home']", "dblclick")
b.mouse("[data-item='admin']", "dblclick")
b.click("button[aria-label='Display as a list']")
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/group/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 bc8b245

Please sign in to comment.