mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
- contract module has not see a release since 2010 - Corresponding PEP has been deferred - python-contract module in Debian is orphaned - We are only using contract module in one method of one module - That can be replaced with one line of check instead of depending on an entire module - The code using contract module does not work - There is already replacement one line code that is actually working
146 lines
4.4 KiB
Python
146 lines
4.4 KiB
Python
#! /usr/bin/env python
|
|
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
|
|
|
|
"""Run specified actions.
|
|
|
|
Actions run commands with this contract (version 1.1):
|
|
|
|
1. (promise) Super-user actions run as root. Normal actions do not.
|
|
|
|
2. (promise) The actions directory can't be changed at run time.
|
|
|
|
This guarantees that we can only select from the correct set of actions.
|
|
|
|
3. (restriction) Only whitelisted actions can run.
|
|
|
|
A. Scripts in a directory above the actions directory can't be run.
|
|
|
|
Arguments (and options) can't coerce the system to run actions in
|
|
directories above the actions directory.
|
|
|
|
Arguments that fail this validation will raise a ValueError.
|
|
|
|
B. Scripts in a directory beneath the actions directory can't be run.
|
|
|
|
Arguments (and options) can't coerce the system to run actions in
|
|
sub-directories of the actions directory.
|
|
|
|
(An important side-effect of this is that the system will not try to
|
|
follow symlinks to other action directories.)
|
|
|
|
Arguments that fail this validation will raise a ValueError.
|
|
|
|
C. Only one action can be called at a time.
|
|
|
|
This prevents us from appending multiple (unexpected) actions to the call.
|
|
|
|
$ action="echo '$options'; echo 'oops'"
|
|
$ options="hi"
|
|
$ $action # oops, the file system is gone!
|
|
|
|
Arguments that fail this validation will raise a ValueError.
|
|
|
|
D. Options can't be used to run other actions:
|
|
|
|
$ action="echo '$options'"
|
|
$ options="hi'; rm -rf /;'"
|
|
$ $action # oops, the file system is gone!
|
|
|
|
Arguments that fail this validation won't, but probably should, raise a
|
|
ValueError. They don't because sanitizing this case is significantly
|
|
easier than detecting if it occurs.
|
|
|
|
The options list is coerced into a space-separated string before being
|
|
shell-escaped. Option lists including shell escape characters may need to
|
|
be unescaped on the receiving end.
|
|
|
|
E. Actions must exist in the actions directory.
|
|
|
|
4. (promise) Options are appended to the action.
|
|
|
|
Options can be provided as a list or strings.
|
|
|
|
5. (promise) Output and error strings are returned from the command.
|
|
|
|
6. (limitation) Providing the process with input is not possible.
|
|
|
|
Don't expect to give the process additional input after it's started. Any
|
|
interaction with the spawned process must be carried out through some other
|
|
method (maybe the process opens a socket, or something).
|
|
|
|
7. Option
|
|
|
|
"""
|
|
|
|
import os
|
|
import pipes, shlex, subprocess
|
|
|
|
|
|
def run(action, options = None, async = False):
|
|
"""Safely run a specific action as the current user.
|
|
|
|
See actions._run for more information.
|
|
|
|
"""
|
|
return _run(action, options, async, False)
|
|
|
|
def superuser_run(action, options = None, async = False):
|
|
"""Safely run a specific action as root.
|
|
|
|
See actions._run for more information.
|
|
|
|
"""
|
|
return _run(action, options, async, True)
|
|
|
|
def _run(action, options = None, async = False, run_as_root = False):
|
|
"""Safely run a specific action as a normal user or root.
|
|
|
|
actions are pulled from the actions directory.
|
|
|
|
options are added to the action command.
|
|
|
|
async: run asynchronously or wait for the command to complete.
|
|
|
|
run_as_root: execute the command through sudo.
|
|
"""
|
|
DIRECTORY = "actions"
|
|
|
|
if options == None:
|
|
options = []
|
|
|
|
# contract 3A and 3B: don't call anything outside of the actions directory.
|
|
if os.sep in action:
|
|
raise ValueError("Action can't contain:" + os.sep)
|
|
|
|
cmd = DIRECTORY + os.sep + action
|
|
|
|
# contract 3C: interpret shell escape sequences as literal file names.
|
|
# contract 3E: fail if the action doesn't exist or exists elsewhere.
|
|
if not os.access(cmd, os.F_OK):
|
|
raise ValueError("Action must exist in action directory.")
|
|
|
|
cmd = [cmd]
|
|
|
|
# contract: 3C, 3D: don't allow users to insert escape characters in options
|
|
if options:
|
|
if not hasattr(options, "__iter__"):
|
|
options = [options]
|
|
|
|
cmd += [pipes.quote(option) for option in options]
|
|
|
|
# contract 1: commands can run via sudo.
|
|
if run_as_root:
|
|
cmd = ["sudo", "-n"] + cmd
|
|
|
|
# contract 3C: don't interpret shell escape sequences.
|
|
# contract 5 (and 6-ish).
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
stdout = subprocess.PIPE,
|
|
stderr= subprocess.PIPE,
|
|
shell=False)
|
|
|
|
if not async:
|
|
output, error = proc.communicate()
|
|
return output, error
|