Upstream proxmox-git/pve-access-control state of the touched files before 726b26a750
This commit is contained in:
1 parent
7676330a5c
commit
c520ce3ec1
3 files changed
+1060
No files matched your search
@@ -0,0 +1,363 @@
|
||||
package PVE::API2::OpenId;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::Tools qw(extract_param);
|
||||
use PVE::RS::OpenId;
|
||||
|
||||
use PVE::Exception qw(raise raise_perm_exc raise_param_exc);
|
||||
use PVE::SafeSyslog;
|
||||
use PVE::RPCEnvironment;
|
||||
use PVE::Cluster qw(cfs_read_file cfs_write_file);
|
||||
use PVE::AccessControl;
|
||||
use PVE::JSONSchema qw(get_standard_option);
|
||||
use PVE::Auth::Plugin;
|
||||
use PVE::Auth::OpenId;
|
||||
|
||||
use PVE::RESTHandler;
|
||||
|
||||
use base qw(PVE::RESTHandler);
|
||||
|
||||
my $openid_state_path = "/var/lib/pve-manager";
|
||||
|
||||
my $lookup_openid_auth = sub {
|
||||
my ($realm, $redirect_url) = @_;
|
||||
|
||||
my $cfg = cfs_read_file('domains.cfg');
|
||||
my $ids = $cfg->{ids};
|
||||
|
||||
die "authentication domain '$realm' does not exist\n" if !$ids->{$realm};
|
||||
|
||||
my $config = $ids->{$realm};
|
||||
die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid";
|
||||
|
||||
my $openid_config = {
|
||||
issuer_url => $config->{'issuer-url'},
|
||||
client_id => $config->{'client-id'},
|
||||
client_key => $config->{'client-key'},
|
||||
};
|
||||
$openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'});
|
||||
|
||||
my $scopes = $config->{'scopes'} // 'email profile';
|
||||
$openid_config->{scopes} = [PVE::Tools::split_list($scopes)];
|
||||
|
||||
if (defined(my $acr = $config->{'acr-values'})) {
|
||||
$openid_config->{acr_values} = [PVE::Tools::split_list($acr)];
|
||||
}
|
||||
|
||||
if (defined(my $audiences = $config->{'audiences'})) {
|
||||
$openid_config->{audiences} = [PVE::Tools::split_list($audiences)];
|
||||
}
|
||||
|
||||
my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url);
|
||||
return ($config, $openid);
|
||||
};
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'index',
|
||||
path => '',
|
||||
method => 'GET',
|
||||
description => "Directory index.",
|
||||
permissions => {
|
||||
user => 'all',
|
||||
},
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {},
|
||||
},
|
||||
returns => {
|
||||
type => 'array',
|
||||
items => {
|
||||
type => "object",
|
||||
properties => {
|
||||
subdir => { type => 'string' },
|
||||
},
|
||||
},
|
||||
links => [{ rel => 'child', href => "{subdir}" }],
|
||||
},
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
return [
|
||||
{ subdir => 'auth-url' }, { subdir => 'login' },
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'auth_url',
|
||||
path => 'auth-url',
|
||||
method => 'POST',
|
||||
protected => 1,
|
||||
description => "Get the OpenId Authorization Url for the specified realm.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
realm => get_standard_option('realm'),
|
||||
'redirect-url' => {
|
||||
description =>
|
||||
"Redirection Url. The client should set this to the used server url (location.origin).",
|
||||
type => 'string',
|
||||
maxLength => 255,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
type => "string",
|
||||
description => "Redirection URL.",
|
||||
},
|
||||
permissions => { user => 'world' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
|
||||
|
||||
my $realm = extract_param($param, 'realm');
|
||||
my $redirect_url = extract_param($param, 'redirect-url');
|
||||
|
||||
my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
|
||||
my $url = $openid->authorize_url($openid_state_path, $realm);
|
||||
|
||||
return $url;
|
||||
},
|
||||
});
|
||||
|
||||
__PACKAGE__->register_method({
|
||||
name => 'login',
|
||||
path => 'login',
|
||||
method => 'POST',
|
||||
protected => 1,
|
||||
description => " Verify OpenID authorization code and create a ticket.",
|
||||
parameters => {
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
'state' => {
|
||||
description => "OpenId state.",
|
||||
type => 'string',
|
||||
maxLength => 1024,
|
||||
},
|
||||
code => {
|
||||
description => "OpenId authorization code.",
|
||||
type => 'string',
|
||||
maxLength => 4096,
|
||||
},
|
||||
'redirect-url' => {
|
||||
description =>
|
||||
"Redirection Url. The client should set this to the used server url (location.origin).",
|
||||
type => 'string',
|
||||
maxLength => 255,
|
||||
},
|
||||
},
|
||||
},
|
||||
returns => {
|
||||
properties => {
|
||||
username => { type => 'string' },
|
||||
ticket => { type => 'string' },
|
||||
CSRFPreventionToken => { type => 'string' },
|
||||
cap => { type => 'object' }, # computed api permissions
|
||||
clustername => { type => 'string', optional => 1 },
|
||||
},
|
||||
},
|
||||
permissions => { user => 'world' },
|
||||
code => sub {
|
||||
my ($param) = @_;
|
||||
|
||||
my $rpcenv = PVE::RPCEnvironment::get();
|
||||
|
||||
my $res;
|
||||
eval {
|
||||
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
|
||||
local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy};
|
||||
|
||||
my ($realm, $private_auth_state) =
|
||||
PVE::RS::OpenId::verify_public_auth_state($openid_state_path, $param->{'state'});
|
||||
|
||||
my $redirect_url = extract_param($param, 'redirect-url');
|
||||
|
||||
my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url);
|
||||
|
||||
my $info = $openid->verify_authorization_code(
|
||||
$param->{code},
|
||||
$private_auth_state,
|
||||
$config->{'query-userinfo'} // 1,
|
||||
);
|
||||
my $subject = $info->{'sub'};
|
||||
|
||||
my $unique_name;
|
||||
|
||||
my $user_attr = $config->{'username-claim'} // 'sub';
|
||||
if (defined($info->{$user_attr})) {
|
||||
$unique_name = $info->{$user_attr};
|
||||
} elsif ($user_attr eq 'subject') { # stay compat with old versions
|
||||
$unique_name = $subject;
|
||||
} elsif ($user_attr eq 'username') { # stay compat with old versions
|
||||
my $username = $info->{'preferred_username'};
|
||||
die "missing claim 'preferred_username'\n" if !defined($username);
|
||||
$unique_name = $username;
|
||||
} else {
|
||||
# neither the attr nor fallback are defined in info..
|
||||
die "missing configured claim '$user_attr' in returned info object\n";
|
||||
}
|
||||
|
||||
my $username = "${unique_name}\@${realm}";
|
||||
|
||||
# first, check if $username respects our naming conventions
|
||||
PVE::Auth::Plugin::verify_username($username);
|
||||
|
||||
if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
|
||||
die "user '$username' already exists\n"
|
||||
if $usercfg->{users}->{$username};
|
||||
|
||||
my $entry = { enable => 1 };
|
||||
if (defined(my $email = $info->{'email'})) {
|
||||
$entry->{email} = $email;
|
||||
}
|
||||
if (defined(my $given_name = $info->{'given_name'})) {
|
||||
$entry->{firstname} = $given_name;
|
||||
}
|
||||
if (defined(my $family_name = $info->{'family_name'})) {
|
||||
$entry->{lastname} = $family_name;
|
||||
}
|
||||
|
||||
$usercfg->{users}->{$username} = $entry;
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"autocreate openid user failed",
|
||||
);
|
||||
} else {
|
||||
# test if user exists and is enabled
|
||||
$rpcenv->check_user_enabled($username);
|
||||
}
|
||||
|
||||
if (defined(my $groups_claim = $config->{'groups-claim'})) {
|
||||
if (defined(my $groups_list = $info->{$groups_claim})) {
|
||||
if (ref($groups_list) eq 'ARRAY') {
|
||||
PVE::AccessControl::lock_user_config(
|
||||
sub {
|
||||
my $usercfg = cfs_read_file("user.cfg");
|
||||
|
||||
my $oidc_groups;
|
||||
for my $group (@$groups_list) {
|
||||
if (PVE::AccessControl::verify_groupname($group, 1)) {
|
||||
# add realm name as suffix to group
|
||||
$oidc_groups->{"$group-$realm"} = 1;
|
||||
} else {
|
||||
# ignore any groups in the list that have invalid characters
|
||||
syslog(
|
||||
'warn',
|
||||
"openid group '$group' contains invalid characters",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# get groups that exist in OIDC and PVE
|
||||
my $groups_intersect;
|
||||
for my $group (keys %$oidc_groups) {
|
||||
$groups_intersect->{$group} = 1
|
||||
if $usercfg->{groups}->{$group};
|
||||
}
|
||||
|
||||
if ($config->{'groups-autocreate'}) {
|
||||
# populate all groups in claim
|
||||
$groups_intersect = $oidc_groups;
|
||||
my $groups_to_create;
|
||||
for my $group (keys %$oidc_groups) {
|
||||
$groups_to_create->{$group} = 1
|
||||
if !$usercfg->{groups}->{$group};
|
||||
}
|
||||
if ($groups_to_create) {
|
||||
# log a messages about created groups here
|
||||
my $groups_to_create_string =
|
||||
join(', ', sort keys %$groups_to_create);
|
||||
syslog(
|
||||
'info',
|
||||
"groups created automatically from openid claim: $groups_to_create_string",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# if groups should be overwritten, delete all the users groups first
|
||||
if ($config->{'groups-overwrite'}) {
|
||||
PVE::AccessControl::delete_user_group(
|
||||
$username, $usercfg,
|
||||
);
|
||||
syslog(
|
||||
'info',
|
||||
"openid overwrite groups enabled; user '$username' removed from all groups",
|
||||
);
|
||||
}
|
||||
|
||||
if (keys %$groups_intersect) {
|
||||
# ensure user is a member of the groups
|
||||
for my $group (keys %$groups_intersect) {
|
||||
PVE::AccessControl::add_user_group(
|
||||
$username,
|
||||
$usercfg,
|
||||
$group,
|
||||
);
|
||||
}
|
||||
|
||||
my $groups_intersect_string =
|
||||
join(', ', sort keys %$groups_intersect);
|
||||
syslog(
|
||||
'info',
|
||||
"openid user '$username' added to groups: $groups_intersect_string",
|
||||
);
|
||||
}
|
||||
|
||||
cfs_write_file("user.cfg", $usercfg);
|
||||
},
|
||||
"openid group mapping failed",
|
||||
);
|
||||
} else {
|
||||
syslog(
|
||||
'err',
|
||||
"openid groups list is not an array; groups will not be updated",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
syslog('err', "openid groups claim '$groups_claim' is not found in claims");
|
||||
}
|
||||
}
|
||||
|
||||
my $ticket = PVE::AccessControl::assemble_ticket($username);
|
||||
my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username);
|
||||
my $cap = $rpcenv->compute_api_permission($username);
|
||||
|
||||
$res = {
|
||||
ticket => $ticket,
|
||||
username => $username,
|
||||
CSRFPreventionToken => $csrftoken,
|
||||
cap => $cap,
|
||||
};
|
||||
|
||||
my $clinfo = PVE::Cluster::get_clinfo();
|
||||
if (
|
||||
$clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)
|
||||
) {
|
||||
$res->{clustername} = $clinfo->{cluster}->{name};
|
||||
}
|
||||
};
|
||||
if (my $err = $@) {
|
||||
my $clientip = $rpcenv->get_client_ip() || '';
|
||||
syslog('err', "openid authentication failure; rhost=$clientip msg=$err");
|
||||
# do not return any info to prevent user enumeration attacks
|
||||
die PVE::Exception->new("authentication failure\n", code => 401);
|
||||
}
|
||||
|
||||
PVE::Cluster::log_msg(
|
||||
'info',
|
||||
'root@pam',
|
||||
"successful openid auth for user '$res->{username}'",
|
||||
);
|
||||
|
||||
return $res;
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user