Merge branch 'upstream'

This commit is contained in:
Andrew A. Vasilyev 2024-02-22 17:31:51 +03:00
commit bf5c737c8f
41 changed files with 1705 additions and 548 deletions

View File

@ -1,12 +0,0 @@
[workspace]
exclude = [ "build", "common-src", "perl", "scripts" ]
members = [
"pve-rs",
"pmg-rs",
]
[patch.crates-io]
# proxmox-tfa requires -time and -uuid as well, so enable *all* or *none* of them
#proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
#proxmox-time = { path = "../proxmox/proxmox-time" }
#proxmox-uuid = { path = "../proxmox/proxmox-uuid" }

101
Makefile
View File

@ -1,33 +1,11 @@
CARGO ?= cargo CARGO ?= cargo
define to_upper
$(shell echo "$(1)" | tr '[:lower:]' '[:upper:]')
endef
ifeq ($(BUILD_MODE), release) ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release --offline CARGO_BUILD_ARGS += --release --offline
DEBUG_LIBPATH := DEBUG_LIBPATH :=
else else
DEBUG_LIBPATH := "-L./target/debug",
endif endif
define package_template
sed -r \
-e 's/\{\{PRODUCT\}\}/$(1)/g;' \
-e 's/\{\{LIBRARY\}\}/$(2)/g;' \
-e 's|\{\{DEBUG_LIBPATH\}\}|$(DEBUG_LIBPATH)|g;' \
Proxmox/Lib/template.pm \
>Proxmox/Lib/$(1).pm
endef
define upload_template
cd build; \
dcmd --deb lib$(1)-rs-perl*.changes \
| grep -v '.changes$$' \
| tar -cf "$@.tar" -T-; \
cat "$@.tar" | ssh -X repoman@repo.proxmox.com upload --product $(2) --dist bullseye
endef
.PHONY: all .PHONY: all
all: all:
ifeq ($(BUILD_TARGET), pve) ifeq ($(BUILD_TARGET), pve)
@ -40,74 +18,29 @@ else
@echo " - make pmg" @echo " - make pmg"
endif endif
.PHONY: pve pmg
pve pmg:
@PERLMOD_PRODUCT=$(call to_upper,$@) \
RUSTFLAGS="-L/usr/lib64/perl5/CORE -lperl" $(CARGO) build $(CARGO_BUILD_ARGS) -p $@-rs
.PHONY: gen
gen:
$(call package_template,PMG,pmg_rs)
$(call package_template,PVE,pve_rs)
perl ./scripts/genpackage.pl Common \
Proxmox::RS::APT::Repositories \
Proxmox::RS::CalendarEvent \
Proxmox::RS::Subscription
perl ./scripts/genpackage.pl PVE \
PVE::RS::APT::Repositories \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::TFA
perl ./scripts/genpackage.pl PMG \
PMG::RS::APT::Repositories \
PMG::RS::Acme \
PMG::RS::CSR \
PMG::RS::OpenId \
PMG::RS::TFA
build: build:
rm -rf build rm -rf build
mkdir build mkdir build
echo system >build/rust-toolchain echo system >build/rust-toolchain
cp -a ./scripts ./build cp -a ./Cargo.toml ./build
cp -a ./common ./build cp -a ./common ./build
cp -a ./pve-rs ./build cp -a ./pve-rs ./build
cp -a ./pmg-rs ./build cp -a ./pmg-rs ./build
cp -a ./Proxmox ./build # Replace the symlinks with copies of the common code in pve/pmg:
$(MAKE) BUILD_MODE=release -C build -f ../Makefile gen cd build; for i in pve pmg; do \
mkdir -p ./build/pve-rs/Proxmox/Lib rm ./$$i-rs/common ; \
mv ./build/Proxmox/Lib/PVE.pm ./build/pve-rs/Proxmox/Lib/PVE.pm mkdir ./$$i-rs/common ; \
mkdir -p ./build/pmg-rs/Proxmox/Lib cp -R ./common/src ./$$i-rs/common/src ; \
mv ./build/Proxmox/Lib/PMG.pm ./build/pmg-rs/Proxmox/Lib/PMG.pm done
mv ./build/PVE ./build/pve-rs
mv ./build/PMG ./build/pmg-rs
mv ./build/Proxmox ./build/common/pkg
# So the common packages end up in ./build, rather than ./build/common # So the common packages end up in ./build, rather than ./build/common
mv ./build/common/pkg ./build/common-pkg mv ./build/common/pkg ./build/common-pkg
# Copy the workspace root into the sources
pve-deb: build mkdir build/pve-rs/.workspace
cd ./build/pve-rs && dpkg-buildpackage -b -uc -us cp -t build/pve-rs/.workspace Cargo.toml
touch $@ sed -i -e '/\[package\]/a\workspace = ".workspace"' build/pve-rs/Cargo.toml
# Clear the member array and replace it with ".."
pmg-deb: build sed -i -e '/^members = \[/,/^]$$/d' build/pve-rs/.workspace/Cargo.toml
cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us sed -i -e '/^\[workspace\]/a\members = [ ".." ]' build/pve-rs/.workspace/Cargo.toml
touch $@ # Copy the cargo config
mkdir build/pve-rs/.cargo
common-deb: build cp -t build/pve-rs/.cargo .cargo/config
cd ./build/common-pkg && dpkg-buildpackage -b -uc -us
touch $@
pve-upload: pve-deb
$(call upload_template,pve,pve)
pmg-upload: pmg-deb
$(call upload_template,pmg,pmg)
# need to put into variable to ensure comma isn't interpreted as param separator on call
common_target=pve,pmg
common-upload: common-deb
$(call upload_template,proxmox,$(common_target))
.PHONY: clean
clean:
cargo clean
rm -rf ./build ./PVE ./PMG ./pve-deb ./pmg-deb ./common-deb

View File

@ -1,68 +0,0 @@
package Proxmox::Lib::{{PRODUCT}};
=head1 NAME
Proxmox::Lib::{{PRODUCT}} - base module for {{PRODUCT}} rust bindings
=head1 SYNOPSIS
package {{PRODUCT}}::RS::SomeBindings;
use base 'Proxmox::Lib::{{PRODUCT}}';
BEGIN { __PACKAGE__->bootstrap(); }
1;
=head1 DESCRIPTION
This is the base module of all {{PRODUCT}} bindings.
Its job is to ensure the 'lib{{LIBRARY}}.so' library is loaded and provide a 'bootstrap' class
method to load the actual code.
=cut
use DynaLoader;
sub library {
return '{{LIBRARY}}';
}
sub load : prototype($) {
my ($pkg) = @_;
my $mod_name = $pkg->library();
my @dirs = (map "-L$_/auto", @INC);
my $mod_file = DynaLoader::dl_findfile({{DEBUG_LIBPATH}}@dirs, $mod_name);
die "failed to locate shared library for $mod_name (lib${mod_name}.so)\n" if !$mod_file;
my $lib = DynaLoader::dl_load_file($mod_file)
or die "failed to load library '$mod_file'\n";
my $data = ($::{'proxmox-rs-library'} //= {});
$data->{$mod_name} = $lib;
$data->{-current} //= $lib;
$data->{-package} //= $pkg;
}
sub bootstrap {
my ($pkg) = @_;
my $mod_name = $pkg->library();
my $bootstrap_name = 'boot_' . ($pkg =~ s/::/__/gr);
my $lib = $::{'proxmox-rs-library'}
or die "rust library not available for '{PRODUCT}'\n";
$lib = $lib->{$mod_name};
my $sym = DynaLoader::dl_find_symbol($lib, $bootstrap_name);
die "failed to locate '$bootstrap_name'\n" if !defined $sym;
my $boot = DynaLoader::dl_install_xsub($bootstrap_name, $sym, "src/FIXME.rs");
$boot->();
}
BEGIN { __PACKAGE__->load(); }
1;

View File

@ -1,4 +1,4 @@
include /usr/share/dpkg/default.mk include /usr/share/dpkg/pkg-info.mk
PACKAGE=libproxmox-rs-perl PACKAGE=libproxmox-rs-perl
@ -8,22 +8,63 @@ export GITVERSION:=$(shell git rev-parse HEAD)
PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};' PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};' PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb DEB=$(PACKAGE)_$(DEB_VERSION)_$(ARCH).deb
DSC=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DESTDIR= DESTDIR=
all: PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
--lib=- \
--lib-tag=proxmox \
--lib-package=Proxmox::Lib::Common \
--lib-prefix=Proxmox
# Point to any generated pm file (Proxmox/ dir is already present in this package)
Proxmox/RS/CalendarEvent.pm:
$(PERLMOD_GENPACKAGE) \
Proxmox::RS::APT::Repositories \
Proxmox::RS::CalendarEvent \
Proxmox::RS::Notify \
Proxmox::RS::Subscription
all: Proxmox/RS/CalendarEvent.pm
true true
.PHONY: install .PHONY: install
install: install: Proxmox/RS/CalendarEvent.pm
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB) install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)
find PVE \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';' find PVE \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
find Proxmox \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';' find Proxmox \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
rm $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/Proxmox/Lib/template.pm'
$(BUILDDIR): debian PVE Proxmox Makefile
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp
cp -t $(BUILDDIR).tmp -a debian PVE Proxmox Makefile
mv $(BUILDDIR).tmp $(BUILDDIR)
.PHONY: deb .PHONY: deb
deb: $(DEB) deb: $(DEB)
$(DEB): build $(DEB): $(BUILDDIR)
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean cd $(BUILDDIR); dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEBS) lintian $(DEB)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)
.PHONY: upload
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEB)
# check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com upload --product pve,pmg --dist $(DEB_DISTRIBUTION)
clean:
rm -f *.deb *.dsc *.tar.* *.build *.buildinfo *.changes
rm -rf $(PACKAGE)-[0-9]*/

View File

@ -0,0 +1,100 @@
package Proxmox::Lib::SslProbe;
use strict;
use warnings;
=head1 Environment Variable Safety
Perl's handling of environment variables was completely messed up until v5.38.
Using `setenv` such as use din the `openssl-probe` crate would cause it to
crash later on, therefore we provide a perl-version of env var probing instead,
and override the crate with one that doesn't replace the variables if they are
already set correctly.
=cut
BEGIN {
# Copied from openssl-probe
my @cert_dirs = (
"/var/ssl",
"/usr/share/ssl",
"/usr/local/ssl",
"/usr/local/openssl",
"/usr/local/etc/openssl",
"/usr/local/share",
"/usr/lib/ssl",
"/usr/ssl",
"/etc/openssl",
"/etc/pki/ca-trust/extracted/pem",
"/etc/pki/tls",
"/etc/ssl",
"/etc/certs",
"/opt/etc/ssl",
"/data/data/com.termux/files/usr/etc/tls",
"/boot/system/data/ssl",
);
# Copied from openssl-probe
my @cert_file_names = (
"cert.pem",
"certs.pem",
"ca-bundle.pem",
"cacert.pem",
"ca-certificates.crt",
"certs/ca-certificates.crt",
"certs/ca-root-nss.crt",
"certs/ca-bundle.crt",
"CARootCertificates.pem",
"tls-ca-bundle.pem",
);
my $probed_ssl_vars = 0;
# The algorithm here is taken from the `openssl-probe` crate and should
# produce the exact same result in order to ensure the rust code does not
# call `setenv()`.
my sub probe_ssl_vars : prototype() {
return if $probed_ssl_vars;
$probed_ssl_vars = 1;
my $result_file = $ENV{SSL_CERT_FILE};
my $result_file_changed = 0;
my $result_dir = $ENV{SSL_CERT_DIR};
my $result_dir_changed = 0;
for my $certs_dir (@cert_dirs) {
if (!defined($result_file)) {
for my $file (@cert_file_names) {
my $path = "$certs_dir/$file";
if (-e $path) {
$result_file = $path;
$result_file_changed = 1;
last;
}
}
}
if (!defined($result_dir)) {
for my $file (@cert_file_names) {
my $path = "$certs_dir/certs";
if (-d $path) {
$result_dir = $path;
$result_dir_changed = 1;
last;
}
}
}
last if defined($result_file) && defined($result_dir);
}
if ($result_file_changed && defined($result_file)) {
$ENV{SSL_CERT_FILE} = $result_file;
}
if ($result_dir_changed && defined($result_dir)) {
$ENV{SSL_CERT_DIR} = $result_dir;
}
}
probe_ssl_vars();
}
1;

View File

@ -1,3 +1,27 @@
libproxmox-rs-perl (0.3.3) bookworm; urgency=medium
* move ssl var probing to Proxmox::Lib::SslProbe
-- Proxmox Support Team <support@proxmox.com> Thu, 07 Dec 2023 09:57:33 +0100
libproxmox-rs-perl (0.3.2) bookworm; urgency=medium
* add Proxmox::Lib::Common::probe_ssl_vars() helper
-- Proxmox Support Team <support@proxmox.com> Tue, 05 Dec 2023 10:46:39 +0100
libproxmox-rs-perl (0.3.1) bookworm; urgency=medium
* add Proxmox::RS::Notify module
-- Proxmox Support Team <support@proxmox.com> Mon, 24 Jul 2023 14:02:17 +0200
libproxmox-rs-perl (0.3.0) bookworm; urgency=medium
* rebuild for Debian 12 Bookworm based release series
-- Proxmox Support Team <support@proxmox.com> Wed, 17 May 2023 15:48:41 +0200
libproxmox-rs-perl (0.2.1) bullseye; urgency=medium libproxmox-rs-perl (0.2.1) bullseye; urgency=medium
* update to proxmox-subscription 0.3 / proxmox-http 0.7 * update to proxmox-subscription 0.3 / proxmox-http 0.7

View File

@ -1 +0,0 @@
12

View File

@ -1,10 +1,9 @@
Source: libproxmox-rs-perl Source: libproxmox-rs-perl
Section: perl Section: perl
Priority: optional Priority: optional
Build-Depends: Build-Depends: debhelper-compat (= 13), perlmod-bin,
debhelper (>= 12),
Maintainer: Proxmox Support Team <support@proxmox.com> Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.5.1 Standards-Version: 4.6.2
Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git
Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git
Homepage: https://www.proxmox.com Homepage: https://www.proxmox.com
@ -14,15 +13,12 @@ Package: libproxmox-rs-perl
Architecture: any Architecture: any
# always bump both versioned Depends and Breaks, otherwise systems with both # always bump both versioned Depends and Breaks, otherwise systems with both
# libpmg-rs-perl and libpve-rs-perl might load an outdated lib and break # libpmg-rs-perl and libpve-rs-perl might load an outdated lib and break
Depends: Depends: libpve-rs-perl (>= 0.8.5) | libpmg-rs-perl (>= 0.6.2),
${misc:Depends}, ${misc:Depends},
${perl:Depends}, ${perl:Depends},
${shlibs:Depends}, ${shlibs:Depends},
libpve-rs-perl (>= 0.7.2) | libpmg-rs-perl (>= 0.6.2), Breaks: libpmg-rs-perl (<< 0.6.2), libpve-rs-perl (<< 0.7.2),
Breaks: Replaces: libpve-rs-perl (<< 0.6.0),
libpve-rs-perl (<< 0.7.2), Description: PVE/PMG common perl parts for Rust perlmod bindings
libpmg-rs-perl (<< 0.6.2), Contains the perl side of modules provided by the libraries of both
Replaces: libpve-rs-perl (<< 0.6.0) libpve-rs-perl and libpmg-rs-perl, loading whichever is available.
Description: PVE/PMG common parts which have been ported to Rust - Perl packages
Contains the perl side of modules provided by the libraries of both libpve-rs-perl and
libpmg-rs-perl, loading whichever is available.

14
common/src/logger.rs Normal file
View File

@ -0,0 +1,14 @@
use env_logger::{Builder, Env};
use std::io::Write;
/// Initialize logging. Should only be called once
pub fn init(env_var_name: &str, default_log_level: &str) {
if let Err(e) = Builder::from_env(Env::new().filter_or(env_var_name, default_log_level))
.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args()))
.write_style(env_logger::WriteStyle::Never)
.format_timestamp(None)
.try_init()
{
eprintln!("could not set up env_logger: {e}");
}
}

View File

@ -1,3 +1,5 @@
pub mod apt; pub mod apt;
mod calendar_event; mod calendar_event;
pub mod logger;
pub mod notify;
mod subscription; mod subscription;

508
common/src/notify.rs Normal file
View File

@ -0,0 +1,508 @@
#[perlmod::package(name = "Proxmox::RS::Notify")]
mod export {
use std::collections::HashMap;
use std::sync::Mutex;
use anyhow::{bail, Error};
use serde_json::Value as JSONValue;
use perlmod::Value;
use proxmox_http_error::HttpError;
use proxmox_notify::endpoints::gotify::{
DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
GotifyPrivateConfigUpdater,
};
use proxmox_notify::endpoints::sendmail::{
DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
};
use proxmox_notify::endpoints::smtp::{
DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpMode, SmtpPrivateConfig,
SmtpPrivateConfigUpdater,
};
use proxmox_notify::matcher::{
CalendarMatcher, DeleteableMatcherProperty, FieldMatcher, MatchModeOperator, MatcherConfig,
MatcherConfigUpdater, SeverityMatcher,
};
use proxmox_notify::{api, Config, Notification, Severity};
pub struct NotificationConfig {
config: Mutex<Config>,
}
perlmod::declare_magic!(Box<NotificationConfig> : &NotificationConfig as "Proxmox::RS::Notify");
/// Support `dclone` so this can be put into the `ccache` of `PVE::Cluster`.
#[export(name = "STORABLE_freeze", raw_return)]
fn storable_freeze(
#[try_from_ref] this: &NotificationConfig,
cloning: bool,
) -> Result<Value, Error> {
if !cloning {
bail!("freezing Notification config not supported!");
}
let mut cloned = Box::new(NotificationConfig {
config: Mutex::new(this.config.lock().unwrap().clone()),
});
let value = Value::new_pointer::<NotificationConfig>(&mut *cloned);
let _perl = Box::leak(cloned);
Ok(value)
}
/// Instead of `thaw` we implement `attach` for `dclone`.
#[export(name = "STORABLE_attach", raw_return)]
fn storable_attach(
#[raw] class: Value,
cloning: bool,
#[raw] serialized: Value,
) -> Result<Value, Error> {
if !cloning {
bail!("STORABLE_attach called with cloning=false");
}
let data = unsafe { Box::from_raw(serialized.pv_raw::<NotificationConfig>()?) };
Ok(perlmod::instantiate_magic!(&class, MAGIC => data))
}
#[export(raw_return)]
fn parse_config(
#[raw] class: Value,
raw_config: &[u8],
raw_private_config: &[u8],
) -> Result<Value, Error> {
let raw_config = std::str::from_utf8(raw_config)?;
let raw_private_config = std::str::from_utf8(raw_private_config)?;
Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
NotificationConfig {
config: Mutex::new(Config::new(raw_config, raw_private_config)?)
}
)))
}
#[export]
fn write_config(#[try_from_ref] this: &NotificationConfig) -> Result<(String, String), Error> {
Ok(this.config.lock().unwrap().write()?)
}
#[export]
fn digest(#[try_from_ref] this: &NotificationConfig) -> String {
let config = this.config.lock().unwrap();
hex::encode(config.digest())
}
#[export(serialize_error)]
fn send(
#[try_from_ref] this: &NotificationConfig,
severity: Severity,
title: String,
body: String,
template_data: Option<JSONValue>,
fields: Option<HashMap<String, String>>,
) -> Result<(), HttpError> {
let config = this.config.lock().unwrap();
let notification = Notification::new_templated(
severity,
title,
body,
template_data.unwrap_or_default(),
fields.unwrap_or_default(),
);
api::common::send(&config, &notification)
}
#[export(serialize_error)]
fn test_target(
#[try_from_ref] this: &NotificationConfig,
target: &str,
) -> Result<(), HttpError> {
let config = this.config.lock().unwrap();
api::common::test_target(&config, target)
}
#[export(serialize_error)]
fn get_sendmail_endpoints(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<SendmailConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::sendmail::get_endpoints(&config)
}
#[export(serialize_error)]
fn get_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<SendmailConfig, HttpError> {
let config = this.config.lock().unwrap();
api::sendmail::get_endpoint(&config, id)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn add_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: String,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: Option<String>,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::sendmail::add_endpoint(
&mut config,
&SendmailConfig {
name,
mailto,
mailto_user,
from_address,
author,
comment,
disable,
filter: None,
origin: None,
},
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: Option<String>,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableSendmailProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::sendmail::update_endpoint(
&mut config,
name,
&SendmailConfigUpdater {
mailto,
mailto_user,
from_address,
author,
comment,
disable,
},
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::sendmail::delete_endpoint(&mut config, name)
}
#[export(serialize_error)]
fn get_gotify_endpoints(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<GotifyConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::gotify::get_endpoints(&config)
}
#[export(serialize_error)]
fn get_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<GotifyConfig, HttpError> {
let config = this.config.lock().unwrap();
api::gotify::get_endpoint(&config, id)
}
#[export(serialize_error)]
fn add_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: String,
server: String,
token: String,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::gotify::add_endpoint(
&mut config,
&GotifyConfig {
name: name.clone(),
server,
comment,
disable,
filter: None,
origin: None,
},
&GotifyPrivateConfig { name, token },
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
server: Option<String>,
token: Option<String>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableGotifyProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::gotify::update_endpoint(
&mut config,
name,
&GotifyConfigUpdater {
server,
comment,
disable,
},
&GotifyPrivateConfigUpdater { token },
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::gotify::delete_gotify_endpoint(&mut config, name)
}
#[export(serialize_error)]
fn get_smtp_endpoints(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<SmtpConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::smtp::get_endpoints(&config)
}
#[export(serialize_error)]
fn get_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<SmtpConfig, HttpError> {
let config = this.config.lock().unwrap();
api::smtp::get_endpoint(&config, id)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn add_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: String,
server: String,
port: Option<u16>,
mode: Option<SmtpMode>,
username: Option<String>,
password: Option<String>,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: String,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::smtp::add_endpoint(
&mut config,
&SmtpConfig {
name: name.clone(),
server,
port,
mode,
username,
mailto,
mailto_user,
from_address,
author,
comment,
disable,
origin: None,
},
&SmtpPrivateConfig { name, password },
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
server: Option<String>,
port: Option<u16>,
mode: Option<SmtpMode>,
username: Option<String>,
password: Option<String>,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: Option<String>,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableSmtpProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::smtp::update_endpoint(
&mut config,
name,
&SmtpConfigUpdater {
server,
port,
mode,
username,
mailto,
mailto_user,
from_address,
author,
comment,
disable,
},
&SmtpPrivateConfigUpdater { password },
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::smtp::delete_endpoint(&mut config, name)
}
#[export(serialize_error)]
fn get_matchers(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<MatcherConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::matcher::get_matchers(&config)
}
#[export(serialize_error)]
fn get_matcher(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<MatcherConfig, HttpError> {
let config = this.config.lock().unwrap();
api::matcher::get_matcher(&config, id)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn add_matcher(
#[try_from_ref] this: &NotificationConfig,
name: String,
target: Option<Vec<String>>,
match_severity: Option<Vec<SeverityMatcher>>,
match_field: Option<Vec<FieldMatcher>>,
match_calendar: Option<Vec<CalendarMatcher>>,
mode: Option<MatchModeOperator>,
invert_match: Option<bool>,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::matcher::add_matcher(
&mut config,
&MatcherConfig {
name,
match_severity,
match_field,
match_calendar,
target,
mode,
invert_match,
comment,
disable,
origin: None,
},
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_matcher(
#[try_from_ref] this: &NotificationConfig,
name: &str,
target: Option<Vec<String>>,
match_severity: Option<Vec<SeverityMatcher>>,
match_field: Option<Vec<FieldMatcher>>,
match_calendar: Option<Vec<CalendarMatcher>>,
mode: Option<MatchModeOperator>,
invert_match: Option<bool>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableMatcherProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::matcher::update_matcher(
&mut config,
name,
&MatcherConfigUpdater {
match_severity,
match_field,
match_calendar,
target,
mode,
invert_match,
comment,
disable,
},
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_matcher(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::matcher::delete_matcher(&mut config, name)
}
#[export]
fn get_referenced_entities(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<Vec<String>, HttpError> {
let config = this.config.lock().unwrap();
api::common::get_referenced_entities(&config, name)
}
fn decode_digest(digest: Option<&str>) -> Result<Option<Vec<u8>>, HttpError> {
digest
.map(hex::decode)
.transpose()
.map_err(|e| api::http_err!(BAD_REQUEST, "invalid digest: {e}"))
}
}

5
pmg-rs/.cargo/config Normal file
View File

@ -0,0 +1,5 @@
[source]
[source.debian-packages]
directory = "/usr/share/cargo/registry"
[source.crates-io]
replace-with = "debian-packages"

View File

@ -1,14 +1,13 @@
[package] [package]
name = "pmg-rs" name = "pmg-rs"
version = "0.6.2" version = "0.7.5"
authors = [
"Proxmox Support Team <support@proxmox.com>",
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
"Fabian Ebner <f.ebner@proxmox.com>",
]
edition = "2018"
license = "AGPL-3"
description = "PMG parts which have been ported to rust" description = "PMG parts which have been ported to rust"
homepage = "https://www.proxmox.com"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2021"
license = "AGPL-3"
repository = "https://git.proxmox.com/?p=proxmox.git"
exclude = [ exclude = [
"build", "build",
"debian", "debian",
@ -20,22 +19,25 @@ crate-type = [ "cdylib" ]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
env_logger = "0.10"
hex = "0.4" hex = "0.4"
http = "0.2.7" http = "0.2.7"
libc = "0.2" libc = "0.2"
nix = "0.24" nix = "0.26"
openssl = "0.10.40" openssl = "0.10.40"
serde = "1.0" serde = "1.0"
serde_bytes = "0.11.3" serde_bytes = "0.11"
serde_json = "1.0" serde_json = "1.0"
url = "2" url = "2"
perlmod = { version = "0.13", features = [ "exporter" ] } perlmod = { version = "0.13.4", features = [ "exporter" ] }
proxmox-acme-rs = { version = "0.4", features = ["client"] } proxmox-acme = { version = "0.5", features = ["client"] }
proxmox-apt = "0.9" proxmox-apt = "0.10"
proxmox-http = { version = "0.7", features = ["client-sync", "client-trait"] } proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
proxmox-subscription = "0.3" proxmox-http-error = "0.1.0"
proxmox-sys = "0.4" proxmox-notify = "0.3.1"
proxmox-tfa = { version = "2.1", features = ["api"] } proxmox-subscription = "0.4"
proxmox-sys = "0.5"
proxmox-tfa = { version = "4.0.4", features = ["api"] }
proxmox-time = "1.1.3" proxmox-time = "1.1.3"

4
pmg-rs/Fixup.pm Normal file
View File

@ -0,0 +1,4 @@
# BEGIN Fixup.pm
# This is prepended to the current PMG.pm to force-include the temporary `openssl-probe` fixup.
use Proxmox::Lib::SslProbe;
# END Fixup.pm

View File

@ -1,4 +1,4 @@
#include /usr/share/dpkg/default.mk #include /usr/share/dpkg/pkg-info.mk
PACKAGE=libpmg-rs-perl PACKAGE=libpmg-rs-perl
@ -8,33 +8,46 @@ export GITVERSION:=$(shell git rev-parse HEAD)
PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};' PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};' PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
MAIN_DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb MAIN_DEB=$(PACKAGE)_$(DEB_VERSION)_$(ARCH).deb
DBGSYM_DEB=${PACKAGE}-dbgsym_${DEB_VERSION}_${ARCH}.deb DBGSYM_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(ARCH).deb
DEBS=$(MAIN_DEB) $(DBGSYM_DEB) DEBS=$(MAIN_DEB) $(DBGSYM_DEB)
DSC=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DESTDIR= DESTDIR=
PM_DIR := PMG PM_DIR := PMG
PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
--lib=pmg_rs \
--lib-tag=proxmox \
--lib-package=Proxmox::Lib::PMG \
--lib-prefix=PMG \
--include-file=Fixup.pm
PERLMOD_PACKAGES := \
PMG::RS::APT::Repositories \
PMG::RS::Acme \
PMG::RS::CSR \
PMG::RS::OpenId \
PMG::RS::TFA
ifeq ($(BUILD_MODE), release) ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release --offline CARGO_BUILD_ARGS += --release --offline
TARGET_DIR=release
else
TARGET_DIR=debug
endif endif
all: all: PMG
ifneq ($(BUILD_MODE), skip)
cargo build $(CARGO_BUILD_ARGS) cargo build $(CARGO_BUILD_ARGS)
endif
# always re-create this dir Proxmox PMG: Proxmox/Lib/PMG.pm
# but also copy the local target/ and PMG/ dirs as a build-cache Proxmox/Lib/PMG.pm: Fixup.pm
.PHONY: build $(PERLMOD_GENPACKAGE) $(PERLMOD_PACKAGES)
build:
rm -rf build
cargo build --release --offline
rsync -a debian Makefile Cargo.toml Cargo.lock src target PMG build/
.PHONY: install .PHONY: install
install: target/release/libpmg_rs.so install: target/release/libpmg_rs.so Proxmox/Lib/PMG.pm PMG
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto
install -m644 target/release/libpmg_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpmg_rs.so install -m644 target/release/libpmg_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpmg_rs.so
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB) install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)
@ -42,25 +55,43 @@ install: target/release/libpmg_rs.so
install -m644 Proxmox/Lib/PMG.pm $(DESTDIR)$(PERL_INSTALLVENDORLIB)/Proxmox/Lib/PMG.pm install -m644 Proxmox/Lib/PMG.pm $(DESTDIR)$(PERL_INSTALLVENDORLIB)/Proxmox/Lib/PMG.pm
find $(PM_DIR) \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';' find $(PM_DIR) \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
.PHONY: deb
deb: $(MAIN_DEB)
$(MAIN_DEB): build
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEBS)
distclean: clean distclean: clean
clean: clean:
rm -rf PMG Proxmox
cargo clean cargo clean
rm -rf *.deb *.dsc *.tar.gz *.buildinfo *.changes Cargo.lock build rm -f *.deb *.dsc *.tar.* *.build *.buildinfo *.changes Cargo.lock
find . -name '*~' -exec rm {} ';' rm -rf $(PACKAGE)-[0-9]*/
.PHONY: dinstall .PHONY: dinstall
dinstall: ${DEBS} dinstall: $(DEBS)
dpkg -i ${DEBS} dpkg -i $(DEBS)
.PHONY: upload .PHONY: upload
upload: ${DEBS} upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEBS)
# check if working directory is clean # check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - ${DEBS} | ssh -X repoman@repo.proxmox.com upload --product pmg --dist bullseye tar cf - $(DEBS) | ssh -X repoman@repo.proxmox.com upload --product pmg --dist $(DEB_DISTRIBUTION)
$(BUILDDIR): src debian common/src Cargo.toml Makefile .cargo/config
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp/common
cp -a -t $(BUILDDIR).tmp src debian Cargo.toml Makefile .cargo Fixup.pm
cp -a -t $(BUILDDIR).tmp/common common/src
mv $(BUILDDIR).tmp $(BUILDDIR)
.PHONY: deb
deb: $(DEBS)
$(DEBS): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -b -us -uc
lintian $(DEBS)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -S -us -uc -d
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)

1
pmg-rs/common Symbolic link
View File

@ -0,0 +1 @@
../common

View File

@ -1,3 +1,49 @@
libpmg-rs-perl (0.7.5) bookworm; urgency=medium
* add EAB binding support to ACME
* make Proxmox::Lib::PMG pull in Proxmox::Lib::SslProbe to work around
an issue where the openssl-probe crate calls setenv() and messes up perl's
view of the environment
-- Proxmox Support Team <support@proxmox.com> Thu, 07 Dec 2023 09:57:43 +0100
libpmg-rs-perl (0.7.4) bookworm; urgency=medium
* update to env logger 0.10
* use declare_magic for ACME
* add Promox::Lib::PMG::use_safe_putenv
-- Proxmox Support Team <support@proxmox.com> Wed, 06 Dec 2023 11:22:56 +0100
libpmg-rs-perl (0.7.3) bookworm; urgency=medium
* reset failure counts when unlocking second factors
-- Proxmox Support Team <support@proxmox.com> Wed, 05 Jul 2023 13:35:23 +0200
libpmg-rs-perl (0.7.2) bookworm; urgency=medium
* set default log level to 'info'
* introduce PMG_LOG environment variable to override log level
* add tfa_lock_status query sub
* add api_unlock_tfa sub
* bump proxmox-tfa to 4.0.2
-- Proxmox Support Team <support@proxmox.com> Tue, 27 Jun 2023 16:01:23 +0200
libpmg-rs-perl (0.7.1) bookworm; urgency=medium
* rebuild for Debian 12 Bookworm based release series
-- Proxmox Support Team <support@proxmox.com> Thu, 18 May 2023 12:01:08 +0200
libpmg-rs-perl (0.6.2) bullseye; urgency=medium libpmg-rs-perl (0.6.2) bullseye; urgency=medium
* update to proxmox-subscription 0.3 / proxmox-http 0.7 * update to proxmox-subscription 0.3 / proxmox-http 0.7

View File

@ -1 +0,0 @@
12

View File

@ -1,43 +1,51 @@
Source: libpmg-rs-perl Source: libpmg-rs-perl
Section: perl Section: perl
Priority: optional Priority: optional
Build-Depends: cargo:native <!nocheck>,
debhelper-compat (= 13),
librust-openssl-probe-dev (= 0.1.5-1~bpo12+pve1),
dh-cargo (>= 25),
librust-anyhow-1+default-dev,
librust-env-logger-0.10+default-dev,
librust-hex-0.4+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-nix-0.26+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.13+default-dev (>= 0.13.4-~~),
librust-perlmod-0.13+exporter-dev (>= 0.13.4-~~),
librust-proxmox-acme-0.5+client-dev,
librust-proxmox-acme-0.5+default-dev,
librust-proxmox-apt-0.10+default-dev,
librust-proxmox-http-0.9+client-sync-dev,
librust-proxmox-http-0.9+client-trait-dev,
librust-proxmox-http-0.9+default-dev,
librust-proxmox-http-error-0.1+default-dev,
librust-proxmox-notify-0.3+default-dev (>= 0.3.1-~~),
librust-proxmox-subscription-0.4+default-dev,
librust-proxmox-sys-0.5+default-dev,
librust-proxmox-tfa-4+api-dev (>= 4.0.4-~~),
librust-proxmox-tfa-4+default-dev (>= 4.0.4-~~),
librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev,
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
libstd-rust-dev <!nocheck>,
perlmod-bin (>= 0.2.0-3),
rustc:native <!nocheck>,
Maintainer: Proxmox Support Team <support@proxmox.com> Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends: Standards-Version: 4.6.1
debhelper (>= 12), Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git
dh-cargo (>= 24), Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git
cargo:native <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev,
librust-hex-0.4+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-nix-0.24+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.13+default-dev,
librust-perlmod-0.13+exporter-dev,
librust-proxmox-acme-rs-0.4+client-dev,
librust-proxmox-acme-rs-0.4+default-dev,
librust-proxmox-apt-0.9+default-dev,
librust-proxmox-http-0.7+client-sync-dev,
librust-proxmox-http-0.7+client-trait-dev,
librust-proxmox-http-0.7+default-dev,
librust-proxmox-subscription-0.3+default-dev,
librust-proxmox-sys-0.4+default-dev,
librust-proxmox-tfa-2+api-dev (>= 2.1-~~),
librust-proxmox-tfa-2+default-dev (>= 2.1-~~),
librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev (>= 0.11.3-~~),
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
Standards-Version: 4.3.0
Homepage: https://www.proxmox.com Homepage: https://www.proxmox.com
Package: libpmg-rs-perl Package: libpmg-rs-perl
Architecture: any Architecture: any
Depends: ${perl:Depends}, Depends: ${misc:Depends},
${perl:Depends},
${shlibs:Depends}, ${shlibs:Depends},
libproxmox-rs-perl (>= 0.3.3),
Description: Components of Proxmox Mail Gateway which have been ported to Rust. Description: Components of Proxmox Mail Gateway which have been ported to Rust.
Contains parts of Proxmox Mail Gateway which have been ported to, or newly Contains parts of Proxmox Mail Gateway which have been ported to, or newly
implemented in the Rust programming language. implemented in the Rust programming language.

View File

@ -1,10 +1,31 @@
# WARNING: this is *NOT* use as canonical source for d/control, but rather occasionally used via
# an invocation like:
# make clean
# rm debian/control
# debcargo package --config debian/debcargo.toml --changelog-ready --no-overlay-write-back --directory libpmg-rs-perl-0.7.1 pmg-rs 0.7.1
# mv libpmg-rs-perl-0.7.1/debian/control debian/control
# to semi.manually refresh the control file
#
# NOTE: debcargo thinks this is a source package, but it isn't! Drop provides, the dependencies of
# the binary package on rust source packages, Multi-Arch same, and other things that do not make
# sense for a combined perl + arch-dependent library package.
overlay = "." overlay = "."
crate_src_path = ".." crate_src_path = ".."
maintainer = "Proxmox Support Team <support@proxmox.com>" maintainer = "Proxmox Support Team <support@proxmox.com>"
[source] [source]
section = "perl" section = "perl"
vcs_git = "git://git.proxmox.com/git/proxmox.git" vcs_git = "git://git.proxmox.com/git/proxmox-perl-rs.git"
vcs_browser = "https://git.proxmox.com/?p=proxmox.git" vcs_browser = "https://git.proxmox.com/?p=proxmox-perl-rs.git"
build_depends = [
"perlmod-bin",
]
[packages.libpmg-rs-perl] [packages.bin]
name = "libpmg-rs-perl"
summary = "Components of Proxmox Mail Gateway which have been ported to Rust."
description = """
Contains parts of Proxmox Mail Gateway which have been ported to, or newly
implemented in the Rust programming language.
"""

View File

@ -1,7 +1,25 @@
#!/usr/bin/make -f #!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
include /usr/share/rustc/architecture.mk
#export DH_VERBOSE=1 #export DH_VERBOSE=1
export BUILD_MODE=release export BUILD_MODE=release
CARGO=/usr/share/cargo/bin/cargo
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
export CARGO_HOME = $(CURDIR)/debian/cargo_home
export DEB_CARGO_CRATE=pmg-rs_$(DEB_VERSION_UPSTREAM)
export DEB_CARGO_PACKAGE=pmg-rs
%: %:
dh $@ dh $@
override_dh_auto_configure:
@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
dh_auto_configure

View File

@ -9,8 +9,8 @@ use std::os::unix::fs::OpenOptionsExt;
use anyhow::{format_err, Error}; use anyhow::{format_err, Error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use proxmox_acme_rs::account::AccountData as AcmeAccountData; use proxmox_acme::account::AccountData as AcmeAccountData;
use proxmox_acme_rs::{Account, Client}; use proxmox_acme::{Account, Client};
/// Our on-disk format inherited from PVE's proxmox-acme code. /// Our on-disk format inherited from PVE's proxmox-acme code.
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -79,6 +79,7 @@ impl Inner {
tos_agreed: bool, tos_agreed: bool,
contact: Vec<String>, contact: Vec<String>,
rsa_bits: Option<u32>, rsa_bits: Option<u32>,
eab_creds: Option<(String, String)>,
) -> Result<(), Error> { ) -> Result<(), Error> {
self.tos = if tos_agreed { self.tos = if tos_agreed {
self.client.terms_of_service_url()?.map(str::to_owned) self.client.terms_of_service_url()?.map(str::to_owned)
@ -86,7 +87,9 @@ impl Inner {
None None
}; };
let _account = self.client.new_account(contact, tos_agreed, rsa_bits)?; let _account = self
.client
.new_account(contact, tos_agreed, rsa_bits, eab_creds)?;
let file = OpenOptions::new() let file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
@ -182,67 +185,45 @@ impl Inner {
#[perlmod::package(name = "PMG::RS::Acme")] #[perlmod::package(name = "PMG::RS::Acme")]
pub mod export { pub mod export {
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::Mutex; use std::sync::Mutex;
use anyhow::Error; use anyhow::Error;
use serde_bytes::{ByteBuf, Bytes}; use serde_bytes::{ByteBuf, Bytes};
use perlmod::Value; use perlmod::Value;
use proxmox_acme_rs::directory::Meta; use proxmox_acme::directory::Meta;
use proxmox_acme_rs::order::OrderData; use proxmox_acme::order::OrderData;
use proxmox_acme_rs::{Authorization, Challenge, Order}; use proxmox_acme::{Authorization, Challenge, Order};
use super::{AccountData, Inner}; use super::{AccountData, Inner};
const CLASSNAME: &str = "PMG::RS::Acme"; perlmod::declare_magic!(Box<Acme> : &Acme as "PMG::RS::Acme");
/// An Acme client instance. /// An Acme client instance.
pub struct Acme { pub struct Acme {
inner: Mutex<Inner>, inner: Mutex<Inner>,
} }
impl<'a> TryFrom<&'a Value> for &'a Acme {
type Error = Error;
fn try_from(value: &'a Value) -> Result<&'a Acme, Error> {
Ok(unsafe { value.from_blessed_box(CLASSNAME)? })
}
}
fn bless(class: Value, mut ptr: Box<Acme>) -> Result<Value, Error> {
let value = Value::new_pointer::<Acme>(&mut *ptr);
let value = Value::new_ref(&value);
let this = value.bless_sv(&class)?;
let _perl = Box::leak(ptr);
Ok(this)
}
/// Create a new ACME client instance given an account path and an API directory URL. /// Create a new ACME client instance given an account path and an API directory URL.
#[export(raw_return)] #[export(raw_return)]
pub fn new(#[raw] class: Value, api_directory: String) -> Result<Value, Error> { pub fn new(#[raw] class: Value, api_directory: String) -> Result<Value, Error> {
bless( Ok(perlmod::instantiate_magic!(
class, &class,
Box::new(Acme { MAGIC => Box::new(Acme {
inner: Mutex::new(Inner::new(api_directory)?), inner: Mutex::new(Inner::new(api_directory)?),
}), })
) ))
} }
/// Load an existing account. /// Load an existing account.
#[export(raw_return)] #[export(raw_return)]
pub fn load(#[raw] class: Value, account_path: String) -> Result<Value, Error> { pub fn load(#[raw] class: Value, account_path: String) -> Result<Value, Error> {
bless( Ok(perlmod::instantiate_magic!(
class, &class,
Box::new(Acme { MAGIC => Box::new(Acme {
inner: Mutex::new(Inner::load(account_path)?), inner: Mutex::new(Inner::load(account_path)?),
}), })
) ))
}
#[export(name = "DESTROY")]
fn destroy(#[raw] this: Value) {
perlmod::destructor!(this, Acme: CLASSNAME);
} }
/// Create a new account. /// Create a new account.
@ -260,11 +241,16 @@ pub mod export {
tos_agreed: bool, tos_agreed: bool,
contact: Vec<String>, contact: Vec<String>,
rsa_bits: Option<u32>, rsa_bits: Option<u32>,
eab_kid: Option<String>,
eab_hmac_key: Option<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
this.inner this.inner.lock().unwrap().new_account(
.lock() account_path,
.unwrap() tos_agreed,
.new_account(account_path, tos_agreed, contact, rsa_bits) contact,
rsa_bits,
eab_kid.zip(eab_hmac_key),
)
} }
/// Get the directory's meta information. /// Get the directory's meta information.

View File

@ -5,7 +5,7 @@ pub mod export {
use anyhow::Error; use anyhow::Error;
use serde_bytes::ByteBuf; use serde_bytes::ByteBuf;
use proxmox_acme_rs::util::Csr; use proxmox_acme::util::Csr;
/// Generates a CSR and its accompanying private key. /// Generates a CSR and its accompanying private key.
/// ///

View File

@ -1,7 +1,25 @@
#[path = "../../common/src/mod.rs"] #[path = "../common/src/mod.rs"]
pub mod common; pub mod common;
pub mod acme; pub mod acme;
pub mod apt; pub mod apt;
pub mod csr; pub mod csr;
pub mod tfa; pub mod tfa;
#[perlmod::package(name = "Proxmox::Lib::PMG", lib = "pmg_rs")]
mod export {
use crate::common;
#[export]
pub fn init() {
common::logger::init("PMG_LOG", "info");
}
/// CLI tools should call this very early. This is a workaround causing environment variable
/// manipulation to leak instead of crash. Required when calling into rust code that causes
/// `setenv` calls, particularly code using the openssl crate.
#[export]
pub fn use_safe_putenv() {
perlmod::ffi::use_safe_putenv(true);
}
}

View File

@ -18,11 +18,13 @@ use nix::errno::Errno;
use nix::sys::stat::Mode; use nix::sys::stat::Mode;
pub(self) use proxmox_tfa::api::{ pub(self) use proxmox_tfa::api::{
RecoveryState, TfaChallenge, TfaConfig, TfaResponse, U2fConfig, WebauthnConfig, RecoveryState, TfaChallenge, TfaConfig, TfaResponse, U2fConfig, UserChallengeAccess,
WebauthnConfig,
}; };
#[perlmod::package(name = "PMG::RS::TFA")] #[perlmod::package(name = "PMG::RS::TFA")]
mod export { mod export {
use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
use std::sync::Mutex; use std::sync::Mutex;
@ -31,7 +33,7 @@ mod export {
use url::Url; use url::Url;
use perlmod::Value; use perlmod::Value;
use proxmox_tfa::api::methods; use proxmox_tfa::api::{methods, TfaResult};
use super::{TfaConfig, UserAccess}; use super::{TfaConfig, UserAccess};
@ -105,7 +107,7 @@ mod export {
) -> Result<String, Error> { ) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_challenge(UserAccess::new(&raw_this)?, userid, description) inner.u2f_registration_challenge(&UserAccess::new(&raw_this)?, userid, description)
} }
/// Finish a u2f registration. This updates temporary data in `/run` and therefore the config /// Finish a u2f registration. This updates temporary data in `/run` and therefore the config
@ -120,7 +122,7 @@ mod export {
) -> Result<String, Error> { ) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_finish(UserAccess::new(&raw_this)?, userid, challenge, response) inner.u2f_registration_finish(&UserAccess::new(&raw_this)?, userid, challenge, response)
} }
/// Check if a user has any TFA entries of a given type. /// Check if a user has any TFA entries of a given type.
@ -203,7 +205,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
match inner.authentication_challenge( match inner.authentication_challenge(
UserAccess::new(&raw_this)?, &UserAccess::new(&raw_this)?,
userid, userid,
origin.as_ref(), origin.as_ref(),
)? { )? {
@ -220,10 +222,7 @@ mod export {
.unwrap() .unwrap()
.users .users
.get(userid) .get(userid)
.and_then(|user| { .and_then(|user| user.recovery_state())
let state = user.recovery_state();
state.is_available().then(move || state)
})
} }
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against /// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
@ -244,15 +243,78 @@ mod export {
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?; let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?; let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
inner let result = inner.verify(
.verify( &UserAccess::new(&raw_this)?,
UserAccess::new(&raw_this)?, userid,
userid, &challenge,
&challenge, response,
response, origin.as_ref(),
origin.as_ref(), );
) match result {
.map(|save| save.needs_saving()) TfaResult::Success { needs_saving } => Ok(needs_saving),
_ => bail!("TFA authentication failed"),
}
}
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
/// it.
///
/// Returns a result hash of the form:
/// ```text
/// {
/// "result": bool, // whether TFA was successful
/// "needs-saving": bool, // whether the user config needs saving
/// "tfa-limit-reached": bool, // whether the TFA limit was reached (config needs saving)
/// "totp-limit-reached": bool, // whether the TOTP limit was reached (config needs saving)
/// }
/// ```
#[export]
fn authentication_verify2(
#[raw] raw_this: Value,
//#[try_from_ref] this: &Tfa,
userid: &str,
challenge: &str, //super::TfaChallenge,
response: &str,
origin: Option<Url>,
) -> Result<TfaReturnValue, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap();
let result = inner.verify(
&UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
);
Ok(match result {
TfaResult::Success { needs_saving } => TfaReturnValue {
result: true,
needs_saving,
..Default::default()
},
TfaResult::Locked => TfaReturnValue::default(),
TfaResult::Failure {
needs_saving,
totp_limit_reached,
tfa_limit_reached,
} => TfaReturnValue {
result: false,
needs_saving,
totp_limit_reached,
tfa_limit_reached,
},
})
}
#[derive(Default, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaReturnValue {
result: bool,
needs_saving: bool,
totp_limit_reached: bool,
tfa_limit_reached: bool,
} }
/// DEBUG HELPER: Get the current TOTP value for a given TOTP URI. /// DEBUG HELPER: Get the current TOTP value for a given TOTP URI.
@ -314,7 +376,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
methods::add_tfa_entry( methods::add_tfa_entry(
&mut this.inner.lock().unwrap(), &mut this.inner.lock().unwrap(),
UserAccess::new(&raw_this)?, &UserAccess::new(&raw_this)?,
userid, userid,
description, description,
totp, totp,
@ -375,6 +437,66 @@ mod export {
Err(methods::EntryNotFound) => bail!("no such entry"), Err(methods::EntryNotFound) => bail!("no such entry"),
} }
} }
#[export]
fn api_unlock_tfa(#[raw] raw_this: Value, userid: &str) -> Result<bool, Error> {
let this: &Tfa = (&raw_this).try_into()?;
Ok(methods::unlock_and_reset_tfa(
&mut this.inner.lock().unwrap(),
&UserAccess::new(&raw_this)?,
userid,
)?)
}
#[derive(serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaLockStatus {
/// Once a user runs into a TOTP limit they get locked out of TOTP until they successfully use
/// a recovery key.
#[serde(skip_serializing_if = "bool_is_false", default)]
totp_locked: bool,
/// If a user hits too many 2nd factor failures, they get completely blocked for a while.
#[serde(skip_serializing_if = "Option::is_none", default)]
#[serde(deserialize_with = "filter_expired_timestamp")]
tfa_locked_until: Option<i64>,
}
impl From<&proxmox_tfa::api::TfaUserData> for TfaLockStatus {
fn from(data: &proxmox_tfa::api::TfaUserData) -> Self {
Self {
totp_locked: data.totp_locked,
tfa_locked_until: data.tfa_locked_until,
}
}
}
fn bool_is_false(b: &bool) -> bool {
!*b
}
#[export]
fn tfa_lock_status(
#[try_from_ref] this: &Tfa,
userid: Option<&str>,
) -> Result<Option<perlmod::Value>, Error> {
let this = this.inner.lock().unwrap();
if let Some(userid) = userid {
if let Some(user) = this.users.get(userid) {
Ok(Some(perlmod::to_value(&TfaLockStatus::from(user))?))
} else {
Ok(None)
}
} else {
Ok(Some(perlmod::to_value(
&HashMap::<String, TfaLockStatus>::from_iter(
this.users
.iter()
.map(|(uid, data)| (uid.clone(), TfaLockStatus::from(data))),
),
)?))
}
}
} }
/// Attach the path to errors from [`nix::mkir()`]. /// Attach the path to errors from [`nix::mkir()`].
@ -440,9 +562,7 @@ fn challenge_data_path(userid: &str, debug: bool) -> PathBuf {
} }
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess { impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
type Data = UserChallengeData; fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error> {
fn open(&self, userid: &str) -> Result<UserChallengeData, Error> {
if self.is_debug() { if self.is_debug() {
mkdir("./local-tfa-challenges", 0o700)?; mkdir("./local-tfa-challenges", 0o700)?;
} else { } else {
@ -485,15 +605,15 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
} }
}; };
Ok(UserChallengeData { Ok(Box::new(UserChallengeData {
inner, inner,
path, path,
lock: file, lock: file,
}) }))
} }
/// `open` without creating the file if it doesn't exist, to finish WA authentications. /// `open` without creating the file if it doesn't exist, to finish WA authentications.
fn open_no_create(&self, userid: &str) -> Result<Option<UserChallengeData>, Error> { fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error> {
let path = challenge_data_path(userid, self.is_debug()); let path = challenge_data_path(userid, self.is_debug());
let mut file = match std::fs::OpenOptions::new() let mut file = match std::fs::OpenOptions::new()
@ -514,11 +634,11 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
format_err!("failed to read challenge data for user {}: {}", userid, err) format_err!("failed to read challenge data for user {}: {}", userid, err)
})?; })?;
Ok(Some(UserChallengeData { Ok(Some(Box::new(UserChallengeData {
inner, inner,
path, path,
lock: file, lock: file,
})) })))
} }
fn remove(&self, userid: &str) -> Result<bool, Error> { fn remove(&self, userid: &str) -> Result<bool, Error> {
@ -529,6 +649,10 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
} }
} }
fn enable_lockout(&self) -> bool {
true
}
} }
/// Container of `TfaUserChallenges` with the corresponding file lock guard. /// Container of `TfaUserChallenges` with the corresponding file lock guard.
@ -546,7 +670,7 @@ impl proxmox_tfa::api::UserChallengeAccess for UserChallengeData {
&mut self.inner &mut self.inner
} }
fn save(self) -> Result<(), Error> { fn save(&mut self) -> Result<(), Error> {
UserChallengeData::save(self) UserChallengeData::save(self)
} }
} }
@ -591,7 +715,7 @@ impl UserChallengeData {
/// ///
/// This currently consumes selfe as we never perform more than 1 insertion/removal, and this /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
/// way also unlocks early. /// way also unlocks early.
fn save(mut self) -> Result<(), Error> { fn save(&mut self) -> Result<(), Error> {
self.rewind()?; self.rewind()?;
serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| { serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {

View File

@ -1,11 +1,12 @@
[package] [package]
name = "pve-rs" name = "pve-rs"
version = "0.7.3" version = "0.8.8"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2018"
license = "AGPL-3"
description = "PVE parts which have been ported to Rust" description = "PVE parts which have been ported to Rust"
homepage = "https://www.proxmox.com" homepage = "https://www.proxmox.com"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2021"
license = "AGPL-3"
repository = "https://git.proxmox.com/?p=proxmox.git"
exclude = [ exclude = [
"debian", "debian",
@ -18,10 +19,12 @@ crate-type = [ "cdylib" ]
anyhow = "1.0" anyhow = "1.0"
base32 = "0.4" base32 = "0.4"
base64 = "0.13" base64 = "0.13"
env_logger = "0.10"
hex = "0.4" hex = "0.4"
http = "0.2.7" http = "0.2.7"
libc = "0.2" libc = "0.2"
nix = "0.24" log = "0.4.17"
nix = "0.26"
openssl = "0.10.40" openssl = "0.10.40"
serde = "1.0" serde = "1.0"
serde_bytes = "0.11" serde_bytes = "0.11"
@ -30,11 +33,13 @@ url = "2"
perlmod = { version = "0.13", features = [ "exporter" ] } perlmod = { version = "0.13", features = [ "exporter" ] }
proxmox-apt = "0.9" proxmox-apt = "0.10.6"
proxmox-http = { version = "0.7", features = ["client-sync", "client-trait"] } proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
proxmox-openid = "0.9.5" proxmox-http-error = "0.1.0"
proxmox-resource-scheduling = "0.1" proxmox-notify = { version = "0.3.1", features = ["pve-context"] }
proxmox-subscription = "0.3" proxmox-openid = "0.10"
proxmox-sys = "0.4" proxmox-resource-scheduling = "0.3.0"
proxmox-tfa = { version = "2.1", features = ["api"] } proxmox-subscription = "0.4"
proxmox-sys = "0.5"
proxmox-tfa = { version = "4.0.4", features = ["api"] }
proxmox-time = "1.1.3" proxmox-time = "1.1.3"

4
pve-rs/Fixup.pm Normal file
View File

@ -0,0 +1,4 @@
# BEGIN Fixup.pm
# This is prepended to the current PMG.pm to force-include the temporary `openssl-probe` fixup.
use Proxmox::Lib::SslProbe;
# END Fixup.pm

View File

@ -1,4 +1,4 @@
#include /usr/share/dpkg/default.mk #include /usr/share/dpkg/pkg-info.mk
PACKAGE=libpve-rs-perl PACKAGE=libpve-rs-perl
export PERLMOD_PRODUCT=PVE export PERLMOD_PRODUCT=PVE
@ -9,33 +9,52 @@ export GITVERSION:=$(shell git rev-parse HEAD)
PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};' PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};' PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
MAIN_DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb MAIN_DEB=$(PACKAGE)_$(DEB_VERSION)_$(ARCH).deb
DBGSYM_DEB=${PACKAGE}-dbgsym_${DEB_VERSION}_${ARCH}.deb DBGSYM_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(ARCH).deb
DEBS=$(MAIN_DEB) $(DBGSYM_DEB) DEBS=$(MAIN_DEB) $(DBGSYM_DEB)
DSC=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DESTDIR= DESTDIR=
PM_DIR := PVE PM_DIR := PVE
PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
--lib=pve_rs \
--lib-tag=proxmox \
--lib-package=Proxmox::Lib::PVE \
--lib-prefix=PVE \
--include-file=Fixup.pm
PERLMOD_PACKAGES := \
PVE::RS::APT::Repositories \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::TFA
ifeq ($(BUILD_MODE), release) ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release --offline CARGO_BUILD_ARGS += --release --offline
TARGET_DIR=release
else
TARGET_DIR=debug
endif endif
all: all: PVE
ifneq ($(BUILD_MODE), skip)
cargo build $(CARGO_BUILD_ARGS) cargo build $(CARGO_BUILD_ARGS)
endif mkdir -p test/Proxmox/Lib
sed -r -e \
's@^sub libfile.*$$@sub libfile { "$(shell pwd)/target/$(TARGET_DIR)/libpve_rs.so"; }@' \
Proxmox/Lib/PVE.pm >test/Proxmox/Lib/PVE.pm
# always re-create this dir Proxmox PVE: Proxmox/Lib/PVE.pm
# but also copy the local target/ and PVE/ dirs as a build-cache Proxmox/Lib/PVE.pm: Fixup.pm
.PHONY: build $(PERLMOD_GENPACKAGE) $(PERLMOD_PACKAGES)
build:
rm -rf build check: all
cargo build --release --offline $(MAKE) -C test test
rsync -a debian Makefile Cargo.toml Cargo.lock src target PVE build/
.PHONY: install .PHONY: install
install: target/release/libpve_rs.so install: target/release/libpve_rs.so Proxmox/Lib/PVE.pm PVE
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto
install -m644 target/release/libpve_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpve_rs.so install -m644 target/release/libpve_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpve_rs.so
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB) install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)
@ -43,25 +62,42 @@ install: target/release/libpve_rs.so
install -m644 Proxmox/Lib/PVE.pm $(DESTDIR)$(PERL_INSTALLVENDORLIB)/Proxmox/Lib/PVE.pm install -m644 Proxmox/Lib/PVE.pm $(DESTDIR)$(PERL_INSTALLVENDORLIB)/Proxmox/Lib/PVE.pm
find $(PM_DIR) \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';' find $(PM_DIR) \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
.PHONY: deb
deb: $(MAIN_DEB)
$(MAIN_DEB): build
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEBS)
distclean: clean
clean: clean:
rm -rf PVE Proxmox
cargo clean cargo clean
rm -rf *.deb *.dsc *.tar.gz *.buildinfo *.changes Cargo.lock build rm -f *.deb *.dsc *.tar.* *.build *.buildinfo *.changes Cargo.lock
find . -name '*~' -exec rm {} ';' rm -rf $(PACKAGE)-[0-9]*/
.PHONY: dinstall .PHONY: dinstall
dinstall: ${DEBS} dinstall: $(DEBS)
dpkg -i ${DEBS} dpkg -i $(DEBS)
.PHONY: upload .PHONY: upload
upload: ${DEBS} upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEBS)
# check if working directory is clean # check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - ${DEBS} | ssh -X repoman@repo.proxmox.com upload --product pve --dist bullseye tar cf - $(DEBS) | ssh -X repoman@repo.proxmox.com upload --product pve --dist $(DEB_DISTRIBUTION)
$(BUILDDIR): src debian test common/src Cargo.toml Makefile .cargo/config
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp/common
cp -a -t $(BUILDDIR).tmp src debian test Cargo.toml Makefile .cargo Fixup.pm
cp -a -t $(BUILDDIR).tmp/common common/src
mv $(BUILDDIR).tmp $(BUILDDIR)
.PHONY: deb
deb: $(DEBS)
$(DEBS): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -b -us -uc
lintian $(DEBS)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -S -us -uc -d
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)

1
pve-rs/common Symbolic link
View File

@ -0,0 +1 @@
../common

View File

@ -1,3 +1,114 @@
libpve-rs-perl (0.8.8) bookworm; urgency=medium
* notify: include 'hostname' and 'type' metadata fields for forwarded mails
* notify: smtp: forward original message instead of nesting
* notify: smtp: add 'Auto-Submitted' header to email body
* notify: api: allow resetting built-in targets if used by a matcher
-- Proxmox Support Team <support@proxmox.com> Wed, 10 Jan 2024 14:19:47 +0100
libpve-rs-perl (0.8.7) bookworm; urgency=medium
* notify: adapt to new matcher-based notification routing
* notify: add bindings for smtp API calls
* pve-rs: notify: remove notify_context for PVE
* notify: add 'disable' parameter
* notify: support 'origin' paramter
-- Proxmox Support Team <support@proxmox.com> Fri, 17 Nov 2023 13:41:17 +0100
libpve-rs-perl (0.8.6) bookworm; urgency=medium
* re-build with newer proxmox-apt depenceny to make Ceph Reef repo available
-- Proxmox Support Team <support@proxmox.com> Tue, 05 Sep 2023 15:37:44 +0200
libpve-rs-perl (0.8.5) bookworm; urgency=medium
* add PVE::RS::Notify module
-- Proxmox Support Team <support@proxmox.com> Mon, 24 Jul 2023 11:18:56 +0200
libpve-rs-perl (0.8.4) bookworm; urgency=medium
* reset failure counts when unlocking second factors
-- Proxmox Support Team <support@proxmox.com> Wed, 05 Jul 2023 13:30:17 +0200
libpve-rs-perl (0.8.3) bookworm; urgency=medium
* set default log level to 'info'
* introduce PVE_LOG environment variable to override log level
* add tfa_lock_status query sub
* bump proxmox-tfa to 4.0.2
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jun 2023 12:55:03 +0200
libpve-rs-perl (0.8.2) bookworm; urgency=medium
* update proxmox-apt which updated repositories info for bookworm
-- Proxmox Support Team <support@proxmox.com> Sun, 04 Jun 2023 18:33:42 +0200
libpve-rs-perl (0.8.1) bookworm; urgency=medium
* bump proxmox-apt,http,openid,subscription,sys crates to their bookworm
versions
* bump proxmox-tfa to 4.0.1 to include the unlock API
* enable TFA lockout and provide the `api_unlock_tfa` call
-- Proxmox Support Team <support@proxmox.com> Wed, 31 May 2023 14:17:31 +0200
libpve-rs-perl (0.8.0) bookworm; urgency=medium
* rebuild for Debian 12 Bookworm based release series
-- Proxmox Support Team <support@proxmox.com> Tue, 16 May 2023 14:26:52 +0200
libpve-rs-perl (0.7.6) bullseye; urgency=medium
* update to new tfa crate
* introduce new authentication_verify2 call to utilize the totp/tfa locking
capabilities of the TFA API
-- Proxmox Support Team <support@proxmox.com> Wed, 10 May 2023 10:54:10 +0200
libpve-rs-perl (0.7.5) bullseye; urgency=medium
* update proxmox-resource-scheduling dependency to 0.2.1 to pull in an
improvement for with services where CPU should matter more if there is no
memory load at all
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Mar 2023 17:58:22 +0100
libpve-rs-perl (0.7.4) bullseye; urgency=medium
* initialize logging when shared library is loaded
* update to new TFA crate
* bump proxmox-resource-scheduling dependency to 0.2 to pull in a fix for
usage calculation for homogeneous nodes
* pve: test: resource scheduling: add test with overcommitted node
* update nix to 0.26
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Mar 2023 15:28:08 +0100
libpve-rs-perl (0.7.3) bullseye; urgency=medium libpve-rs-perl (0.7.3) bullseye; urgency=medium
* add PVE::RS::ResourceScheduling::Static and tests * add PVE::RS::ResourceScheduling::Static and tests

View File

@ -1 +0,0 @@
10

View File

@ -1,39 +1,44 @@
Source: libpve-rs-perl Source: libpve-rs-perl
Section: perl Section: perl
Priority: optional Priority: optional
Build-Depends: Build-Depends: cargo:native <!nocheck>,
debhelper (>= 12), debhelper-compat (= 13),
dh-cargo (>= 24), dh-cargo (>= 25),
cargo:native <!nocheck>, librust-anyhow-1+default-dev,
rustc:native <!nocheck>, librust-base32-0.4+default-dev,
libstd-rust-dev <!nocheck>, librust-base64-0.13+default-dev,
librust-anyhow-1+default-dev, librust-env-logger-0.10+default-dev,
librust-base32-0.4+default-dev, librust-hex-0.4+default-dev,
librust-base64-0.13+default-dev, librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-hex-0.4+default-dev, librust-libc-0.2+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~), librust-log-0.4+default-dev (>= 0.4.17-~~),
librust-libc-0.2+default-dev, librust-nix-0.26+default-dev,
librust-nix-0.24+default-dev, librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-openssl-0.10+default-dev (>= 0.10.40-~~), librust-perlmod-0.13+default-dev,
librust-perlmod-0.13+default-dev, librust-perlmod-0.13+exporter-dev,
librust-perlmod-0.13+exporter-dev, librust-proxmox-apt-0.10+default-dev (>= 0.10.6-~~),
librust-proxmox-apt-0.9+default-dev, librust-proxmox-http-0.9+client-sync-dev,
librust-proxmox-http-0.7+client-sync-dev, librust-proxmox-http-0.9+client-trait-dev,
librust-proxmox-http-0.7+client-trait-dev, librust-proxmox-http-0.9+default-dev,
librust-proxmox-http-0.7+default-dev, librust-proxmox-http-error-0.1+default-dev,
librust-proxmox-openid-0.9+default-dev (>= 0.9.5-~~), librust-proxmox-notify-0.3+default-dev (>= 0.3.1-~~),
librust-proxmox-resource-scheduling-0.1+default-dev, librust-proxmox-notify-0.3+pve-context-dev (>= 0.3.1-~~),
librust-proxmox-subscription-0.3+default-dev, librust-proxmox-openid-0.10+default-dev,
librust-proxmox-sys-0.4+default-dev, librust-proxmox-resource-scheduling-0.3+default-dev,
librust-proxmox-tfa-2+api-dev (>= 2.1-~~), librust-proxmox-subscription-0.4+default-dev,
librust-proxmox-tfa-2+default-dev (>= 2.1-~~), librust-proxmox-sys-0.5+default-dev,
librust-proxmox-time-1+default-dev (>= 1.1.3-~~), librust-proxmox-tfa-4+api-dev (>= 4.0.4-~~),
librust-serde-1+default-dev, librust-proxmox-tfa-4+default-dev (>= 4.0.4-~~),
librust-serde-bytes-0.11+default-dev, librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-json-1+default-dev, librust-serde-1+default-dev,
librust-url-2+default-dev, librust-serde-bytes-0.11+default-dev,
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
libstd-rust-dev <!nocheck>,
perlmod-bin (>= 0.2.0-3),
rustc:native <!nocheck>,
Maintainer: Proxmox Support Team <support@proxmox.com> Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.5.1 Standards-Version: 4.6.1
Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git
Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git
Homepage: https://www.proxmox.com Homepage: https://www.proxmox.com
@ -41,14 +46,13 @@ Rules-Requires-Root: no
Package: libpve-rs-perl Package: libpve-rs-perl
Architecture: any Architecture: any
Depends: Depends: ${misc:Depends},
${misc:Depends}, ${perl:Depends},
${perl:Depends}, ${shlibs:Depends},
${shlibs:Depends}, libproxmox-rs-perl (>= 0.3.3),
Breaks: Breaks: libpve-access-control (<< 7.1-3),
libpve-access-control (<< 7.1-3), libpve-common-perl (<< 7.1-4),
libpve-common-perl (<< 7.1-4), pve-manager (<< 7.1-11),
pve-manager (<< 7.1-11),
Description: PVE parts which have been ported to Rust - Rust source code Description: PVE parts which have been ported to Rust - Rust source code
This package contains the source for the Rust pve-rs crate, packaged by This package contains the source for the Rust pve-rs crate, packaged by
debcargo for use with cargo and dh-cargo. debcargo for use with cargo and dh-cargo.

View File

@ -1,7 +1,25 @@
#!/usr/bin/make -f #!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
include /usr/share/rustc/architecture.mk
#export DH_VERBOSE=1 #export DH_VERBOSE=1
export BUILD_MODE=release export BUILD_MODE=release
CARGO=/usr/share/cargo/bin/cargo
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
export CARGO_HOME = $(CURDIR)/debian/cargo_home
export DEB_CARGO_CRATE=pve-rs_$(DEB_VERSION_UPSTREAM)
export DEB_CARGO_PACKAGE=pve-rs
%: %:
dh $@ dh $@
override_dh_auto_configure:
@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
dh_auto_configure

View File

@ -1,9 +1,22 @@
//! Rust library for the Proxmox VE code base. //! Rust library for the Proxmox VE code base.
#[path = "../../common/src/mod.rs"] #[path = "../common/src/mod.rs"]
pub mod common; pub mod common;
//pub mod apt; //pub mod apt;
pub mod openid; pub mod openid;
pub mod resource_scheduling; pub mod resource_scheduling;
pub mod tfa; pub mod tfa;
#[perlmod::package(name = "Proxmox::Lib::PVE", lib = "pve_rs")]
mod export {
use proxmox_notify::context::pve::PVE_CONTEXT;
use crate::common;
#[export]
pub fn init() {
common::logger::init("PVE_LOG", "info");
proxmox_notify::context::set_context(&PVE_CONTEXT)
}
}

View File

@ -1,6 +1,5 @@
#[perlmod::package(name = "PVE::RS::OpenId", lib = "pve_rs")] #[perlmod::package(name = "PVE::RS::OpenId", lib = "pve_rs")]
mod export { mod export {
use std::convert::TryFrom;
use std::sync::Mutex; use std::sync::Mutex;
use anyhow::Error; use anyhow::Error;
@ -9,34 +8,13 @@ mod export {
use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig, PrivateAuthState}; use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig, PrivateAuthState};
const CLASSNAME: &str = "PVE::RS::OpenId"; perlmod::declare_magic!(Box<OpenId> : &OpenId as "PVE::RS::OpenId");
/// An OpenIdAuthenticator client instance. /// An OpenIdAuthenticator client instance.
pub struct OpenId { pub struct OpenId {
inner: Mutex<OpenIdAuthenticator>, inner: Mutex<OpenIdAuthenticator>,
} }
impl<'a> TryFrom<&'a Value> for &'a OpenId {
type Error = Error;
fn try_from(value: &'a Value) -> Result<&'a OpenId, Error> {
Ok(unsafe { value.from_blessed_box(CLASSNAME)? })
}
}
fn bless(class: Value, mut ptr: Box<OpenId>) -> Result<Value, Error> {
let value = Value::new_pointer::<OpenId>(&mut *ptr);
let value = Value::new_ref(&value);
let this = value.bless_sv(&class)?;
let _perl = Box::leak(ptr);
Ok(this)
}
#[export(name = "DESTROY")]
fn destroy(#[raw] this: Value) {
perlmod::destructor!(this, OpenId: CLASSNAME);
}
/// Create a new OpenId client instance /// Create a new OpenId client instance
#[export(raw_return)] #[export(raw_return)]
pub fn discover( pub fn discover(
@ -45,12 +23,12 @@ mod export {
redirect_url: &str, redirect_url: &str,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
let open_id = OpenIdAuthenticator::discover(&config, redirect_url)?; let open_id = OpenIdAuthenticator::discover(&config, redirect_url)?;
bless( Ok(perlmod::instantiate_magic!(
class, &class,
Box::new(OpenId { MAGIC => Box::new(OpenId {
inner: Mutex::new(open_id), inner: Mutex::new(open_id),
}), })
) ))
} }
#[export] #[export]

View File

@ -21,11 +21,13 @@ use nix::sys::stat::Mode;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
pub(self) use proxmox_tfa::api::{ pub(self) use proxmox_tfa::api::{
RecoveryState, TfaChallenge, TfaConfig, TfaResponse, TfaUserData, U2fConfig, WebauthnConfig, RecoveryState, TfaChallenge, TfaConfig, TfaResponse, TfaUserData, U2fConfig,
UserChallengeAccess, WebauthnConfig,
}; };
#[perlmod::package(name = "PVE::RS::TFA")] #[perlmod::package(name = "PVE::RS::TFA")]
mod export { mod export {
use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
use std::sync::Mutex; use std::sync::Mutex;
@ -34,7 +36,7 @@ mod export {
use url::Url; use url::Url;
use perlmod::Value; use perlmod::Value;
use proxmox_tfa::api::methods; use proxmox_tfa::api::{methods, TfaResult};
use super::{TfaConfig, UserAccess}; use super::{TfaConfig, UserAccess};
@ -173,7 +175,7 @@ mod export {
) -> Result<String, Error> { ) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_challenge(UserAccess::new(&raw_this)?, userid, description) inner.u2f_registration_challenge(&UserAccess::new(&raw_this)?, userid, description)
} }
/// Finish a u2f registration. This updates temporary data in `/run` and therefore the config /// Finish a u2f registration. This updates temporary data in `/run` and therefore the config
@ -188,7 +190,7 @@ mod export {
) -> Result<String, Error> { ) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_finish(UserAccess::new(&raw_this)?, userid, challenge, response) inner.u2f_registration_finish(&UserAccess::new(&raw_this)?, userid, challenge, response)
} }
/// Check if a user has any TFA entries of a given type. /// Check if a user has any TFA entries of a given type.
@ -249,7 +251,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
match inner.authentication_challenge( match inner.authentication_challenge(
UserAccess::new(&raw_this)?, &UserAccess::new(&raw_this)?,
userid, userid,
origin.as_ref(), origin.as_ref(),
)? { )? {
@ -266,10 +268,7 @@ mod export {
.unwrap() .unwrap()
.users .users
.get(userid) .get(userid)
.and_then(|user| { .and_then(|user| user.recovery_state())
let state = user.recovery_state();
state.is_available().then(move || state)
})
} }
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against /// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
@ -277,6 +276,8 @@ mod export {
/// ///
/// NOTE: This returns a boolean whether the config data needs to be *saved* after this call /// NOTE: This returns a boolean whether the config data needs to be *saved* after this call
/// (to use up recovery keys!). /// (to use up recovery keys!).
///
/// WARNING: This method is now deprecated, as it failures were communicated via croaking.
#[export] #[export]
fn authentication_verify( fn authentication_verify(
#[raw] raw_this: Value, #[raw] raw_this: Value,
@ -290,15 +291,78 @@ mod export {
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?; let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?; let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap(); let mut inner = this.inner.lock().unwrap();
inner let result = inner.verify(
.verify( &UserAccess::new(&raw_this)?,
UserAccess::new(&raw_this)?, userid,
userid, &challenge,
&challenge, response,
response, origin.as_ref(),
origin.as_ref(), );
) match result {
.map(|save| save.needs_saving()) TfaResult::Success { needs_saving } => Ok(needs_saving),
_ => bail!("TFA authentication failed"),
}
}
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
/// it.
///
/// Returns a result hash of the form:
/// ```text
/// {
/// "result": bool, // whether TFA was successful
/// "needs-saving": bool, // whether the user config needs saving
/// "tfa-limit-reached": bool, // whether the TFA limit was reached (config needs saving)
/// "totp-limit-reached": bool, // whether the TOTP limit was reached (config needs saving)
/// }
/// ```
#[export]
fn authentication_verify2(
#[raw] raw_this: Value,
//#[try_from_ref] this: &Tfa,
userid: &str,
challenge: &str, //super::TfaChallenge,
response: &str,
origin: Option<Url>,
) -> Result<TfaReturnValue, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap();
let result = inner.verify(
&UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
);
Ok(match result {
TfaResult::Success { needs_saving } => TfaReturnValue {
result: true,
needs_saving,
..Default::default()
},
TfaResult::Locked => TfaReturnValue::default(),
TfaResult::Failure {
needs_saving,
totp_limit_reached,
tfa_limit_reached,
} => TfaReturnValue {
result: false,
needs_saving,
totp_limit_reached,
tfa_limit_reached,
},
})
}
#[derive(Default, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaReturnValue {
result: bool,
needs_saving: bool,
totp_limit_reached: bool,
tfa_limit_reached: bool,
} }
/// DEBUG HELPER: Get the current TOTP value for a given TOTP URI. /// DEBUG HELPER: Get the current TOTP value for a given TOTP URI.
@ -360,7 +424,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?; let this: &Tfa = (&raw_this).try_into()?;
methods::add_tfa_entry( methods::add_tfa_entry(
&mut this.inner.lock().unwrap(), &mut this.inner.lock().unwrap(),
UserAccess::new(&raw_this)?, &UserAccess::new(&raw_this)?,
userid, userid,
description, description,
totp, totp,
@ -421,6 +485,66 @@ mod export {
Err(methods::EntryNotFound) => bail!("no such entry"), Err(methods::EntryNotFound) => bail!("no such entry"),
} }
} }
#[export]
fn api_unlock_tfa(#[raw] raw_this: Value, userid: &str) -> Result<bool, Error> {
let this: &Tfa = (&raw_this).try_into()?;
Ok(methods::unlock_and_reset_tfa(
&mut this.inner.lock().unwrap(),
&UserAccess::new(&raw_this)?,
userid,
)?)
}
#[derive(serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaLockStatus {
/// Once a user runs into a TOTP limit they get locked out of TOTP until they successfully use
/// a recovery key.
#[serde(skip_serializing_if = "bool_is_false", default)]
totp_locked: bool,
/// If a user hits too many 2nd factor failures, they get completely blocked for a while.
#[serde(skip_serializing_if = "Option::is_none", default)]
#[serde(deserialize_with = "filter_expired_timestamp")]
tfa_locked_until: Option<i64>,
}
impl From<&proxmox_tfa::api::TfaUserData> for TfaLockStatus {
fn from(data: &proxmox_tfa::api::TfaUserData) -> Self {
Self {
totp_locked: data.totp_locked,
tfa_locked_until: data.tfa_locked_until,
}
}
}
fn bool_is_false(b: &bool) -> bool {
!*b
}
#[export]
fn tfa_lock_status(
#[try_from_ref] this: &Tfa,
userid: Option<&str>,
) -> Result<Option<perlmod::Value>, Error> {
let this = this.inner.lock().unwrap();
if let Some(userid) = userid {
if let Some(user) = this.users.get(userid) {
Ok(Some(perlmod::to_value(&TfaLockStatus::from(user))?))
} else {
Ok(None)
}
} else {
Ok(Some(perlmod::to_value(
&HashMap::<String, TfaLockStatus>::from_iter(
this.users
.iter()
.map(|(uid, data)| (uid.clone(), TfaLockStatus::from(data))),
),
)?))
}
}
} }
/// Version 1 format of `/etc/pve/priv/tfa.cfg` /// Version 1 format of `/etc/pve/priv/tfa.cfg`
@ -514,6 +638,7 @@ fn decode_old_entry(ty: &[u8], data: &[u8], user: &str) -> Result<TfaUserData, E
b"oath" => user_data.totp.extend( b"oath" => user_data.totp.extend(
decode_old_oath_entry(value, user)? decode_old_oath_entry(value, user)?
.into_iter() .into_iter()
.map(proxmox_tfa::api::TotpEntry::new)
.map(move |entry| proxmox_tfa::api::TfaEntry::from_parts(info.clone(), entry)), .map(move |entry| proxmox_tfa::api::TfaEntry::from_parts(info.clone(), entry)),
), ),
b"yubico" => user_data.yubico.extend( b"yubico" => user_data.yubico.extend(
@ -841,9 +966,7 @@ fn challenge_data_path(userid: &str, debug: bool) -> PathBuf {
} }
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess { impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
type Data = UserChallengeData; fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error> {
fn open(&self, userid: &str) -> Result<UserChallengeData, Error> {
if self.is_debug() { if self.is_debug() {
mkdir("./local-tfa-challenges", 0o700)?; mkdir("./local-tfa-challenges", 0o700)?;
} else { } else {
@ -886,15 +1009,15 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
} }
}; };
Ok(UserChallengeData { Ok(Box::new(UserChallengeData {
inner, inner,
path, path,
lock: file, lock: file,
}) }))
} }
/// `open` without creating the file if it doesn't exist, to finish WA authentications. /// `open` without creating the file if it doesn't exist, to finish WA authentications.
fn open_no_create(&self, userid: &str) -> Result<Option<UserChallengeData>, Error> { fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error> {
let path = challenge_data_path(userid, self.is_debug()); let path = challenge_data_path(userid, self.is_debug());
let mut file = match std::fs::OpenOptions::new() let mut file = match std::fs::OpenOptions::new()
@ -915,11 +1038,11 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
format_err!("failed to read challenge data for user {}: {}", userid, err) format_err!("failed to read challenge data for user {}: {}", userid, err)
})?; })?;
Ok(Some(UserChallengeData { Ok(Some(Box::new(UserChallengeData {
inner, inner,
path, path,
lock: file, lock: file,
})) })))
} }
fn remove(&self, userid: &str) -> Result<bool, Error> { fn remove(&self, userid: &str) -> Result<bool, Error> {
@ -930,6 +1053,10 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
} }
} }
fn enable_lockout(&self) -> bool {
true
}
} }
/// Container of `TfaUserChallenges` with the corresponding file lock guard. /// Container of `TfaUserChallenges` with the corresponding file lock guard.
@ -947,7 +1074,7 @@ impl proxmox_tfa::api::UserChallengeAccess for UserChallengeData {
&mut self.inner &mut self.inner
} }
fn save(self) -> Result<(), Error> { fn save(&mut self) -> Result<(), Error> {
UserChallengeData::save(self) UserChallengeData::save(self)
} }
} }
@ -992,7 +1119,7 @@ impl UserChallengeData {
/// ///
/// This currently consumes selfe as we never perform more than 1 insertion/removal, and this /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
/// way also unlocks early. /// way also unlocks early.
fn save(mut self) -> Result<(), Error> { fn save(&mut self) -> Result<(), Error> {
self.rewind()?; self.rewind()?;
serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| { serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {

View File

@ -1,4 +1,9 @@
.PHONY: test .PHONY: test
test: test: Proxmox/Lib/PVE.pm
@echo "-- running pve-rs tests --" @echo "-- running pve-rs tests --"
./resource_scheduling.pl perl -I. -I.. -I../.. ./resource_scheduling.pl
# The test stub, we don't know where to look for the library from in here!
Proxmox/Lib/PVE.pm:
@echo "run 'make' in the pve-rs/ dir first"
@exit 1

View File

@ -5,35 +5,73 @@ use warnings;
use Test::More; use Test::More;
# FIXME ensure that the just built library is loaded rather than the installed one and add a test
# target to pve-rs/Makefile afterwards. Issue is that the loader looks into an $PATH/auto directory,
# so it's not enough to use lib qw(../target/release)
# Also might be a good idea to test for existence of the files to avoid surprises if the directory
# structure changes in the future.
#use lib qw(..);
#use lib qw(../target/release);
use PVE::RS::ResourceScheduling::Static; use PVE::RS::ResourceScheduling::Static;
my $static = PVE::RS::ResourceScheduling::Static->new(); sub test_basic {
is(scalar($static->list_nodes()->@*), 0, 'node list empty'); my $static = PVE::RS::ResourceScheduling::Static->new();
$static->add_node("A", 10, 100_000_000_000); is(scalar($static->list_nodes()->@*), 0, 'node list empty');
is(scalar($static->list_nodes()->@*), 1, '1 node added'); $static->add_node("A", 10, 100_000_000_000);
$static->add_node("B", 20, 200_000_000_000); is(scalar($static->list_nodes()->@*), 1, '1 node added');
is(scalar($static->list_nodes()->@*), 2, '2nd node'); $static->add_node("B", 20, 200_000_000_000);
$static->add_node("C", 30, 300_000_000_000); is(scalar($static->list_nodes()->@*), 2, '2nd node');
is(scalar($static->list_nodes()->@*), 3, '3rd node'); $static->add_node("C", 30, 300_000_000_000);
$static->remove_node("C"); is(scalar($static->list_nodes()->@*), 3, '3rd node');
is(scalar($static->list_nodes()->@*), 2, '3rd removed should be 2'); $static->remove_node("C");
ok($static->contains_node("A"), 'should contain a node A'); is(scalar($static->list_nodes()->@*), 2, '3rd removed should be 2');
ok($static->contains_node("B"), 'should contain a node B'); ok($static->contains_node("A"), 'should contain a node A');
ok(!$static->contains_node("C"), 'should not contain a node C'); ok($static->contains_node("B"), 'should contain a node B');
ok(!$static->contains_node("C"), 'should not contain a node C');
}
my $service = { sub test_balance {
maxcpu => 4, my $static = PVE::RS::ResourceScheduling::Static->new();
maxmem => 20_000_000_000, $static->add_node("A", 10, 100_000_000_000);
}; $static->add_node("B", 20, 200_000_000_000);
my $service = {
maxcpu => 4,
maxmem => 20_000_000_000,
};
for (my $i = 0; $i < 15; $i++) {
my $score_list = $static->score_nodes_to_start_service($service);
# imitate HA manager
my $scores = { map { $_->[0] => -$_->[1] } $score_list->@* };
my @nodes = sort {
$scores->{$a} <=> $scores->{$b} || $a cmp $b
} keys $scores->%*;
if ($i % 3 == 2) {
is($nodes[0], "A", 'first should be A');
is($nodes[1], "B", 'second should be A');
} else {
is($nodes[0], "B", 'first should be B');
is($nodes[1], "A", 'second should be A');
}
$static->add_service_usage_to_node($nodes[0], $service);
}
}
sub test_overcommitted {
my $static = PVE::RS::ResourceScheduling::Static->new();
$static->add_node("A", 4, 4_102_062_080);
$static->add_node("B", 4, 4_102_062_080);
$static->add_node("C", 4, 4_102_053_888);
$static->add_node("D", 4, 4_102_053_888);
my $service = {
maxcpu => 1,
maxmem => 536_870_912,
};
$static->add_service_usage_to_node("A", $service);
$static->add_service_usage_to_node("A", $service);
$static->add_service_usage_to_node("A", $service);
$static->add_service_usage_to_node("B", $service);
$static->add_service_usage_to_node("A", $service);
for (my $i = 0; $i < 15; $i++) {
my $score_list = $static->score_nodes_to_start_service($service); my $score_list = $static->score_nodes_to_start_service($service);
# imitate HA manager # imitate HA manager
@ -42,15 +80,65 @@ for (my $i = 0; $i < 15; $i++) {
$scores->{$a} <=> $scores->{$b} || $a cmp $b $scores->{$a} <=> $scores->{$b} || $a cmp $b
} keys $scores->%*; } keys $scores->%*;
if ($i % 3 == 2) { is($nodes[0], "C", 'first should be C');
is($nodes[0], "A", 'first should be A'); is($nodes[1], "D", 'second should be D');
is($nodes[1], "B", 'second should be A'); is($nodes[2], "B", 'third should be B');
} else { is($nodes[3], "A", 'fourth should be A');
is($nodes[0], "B", 'first should be B');
is($nodes[1], "A", 'second should be A');
}
$static->add_service_usage_to_node($nodes[0], $service);
} }
sub test_balance_small_memory_difference {
my ($with_start_load) = @_;
my $static = PVE::RS::ResourceScheduling::Static->new();
# Memory is different to avoid flaky results with what would otherwise be ties.
$static->add_node("A", 8, 10_000_000_000);
$static->add_node("B", 4, 9_000_000_000);
$static->add_node("C", 4, 8_000_000_000);
if ($with_start_load) {
$static->add_service_usage_to_node("A", { maxcpu => 4, maxmem => 1_000_000_000 });
$static->add_service_usage_to_node("B", { maxcpu => 2, maxmem => 1_000_000_000 });
$static->add_service_usage_to_node("C", { maxcpu => 2, maxmem => 1_000_000_000 });
}
my $service = {
maxcpu => 3,
maxmem => 16_000_000,
};
for (my $i = 0; $i < 20; $i++) {
my $score_list = $static->score_nodes_to_start_service($service);
# imitate HA manager
my $scores = { map { $_->[0] => -$_->[1] } $score_list->@* };
my @nodes = sort {
$scores->{$a} <=> $scores->{$b} || $a cmp $b
} keys $scores->%*;
if ($i % 4 <= 1) {
is($nodes[0], "A", 'first should be A');
is($nodes[1], "B", 'second should be B');
is($nodes[2], "C", 'third should be C');
} elsif ($i % 4 == 2) {
is($nodes[0], "B", 'first should be B');
is($nodes[1], "C", 'second should be C');
is($nodes[2], "A", 'third should be A');
} elsif ($i % 4 == 3) {
is($nodes[0], "C", 'first should be C');
is($nodes[1], "A", 'second should be A');
is($nodes[2], "B", 'third should be B');
} else {
die "internal error, got $i % 4 == " . ($i % 4) . "\n";
}
$static->add_service_usage_to_node($nodes[0], $service);
}
}
test_basic();
test_balance();
test_overcommitted();
test_balance_small_memory_difference(1);
test_balance_small_memory_difference(0);
done_testing(); done_testing();

View File

@ -1,32 +0,0 @@
#!/usr/bin/env perl
# Create a perl package given a product and package name.
use strict;
use warnings;
use File::Path qw(make_path);
my $product = shift @ARGV or die "missing product name (PVE, PMG or Common)\n";
die "missing package name\n" if !@ARGV;
for my $package (@ARGV) {
my $path = ($package =~ s@::@/@gr) . ".pm";
print "Generating $path\n";
$path =~ m@^(.*)/[^/]+@;
make_path($1, { mode => 0755 });
open(my $fh, '>', $path) or die "failed to open '$path' for writing: $!\n";
print {$fh} <<"EOF";
package $package;
use base 'Proxmox::Lib::$product';
BEGIN { __PACKAGE__->bootstrap(); }
1;
EOF
close($fh);
}