mirror of
https://git.proxmox.com/git/pve-access-control
synced 2025-10-05 20:32:15 +00:00
api: implement openid API
This moves compute_api_permission() into RPCEnvironment.pm. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
parent
f0c9ef167b
commit
ac344d7df3
@ -19,9 +19,9 @@ use PVE::API2::User;
|
|||||||
use PVE::API2::Group;
|
use PVE::API2::Group;
|
||||||
use PVE::API2::Role;
|
use PVE::API2::Role;
|
||||||
use PVE::API2::ACL;
|
use PVE::API2::ACL;
|
||||||
|
use PVE::API2::OpenId;
|
||||||
use PVE::Auth::Plugin;
|
use PVE::Auth::Plugin;
|
||||||
use PVE::OTP;
|
use PVE::OTP;
|
||||||
use PVE::Tools;
|
|
||||||
|
|
||||||
my $u2f_available = 0;
|
my $u2f_available = 0;
|
||||||
eval {
|
eval {
|
||||||
@ -56,6 +56,11 @@ __PACKAGE__->register_method ({
|
|||||||
path => 'domains',
|
path => 'domains',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
__PACKAGE__->register_method ({
|
||||||
|
subclass => "PVE::API2::OpenId",
|
||||||
|
path => 'openid',
|
||||||
|
});
|
||||||
|
|
||||||
__PACKAGE__->register_method ({
|
__PACKAGE__->register_method ({
|
||||||
name => 'index',
|
name => 'index',
|
||||||
path => '',
|
path => '',
|
||||||
@ -165,55 +170,6 @@ my $create_ticket = sub {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
my $compute_api_permission = sub {
|
|
||||||
my ($rpcenv, $authuser) = @_;
|
|
||||||
|
|
||||||
my $usercfg = $rpcenv->{user_cfg};
|
|
||||||
|
|
||||||
my $res = {};
|
|
||||||
my $priv_re_map = {
|
|
||||||
vms => qr/VM\.|Permissions\.Modify/,
|
|
||||||
access => qr/(User|Group)\.|Permissions\.Modify/,
|
|
||||||
storage => qr/Datastore\.|Permissions\.Modify/,
|
|
||||||
nodes => qr/Sys\.|Permissions\.Modify/,
|
|
||||||
sdn => qr/SDN\.|Permissions\.Modify/,
|
|
||||||
dc => qr/Sys\.Audit|SDN\./,
|
|
||||||
};
|
|
||||||
map { $res->{$_} = {} } keys %$priv_re_map;
|
|
||||||
|
|
||||||
my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn'];
|
|
||||||
|
|
||||||
my $checked_paths = {};
|
|
||||||
foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) {
|
|
||||||
next if $checked_paths->{$path};
|
|
||||||
$checked_paths->{$path} = 1;
|
|
||||||
|
|
||||||
my $path_perm = $rpcenv->permissions($authuser, $path);
|
|
||||||
|
|
||||||
my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
|
|
||||||
if ($toplevel eq 'pool') {
|
|
||||||
foreach my $priv (keys %$path_perm) {
|
|
||||||
if ($priv =~ m/^VM\./) {
|
|
||||||
$res->{vms}->{$priv} = 1;
|
|
||||||
} elsif ($priv =~ m/^Datastore\./) {
|
|
||||||
$res->{storage}->{$priv} = 1;
|
|
||||||
} elsif ($priv eq 'Permissions.Modify') {
|
|
||||||
$res->{storage}->{$priv} = 1;
|
|
||||||
$res->{vms}->{$priv} = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
my $priv_regex = $priv_re_map->{$toplevel} // next;
|
|
||||||
foreach my $priv (keys %$path_perm) {
|
|
||||||
next if $priv !~ m/^($priv_regex)/;
|
|
||||||
$res->{$toplevel}->{$priv} = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $res;
|
|
||||||
};
|
|
||||||
|
|
||||||
__PACKAGE__->register_method ({
|
__PACKAGE__->register_method ({
|
||||||
name => 'get_ticket',
|
name => 'get_ticket',
|
||||||
path => 'ticket',
|
path => 'ticket',
|
||||||
@ -314,7 +270,7 @@ __PACKAGE__->register_method ({
|
|||||||
die PVE::Exception->new("authentication failure\n", code => 401);
|
die PVE::Exception->new("authentication failure\n", code => 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
$res->{cap} = &$compute_api_permission($rpcenv, $username)
|
$res->{cap} = $rpcenv->compute_api_permission($username)
|
||||||
if !defined($res->{NeedTFA});
|
if !defined($res->{NeedTFA});
|
||||||
|
|
||||||
my $clinfo = PVE::Cluster::get_clinfo();
|
my $clinfo = PVE::Cluster::get_clinfo();
|
||||||
@ -659,7 +615,7 @@ __PACKAGE__->register_method({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ticket => PVE::AccessControl::assemble_ticket($authuser),
|
ticket => PVE::AccessControl::assemble_ticket($authuser),
|
||||||
cap => &$compute_api_permission($rpcenv, $authuser),
|
cap => $rpcenv->compute_api_permission($authuser),
|
||||||
}
|
}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@ API2_SOURCES= \
|
|||||||
ACL.pm \
|
ACL.pm \
|
||||||
Role.pm \
|
Role.pm \
|
||||||
Group.pm \
|
Group.pm \
|
||||||
User.pm
|
User.pm \
|
||||||
|
OpenId.pm
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install:
|
install:
|
||||||
|
211
src/PVE/API2/OpenId.pm
Normal file
211
src/PVE/API2/OpenId.pm
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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);
|
||||||
|
use PVE::AccessControl;
|
||||||
|
use PVE::JSONSchema qw(get_standard_option);
|
||||||
|
|
||||||
|
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'},
|
||||||
|
};
|
||||||
|
|
||||||
|
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 $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 => 1024,
|
||||||
|
},
|
||||||
|
'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 ($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);
|
||||||
|
my $subject = $info->{'sub'};
|
||||||
|
|
||||||
|
die "missing openid claim 'sub'\n" if !defined($subject);
|
||||||
|
|
||||||
|
my $unique_name = $subject; # default
|
||||||
|
if (defined(my $user_attr = $config->{'username-claim'})) {
|
||||||
|
if ($user_attr eq 'subject') {
|
||||||
|
$unique_name = $subject;
|
||||||
|
} elsif ($user_attr eq 'username') {
|
||||||
|
my $username = $info->{'preferred_username'};
|
||||||
|
die "missing claim 'preferred_username'\n" if !defined($username);
|
||||||
|
$unique_name = $username;
|
||||||
|
} elsif ($user_attr eq 'email') {
|
||||||
|
my $email = $info->{'email'};
|
||||||
|
die "missing claim 'email'\n" if !defined($email);
|
||||||
|
$unique_name = $email;
|
||||||
|
} else {
|
||||||
|
die "got unexpected value for 'username-claim': '${user_attr}'\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
my $username = "${unique_name}\@${realm}";
|
||||||
|
|
||||||
|
# test if user exists and is enabled
|
||||||
|
$rpcenv->check_user_enabled($username);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}});
|
@ -126,6 +126,55 @@ sub permissions {
|
|||||||
return &$compile_acl_path($self, $user, $path);
|
return &$compile_acl_path($self, $user, $path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub compute_api_permission {
|
||||||
|
my ($self, $authuser) = @_;
|
||||||
|
|
||||||
|
my $usercfg = $self->{user_cfg};
|
||||||
|
|
||||||
|
my $res = {};
|
||||||
|
my $priv_re_map = {
|
||||||
|
vms => qr/VM\.|Permissions\.Modify/,
|
||||||
|
access => qr/(User|Group)\.|Permissions\.Modify/,
|
||||||
|
storage => qr/Datastore\.|Permissions\.Modify/,
|
||||||
|
nodes => qr/Sys\.|Permissions\.Modify/,
|
||||||
|
sdn => qr/SDN\.|Permissions\.Modify/,
|
||||||
|
dc => qr/Sys\.Audit|SDN\./,
|
||||||
|
};
|
||||||
|
map { $res->{$_} = {} } keys %$priv_re_map;
|
||||||
|
|
||||||
|
my $required_paths = ['/', '/nodes', '/access/groups', '/vms', '/storage', '/sdn'];
|
||||||
|
|
||||||
|
my $checked_paths = {};
|
||||||
|
foreach my $path (@$required_paths, keys %{$usercfg->{acl}}) {
|
||||||
|
next if $checked_paths->{$path};
|
||||||
|
$checked_paths->{$path} = 1;
|
||||||
|
|
||||||
|
my $path_perm = $self->permissions($authuser, $path);
|
||||||
|
|
||||||
|
my $toplevel = ($path =~ /^\/(\w+)/) ? $1 : 'dc';
|
||||||
|
if ($toplevel eq 'pool') {
|
||||||
|
foreach my $priv (keys %$path_perm) {
|
||||||
|
if ($priv =~ m/^VM\./) {
|
||||||
|
$res->{vms}->{$priv} = 1;
|
||||||
|
} elsif ($priv =~ m/^Datastore\./) {
|
||||||
|
$res->{storage}->{$priv} = 1;
|
||||||
|
} elsif ($priv eq 'Permissions.Modify') {
|
||||||
|
$res->{storage}->{$priv} = 1;
|
||||||
|
$res->{vms}->{$priv} = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
my $priv_regex = $priv_re_map->{$toplevel} // next;
|
||||||
|
foreach my $priv (keys %$path_perm) {
|
||||||
|
next if $priv !~ m/^($priv_regex)/;
|
||||||
|
$res->{$toplevel}->{$priv} = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
sub get_effective_permissions {
|
sub get_effective_permissions {
|
||||||
my ($self, $user) = @_;
|
my ($self, $user) = @_;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user