pkg/apps/manifest.json | 3 +- pkg/base1/test-permissions.js | 2 +- pkg/lib/cockpit-components-password.jsx | 63 ++++++++++++++++++++++++++++++--- pkg/storaged/manifest.json | 1 + pkg/users/account-create-dialog.js | 7 ++-- pkg/users/account-details.js | 2 +- pkg/users/account-logs-panel.jsx | 18 ++++++---- pkg/users/accounts-list.js | 2 +- pkg/users/expiration-dialogs.js | 2 +- pkg/users/index.html | 1 + pkg/users/manifest.json | 6 ++++ pkg/users/password-dialogs.js | 17 +++++---- src/bridge/test-connect.c | 10 +++--- src/bridge/test-stream.c | 10 +++--- src/common/cockpitloopback.c | 23 ++++++++++-- 15 files changed, 128 insertions(+), 39 deletions(-) diff --git a/pkg/apps/manifest.json b/pkg/apps/manifest.json index 0679c1b3b..e09a2d587 100644 --- a/pkg/apps/manifest.json +++ b/pkg/apps/manifest.json @@ -17,7 +17,8 @@ "debian": ["appstream"], "ubuntu": ["appstream"] }, "appstream_data_packages": { - "fedora": ["appstream-data"], "rhel": ["appstream-data"] + "fedora": ["appstream-data"], "rhel": ["appstream-data"], + "altlinux": ["appstream-data"] } } } diff --git a/pkg/base1/test-permissions.js b/pkg/base1/test-permissions.js index 956395858..123943fad 100644 --- a/pkg/base1/test-permissions.js +++ b/pkg/base1/test-permissions.js @@ -9,7 +9,7 @@ const root_user = { const priv_user = { name: "user", - id: 1000, + id: 500, groups: ["user", "agroup"] }; diff --git a/pkg/lib/cockpit-components-password.jsx b/pkg/lib/cockpit-components-password.jsx index 08cfc51b6..3e7f8f152 100644 --- a/pkg/lib/cockpit-components-password.jsx +++ b/pkg/lib/cockpit-components-password.jsx @@ -17,6 +17,7 @@ * along with Cockpit; If not, see . */ import cockpit from 'cockpit'; +import { read_os_release } from "os-release.js"; import React, { useState } from 'react'; import { FormGroup, Popover, Progress, ProgressSize, ProgressMeasureLocation, TextInput } from '@patternfly/react-core'; import { HelpIcon } from '@patternfly/react-icons'; @@ -25,10 +26,34 @@ import './cockpit-components-password.scss'; const _ = cockpit.gettext; -export function password_quality(password, force) { +export function password_quality_proxy(new_password, old_password, user, force) { + function get_config(name, distro_id, def) { + if (cockpit.manifests.users && cockpit.manifests.users.config) { + let val = cockpit.manifests.users.config[name]; + if (typeof val === 'object' && val !== null) + val = val[distro_id]; + return val !== undefined ? val : def; + } else { + return def; + } + } + + const password_quality_libs = { + libpasswdqc: pw_libpasswdqc, + libpwquality: pw_libpwquality, + }; + + return read_os_release().then(os_release => { + const quality_lib = get_config('password_quality_lib', os_release.ID, 'libpwquality'); + const quality_function = password_quality_libs[quality_lib]; + return quality_function(new_password, old_password, user, force); + }); +} + +function pw_libpwquality(new_password, old_password, user, force) { return new Promise((resolve, reject) => { cockpit.spawn('/usr/bin/pwscore', { err: "message" }) - .input(password) + .input(new_password) .done(function(content) { const quality = parseInt(content, 10); if (quality === 0) @@ -45,12 +70,42 @@ export function password_quality(password, force) { }); } +function pw_libpasswdqc(new_password, old_password, user, force) { + return new Promise((resolve, reject) => { + if (!user || !new_password) + reject(new Error(_("Username or password is empty"))); + + /* pwqcheck doesn't accept nonexistent users */ + cockpit.spawn('/usr/bin/pwqcheck', { err: "message" }) + .input( + [ + new_password, + old_password, + `${user}:::::/dev/null:/sbin/nologin`, + ].join('\n') + ) + .done(function(content) { + if (content === 'OK\n') + resolve({ value: '', message: undefined }); + else + reject(new Error(_("Password is too weak"))); + }) + .fail(function(ex) { + if (!force) + reject(new Error(ex.message || _("Password is not acceptable"))); + else + resolve({ value: 0 }); + }); + }); +} + export const PasswordFormFields = ({ password_label, password_confirm_label, password_label_info, initial_password, error_password, error_password_confirm, - idPrefix, change + idPrefix, change, + user_name }) => { const [password, setPassword] = useState(initial_password); const [passwordConfirm, setConfirmPassword] = useState(undefined); @@ -62,7 +117,7 @@ export const PasswordFormFields = ({ change("password", value); if (value) { - password_quality(value) + password_quality_proxy(value, '', user_name) .catch(() => { return { value: 0 }; }) diff --git a/pkg/storaged/manifest.json b/pkg/storaged/manifest.json index 1e686ab11..3774b405d 100644 --- a/pkg/storaged/manifest.json +++ b/pkg/storaged/manifest.json @@ -53,6 +53,7 @@ "config": { "nfs_client_package": { "rhel": "nfs-utils", "fedora": "nfs-utils", + "altlinux": "nfs-utils", "opensuse": "nfs-client", "opensuse-leap": "nfs-client", "debian": "nfs-common", "ubuntu": "nfs-common", "arch": "nfs-utils" diff --git a/pkg/users/account-create-dialog.js b/pkg/users/account-create-dialog.js index 2f66c2775..0ef784aa2 100644 --- a/pkg/users/account-create-dialog.js +++ b/pkg/users/account-create-dialog.js @@ -23,7 +23,7 @@ import React from 'react'; import { Checkbox, Form, FormGroup, TextInput, Popover, Flex, FlexItem, Radio } from '@patternfly/react-core'; import { has_errors } from "./dialog-utils.js"; import { passwd_change } from "./password-dialogs.js"; -import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx"; +import { password_quality_proxy, PasswordFormFields } from "cockpit-components-password.jsx"; import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx"; import { HelpIcon } from '@patternfly/react-icons'; @@ -60,7 +60,8 @@ function AccountCreateBody({ state, errors, change }) { error_password={errors && errors.password} error_password_confirm={errors && errors.password_confirm} idPrefix="accounts-create-password" - change={change} /> + change={change} + user_name={user_name} /> { errs.password = (ex.message || ex.toString()).replaceAll("\n", " "); }) diff --git a/pkg/users/account-details.js b/pkg/users/account-details.js index f8d4e1079..5ebbcc9ef 100644 --- a/pkg/users/account-details.js +++ b/pkg/users/account-details.js @@ -49,7 +49,7 @@ function log_unexpected_error(error) { } function get_locked(name) { - return cockpit.spawn(["/usr/bin/passwd", "-S", name], { environ: ["LC_ALL=C"], superuser: "require" }) + return cockpit.spawn(["/usr/bin/getent", "shadow", name], { environ: ["LC_ALL=C"], superuser: "require" }) .catch(() => "") .then(content => { const status = content.split(" ")[1]; diff --git a/pkg/users/account-logs-panel.jsx b/pkg/users/account-logs-panel.jsx index 6e3cbac47..8b0210381 100644 --- a/pkg/users/account-logs-panel.jsx +++ b/pkg/users/account-logs-panel.jsx @@ -23,15 +23,13 @@ import React, { useState, useEffect } from 'react'; import { Card, CardTitle, CardBody, Text } from '@patternfly/react-core'; import { ListingTable } from 'cockpit-components-table.jsx'; -import * as timeformat from "timeformat.js"; - const _ = cockpit.gettext; export function AccountLogs({ name }) { const [logins, setLogins] = useState([]); useEffect(() => { if (logins.length === 0) { - cockpit.spawn(["/usr/bin/last", "--time-format", "iso", "-n", 25, name], { environ: ["LC_ALL=C"] }) + cockpit.spawn(["/usr/bin/last", "-F", "-w", "-n", 25, name], { environ: ["LC_ALL=C"] }) .then(data => { let logins = []; data.split('\n').forEach(line => { @@ -46,6 +44,7 @@ export function AccountLogs({ name }) { // format: // admin web console ::ffff:172.27.0. 2021-09-24T09:02:13+00:00 - 2021-09-24T09:04:20+00:00 (00:02) + /* const lines = line.split(/ +/); const ended = new Date(lines[lines.length - 2]); const started = new Date(lines[lines.length - 4]); @@ -59,6 +58,13 @@ export function AccountLogs({ name }) { ended, from }); + */ + /* + one of last's format is + "%-8.*s %-12.12s %-16.*s %-24.24s %-26.26s %-12.12s\n", + which in unparsable(?), thereby, print it as is + */ + logins.push(line); }); // Only show 15 login lines @@ -79,13 +85,11 @@ export function AccountLogs({ name }) { ({ props: { key: index }, - columns: [timeformat.dateTime(line.started), timeformat.dateTime(line.ended), line.from] + columns: [line] }))} /> diff --git a/pkg/users/accounts-list.js b/pkg/users/accounts-list.js index 222cb644a..67bc72fa1 100644 --- a/pkg/users/accounts-list.js +++ b/pkg/users/accounts-list.js @@ -57,7 +57,7 @@ function AccountItem({ account, current }) { export function AccountsList({ accounts, current_user }) { const filtered_accounts = accounts.filter(function(account) { - return !((account.uid < 1000 && account.uid !== 0) || + return !((account.uid < 500 && account.uid !== 0) || account.shell.match(/^(\/usr)?\/sbin\/nologin/) || account.shell === '/bin/false'); }); diff --git a/pkg/users/expiration-dialogs.js b/pkg/users/expiration-dialogs.js index 4ae408596..b2a287f94 100644 --- a/pkg/users/expiration-dialogs.js +++ b/pkg/users/expiration-dialogs.js @@ -218,7 +218,7 @@ export function password_expiration_dialog(account, expire_days) { clicked: () => { if (validate()) { const days = state.mode == "expires" ? parseInt(state.days) : 99999; - return cockpit.spawn(["/usr/bin/passwd", "-x", String(days), account.name], + return cockpit.spawn(["/usr/bin/chage", "-M", String(days), account.name], { superuser: true, err: "message" }); } else { update(); diff --git a/pkg/users/index.html b/pkg/users/index.html index 28766bc76..84f65f089 100644 --- a/pkg/users/index.html +++ b/pkg/users/index.html @@ -25,6 +25,7 @@ + diff --git a/pkg/users/manifest.json b/pkg/users/manifest.json index 4ee89f263..52c42ed0a 100644 --- a/pkg/users/manifest.json +++ b/pkg/users/manifest.json @@ -15,5 +15,11 @@ } ] } + }, + + "config": { + "password_quality_lib": { + "altlinux": "libpasswdqc" + } } } diff --git a/pkg/users/password-dialogs.js b/pkg/users/password-dialogs.js index ade6b2079..1bd3a7016 100644 --- a/pkg/users/password-dialogs.js +++ b/pkg/users/password-dialogs.js @@ -24,7 +24,7 @@ import { Form, FormGroup, TextInput } from '@patternfly/react-core'; import { has_errors } from "./dialog-utils.js"; import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx"; -import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx"; +import { password_quality_proxy, PasswordFormFields } from "cockpit-components-password.jsx"; const _ = cockpit.gettext; @@ -38,7 +38,8 @@ function passwd_self(old_pass, new_pass) { /.*New password: $/, /.*Retype new password: $/, /.*Enter new \w*\s?password: $/, - /.*Retype new \w*\s?password: $/ + /.*Retype new \w*\s?password: $/, + /.*Re-type new password: $/ ]; const bad_exps = [ /.*BAD PASSWORD:.*/ @@ -128,7 +129,7 @@ export function passwd_change(user, new_pass) { } function SetPasswordDialogBody({ state, errors, change }) { - const { need_old, password_old, current_user } = state; + const { need_old, password_old, current_user, user_name } = state; return (
@@ -148,7 +149,8 @@ function SetPasswordDialogBody({ state, errors, change }) { error_password={errors && errors.password} error_password_confirm={errors && errors.password_confirm} idPrefix="account-set-password" - change={change} /> + change={change} + user_name={user_name} />
); } @@ -165,6 +167,7 @@ export function set_password_dialog(account, current_user) { password: "", password_confirm: "", confirm_weak: false, + user_name: account.name, }; let errors = { }; @@ -183,7 +186,7 @@ export function set_password_dialog(account, current_user) { update(); } - function validate(force, password, password_confirm) { + function validate(force, password, password_confirm, password_old) { const errs = { }; if (password != password_confirm) @@ -192,7 +195,7 @@ export function set_password_dialog(account, current_user) { if (password.length > 256) errs.password = _("Password is longer than 256 characters"); - return password_quality(password, force) + return password_quality_proxy(password, password_old, account.name, force) .catch(ex => { errs.password = (ex.message || ex.toString()).replaceAll("\n", " "); }) @@ -277,7 +280,7 @@ export function reset_password_dialog(account) { caption: _("Reset password"), style: "primary", clicked: () => { - return cockpit.spawn(["/usr/bin/passwd", "-e", account.name], + return cockpit.spawn(["/usr/bin/chage", "-d", "0", account.name], { superuser: true, err: "message" }); } } diff --git a/src/bridge/test-connect.c b/src/bridge/test-connect.c index 25d147948..3e8ca6d20 100644 --- a/src/bridge/test-connect.c +++ b/src/bridge/test-connect.c @@ -181,11 +181,6 @@ setup_connect (TestConnect *tc, tc->listen_sock = g_socket_new (family, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT, &error); - g_assert_no_error (error); - - g_socket_bind (tc->listen_sock, address, TRUE, &error); - g_object_unref (address); - if (error != NULL && family == G_SOCKET_FAMILY_IPV6) { /* Some test runners don't have IPv6 loopback, strangely enough */ @@ -196,6 +191,11 @@ setup_connect (TestConnect *tc, g_assert_no_error (error); + g_socket_bind (tc->listen_sock, address, TRUE, &error); + g_object_unref (address); + + g_assert_no_error (error); + tc->address = g_socket_get_local_address (tc->listen_sock, &error); g_assert_no_error (error); diff --git a/src/bridge/test-stream.c b/src/bridge/test-stream.c index 227d72911..4af12372f 100644 --- a/src/bridge/test-stream.c +++ b/src/bridge/test-stream.c @@ -731,11 +731,6 @@ setup_connect (TestConnect *tc, tc->listen_sock = g_socket_new (family, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT, &error); - g_assert_no_error (error); - - g_socket_bind (tc->listen_sock, address, TRUE, &error); - g_object_unref (address); - if (error != NULL && family == G_SOCKET_FAMILY_IPV6) { /* Some test runners don't have IPv6 loopback, strangely enough */ @@ -746,6 +741,11 @@ setup_connect (TestConnect *tc, g_assert_no_error (error); + g_socket_bind (tc->listen_sock, address, TRUE, &error); + g_object_unref (address); + + g_assert_no_error (error); + tc->address = g_socket_get_local_address (tc->listen_sock, &error); g_assert_no_error (error); diff --git a/src/common/cockpitloopback.c b/src/common/cockpitloopback.c index 2a2cb6f75..b70fab6c9 100644 --- a/src/common/cockpitloopback.c +++ b/src/common/cockpitloopback.c @@ -127,6 +127,20 @@ cockpit_loopback_connectable_iface (GSocketConnectableIface *iface) iface->proxy_enumerate = cockpit_loopback_enumerate; } +static gboolean +ipv6_enabled (void) +{ + GSocket *socket; + socket = g_socket_new (G_SOCKET_FAMILY_IPV6, G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, NULL); + if (socket) + { + g_object_unref (socket); + return TRUE; + } + return FALSE; +} + GSocketConnectable * cockpit_loopback_new (guint16 port) { @@ -135,9 +149,12 @@ cockpit_loopback_new (guint16 port) self = g_object_new (COCKPIT_TYPE_LOOPBACK, NULL); - addr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV6); - g_queue_push_tail (&self->addresses, g_inet_socket_address_new (addr, port)); - g_object_unref (addr); + if (ipv6_enabled()) + { + addr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV6); + g_queue_push_tail (&self->addresses, g_inet_socket_address_new (addr, port)); + g_object_unref (addr); + } addr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4); g_queue_push_tail (&self->addresses, g_inet_socket_address_new (addr, port));