LCOV - code coverage report
Current view: top level - pkg/users - authorized-keys.js Coverage Total Hit
Test: cockpit Lines: 71.1 % 114 81
Test Date: 2026-07-13 10:00:01

            Line data    Source code
       1              : // SPDX-License-Identifier: LGPL-2.1-or-later
       2           19 : import cockpit from "cockpit";
       3              : 
       4              : import lister from "./ssh-list-public-keys.sh";
       5              : import adder from "./ssh-add-public-key.sh";
       6              : 
       7           19 : const _ = cockpit.gettext;
       8              : 
       9           11 : function AuthorizedKeys (user_name, home_dir) {
      10           11 :     const self = this;
      11           11 :     const dir = home_dir + "/.ssh";
      12           11 :     const filename = dir + "/authorized_keys";
      13           11 :     let file = null;
      14           11 :     let watch = null;
      15           11 :     let last_tag = null;
      16              : 
      17           11 :     cockpit.event_target(self);
      18              : 
      19           11 :     self.keys = [];
      20           11 :     self.state = "loading";
      21              : 
      22            0 :     function process_failure (ex) {
      23            0 :         self.keys = [];
      24            0 :         if (ex.problem == "access-denied") {
      25            0 :             self.state = ex.problem;
      26            0 :         } else if (ex.problem == "not-found") {
      27            0 :             self.state = "ready";
      28            0 :         } else {
      29            0 :             self.state = "failed";
      30            0 :             console.warn("Error processing authentication keys: " + ex);
      31            0 :         }
      32            0 :         self.dispatchEvent("changed");
      33            0 :     }
      34              : 
      35           11 :     function update_keys(keys, tag) {
      36           11 :         if (tag !== last_tag)
      37           11 :             return;
      38              : 
      39           11 :         self.keys = keys;
      40           11 :         self.state = "ready";
      41           11 :         self.dispatchEvent("changed");
      42           11 :     }
      43              : 
      44              :     /*
      45              :      * Splits up a strings like:
      46              :      *
      47              :      * 2048 SHA256:AAAAB3NzaC1yc2EAAAADAQ Comment Here (RSA)
      48              :      * 2048 SHA256:AAAAB3NzaC1yc2EAAAADAQ (RSA)
      49              :      */
      50           11 :     const PUBKEY_RE = /^(\S+)\s+(\S+)\s+(.*)\((\S+)\)$/;
      51              : 
      52            6 :     function parse_pubkeys(input) {
      53            6 :         const keys = [];
      54              : 
      55            6 :         return cockpit.script(lister)
      56            6 :                 .input(input + "\n")
      57            6 :                 .then(output => {
      58            6 :                     const lines = output.split("\n");
      59              : 
      60            6 :                     for (let i = 0; i + 1 < lines.length; i += 2) {
      61            6 :                         const obj = { raw: lines[i + 1] };
      62            6 :                         keys.push(obj);
      63            6 :                         const match = lines[i].trim().match(PUBKEY_RE);
      64            6 :                         obj.valid = !!match && !!obj.raw;
      65            6 :                         if (match) {
      66            6 :                             obj.size = match[1];
      67            6 :                             obj.fp = match[2];
      68            6 :                             obj.comment = match[3].trim();
      69            6 :                             if (obj.comment == "authorized_keys" || obj.comment == "no comment")
      70            1 :                                 obj.comment = null;
      71            6 :                             obj.algorithm = match[4];
      72              : 
      73              :                             /* Old ssh-keygen versions need us to find the comment ourselves */
      74            1 :                             if (!obj.comment && obj.raw)
      75            1 :                                 obj.comment = obj.raw
      76            1 :                                         .split(" ")
      77            1 :                                         .splice(0, 2)
      78            1 :                                         .join(" ") || null;
      79            6 :                         }
      80            6 :                     }
      81            6 :                     return keys;
      82            6 :                 })
      83            0 :                 .catch(ex => { // not-covered: OS error
      84            0 :                     console.warn("Failed to list public keys:", ex.toString()); // not-covered: OS error
      85            0 :                     return []; // not-covered: OS error
      86            0 :                 });
      87            6 :     }
      88              : 
      89           11 :     function parse_keys(content, tag, ex) {
      90           11 :         last_tag = tag;
      91              : 
      92           11 :         if (ex)
      93            2 :             return process_failure(ex);
      94              : 
      95           11 :         if (!content)
      96            8 :             return update_keys([], tag);
      97              : 
      98            6 :         parse_pubkeys(content)
      99            6 :                 .then(keys => update_keys(keys, tag));
     100           11 :     }
     101              : 
     102            0 :     self.add_key = function(key) {
     103            0 :         return parse_pubkeys(key)
     104            0 :                 .then(keys => {
     105            0 :                     const obj = keys[0];
     106            0 :                     if (obj?.valid) {
     107            0 :                         return cockpit
     108            0 :                                 .script(adder, [user_name, home_dir], { superuser: "try", err: "message" })
     109            0 :                                 .input(obj.raw + "\n")
     110              :                                 // eslint-disable-next-line prefer-promise-reject-errors
     111            0 :                                 .catch(ex => Promise.reject(_("Error saving authorized keys: ") + ex)); // not-covered: OS error
     112            0 :                     } else {
     113            0 :                         return Promise.reject(_("The key you provided was not valid."));
     114            0 :                     }
     115            0 :                 });
     116            0 :     };
     117              : 
     118            1 :     self.remove_key = function(key) {
     119            1 :         return file.modify(function(content) {
     120            1 :             let lines = null;
     121            1 :             const new_lines = [];
     122              : 
     123            1 :             if (!content)
     124            0 :                 return null;
     125              : 
     126            1 :             lines = content.trim().split('\n');
     127            1 :             for (let i = 0; i < lines.length; i++) {
     128            1 :                 if (lines[i] === key)
     129            0 :                     key = undefined;
     130              :                 else
     131            0 :                     new_lines.push(lines[i]);
     132            1 :             }
     133            1 :             return new_lines.join("\n") || null;
     134            1 :         });
     135            1 :     };
     136              : 
     137            4 :     self.close = function() {
     138            4 :         if (watch)
     139            4 :             watch.remove();
     140              : 
     141            4 :         if (file)
     142            4 :             file.close();
     143            4 :     };
     144              : 
     145           11 :     file = cockpit.file(filename, { superuser: 'try' });
     146           11 :     watch = file.watch(parse_keys);
     147           11 : }
     148              : 
     149           11 : export function instance(user_name, home_dir) {
     150           11 :     return new AuthorizedKeys(user_name, home_dir);
     151           11 : }
        

Generated by: LCOV version 2.0-1