mirror of
https://github.com/webmin/webmin.git
synced 2026-10-30 07:01:10 +00:00
This PR adds an `api_only` global permission that restricts a Webmin account to one supported API endpoint, such as virtual-server/remote.cgi. External integrations need credentials that can call an API without also granting access to the Webmin interface or generic RPC. Existing module permissions do not provide this endpoint-level restriction. The new setting allows administrators to create dedicated integration accounts while Webmin continues performing authorized operations through its privileged backend. The restriction is enforced in two places: - MiniServ checks the authenticated account's policy before dispatching CGI, static-file, DAV, or WebSocket requests. Requests outside the configured endpoint receive HTTP 403. - `init_config` requires the permitted CGI to explicitly opt in before initialization. This also protects Webmin-initializing CGIs hosted by another web server. Generic RPC is denied for API-only accounts, even when an existing RPC permission would otherwise allow it. The endpoint's own module and operation permissions still apply. Selecting an endpoint does not grant permission to execute every API command. Webmin Users gains an API-only setting for users and groups. Existing restrictions survive ordinary edits, resets, copies, cloning, renaming, group propagation, and cluster synchronization. Clearing a restriction requires an explicit permission edit. Group updates preserve restrictions already assigned to individual members. MiniServ reads the policy on each request from the account's existing storage backend: local files, MySQL, PostgreSQL, or LDAP. Policy-read errors deny access instead of treating the account as unrestricted. Validation passed with the four separately submitted supporting fixes (#2897, #2898, #2899, #2900) ,applied: 169 focused tests, syntax checks, and live Debian tests using file-backed and PostgreSQL accounts. Live checks covered account creation, lifecycle operations, both CGI modes, sessions, RPC, static files, DAV, and WebSockets. The supporting fixes remain separate PRs. This API-only branch currently has seven lifecycle test failures that depend on the file-cache and group ACL cleanup fixes landing.
160 lines
4.3 KiB
Perl
Executable File
160 lines
4.3 KiB
Perl
Executable File
#!/usr/local/bin/perl
|
|
# save_group.cgi
|
|
# Create, modify or delete a webmin group
|
|
|
|
use strict;
|
|
use warnings;
|
|
no warnings 'redefine';
|
|
no warnings 'uninitialized';
|
|
require './acl-lib.pl'; ## no critic
|
|
our (%in, %text, %config, %access, $config_directory);
|
|
&ReadParse();
|
|
# Only permission editors may change global ACLs, including API-only access.
|
|
if ($in{'acl_security_form'} && !$access{'acl'}) {
|
|
&error($text{'acl_emod'});
|
|
}
|
|
$access{'groups'} || &error($text{'gedit_ecannot'});
|
|
|
|
# Check for special button clicks, and redirect
|
|
if ($in{'but_clone'}) {
|
|
&redirect("edit_group.cgi?clone=".&urlize($in{'old'}));
|
|
exit;
|
|
}
|
|
elsif ($in{'but_delete'}) {
|
|
&redirect("delete_group.cgi?group=".&urlize($in{'old'}));
|
|
exit;
|
|
}
|
|
|
|
my (%group, $old);
|
|
if ($in{'old'}) {
|
|
# Get the original group
|
|
$old = &get_group($in{'old'});
|
|
$old || &error($text{'gedit_egone'});
|
|
$group{'members'} = $old->{'members'};
|
|
$group{'proto'} = $old->{'proto'};
|
|
$group{'id'} = $old->{'id'};
|
|
}
|
|
&error_setup($text{'gsave_err'});
|
|
|
|
# Check for duplicate group names
|
|
$in{'name'} =~ /^[A-z0-9\-\_\.\@]+$/ && $in{'name'} !~ /^\@/ ||
|
|
&error(&text('gsave_ename', $in{'name'}));
|
|
$in{'name'} eq 'webmin' && &error($text{'gsave_enamewebmin'});
|
|
if (!$in{'old'} || $in{'old'} ne $in{'name'}) {
|
|
my $clash = &get_group($in{'name'});
|
|
$clash && &error(&text('gsave_edup', $in{'name'}));
|
|
}
|
|
$in{'desc'} !~ /:/ || &error($text{'gsave_edesc'});
|
|
|
|
# Find the current parent group
|
|
my $oldgroup;
|
|
if ($in{'old'}) {
|
|
foreach my $g (&list_groups()) {
|
|
if (&indexof('@'.$in{'old'}, @{$g->{'members'}}) >= 0) {
|
|
$oldgroup = $g;
|
|
}
|
|
}
|
|
}
|
|
|
|
my $newgroup;
|
|
if (defined($in{'group'})) {
|
|
# Check if group is allowed
|
|
if ($access{'gassign'} ne '*') {
|
|
my @gcan = split(/\s+/, $access{'gassign'});
|
|
$in{'group'} && &indexof($in{'group'}, @gcan) >= 0 ||
|
|
!$in{'group'} && &indexof('_none', @gcan) >= 0 ||
|
|
$oldgroup && $oldgroup->{'name'} eq $in{'group'} ||
|
|
&error($text{'save_egroup'});
|
|
}
|
|
|
|
# Store parent group membership
|
|
$newgroup = &get_group($in{'group'});
|
|
if ($in{'group'} ne ($oldgroup ? $oldgroup->{'name'} : '')) {
|
|
# Group has changed - update the member lists
|
|
if ($oldgroup) {
|
|
$oldgroup->{'members'} =
|
|
[ grep { $_ ne '@'.$in{'old'} }
|
|
@{$oldgroup->{'members'}} ];
|
|
&modify_group($oldgroup->{'name'}, $oldgroup);
|
|
}
|
|
if ($newgroup) {
|
|
push(@{$newgroup->{'members'}}, '@'.$in{'name'});
|
|
&modify_group($in{'group'}, $newgroup);
|
|
}
|
|
}
|
|
}
|
|
|
|
# Work out group modules
|
|
my @mods = split(/\0/, $in{'mod'});
|
|
|
|
if ($oldgroup) {
|
|
# Remove modules from the old parent group
|
|
@mods = grep { &indexof($_, @{$oldgroup->{'modules'}}) < 0 } @mods;
|
|
}
|
|
|
|
if ($newgroup) {
|
|
# Add modules from parent group to list
|
|
my @ownmods;
|
|
foreach my $m (@mods) {
|
|
push(@ownmods, $m)
|
|
if (&indexof($m, @{$newgroup->{'modules'}}) < 0);
|
|
}
|
|
@mods = &unique(@mods, @{$newgroup->{'modules'}});
|
|
$group{'ownmods'} = \@ownmods;
|
|
|
|
# Copy ACL files for parent group
|
|
my $name = $in{'old'} ? $in{'old'} : $in{'name'};
|
|
©_group_acl_files($in{'group'}, $name,
|
|
[ @{$newgroup->{'modules'}}, "" ]);
|
|
}
|
|
|
|
# Store group options
|
|
$group{'modules'} = \@mods;
|
|
$group{'name'} = $in{'name'};
|
|
$group{'desc'} = $in{'desc'};
|
|
|
|
if ($in{'old'}) {
|
|
# update group
|
|
&modify_group($in{'old'}, \%group);
|
|
|
|
# recursively update all member users and groups
|
|
my @glist = &list_groups();
|
|
my @ulist = &list_users();
|
|
&update_members(\@ulist, \@glist, $group{'modules'},
|
|
$old->{'members'});
|
|
}
|
|
else {
|
|
# create group
|
|
&create_group(\%group, $in{'clone'});
|
|
}
|
|
|
|
if ($in{'old'} && $in{'acl_security_form'}) {
|
|
# Update group's global ACL
|
|
&foreign_require("", "acl_security.pl");
|
|
my %uaccess;
|
|
&foreign_call("", "acl_security_save", \%uaccess, \%in);
|
|
my $aclfile = "$config_directory/$in{'name'}.gacl";
|
|
&lock_file($aclfile);
|
|
&save_group_module_acl(\%uaccess, $in{'name'}, "", 1);
|
|
&set_ownership_permissions(undef, undef, 0640, $aclfile);
|
|
&unlock_file($aclfile);
|
|
# Apply a newly selected endpoint to existing members as well as future
|
|
# members. Keep any endpoint already set on a user or nested group.
|
|
if ($uaccess{'api_only'}) {
|
|
my @users = &list_users();
|
|
my @groups = &list_groups();
|
|
&set_acl_files(\@users, \@groups, '', $group{'members'},
|
|
\%uaccess);
|
|
}
|
|
}
|
|
|
|
&reload_miniserv();
|
|
if ($in{'old'}) {
|
|
&webmin_log("modify", "group", $in{'old'}, \%in);
|
|
}
|
|
else {
|
|
&webmin_log("create", "group", $group{'name'}, \%in);
|
|
}
|
|
&redirect("");
|
|
|