package PVE::Auth::Plugin; use strict; use warnings; use Encode; use Digest::SHA; use PVE::Tools; use PVE::SectionConfig; use PVE::JSONSchema qw(get_standard_option); use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file); use Data::Dumper; use base qw(PVE::SectionConfig); my $domainconfigfile = "domains.cfg"; cfs_register_file($domainconfigfile, sub { __PACKAGE__->parse_config(@_); }, sub { __PACKAGE__->write_config(@_); }); sub lock_domain_config { my ($code, $errmsg) = @_; cfs_lock_file($domainconfigfile, undef, $code); my $err = $@; if ($err) { $errmsg ? die "$errmsg: $err" : die $err; } } my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/; PVE::JSONSchema::register_format('pve-realm', \&pve_verify_realm); sub pve_verify_realm { my ($realm, $noerr) = @_; if ($realm !~ m/^${realm_regex}$/) { return undef if $noerr; die "value does not look like a valid realm\n"; } return $realm; } PVE::JSONSchema::register_standard_option('realm', { description => "Authentication domain ID", type => 'string', format => 'pve-realm', maxLength => 32, }); PVE::JSONSchema::register_format('pve-userid', \&verify_username); sub verify_username { my ($username, $noerr) = @_; $username = '' if !$username; my $len = length($username); if ($len < 3) { die "user name '$username' is too short\n" if !$noerr; return undef; } if ($len > 64) { die "user name '$username' is too long ($len > 64)\n" if !$noerr; return undef; } # we only allow a limited set of characters # colon is not allowed, because we store usernames in # colon separated lists)! # slash is not allowed because it is used as pve API delimiter # also see "man useradd" if ($username =~ m!^([^\s:/]+)\@(${realm_regex})$!) { return wantarray ? ($username, $1, $2) : $username; } die "value '$username' does not look like a valid user name\n" if !$noerr; return undef; } PVE::JSONSchema::register_standard_option('userid', { description => "User ID", type => 'string', format => 'pve-userid', maxLength => 64, }); PVE::JSONSchema::register_format('pve-tfa-config', \&verify_tfa_config); sub verify_tfa_config { my ($value, $noerr) = @_; return $value if parse_tfa_config($value); return undef if $noerr; die "unable to parse tfa option\n"; } PVE::JSONSchema::register_standard_option('tfa', { description => "Use Two-factor authentication.", type => 'string', format => 'pve-tfa-config', optional => 1, maxLength => 128, }); sub parse_tfa_config { my ($data) = @_; my $res = {}; foreach my $kvp (split(/,/, $data)) { if ($kvp =~ m/^type=(yubico|oath)$/) { $res->{type} = $1; } elsif ($kvp =~ m/^id=(\S+)$/) { $res->{id} = $1; } elsif ($kvp =~ m/^key=(\S+)$/) { $res->{key} = $1; } elsif ($kvp =~ m/^url=(\S+)$/) { $res->{url} = $1; } elsif ($kvp =~ m/^digits=([6|7|8])$/) { $res->{digits} = $1; } elsif ($kvp =~ m/^step=([1-9]\d+)$/) { $res->{step} = $1; } else { return undef; } } return undef if !$res->{type}; return $res; } my $salt_starter = time(); sub encrypt_pw { my ($pw) = @_; $salt_starter++; my $salt = substr(Digest::SHA::sha1_base64(time() + $salt_starter + $$), 0, 8); # crypt does not want '+' in salt (see 'man crypt') $salt =~ s/\+/X/g; return crypt(encode("utf8", $pw), "\$5\$$salt\$"); } my $defaultData = { propertyList => { type => { description => "Realm type." }, realm => get_standard_option('realm'), }, }; sub private { return $defaultData; } sub parse_section_header { my ($class, $line) = @_; if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { my ($type, $realm) = (lc($1), $2); my $errmsg = undef; # set if you want to skip whole section eval { pve_verify_realm($realm); }; $errmsg = $@ if $@; my $config = {}; # to return additional attributes return ($type, $realm, $errmsg, $config); } return undef; } sub parse_config { my ($class, $filename, $raw) = @_; my $cfg = $class->SUPER::parse_config($filename, $raw); my $default; foreach my $realm (keys %{$cfg->{ids}}) { my $data = $cfg->{ids}->{$realm}; # make sure there is only one default marker if ($data->{default}) { if ($default) { delete $data->{default}; } else { $default = $realm; } } if ($data->{comment}) { $data->{comment} = PVE::Tools::decode_text($data->{comment}); } } # add default domains $cfg->{ids}->{pve}->{type} = 'pve'; # force type $cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server" if !$cfg->{ids}->{pve}->{comment}; $cfg->{ids}->{pam}->{type} = 'pam'; # force type $cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM'; $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication" if !$cfg->{ids}->{pam}->{comment}; return $cfg; }; sub write_config { my ($class, $filename, $cfg) = @_; foreach my $realm (keys %{$cfg->{ids}}) { my $data = $cfg->{ids}->{$realm}; if ($data->{comment}) { $data->{comment} = PVE::Tools::encode_text($data->{comment}); } } $class->SUPER::write_config($filename, $cfg); } sub authenticate_user { my ($class, $config, $realm, $username, $password) = @_; die "overwrite me"; } sub store_password { my ($class, $config, $realm, $username, $password) = @_; my $type = $class->type(); die "can't set password on auth type '$type'\n"; } sub delete_user { my ($class, $config, $realm, $username) = @_; # do nothing by default } 1;