Skip to content

Commit 87d3b31

Browse files
committed
fix: Use the correct regular expression to parse Cmnd_Alias and other aliases.
Cause: The regex was not taking into consideration that the Cmnd_Alias value does not have to have spaces on either side of the `=`. The same is true for other Alias values. Consequence: The regex would never terminate, and the role would appear to hang. Fix: Ensure the regex complies with the eBNF definition of the field from the sudoers file specification. Result: The Alias values are parsed correctly. https://issues.redhat.com/browse/RHEL-106261 Signed-off-by: Rich Megginson <rmeggins@redhat.com>
1 parent c82f25b commit 87d3b31

File tree

2 files changed

+110
-4
lines changed

2 files changed

+110
-4
lines changed

library/scan_sudoers.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,17 +187,22 @@ def get_config_lines(path, params):
187187
comment_re = re.compile(r"^#+")
188188
include_re = re.compile(r"^#include")
189189
defaults_re = re.compile(r"^(Defaults)+\s+(.*$)")
190+
# NOTE: The spec https://www.sudo.ws/docs/man/1.9.17/sudoers.man/ says
191+
# NAME ::= A-Z*
192+
# A NAME is a string of uppercase letters, numbers, and underscore characters ('_').
193+
# A NAME must start with an uppercase letter.
194+
# I'm assuming these are ASCII - so the pattern used for NAME is ([A-Z][A-Z0-9_]*)
190195
cmnd_alias_re = re.compile(
191-
r"(^Cmnd_Alias)+\s+(\S+)+\s*\={1}\s*((\S+,{1}\s*)+\S+|\S+)\s*(\:)*(.*)*$"
196+
r"(^Cmnd_Alias)+\s+([A-Z][A-Z0-9_]*)\s*\=\s*((\S+,\s*)+\S+|\S+)\s*(\:)*(.*)*$"
192197
)
193198
host_alias_re = re.compile(
194-
r"(^Host_Alias)+\s+(\S+)+\s*\={1}\s*((\S+,{1}\s*)+\S+|\S+)\s*(\:)*(.*)*$"
199+
r"(^Host_Alias)+\s+([A-Z][A-Z0-9_]*)\s*\=\s*((\S+,\s*)+\S+|\S+)\s*(\:)*(.*)*$"
195200
)
196201
runas_alias_re = re.compile(
197-
r"(^Runas_Alias)+\s+(\S+)+\s*\={1}\s*((\S+,{1}\s*)+\S+|\S+)\s*(\:)*(.*)*$"
202+
r"(^Runas_Alias)+\s+([A-Z][A-Z0-9_]*)\s*\=\s*((\S+,\s*)+\S+|\S+)\s*(\:)*(.*)*$"
198203
)
199204
user_alias_re = re.compile(
200-
r"(^User_Alias)+\s+(\S+)+\s*\={1}\s*((\S+,{1}\s*)+\S+|\S+)\s*(\:)*(.*)*$"
205+
r"(^User_Alias)+\s+([A-Z][A-Z0-9_]*)\s*\=\s*((\S+,\s*)+\S+|\S+)\s*(\:)*(.*)*$"
201206
)
202207

203208
# Defaults Parsing vars

tests/tests_scan_sudoers.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# SPDX-License-Identifier: MIT
2+
---
3+
- name: Ensure that the role can parse existing sudoers
4+
hosts: all
5+
gather_facts: false # test that role works in this case
6+
vars:
7+
alias_values:
8+
Cmnd_Alias:
9+
- name: MY_CMND_NO_SPACES
10+
commands:
11+
- /usr/local/bin/my_cmd_alias.sh
12+
- name: MY_CMND_SPACES
13+
commands:
14+
- /usr/local/bin/my_cmd_alias.sh
15+
Host_Alias:
16+
- name: MY_HOST_NO_SPACES
17+
hosts:
18+
- myhostalias.example.com
19+
- name: MY_HOST_SPACES
20+
hosts:
21+
- myhostalias.example.com
22+
User_Alias:
23+
- name: MY_USER_NO_SPACES
24+
users:
25+
- myuser
26+
- name: MY_USER_SPACES
27+
users:
28+
- myuser
29+
Runas_Alias:
30+
- name: MY_RUNAS_NO_SPACES
31+
users:
32+
- myrunasuser
33+
- name: MY_RUNAS_SPACES
34+
users:
35+
- myrunasuser
36+
alias_keys: "{{ alias_values.keys() | list }}"
37+
alias_names: "{{ alias_keys | zip(alias_keys) | flatten | list }}"
38+
alias_vals: "{{ alias_values.values() | flatten | list }}"
39+
names_vals: "{{ alias_names | zip(alias_vals) | list }}"
40+
tasks:
41+
- name: Run tests
42+
block:
43+
- name: Test setup
44+
include_tasks: tasks/setup.yml
45+
46+
- name: Try with no spaces in alias definitions
47+
copy:
48+
dest: /etc/sudoers
49+
content: |
50+
{% for alias in names_vals %}
51+
{% set itemvals = alias.1.values() | list %}
52+
{% set space = ("NO_SPACES" in itemvals.0) | ternary("", " ") %}
53+
{{ alias.0 }} {{ itemvals.0 }}{{ space }}={{ space }}{{ itemvals.1 | join("") }}
54+
{% endfor %}
55+
mode: preserve
56+
57+
- name: Run the role
58+
include_role:
59+
name: linux-system-roles.sudo
60+
vars:
61+
sudo_rewrite_default_sudoers_file: true
62+
sudo_remove_unauthorized_included_files: true
63+
sudo_sudoers_files:
64+
- path: /etc/sudoers
65+
aliases: "{{ aliases }}"
66+
aliases: "{{ dict(keys | zip(vals)) }}"
67+
keys: "{{ alias_values | dict2items | map(attribute='key') | map('lower') | list }}"
68+
vals: "{{ alias_values | dict2items | map(attribute='value') | list }}"
69+
70+
- name: Get sudoers
71+
slurp:
72+
path: /etc/sudoers
73+
register: __check_sudoers
74+
75+
- name: Check that lines are properly formatted
76+
debug:
77+
msg: expected {{ expected }} in actual {{ actual }}
78+
loop: "{{ names_vals }}"
79+
vars:
80+
expected: "{{ item.0 }} {{ vals.0 }} = {{ vals.1.0 }}"
81+
vals: "{{ item.1.values() | list }}"
82+
actual: "{{ __check_sudoers.content | b64decode }}"
83+
84+
- name: Check that lines are properly formatted
85+
assert:
86+
that: expected in actual
87+
loop: "{{ names_vals }}"
88+
vars:
89+
expected: "{{ item.0 }} {{ vals.0 }} = {{ vals.1.0 }}"
90+
vals: "{{ item.1.values() | list }}"
91+
actual: "{{ __check_sudoers.content | b64decode }}"
92+
93+
- name: Check header for ansible_managed, fingerprint
94+
include_tasks: tasks/check_present_header.yml
95+
vars:
96+
__file: /etc/sudoers
97+
__fingerprint: system_role:sudo
98+
99+
always:
100+
- name: Test cleanup
101+
include_tasks: tasks/cleanup.yml

0 commit comments

Comments
 (0)