From a7767818984bc28818f9df93acdfbd65b617ff1c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 6 Nov 2024 11:56:46 +0100 Subject: [PATCH] ask-password: add Varlink API for querying passwords from the user This turns systemd-ask-password into a small Varlink service, so that there's an standard IPC way to ask for a password. It mostly directly exposes the functionality of the Varlink service. --- src/ask-password/ask-password.c | 166 +++++++++++++++++- .../io.systemd.ask-password.policy | 31 ++++ src/shared/meson.build | 1 + src/shared/varlink-io.systemd.AskPassword.c | 53 ++++++ src/shared/varlink-io.systemd.AskPassword.h | 6 + src/test/test-varlink-idl.c | 3 + units/meson.build | 5 + units/systemd-ask-password.socket | 21 +++ units/systemd-ask-password@.service | 16 ++ units/user/meson.build | 84 ++++++--- units/user/systemd-ask-password.socket | 20 +++ units/user/systemd-ask-password@.service | 16 ++ 12 files changed, 394 insertions(+), 28 deletions(-) create mode 100644 src/ask-password/io.systemd.ask-password.policy create mode 100644 src/shared/varlink-io.systemd.AskPassword.c create mode 100644 src/shared/varlink-io.systemd.AskPassword.h create mode 100644 units/systemd-ask-password.socket create mode 100644 units/systemd-ask-password@.service create mode 100644 units/user/systemd-ask-password.socket create mode 100644 units/user/systemd-ask-password@.service diff --git a/src/ask-password/ask-password.c b/src/ask-password/ask-password.c index 154aaa030e8..4f6fd35393a 100644 --- a/src/ask-password/ask-password.c +++ b/src/ask-password/ask-password.c @@ -5,16 +5,23 @@ #include #include +#include "sd-varlink.h" + #include "ask-password-api.h" #include "build.h" +#include "bus-polkit.h" #include "constants.h" +#include "json-util.h" #include "log.h" #include "macro.h" #include "main-func.h" #include "parse-argument.h" #include "pretty-print.h" +#include "string-table.h" #include "strv.h" #include "terminal-util.h" +#include "varlink-io.systemd.AskPassword.h" +#include "varlink-util.h" static const char *arg_icon = NULL; static const char *arg_id = NULL; /* identifier for 'ask-password' protocol */ @@ -26,6 +33,7 @@ static bool arg_multiple = false; static bool arg_no_output = false; static AskPasswordFlags arg_flags = ASK_PASSWORD_PUSH_CACHE; static bool arg_newline = true; +static bool arg_varlink = false; STATIC_DESTRUCTOR_REGISTER(arg_message, freep); @@ -213,7 +221,7 @@ static int parse_argv(int argc, char *argv[]) { else { r = parse_boolean_argument("--emoji=", emoji, NULL); if (r < 0) - return r; + return r; SET_FLAG(arg_flags, ASK_PASSWORD_HIDE_EMOJI, !r); } @@ -232,9 +240,162 @@ static int parse_argv(int argc, char *argv[]) { return log_oom(); } + r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); + if (r < 0) + return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); + if (r > 0) + arg_varlink = true; + return 1; } +typedef enum EchoMode { + ECHO_OFF, + ECHO_ON, + ECHO_MASKED, + _ECHO_MODE_MAX, + _ECHO_MODE_INVALID = -EINVAL, +} EchoMode; + +static const char* echo_mode_table[_ECHO_MODE_MAX] = { + [ECHO_OFF] = "off", + [ECHO_ON] = "on", + [ECHO_MASKED] = "masked", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(echo_mode, EchoMode, ECHO_ON); + +static JSON_DISPATCH_ENUM_DEFINE(dispatch_echo_mode, EchoMode, echo_mode_from_string); + +typedef struct MethodAskParameters { + const char *message; + const char *keyring; + const char *icon; + const char *id; + uint64_t timeout_usec; + uint64_t until_usec; + int accept_cached; + int push_cache; + EchoMode echo_mode; +} MethodAskParameters; + +static int vl_method_ask(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "message", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodAskParameters, message), 0 }, + { "keyname", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodAskParameters, keyring), 0 }, + { "icon", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodAskParameters, icon), 0 }, + { "id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodAskParameters, id), 0 }, + { "timeoutUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(MethodAskParameters, timeout_usec), 0 }, + { "untilUSec", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(MethodAskParameters, until_usec), 0 }, + { "acceptCached", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodAskParameters, accept_cached), 0 }, + { "pushCache", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodAskParameters, push_cache) , 0 }, + { "echo", SD_JSON_VARIANT_STRING, dispatch_echo_mode, offsetof(MethodAskParameters, echo_mode), 0 }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + Hashmap **polkit_registry = ASSERT_PTR(userdata); + MethodAskParameters p = { + .timeout_usec = DEFAULT_TIMEOUT_USEC, + .until_usec = UINT64_MAX, + .accept_cached = -1, + .push_cache = -1, + .echo_mode = _ECHO_MODE_INVALID, + }; + int r; + + assert(link); + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + r = varlink_verify_polkit_async_full( + link, + /* bus= */ NULL, + "io.systemd.ask-password.ask", + /* details= */ NULL, + /* good_user= */ FLAGS_SET(arg_flags, ASK_PASSWORD_USER) ? getuid() : UID_INVALID, + /* flags= */ 0, + polkit_registry); + if (r <= 0) + return r; + + AskPasswordRequest req = { + .tty_fd = -EBADF, + .message = p.message ?: arg_message, + .icon = p.icon ?: arg_icon, + .id = p.id ?: arg_id, + .keyring = p.keyring ?: arg_key_name, + .credential = arg_credential_name, + .hup_fd = sd_varlink_get_input_fd(link), + }; + + if (p.timeout_usec != 0 && p.until_usec != 0) + req.until = MIN(usec_add(now(CLOCK_MONOTONIC), p.timeout_usec), p.until_usec); + + /* If the timeout is set to zero, don't ask agents, just stick to cache */ + SET_FLAG(arg_flags, ASK_PASSWORD_NO_AGENT, req.until == 0); + + if (p.accept_cached >= 0) + SET_FLAG(arg_flags, ASK_PASSWORD_ACCEPT_CACHED, p.accept_cached); + + if (p.push_cache >= 0) + SET_FLAG(arg_flags, ASK_PASSWORD_PUSH_CACHE, p.push_cache); + + if (p.echo_mode >= 0) { + SET_FLAG(arg_flags, ASK_PASSWORD_ECHO, p.echo_mode == ECHO_ON); + SET_FLAG(arg_flags, ASK_PASSWORD_SILENT, p.echo_mode == ECHO_OFF); + } + + _cleanup_strv_free_erase_ char **l = NULL; + r = ask_password_auto(&req, arg_flags, &l); + if (r == -EUNATCH) + return sd_varlink_error(link, "io.systemd.AskPassword.NoPasswordAvailable", NULL); + if (r == -ETIME) + return sd_varlink_error(link, "io.systemd.AskPassword.TimeoutReached", NULL); + if (r == -ECONNRESET) { /* POLLHUP on the varlink fd we passed in via .hup_fd */ + sd_varlink_close(link); + return 1; + } + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *vl = NULL; + r = sd_json_variant_new_array_strv(&vl, l); + if (r < 0) + return r; + + sd_json_variant_sensitive(vl); + + return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR("passwords", SD_JSON_BUILD_VARIANT(vl))); +} + +static int vl_server(void) { + _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL; + _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL; + int r; + + r = varlink_server_new(&varlink_server, SD_VARLINK_SERVER_INHERIT_USERDATA, /* userdata= */ &polkit_registry); + if (r < 0) + return log_error_errno(r, "Failed to allocate Varlink server: %m"); + + r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_AskPassword); + if (r < 0) + return log_error_errno(r, "Failed to add Varlink interface: %m"); + + r = sd_varlink_server_bind_method(varlink_server, "io.systemd.AskPassword.Ask", vl_method_ask); + if (r < 0) + return log_error_errno(r, "Failed to bind Varlink method: %m"); + + r = sd_varlink_server_loop_auto(varlink_server); + if (r < 0) + return log_error_errno(r, "Failed to run Varlink event loop: %m"); + + return 0; +} + static int run(int argc, char *argv[]) { _cleanup_strv_free_erase_ char **l = NULL; usec_t timeout; @@ -249,6 +410,9 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; + if (arg_varlink) + return vl_server(); /* Invocation as Varlink service */ + timeout = arg_timeout > 0 ? usec_add(now(CLOCK_MONOTONIC), arg_timeout) : 0; AskPasswordRequest req = { diff --git a/src/ask-password/io.systemd.ask-password.policy b/src/ask-password/io.systemd.ask-password.policy new file mode 100644 index 00000000000..8fddf5c07e0 --- /dev/null +++ b/src/ask-password/io.systemd.ask-password.policy @@ -0,0 +1,31 @@ + + + + + + + + The systemd Project + https://systemd.io + + + Allow to query the user interactively for a password + Authentication is required for an application to ask the user interactively for a password. + + auth_admin_keep + auth_admin_keep + yes + + + + diff --git a/src/shared/meson.build b/src/shared/meson.build index af9ef74b329..213d12d7a8b 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -177,6 +177,7 @@ shared_sources = files( 'userdb-dropin.c', 'userdb.c', 'varlink-idl-common.c', + 'varlink-io.systemd.AskPassword.c', 'varlink-io.systemd.BootControl.c', 'varlink-io.systemd.Credentials.c', 'varlink-io.systemd.Hostname.c', diff --git a/src/shared/varlink-io.systemd.AskPassword.c b/src/shared/varlink-io.systemd.AskPassword.c new file mode 100644 index 00000000000..0e60c66457c --- /dev/null +++ b/src/shared/varlink-io.systemd.AskPassword.c @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "bus-polkit.h" +#include "varlink-io.systemd.AskPassword.h" + +static SD_VARLINK_DEFINE_ENUM_TYPE( + EchoMode, + SD_VARLINK_FIELD_COMMENT("Request that the password is prompted for without any visual feedback"), + SD_VARLINK_DEFINE_ENUM_VALUE(off), + SD_VARLINK_FIELD_COMMENT("Show the password in plaintext as it is typed in"), + SD_VARLINK_DEFINE_ENUM_VALUE(on), + SD_VARLINK_FIELD_COMMENT("Provide visual feedback as the password is typed, but mask the password plaintext"), + SD_VARLINK_DEFINE_ENUM_VALUE(masked)); + +static SD_VARLINK_DEFINE_METHOD( + Ask, + SD_VARLINK_FIELD_COMMENT("The message to show when prompting for the password"), + SD_VARLINK_DEFINE_INPUT(message, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The name for the kernel keyring entry used for caching"), + SD_VARLINK_DEFINE_INPUT(keyname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The icon name to display, following the freedesktop.org icon naming specification"), + SD_VARLINK_DEFINE_INPUT(icon, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("An recognizable id for the password prompt"), + SD_VARLINK_DEFINE_INPUT(id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Timeout in µs (relative, CLOCK_MONOTONIC)"), + SD_VARLINK_DEFINE_INPUT(timeoutUSec, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Timeout in µs (absolute, CLOCK_MONOTONIC; if both timeoutUSec and untilUSec are specified the earlier of the two is used)"), + SD_VARLINK_DEFINE_INPUT(untilUSec, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether to accept cached passwords from the kernel keyring"), + SD_VARLINK_DEFINE_INPUT(acceptCached, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether to push acquired passwords into the kernel keyring"), + SD_VARLINK_DEFINE_INPUT(pushCache, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether to give visual feedback when typing in the password"), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(echo, EchoMode, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT, + SD_VARLINK_FIELD_COMMENT("List of acquired passwords. This typically contains one entry, but might contain more in case multiple passwords were previously cached."), + SD_VARLINK_DEFINE_OUTPUT(passwords, SD_VARLINK_STRING, SD_VARLINK_ARRAY)); + +static SD_VARLINK_DEFINE_ERROR(NoPasswordAvailable); +static SD_VARLINK_DEFINE_ERROR(TimeoutReached); + +SD_VARLINK_DEFINE_INTERFACE( + io_systemd_AskPassword, + "io.systemd.AskPassword", + SD_VARLINK_INTERFACE_COMMENT("An interface for interactively asking the user for a password"), + SD_VARLINK_SYMBOL_COMMENT("Encodes whether to provide visual feedback as the password is typed in"), + &vl_type_EchoMode, + SD_VARLINK_SYMBOL_COMMENT("Interactively ask the user for a password, or answer from a previously cached entry"), + &vl_method_Ask, + SD_VARLINK_SYMBOL_COMMENT("No password available, because none was provided in the cache, and no agent was asked"), + &vl_error_NoPasswordAvailable, + SD_VARLINK_SYMBOL_COMMENT("Query timeout reached, user did not provide a password in time"), + &vl_error_TimeoutReached); diff --git a/src/shared/varlink-io.systemd.AskPassword.h b/src/shared/varlink-io.systemd.AskPassword.h new file mode 100644 index 00000000000..1be83f4c90e --- /dev/null +++ b/src/shared/varlink-io.systemd.AskPassword.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-varlink-idl.h" + +extern const sd_varlink_interface vl_interface_io_systemd_AskPassword; diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index 182d59bd206..74c64d392ab 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -11,6 +11,7 @@ #include "varlink-idl-util.h" #include "varlink-io.systemd.h" #include "varlink-io.systemd.BootControl.h" +#include "varlink-io.systemd.AskPassword.h" #include "varlink-io.systemd.Credentials.h" #include "varlink-io.systemd.Import.h" #include "varlink-io.systemd.Journal.h" @@ -194,6 +195,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_MachineImage); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_AskPassword); + print_separator(); test_parse_format_one(&vl_interface_xyz_test); } diff --git a/units/meson.build b/units/meson.build index 00978a18b00..f6d661da975 100644 --- a/units/meson.build +++ b/units/meson.build @@ -245,6 +245,11 @@ units = [ { 'file' : 'system-update-cleanup.service' }, { 'file' : 'system-update-pre.target' }, { 'file' : 'system-update.target' }, + { + 'file' : 'systemd-ask-password.socket', + 'symlinks' : ['sockets.target.wants/'] + }, + { 'file' : 'systemd-ask-password@.service' }, { 'file' : 'systemd-ask-password-console.path', 'symlinks' : ['sysinit.target.wants/'], diff --git a/units/systemd-ask-password.socket b/units/systemd-ask-password.socket new file mode 100644 index 00000000000..c8d203b077b --- /dev/null +++ b/units/systemd-ask-password.socket @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Query the User Interactively for a Password +Documentation=man:systemd-ask-password(1) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/io.systemd.AskPassword +FileDescriptorName=varlink +SocketMode=0666 +Accept=yes +MaxConnectionsPerSource=16 diff --git a/units/systemd-ask-password@.service b/units/systemd-ask-password@.service new file mode 100644 index 00000000000..07bbd45a7b7 --- /dev/null +++ b/units/systemd-ask-password@.service @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Query the User Interactively for a Password +Documentation=man:systemd-ask-password(1) +DefaultDependencies=no + +[Service] +ExecStart=-systemd-ask-password --system --no-tty diff --git a/units/user/meson.build b/units/user/meson.build index 21070f7ef85..290f268ef1d 100644 --- a/units/user/meson.build +++ b/units/user/meson.build @@ -1,34 +1,64 @@ # SPDX-License-Identifier: LGPL-2.1-or-later units = [ - 'app.slice', - 'background.slice', - 'basic.target', - 'bluetooth.target', - 'capsule@.target', - 'default.target', - 'exit.target', - 'graphical-session-pre.target', - 'graphical-session.target', - 'paths.target', - 'printer.target', - 'session.slice', - 'shutdown.target', - 'smartcard.target', - 'sockets.target', - 'sound.target', - 'systemd-exit.service', - 'systemd-tmpfiles-clean.service', - 'systemd-tmpfiles-clean.timer', - 'systemd-tmpfiles-setup.service', - 'timers.target', + { 'file' : 'app.slice' }, + { 'file' : 'background.slice' }, + { 'file' : 'basic.target' }, + { 'file' : 'bluetooth.target' }, + { 'file' : 'capsule@.target' }, + { 'file' : 'default.target' }, + { 'file' : 'exit.target' }, + { 'file' : 'graphical-session-pre.target' }, + { 'file' : 'graphical-session.target' }, + { 'file' : 'paths.target' }, + { 'file' : 'printer.target' }, + { 'file' : 'session.slice' }, + { 'file' : 'shutdown.target' }, + { 'file' : 'smartcard.target' }, + { 'file' : 'sockets.target' }, + { 'file' : 'sound.target' }, + { + 'file' : 'systemd-ask-password.socket', + 'symlinks' : ['sockets.target.wants/'] + }, + { 'file' : 'systemd-ask-password@.service' }, + { 'file' : 'systemd-exit.service' }, + { 'file' : 'systemd-tmpfiles-clean.service' }, + { 'file' : 'systemd-tmpfiles-clean.timer' }, + { 'file' : 'systemd-tmpfiles-setup.service' }, + { 'file' : 'timers.target' }, + { + 'file' : 'xdg-desktop-autostart.target', + 'conditions': ['ENABLE_XDG_AUTOSTART'], + } ] -if conf.get('ENABLE_XDG_AUTOSTART') == 1 - units += 'xdg-desktop-autostart.target' -endif +foreach unit : units + file = unit.get('file') -foreach file : units - install_data(file, - install_dir : userunitdir) + install = true + foreach cond : unit.get('conditions', []) + if conf.get(cond) != 1 + install = false + break + endif + endforeach + + if install + install_data(file, + install_dir : userunitdir) + + foreach target : unit.get('symlinks', []) + if target.endswith('/') + install_emptydir(userunitdir / target) + meson.add_install_script(sh, '-c', + ln_s.format(userunitdir / file, + userunitdir / target / file)) + else + meson.add_install_script(sh, '-c', + ln_s.format(userunitdir / file, + userunitdir / target)) + endif + endforeach + endif endforeach diff --git a/units/user/systemd-ask-password.socket b/units/user/systemd-ask-password.socket new file mode 100644 index 00000000000..ce9ccd9e55a --- /dev/null +++ b/units/user/systemd-ask-password.socket @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Query the User Interactively for a Password +Documentation=man:systemd-ask-password(1) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=%t/systemd/io.systemd.AskPassword +FileDescriptorName=varlink +SocketMode=0600 +Accept=yes diff --git a/units/user/systemd-ask-password@.service b/units/user/systemd-ask-password@.service new file mode 100644 index 00000000000..e5d72ff3435 --- /dev/null +++ b/units/user/systemd-ask-password@.service @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Query the User Interactively for a Password +Documentation=man:systemd-ask-password(1) +DefaultDependencies=no + +[Service] +ExecStart=-systemd-ask-password --user --no-tty