diff --git a/README.rst b/README.rst index de609df..32d5232 100644 --- a/README.rst +++ b/README.rst @@ -263,6 +263,31 @@ Example: allow to run `ip netns exec ` as long as ``ip: IpNetnsExecFilter, ip, root`` +ChainingRegExpFilter +-------------------- + +Filter that allows to run the prefix command, if the beginning of its arguments +match to a list of regular expressions, and if remaining arguments are any +otherwise-allowed command. Parameters are: + +1. Executable allowed +2. User to run the command under +3. (and following) Regular expressions to use to match first (and subsequent) + command arguments. + +This filter regards the length of the regular expressions list as the number of +arguments to be checked, and remaining parts are checked by other filters. + +Example: allow to run `/usr/bin/nice`, but only with first two parameters being +-n and integer, and followed by any allowed command by the other filters: + +``nice: /usr/bin/nice, root, nice, -n, -?\d+`` + +Note: this filter can't be used to impose that the subcommand is always run +under the prefix command. In particular, it can't enforce that a particular +command is only run under "nice", since the subcommand can explicitly be +called directly. + Calling rootwrap from OpenStack services ============================================= diff --git a/oslo/rootwrap/filters.py b/oslo/rootwrap/filters.py index 07d015f..d954e2e 100644 --- a/oslo/rootwrap/filters.py +++ b/oslo/rootwrap/filters.py @@ -316,3 +316,32 @@ class IpNetnsExecFilter(ChainingFilter): if args: args[0] = os.path.basename(args[0]) return args + + +class ChainingRegExpFilter(ChainingFilter): + """Command filter doing regexp matching for prefix commands. + Remaining arguments are filtered again. This means that the command + specified as the arguments must be also allowed to execute directly. + """ + + def match(self, userargs): + # Early skip if number of args is smaller than the filter + if (not userargs or len(self.args) > len(userargs)): + return False + # Compare each arg (anchoring pattern explicitly at end of string) + for (pattern, arg) in zip(self.args, userargs): + try: + if not re.match(pattern + '$', arg): + # DENY: Some arguments did not match + return False + except re.error: + # DENY: Badly-formed filter + return False + # ALLOW: All arguments matched + return True + + def exec_args(self, userargs): + args = userargs[len(self.args):] + if args: + args[0] = os.path.basename(args[0]) + return args diff --git a/tests/test_rootwrap.py b/tests/test_rootwrap.py index 610f2e3..8fa8424 100644 --- a/tests/test_rootwrap.py +++ b/tests/test_rootwrap.py @@ -316,6 +316,30 @@ class RootwrapTestCase(testtools.TestCase): self.assertRaises(wrapper.NoFilterMatched, wrapper.match_filter, filter_list, args) + def test_ChainingRegExpFilter_match(self): + filter_list = [filters.ChainingRegExpFilter('nice', 'root', + 'nice', '-?\d+'), + filters.CommandFilter('cat', 'root')] + args = ['nice', '5', 'cat', '/a'] + dirs = ['/bin', '/usr/bin'] + + self.assertIsNotNone(wrapper.match_filter(filter_list, args, dirs)) + + def test_ChainingRegExpFilter_not_match(self): + filter_list = [filters.ChainingRegExpFilter('nice', 'root', + 'nice', '-?\d+'), + filters.CommandFilter('cat', 'root')] + args_invalid = (['nice', '5', 'ls', '/a'], + ['nice', '--5', 'cat', '/a'], + ['nice2', '5', 'cat', '/a'], + ['nice', 'cat', '/a'], + ['nice', '5']) + dirs = ['/bin', '/usr/bin'] + + for args in args_invalid: + self.assertRaises(wrapper.NoFilterMatched, + wrapper.match_filter, filter_list, args, dirs) + def test_ReadFileFilter_empty_args(self): goodfn = '/good/file.name' f = filters.ReadFileFilter(goodfn)