From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH access-control 06/10] add pbs-style TFA API implementation
Date: Tue, 9 Nov 2021 12:27:01 +0100 [thread overview]
Message-ID: <20211109112721.130935-13-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20211109112721.130935-1-w.bumiller@proxmox.com>
implements the same api paths as in pbs by forwarding the
api methods to the rust implementation after performing the
product-specific checks
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
src/PVE/API2/TFA.pm | 332 +++++++++++++++++++++++++++++++++------
src/PVE/AccessControl.pm | 15 ++
2 files changed, 301 insertions(+), 46 deletions(-)
diff --git a/src/PVE/API2/TFA.pm b/src/PVE/API2/TFA.pm
index 76daef9..1888699 100644
--- a/src/PVE/API2/TFA.pm
+++ b/src/PVE/API2/TFA.pm
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PVE::AccessControl;
-use PVE::Cluster qw(cfs_read_file);
+use PVE::Cluster qw(cfs_read_file cfs_write_file);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
use PVE::RPCEnvironment;
@@ -15,6 +15,110 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
+my $OPTIONAL_PASSWORD_SCHEMA = {
+ description => "The current password.",
+ type => 'string',
+ optional => 1, # Only required if not root@pam
+ minLength => 5,
+ maxLength => 64
+};
+
+my $TFA_TYPE_SCHEMA = {
+ type => 'string',
+ description => 'TFA Entry Type.',
+ enum => [qw(totp u2f webauthn recovery yubico)],
+};
+
+my %TFA_INFO_PROPERTIES = (
+ id => {
+ type => 'string',
+ description => 'The id used to reference this entry.',
+ },
+ description => {
+ type => 'string',
+ description => 'User chosen description for this entry.',
+ },
+ created => {
+ type => 'integer',
+ description => 'Creation time of this entry as unix epoch.',
+ },
+ enable => {
+ type => 'boolean',
+ description => 'Whether this TFA entry is currently enabled.',
+ optional => 1,
+ default => 1,
+ },
+);
+
+my $TYPED_TFA_ENTRY_SCHEMA = {
+ type => 'object',
+ description => 'TFA Entry.',
+ properties => {
+ type => $TFA_TYPE_SCHEMA,
+ %TFA_INFO_PROPERTIES,
+ },
+};
+
+my $TFA_ID_SCHEMA = {
+ type => 'string',
+ description => 'A TFA entry id.',
+};
+
+my $TFA_UPDATE_INFO_SCHEMA = {
+ type => 'object',
+ properties => {
+ id => {
+ type => 'string',
+ description => 'The id of a newly added TFA entry.',
+ },
+ challenge => {
+ type => 'string',
+ optional => 1,
+ description =>
+ 'When adding u2f entries, this contains a challenge the user must respond to in order'
+ .' to finish the registration.'
+ },
+ recovery => {
+ type => 'array',
+ optional => 1,
+ description =>
+ 'When adding recovery codes, this contains the list of codes to be displayed to'
+ .' the user',
+ items => {
+ type => 'string',
+ description => 'A recovery entry.'
+ },
+ },
+ },
+};
+
+# Only root may modify root, regular users need to specify their password.
+#
+# Returns the userid returned from `verify_username`.
+# Or ($userid, $realm) in list context.
+my sub root_permission_check : prototype($$$$) {
+ my ($rpcenv, $authuser, $userid, $password) = @_;
+
+ ($userid, my $ruid, my $realm) = PVE::AccessControl::verify_username($userid);
+ $rpcenv->check_user_exist($userid);
+
+ raise_perm_exc() if $userid eq 'root@pam' && $authuser ne 'root@pam';
+
+ # Regular users need to confirm their password to change TFA settings.
+ if ($authuser ne 'root@pam') {
+ raise_param_exc({ 'password' => 'password is required to modify TFA data' })
+ if !defined($password);
+
+ my $domain_cfg = cfs_read_file('domains.cfg');
+ my $cfg = $domain_cfg->{ids}->{$realm};
+ die "auth domain '$realm' does not exist\n" if !$cfg;
+ my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
+ $plugin->authenticate_user($cfg, $realm, $ruid, $password);
+ }
+
+ return wantarray ? ($userid, $realm) : $userid;
+}
+
### OLD API
__PACKAGE__->register_method({
@@ -89,47 +193,6 @@ __PACKAGE__->register_method({
### END OLD API
-my $TFA_TYPE_SCHEMA = {
- type => 'string',
- description => 'TFA Entry Type.',
- enum => [qw(totp u2f webauthn recovery yubico)],
-};
-
-my %TFA_INFO_PROPERTIES = (
- id => {
- type => 'string',
- description => 'The id used to reference this entry.',
- },
- description => {
- type => 'string',
- description => 'User chosen description for this entry.',
- },
- created => {
- type => 'integer',
- description => 'Creation time of this entry as unix epoch.',
- },
- enable => {
- type => 'boolean',
- description => 'Whether this TFA entry is currently enabled.',
- optional => 1,
- default => 1,
- },
-);
-
-my $TYPED_TFA_ENTRY_SCHEMA = {
- type => 'object',
- description => 'TFA Entry.',
- properties => {
- type => $TFA_TYPE_SCHEMA,
- %TFA_INFO_PROPERTIES,
- },
-};
-
-my $TFA_ID_SCHEMA = {
- type => 'string',
- description => 'A TFA entry id.',
-};
-
__PACKAGE__->register_method ({
name => 'list_user_tfa',
path => '{userid}',
@@ -174,7 +237,7 @@ __PACKAGE__->register_method ({
},
protected => 1, # else we can't access shadow files
allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
- description => 'A requested TFA entry if present.',
+ description => 'Fetch a requested TFA entry if present.',
parameters => {
additionalProperties => 0,
properties => {
@@ -188,7 +251,51 @@ __PACKAGE__->register_method ({
code => sub {
my ($param) = @_;
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
- return $tfa_cfg->api_get_tfa_entry($param->{userid}, $param->{id});
+ my $id = $param->{id};
+ my $entry = $tfa_cfg->api_get_tfa_entry($param->{userid}, $id);
+ raise("No such tfa entry '$id'", 404) if !$entry;
+ return $entry;
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'delete_tfa',
+ path => '{userid}/{id}',
+ method => 'DELETE',
+ permissions => {
+ check => [ 'or',
+ ['userid-param', 'self'],
+ ['userid-group', ['User.Modify']],
+ ],
+ },
+ protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
+ description => 'Delete a TFA entry by ID.',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid', {
+ completion => \&PVE::AccessControl::complete_username,
+ }),
+ id => $TFA_ID_SCHEMA,
+ password => $OPTIONAL_PASSWORD_SCHEMA,
+ }
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::assert_new_tfa_config_available();
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $userid =
+ root_permission_check($rpcenv, $authuser, $param->{userid}, $param->{password});
+
+ return PVE::AccessControl::lock_tfa_config(sub {
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+ $tfa_cfg->api_delete_tfa($userid, $param->{id});
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+ });
}});
__PACKAGE__->register_method ({
@@ -228,12 +335,145 @@ __PACKAGE__->register_method ({
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
-
-
my $top_level_allowed = ($authuser eq 'root@pam');
my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
return $tfa_cfg->api_list_tfa($authuser, $top_level_allowed);
}});
+__PACKAGE__->register_method ({
+ name => 'add_tfa_entry',
+ path => '{userid}',
+ method => 'POST',
+ permissions => {
+ check => [ 'or',
+ ['userid-param', 'self'],
+ ['userid-group', ['User.Modify']],
+ ],
+ },
+ protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
+ description => 'Add a TFA entry for a user.',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid', {
+ completion => \&PVE::AccessControl::complete_username,
+ }),
+ type => $TFA_TYPE_SCHEMA,
+ description => {
+ type => 'string',
+ description => 'A description to distinguish multiple entries from one another',
+ maxLength => 255,
+ optional => 1,
+ },
+ totp => {
+ type => 'string',
+ description => "A totp URI.",
+ optional => 1,
+ },
+ value => {
+ type => 'string',
+ description =>
+ 'The current value for the provided totp URI, or a Webauthn/U2F'
+ .' challenge response',
+ optional => 1,
+ },
+ challenge => {
+ type => 'string',
+ description => 'When responding to a u2f challenge: the original challenge string',
+ optional => 1,
+ },
+ password => $OPTIONAL_PASSWORD_SCHEMA,
+ },
+ },
+ returns => $TFA_UPDATE_INFO_SCHEMA,
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::assert_new_tfa_config_available();
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $userid =
+ root_permission_check($rpcenv, $authuser, $param->{userid}, $param->{password});
+
+ return PVE::AccessControl::lock_tfa_config(sub {
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+ PVE::AccessControl::configure_u2f_and_wa($tfa_cfg);
+
+ my $response = $tfa_cfg->api_add_tfa_entry(
+ $userid,
+ $param->{description},
+ $param->{totp},
+ $param->{value},
+ $param->{challenge},
+ $param->{type},
+ );
+
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+
+ return $response;
+ });
+ }});
+
+__PACKAGE__->register_method ({
+ name => 'update_tfa_entry',
+ path => '{userid}/{id}',
+ method => 'PUT',
+ permissions => {
+ check => [ 'or',
+ ['userid-param', 'self'],
+ ['userid-group', ['User.Modify']],
+ ],
+ },
+ protected => 1, # else we can't access shadow files
+ allowtoken => 0, # we don't want tokens to change the regular user's TFA settings
+ description => 'Add a TFA entry for a user.',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ userid => get_standard_option('userid', {
+ completion => \&PVE::AccessControl::complete_username,
+ }),
+ id => $TFA_ID_SCHEMA,
+ description => {
+ type => 'string',
+ description => 'A description to distinguish multiple entries from one another',
+ maxLength => 255,
+ optional => 1,
+ },
+ enable => {
+ type => 'boolean',
+ description => 'Whether the entry should be enabled for login.',
+ optional => 1,
+ },
+ password => $OPTIONAL_PASSWORD_SCHEMA,
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ PVE::AccessControl::assert_new_tfa_config_available();
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+ my $userid =
+ root_permission_check($rpcenv, $authuser, $param->{userid}, $param->{password});
+
+ PVE::AccessControl::lock_tfa_config(sub {
+ my $tfa_cfg = cfs_read_file('priv/tfa.cfg');
+
+ $tfa_cfg->api_update_tfa_entry(
+ $userid,
+ $param->{id},
+ $param->{description},
+ $param->{enable},
+ );
+
+ cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+ });
+ }});
+
1;
diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index c3d3d16..fd80368 100644
--- a/src/PVE/AccessControl.pm
+++ b/src/PVE/AccessControl.pm
@@ -77,6 +77,17 @@ sub lock_user_config {
}
}
+sub lock_tfa_config {
+ my ($code, $errmsg) = @_;
+
+ my $res = cfs_lock_file("priv/tfa.cfg", undef, $code);
+ if (my $err = $@) {
+ $errmsg ? die "$errmsg: $err" : die $err;
+ }
+
+ return $res;
+}
+
my $cache_read_key = sub {
my ($type) = @_;
@@ -1720,6 +1731,10 @@ my $USER_CONTROLLED_TFA_TYPES = {
oath => 1,
};
+sub assert_new_tfa_config_available() {
+ # FIXME: Assert cluster-wide new-tfa-config support!
+}
+
sub user_get_tfa : prototype($$$) {
my ($username, $realm, $new_format) = @_;
--
2.30.2
next prev parent reply other threads:[~2021-11-09 11:27 UTC|newest]
Thread overview: 43+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-11-09 11:26 [pve-devel] [PATCH multiple 0/9] PBS-like TFA support in PVE Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 1/6] import basic skeleton Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 2/6] import pve-rs Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 3/6] move apt to /perl-apt, use PERLMOD_PRODUCT env var Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 4/6] pve: add tfa api Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 5/6] build fix: pmg-rs is not here yet Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH proxmox-perl-rs 6/6] Add some dev tips to a README Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 01/10] use rust parser for TFA config Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 02/10] update read_user_tfa_type call Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 03/10] use PBS-like auth api call flow Wolfgang Bumiller
2021-11-09 11:26 ` [pve-devel] [PATCH access-control 04/10] handle yubico authentication in new path Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 05/10] move TFA api path into its own module Wolfgang Bumiller
2021-11-09 11:27 ` Wolfgang Bumiller [this message]
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 07/10] support registering yubico otp keys Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 08/10] update tfa cleanup when deleting users Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 09/10] pveum: update tfa delete command Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH access-control 10/10] set/remove 'x' for tfa keys in user.cfg in new api Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH cluster] add webauthn configuration to datacenter.cfg Wolfgang Bumiller
2021-11-10 10:12 ` [pve-devel] applied: " Thomas Lamprecht
2021-11-09 11:27 ` [pve-devel] [PATCH common] Ticket: uri-escape colons Wolfgang Bumiller
2021-11-09 12:26 ` [pve-devel] applied: " Thomas Lamprecht
2021-11-09 11:27 ` [pve-devel] [PATCH manager 1/7] www: use render_u2f_error from wtk Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 2/7] www: use UserSelector " Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 3/7] use u2f-api.js and qrcode.min.js " Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 4/7] www: switch to new tfa login format Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 5/7] www: use af-address-book-o for realms Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 6/7] www: add TFA view to config Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH manager 7/7] www: redirect user TFA button to TFA view Wolfgang Bumiller
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 1/7] add pmxUserSelector Wolfgang Bumiller
2021-11-10 8:29 ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 2/7] add Utils used for u2f and webauthn Wolfgang Bumiller
2021-11-10 8:30 ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 3/7] add u2f-api.js and qrcode.min.js Wolfgang Bumiller
2021-11-10 8:31 ` Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 4/7] add Proxmox.window.TfaLoginWindow Wolfgang Bumiller
2021-11-10 8:30 ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 5/7] add totp, wa and recovery creation and tfa edit windows Wolfgang Bumiller
2021-11-10 8:30 ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 6/7] add Proxmox.panel.TfaView Wolfgang Bumiller
2021-11-10 8:30 ` [pve-devel] applied: " Dominik Csapak
2021-11-09 11:27 ` [pve-devel] [PATCH widget-toolkit 7/7] add yubico otp windows & login support Wolfgang Bumiller
2021-11-10 8:30 ` [pve-devel] applied: " Dominik Csapak
2021-11-11 15:52 ` [pve-devel] applied-series: [PATCH multiple 0/9] PBS-like TFA support in PVE Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20211109112721.130935-13-w.bumiller@proxmox.com \
--to=w.bumiller@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal