Skip to content

Commit 133cb0c

Browse files
authored
Merge pull request #4674 from dmbaturin/op-mode-permissions
op-mode: T7745: add a CLI for operator user command permissions
2 parents d300395 + 0217256 commit 133cb0c

File tree

6 files changed

+117
-3
lines changed

6 files changed

+117
-3
lines changed

data/config.boot.default

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ system {
3333
}
3434
host-name "vyos"
3535
login {
36+
operator-group default {
37+
command-policy {
38+
allow "*"
39+
}
40+
}
3641
user vyos {
3742
authentication {
3843
encrypted-password "$6$QxPS.uk6mfo$9QBSo8u1FkH16gMyAVhus6fU3LOzvLR9Z9.82m3tiHFAxTtIkhaZSWssSgzt4v4dGAL8rhVQxTg0oAG9/q11h/"

interface-definitions/system_login.xml.in

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,38 @@
88
<priority>400</priority>
99
</properties>
1010
<children>
11+
<tagNode name="operator-group">
12+
<properties>
13+
<help>Operator group</help>
14+
<constraint>
15+
#include <include/constraint/login-username.xml.i>
16+
</constraint>
17+
<constraintErrorMessage>Operator group name contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
18+
</properties>
19+
<children>
20+
<node name="command-policy">
21+
<properties>
22+
<help>Command policy</help>
23+
</properties>
24+
<children>
25+
<leafNode name="allow">
26+
<properties>
27+
<multi/>
28+
<help>Command subtree allowed to execute</help>
29+
<valueHelp>
30+
<format>txt</format>
31+
<description>Exact command (e.g., 'show interfaces')</description>
32+
</valueHelp>
33+
<valueHelp>
34+
<format>txt</format>
35+
<description>Command wildcard (e.g., '* vpn')</description>
36+
</valueHelp>
37+
</properties>
38+
</leafNode>
39+
</children>
40+
</node>
41+
</children>
42+
</tagNode>
1143
<tagNode name="user">
1244
<properties>
1345
<help>Local user account information</help>
@@ -17,6 +49,26 @@
1749
<constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage>
1850
</properties>
1951
<children>
52+
<node name="operator">
53+
<properties>
54+
<help>Restrict the user to operational mode</help>
55+
</properties>
56+
<children>
57+
<leafNode name="group">
58+
<properties>
59+
<multi/>
60+
<help>Operator group</help>
61+
<completionHelp>
62+
<path>system login operator-group</path>
63+
</completionHelp>
64+
<valueHelp>
65+
<format>txt</format>
66+
<description>Operator group name</description>
67+
</valueHelp>
68+
</properties>
69+
</leafNode>
70+
</children>
71+
</node>
2072
<node name="authentication">
2173
<properties>
2274
<help>Authentication settings</help>

src/conf_mode/system_login.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
# You should have received a copy of the GNU General Public License
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
import re
1718
import os
19+
import json
1820

1921
from passlib.hosts import linux_context
2022
from psutil import users
@@ -171,6 +173,15 @@ def verify(login):
171173
if 'key' not in pubkey_options:
172174
raise ConfigError(f'Missing key for public-key "{pubkey}"!')
173175

176+
if 'operator' in user_config:
177+
op_groups = dict_search('operator.group', user_config)
178+
if op_groups:
179+
for og in op_groups:
180+
if dict_search(f'operator_group.{og}', login) is None:
181+
raise ConfigError(f'Operator group {og} does not exist')
182+
else:
183+
raise ConfigError(f'User {user} is configured as an operator but is not assigned to any operator groups')
184+
174185
if {'radius', 'tacacs'} <= set(login):
175186
raise ConfigError('Using both RADIUS and TACACS at the same time is not supported!')
176187

@@ -306,6 +317,28 @@ def generate(login):
306317
if os.path.isfile(autologout_file):
307318
os.unlink(autologout_file)
308319

320+
# Operator groups and group membership
321+
operator_config = {'users': {}, 'groups': {}}
322+
if 'user' in login:
323+
for user, user_config in login['user'].items():
324+
op_groups = dict_search('operator.group', user_config)
325+
if op_groups:
326+
operator_config['users'][user] = op_groups
327+
328+
if 'operator_group' in login:
329+
operator_config['groups'] = login['operator_group']
330+
331+
# Convert permissions strings to list
332+
# so that the operational command runner doesn't have to
333+
for g in operator_config['groups']:
334+
policy = dict_search(f'command_policy.allow', operator_config['groups'][g])
335+
if policy is not None:
336+
policy = list(map(lambda s: re.split(r'\s+', s), policy))
337+
operator_config['groups'][g]['command_policy']['allow'] = policy
338+
339+
with open('/etc/vyos/operators.json', 'w') as of:
340+
json.dump(operator_config, of)
341+
309342
return None
310343

311344

@@ -335,7 +368,11 @@ def apply(login):
335368
if tmp: command += f" --home '{tmp}'"
336369
else: command += f" --home '/home/{user}'"
337370

338-
command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}'
371+
if 'operator' not in user_config:
372+
command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea'
373+
374+
command += f' {user}'
375+
339376
try:
340377
cmd(command)
341378
# we should not rely on the value stored in user_config['home_directory'], as a

src/opt/vyatta/etc/shell/level/users/allowed-op

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ exit
1111
force
1212
monitor
1313
ping
14+
poweroff
15+
reboot
1416
reset
17+
restart
1518
release
1619
renew
1720
set

src/opt/vyatta/etc/shell/level/users/allowed-op.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ exit
77
force
88
monitor
99
ping
10+
poweroff
11+
reboot
1012
reset
13+
restart
1114
release
1215
renew
1316
set

src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ _vyatta_op_run ()
217217
local run_cmd=$(_vyatta_op_get_node_def_field $tpath/node.def run)
218218
run_cmd=$(_vyatta_op_conv_run_cmd "$run_cmd") # convert the positional parameters
219219
local ret=0
220+
221+
if groups "$(whoami)" | grep -q -E ' vyattacfg(\s|$)'; then
222+
# If the user is an admin,
223+
# use sudo directly for now
224+
op_runner="sudo"
225+
else
226+
# If the user is an operator,
227+
# use the new operational command runner
228+
# (operator users have no sudo permissions)
229+
# and give it the original VyOS command
230+
op_runner="vyos-op-run"
231+
run_cmd="$@"
232+
fi
233+
220234
# Exception for the `show file` command
221235
local file_cmd='\$\{vyos_op_scripts_dir\}\/file\.py'
222236
local cmd_regex="^(LESSOPEN=|less|pager|tail|(sudo )?$file_cmd).*"
@@ -234,9 +248,9 @@ _vyatta_op_run ()
234248
# to be able to do their job.
235249
eval "$run_cmd"
236250
elif [[ -t 1 && "${args[1]}" == "show" && ! $run_cmd =~ $cmd_regex ]] ; then
237-
eval "(sudo $run_cmd) | ${VYATTA_PAGER:-cat}"
251+
eval "($op_runner $run_cmd) | ${VYATTA_PAGER:-cat}"
238252
else
239-
eval "sudo $run_cmd"
253+
eval "$op_runner $run_cmd"
240254
fi
241255
else
242256
echo -ne "\n Incomplete command: ${args[@]}\n\n" >&2

0 commit comments

Comments
 (0)