LCOV - code coverage report
Current view: top level - pkg/users - authorized-keys-panel.js Coverage Total Hit
Test: cockpit Lines: 60.5 % 114 69
Test Date: 2026-07-03 07:31:16

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2020 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           19 : import cockpit from 'cockpit';
       7           19 : import React from 'react';
       8              : import { useObject, useEvent } from 'hooks.js';
       9              : 
      10              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      11              : import { Card, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
      12              : import { OverflowMenu, OverflowMenuContent, OverflowMenuControl, OverflowMenuDropdownItem, OverflowMenuGroup, OverflowMenuItem } from "@patternfly/react-core/dist/esm/components/OverflowMenu/index.js";
      13              : import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
      14              : import { show_modal_dialog } from "cockpit-components-dialog.jsx";
      15              : import { ListingTable } from 'cockpit-components-table.jsx';
      16              : import { KebabDropdown } from 'cockpit-components-dropdown.jsx';
      17              : import { show_unexpected_error } from "./dialog-utils.js";
      18              : import * as authorized_keys from './authorized-keys.js';
      19              : 
      20           19 : const _ = cockpit.gettext;
      21              : 
      22            0 : function AddAuthorizedKeyDialogBody({ state, change }) {
      23            0 :     const { text } = state;
      24              : 
      25            0 :     return (
      26            0 :         <TextArea id="authorized-keys-text"
      27            0 :                   placeholder={_("Paste the contents of your public SSH key file here")}
      28            0 :                   className="form-control"
      29            0 :                   value={text} onChange={(_event, value) => change("text", value)} />
      30              :     );
      31            0 : }
      32              : 
      33            0 : function add_authorized_key_dialog(keys) {
      34            0 :     let dlg = null;
      35            0 :     const state = {
      36            0 :         text: ""
      37            0 :     };
      38              : 
      39            0 :     function change(field, value) {
      40            0 :         state[field] = value;
      41            0 :         update();
      42            0 :     }
      43              : 
      44            0 :     function update() {
      45            0 :         const props = {
      46            0 :             id: "add-authorized-key-dialog",
      47            0 :             title: _("Add public key"),
      48            0 :             body: <AddAuthorizedKeyDialogBody state={state} change={change} />
      49            0 :         };
      50              : 
      51            0 :         const footer = {
      52            0 :             actions: [
      53            0 :                 {
      54            0 :                     caption: _("Add"),
      55            0 :                     style: "primary",
      56            0 :                     clicked: () => {
      57            0 :                         return keys.add_key(state.text);
      58            0 :                     }
      59            0 :                 }
      60            0 :             ]
      61            0 :         };
      62              : 
      63            0 :         if (!dlg)
      64            0 :             dlg = show_modal_dialog(props, footer);
      65            0 :         else {
      66            0 :             dlg.setProps(props);
      67            0 :             dlg.setFooterProps(footer);
      68            0 :         }
      69            0 :     }
      70              : 
      71            0 :     update();
      72            0 : }
      73              : 
      74           11 : export function AuthorizedKeys({ name, home, allow_mods }) {
      75           11 :     const manager = useObject(() => authorized_keys.instance(name, home),
      76            4 :                               manager => manager.close(),
      77           11 :                               [name, home]);
      78           11 :     useEvent(manager, "changed");
      79              : 
      80            1 :     function remove_key(raw) {
      81            1 :         manager.remove_key(raw).catch(show_unexpected_error);
      82            1 :     }
      83              : 
      84           11 :     const { state, keys } = manager;
      85           11 :     let error = "";
      86              : 
      87           11 :     if (state == "access-denied")
      88            2 :         error = _("You do not have permission to view the authorized public keys for this account.");
      89           11 :     else if (state == "failed")
      90            2 :         error = _("Failed to load authorized keys.");
      91           11 :     else if (state == "ready")
      92           11 :         error = _("There are no authorized public keys for this account.");
      93              :     else
      94           11 :         return null;
      95              : 
      96           11 :     const actions = allow_mods && (
      97            0 :         <Button variant="secondary" id="authorized-key-add" onClick={() => add_authorized_key_dialog(manager)}>
      98           11 :             {_("Add key")}
      99           11 :         </Button>
     100              :     );
     101              : 
     102           11 :     return (
     103           11 :         <Card isPlain id="account-authorized-keys">
     104           11 :             <CardHeader actions={{ actions }}>
     105           11 :                 <CardTitle component="h2">{_("Authorized public SSH keys")}</CardTitle>
     106           11 :             </CardHeader>
     107           11 :             <ListingTable
     108           11 :                 aria-label={ _("Authorized public SSH keys") }
     109           11 :                 id="account-authorized-keys-list"
     110           11 :                 showHeader={false}
     111           11 :                 emptyCaption={error}
     112           11 :                 columns={ [
     113           11 :                     { title: _("Name"), header: true },
     114           11 :                     { title: _("Fingerprint") },
     115           11 :                     { title: "" },
     116           11 :                 ] }
     117            6 :                 rows={keys.map(k => {
     118            6 :                     return {
     119            6 :                         columns: [
     120            1 :                             { title: k.comment || _("Unnamed"), props: { width: 20 } },
     121            1 :                             { title: k.fp || _("Invalid key"), props: { width: 80 } },
     122            6 :                             {
     123            6 :                                 title: <OverflowMenu breakpoint="lg">
     124            6 :                                     <OverflowMenuContent>
     125            6 :                                         <OverflowMenuGroup groupType="button">
     126            6 :                                             <OverflowMenuItem>
     127            1 :                                                 <Button key={k.fp} variant="secondary" onClick={() => remove_key(k.raw)}>
     128            6 :                                                     {_("Remove")}
     129            6 :                                                 </Button>
     130            6 :                                             </OverflowMenuItem>
     131            6 :                                         </OverflowMenuGroup>
     132            6 :                                     </OverflowMenuContent>
     133            6 :                                     <OverflowMenuControl>
     134            6 :                                         <KebabDropdown position="right" dropdownItems={[
     135            0 :                                             <OverflowMenuDropdownItem key="delete" isShared onClick={() => remove_key(k.raw)}>
     136            6 :                                                 {_("Remove")}
     137            6 :                                             </OverflowMenuDropdownItem>]}
     138            6 :                                         />
     139            6 :                                     </OverflowMenuControl>
     140            6 :                                 </OverflowMenu>,
     141            6 :                                 props: { className: "pf-v6-c-table__action" }
     142            6 :                             }
     143            6 :                         ],
     144            6 :                         props: { key: k.fp }
     145            6 :                     };
     146            6 :                 })} />
     147           11 :         </Card>
     148              :     );
     149           11 : }
        

Generated by: LCOV version 2.0-1