From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <d.csapak@proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by lists.proxmox.com (Postfix) with ESMTPS id 942238AB1B
 for <pve-devel@lists.proxmox.com>; Fri, 21 Oct 2022 10:31:50 +0200 (CEST)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id 68F1C1F039
 for <pve-devel@lists.proxmox.com>; Fri, 21 Oct 2022 10:31:20 +0200 (CEST)
Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com
 [94.136.29.106])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by firstgate.proxmox.com (Proxmox) with ESMTPS
 for <pve-devel@lists.proxmox.com>; Fri, 21 Oct 2022 10:31:19 +0200 (CEST)
Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1])
 by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4E3AC44B05
 for <pve-devel@lists.proxmox.com>; Fri, 21 Oct 2022 10:31:19 +0200 (CEST)
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Date: Fri, 21 Oct 2022 10:31:15 +0200
Message-Id: <20221021083117.1239396-2-d.csapak@proxmox.com>
X-Mailer: git-send-email 2.30.2
In-Reply-To: <20221021083117.1239396-1-d.csapak@proxmox.com>
References: <20221021083117.1239396-1-d.csapak@proxmox.com>
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL 0.067 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See
 http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more
 information. [accesscontrol.pm]
Subject: [pve-devel] [PATCH access-control v2 1/3] authenticate_2nd_new:
 only lock tfa config for recovery keys
X-BeenThere: pve-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Fri, 21 Oct 2022 08:31:50 -0000

we currently only need to lock the tfa config when we got a recovery key
as a tfa challenge response, since that's the only thing that can
actually change the tfa config (every other method only reads from
there).

so to do that, factor out the code that was inside the lock, and call it
with/without lock depending on the tfa challenge response

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/PVE/AccessControl.pm | 127 +++++++++++++++++++++------------------
 1 file changed, 69 insertions(+), 58 deletions(-)

diff --git a/src/PVE/AccessControl.pm b/src/PVE/AccessControl.pm
index c32dcc3..f4ea358 100644
--- a/src/PVE/AccessControl.pm
+++ b/src/PVE/AccessControl.pm
@@ -786,79 +786,90 @@ sub authenticate_2nd_old : prototype($$$) {
     return wantarray ? ($username, $tfa_data) : $username;
 }
 
-# Returns a tfa challenge or undef.
-sub authenticate_2nd_new : prototype($$$$) {
+sub authenticate_2nd_new_do : prototype($$$$) {
     my ($username, $realm, $otp, $tfa_challenge) = @_;
+    my ($tfa_cfg, $realm_tfa) = user_get_tfa($username, $realm, 1);
 
-    my $result = lock_tfa_config(sub {
-	my ($tfa_cfg, $realm_tfa) = user_get_tfa($username, $realm, 1);
-
-	if (!defined($tfa_cfg)) {
-	    return undef;
-	}
-
-	my $realm_type = $realm_tfa && $realm_tfa->{type};
-	# verify realm type unless using recovery keys:
-	if (defined($realm_type)) {
-	    $realm_type = 'totp' if $realm_type eq 'oath'; # we used to call it that
-	    if ($realm_type eq 'yubico') {
-		# Yubico auth will not be supported in rust for now...
-		if (!defined($tfa_challenge)) {
-		    my $challenge = { yubico => JSON::true };
-		    # Even with yubico auth we do allow recovery keys to be used:
-		    if (my $recovery = $tfa_cfg->recovery_state($username)) {
-			$challenge->{recovery} = $recovery;
-		    }
-		    return to_json($challenge);
-		}
+    if (!defined($tfa_cfg)) {
+	return undef;
+    }
 
-		if ($otp =~ /^yubico:(.*)$/) {
-		    $otp = $1;
-		    # Defer to after unlocking the TFA config:
-		    return sub {
-			authenticate_yubico_new(
-			    $tfa_cfg, $username, $realm_tfa, $tfa_challenge, $otp,
-			);
-		    };
+    my $realm_type = $realm_tfa && $realm_tfa->{type};
+    # verify realm type unless using recovery keys:
+    if (defined($realm_type)) {
+	$realm_type = 'totp' if $realm_type eq 'oath'; # we used to call it that
+	if ($realm_type eq 'yubico') {
+	    # Yubico auth will not be supported in rust for now...
+	    if (!defined($tfa_challenge)) {
+		my $challenge = { yubico => JSON::true };
+		# Even with yubico auth we do allow recovery keys to be used:
+		if (my $recovery = $tfa_cfg->recovery_state($username)) {
+		    $challenge->{recovery} = $recovery;
 		}
+		return to_json($challenge);
 	    }
 
-	    my $response_type;
-	    if (defined($otp)) {
-		if ($otp !~ /^([^:]+):/) {
-		    die "bad otp response\n";
-		}
-		$response_type = $1;
+	    if ($otp =~ /^yubico:(.*)$/) {
+		$otp = $1;
+		# Defer to after unlocking the TFA config:
+		return sub {
+		    authenticate_yubico_new(
+			$tfa_cfg, $username, $realm_tfa, $tfa_challenge, $otp,
+		    );
+		};
 	    }
+	}
 
-	    die "realm requires $realm_type authentication\n"
-		if $response_type && $response_type ne 'recovery' && $response_type ne $realm_type;
+	my $response_type;
+	if (defined($otp)) {
+	    if ($otp !~ /^([^:]+):/) {
+		die "bad otp response\n";
+	    }
+	    $response_type = $1;
 	}
 
-	configure_u2f_and_wa($tfa_cfg);
+	die "realm requires $realm_type authentication\n"
+	    if $response_type && $response_type ne 'recovery' && $response_type ne $realm_type;
+    }
 
-	my $must_save = 0;
-	if (defined($tfa_challenge)) {
-	    $tfa_challenge = verify_ticket($tfa_challenge, 0, $username);
-	    $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
-	    $tfa_challenge = undef;
-	} else {
-	    $tfa_challenge = $tfa_cfg->authentication_challenge($username);
-	    if (defined($otp)) {
-		if (defined($tfa_challenge)) {
-		    $must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
-		} else {
-		    die "no such challenge\n";
-		}
+    configure_u2f_and_wa($tfa_cfg);
+
+    my $must_save = 0;
+    if (defined($tfa_challenge)) {
+	$tfa_challenge = verify_ticket($tfa_challenge, 0, $username);
+	$must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
+	$tfa_challenge = undef;
+    } else {
+	$tfa_challenge = $tfa_cfg->authentication_challenge($username);
+	if (defined($otp)) {
+	    if (defined($tfa_challenge)) {
+		$must_save = $tfa_cfg->authentication_verify($username, $tfa_challenge, $otp);
+	    } else {
+		die "no such challenge\n";
 	    }
 	}
+    }
 
-	if ($must_save) {
-	    cfs_write_file('priv/tfa.cfg', $tfa_cfg);
-	}
+    if ($must_save) {
+	cfs_write_file('priv/tfa.cfg', $tfa_cfg);
+    }
 
-	return $tfa_challenge;
-    });
+    return $tfa_challenge;
+}
+
+# Returns a tfa challenge or undef.
+sub authenticate_2nd_new : prototype($$$$) {
+    my ($username, $realm, $otp, $tfa_challenge) = @_;
+
+    my $result;
+
+    if (defined($otp) && $otp =~ m/^recovery:/) {
+	$result = lock_tfa_config(sub {
+	    authenticate_2nd_new_do($username, $realm, $otp, $tfa_challenge);
+	});
+    } else {
+	$result = authenticate_2nd_new_do($username, $realm, $otp, $tfa_challenge);
+    }
 
     # Yubico auth returns the authentication sub:
     if (ref($result) eq 'CODE') {
-- 
2.30.2