mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
Merged async actions.
This commit is contained in:
commit
e23650232b
@ -1,17 +1,17 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
|
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
|
||||||
|
|
||||||
"""Run specified privileged actions as root.
|
"""Run specified actions.
|
||||||
|
|
||||||
Privileged actions run commands with this contract (version 1.0):
|
Actions run commands with this contract (version 1.1):
|
||||||
|
|
||||||
1. (promise) Privileged actions run as root.
|
1. (promise) Super-user actions run as root. Normal actions do not.
|
||||||
|
|
||||||
2. (promise) The actions directory can't be changed at run time.
|
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.
|
This guarantees that we can only select from the correct set of actions.
|
||||||
|
|
||||||
3. (restriction) Only whitelisted privileged actions can run.
|
3. (restriction) Only whitelisted actions can run.
|
||||||
|
|
||||||
A. Scripts in a directory above the actions directory can't be run.
|
A. Scripts in a directory above the actions directory can't be run.
|
||||||
|
|
||||||
@ -58,6 +58,8 @@ Privileged actions run commands with this contract (version 1.0):
|
|||||||
|
|
||||||
4. (promise) Options are appended to the action.
|
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.
|
5. (promise) Output and error strings are returned from the command.
|
||||||
|
|
||||||
6. (limitation) Providing the process with input is not possible.
|
6. (limitation) Providing the process with input is not possible.
|
||||||
@ -66,26 +68,36 @@ Privileged actions run commands with this contract (version 1.0):
|
|||||||
interaction with the spawned process must be carried out through some other
|
interaction with the spawned process must be carried out through some other
|
||||||
method (maybe the process opens a socket, or something).
|
method (maybe the process opens a socket, or something).
|
||||||
|
|
||||||
|
7. Option
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contract
|
import contract
|
||||||
import os
|
import os
|
||||||
import pipes
|
import pipes, shlex, subprocess
|
||||||
import shlex
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
contract.checkmod(__name__)
|
contract.checkmod(__name__)
|
||||||
|
|
||||||
def privilegedaction_run(action, options = None):
|
def run(action, options = None, async = False):
|
||||||
|
|
||||||
|
return _run(action, options, async, False)
|
||||||
|
|
||||||
|
def superuser_run(action, options = None, async = False):
|
||||||
|
|
||||||
|
return _run(action, options, async, True)
|
||||||
|
|
||||||
|
def _run(action, options = None, async = False, run_as_root = False):
|
||||||
"""Safely run a specific action as root.
|
"""Safely run a specific action as root.
|
||||||
|
|
||||||
pre:
|
pre:
|
||||||
os.sep not in actions
|
os.sep not in action
|
||||||
inv:
|
inv:
|
||||||
True # Actions directory hasn't changed. It's hardcoded :)
|
True # Actions directory hasn't changed. It's hardcoded :)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
DIRECTORY = "actions"
|
DIRECTORY = "actions"
|
||||||
|
if options == None:
|
||||||
|
options = []
|
||||||
|
|
||||||
# contract 3A and 3B: don't call anything outside of the actions directory.
|
# contract 3A and 3B: don't call anything outside of the actions directory.
|
||||||
if os.sep in action:
|
if os.sep in action:
|
||||||
@ -98,18 +110,27 @@ def privilegedaction_run(action, options = None):
|
|||||||
if not os.access(cmd, os.F_OK):
|
if not os.access(cmd, os.F_OK):
|
||||||
raise ValueError("Action must exist in action directory.")
|
raise ValueError("Action must exist in action directory.")
|
||||||
|
|
||||||
if hasattr(options, "__iter__"):
|
cmd = [cmd]
|
||||||
options = " ".join(options)
|
|
||||||
|
|
||||||
# contract: 3C, 3D: don't allow users to insert escape characters
|
# contract: 3C, 3D: don't allow users to insert escape characters in options
|
||||||
cmd = ["sudo", "-n", cmd, pipes.quote(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 3C: don't interpret shell escape sequences.
|
||||||
# contract 5 (and 6-ish).
|
# contract 5 (and 6-ish).
|
||||||
output, error = \
|
proc = subprocess.Popen(
|
||||||
subprocess.Popen(cmd,
|
cmd,
|
||||||
stdout = subprocess.PIPE,
|
stdout = subprocess.PIPE,
|
||||||
stderr= subprocess.PIPE,
|
stderr= subprocess.PIPE,
|
||||||
shell=False).communicate()
|
shell=False)
|
||||||
|
|
||||||
return output, error
|
if not async:
|
||||||
|
output, error = proc.communicate()
|
||||||
|
return output, error
|
||||||
@ -3,6 +3,7 @@ from gettext import gettext as _
|
|||||||
from modules.auth import require
|
from modules.auth import require
|
||||||
from plugin_mount import PagePlugin
|
from plugin_mount import PagePlugin
|
||||||
from forms import Form
|
from forms import Form
|
||||||
|
from actions import superuser_run
|
||||||
import cfg
|
import cfg
|
||||||
|
|
||||||
class Apps(PagePlugin):
|
class Apps(PagePlugin):
|
||||||
@ -16,7 +17,7 @@ class Apps(PagePlugin):
|
|||||||
def index(self):
|
def index(self):
|
||||||
main = """
|
main = """
|
||||||
<p>User Applications are web apps hosted on your %s.</p>
|
<p>User Applications are web apps hosted on your %s.</p>
|
||||||
|
|
||||||
<p>Eventually this box could be your photo sharing site, your
|
<p>Eventually this box could be your photo sharing site, your
|
||||||
instant messaging site, your social networking site, your news
|
instant messaging site, your social networking site, your news
|
||||||
site. Remember web portals? We can be one of those too.
|
site. Remember web portals? We can be one of those too.
|
||||||
@ -35,4 +36,3 @@ investment in the sentimental value of your family snaps? Keep those
|
|||||||
photos local, backed up, easily accessed and free from the whims of
|
photos local, backed up, easily accessed and free from the whims of
|
||||||
some other websites business model.</p>
|
some other websites business model.</p>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from modules.auth import require
|
|||||||
from plugin_mount import PagePlugin, FormPlugin
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
import cfg
|
import cfg
|
||||||
from forms import Form
|
from forms import Form
|
||||||
from privilegedactions import privilegedaction_run
|
from actions import superuser_run
|
||||||
from util import Message
|
from util import Message
|
||||||
|
|
||||||
class xmpp(PagePlugin):
|
class xmpp(PagePlugin):
|
||||||
@ -30,7 +30,7 @@ class configure(FormPlugin, PagePlugin):
|
|||||||
sidebar_right = _("<strong>Configure XMPP Server</strong>")
|
sidebar_right = _("<strong>Configure XMPP Server</strong>")
|
||||||
|
|
||||||
def main(self, xmpp_inband_enable=False, message=None, *args, **kwargs):
|
def main(self, xmpp_inband_enable=False, message=None, *args, **kwargs):
|
||||||
output, error = privilegedaction_run("xmpp-setup", 'status')
|
output, error = superuser_run("xmpp-setup", 'status')
|
||||||
if error:
|
if error:
|
||||||
raise Exception("something is wrong: " + error)
|
raise Exception("something is wrong: " + error)
|
||||||
if "inband_enable" in output.split():
|
if "inband_enable" in output.split():
|
||||||
@ -40,7 +40,7 @@ class configure(FormPlugin, PagePlugin):
|
|||||||
action=cfg.server_dir + "/services/xmpp/configure/index",
|
action=cfg.server_dir + "/services/xmpp/configure/index",
|
||||||
name="configure_xmpp_form",
|
name="configure_xmpp_form",
|
||||||
message=message)
|
message=message)
|
||||||
form.checkbox(_("Allow In-Band Registration"), name="xmpp_inband_enable",
|
form.checkbox(_("Allow In-Band Registration"), name="xmpp_inband_enable",
|
||||||
id="xmpp_inband_enable", checked=xmpp_inband_enable)
|
id="xmpp_inband_enable", checked=xmpp_inband_enable)
|
||||||
# hidden field is needed because checkbox doesn't post if not checked
|
# hidden field is needed because checkbox doesn't post if not checked
|
||||||
form.hidden(name="submitted", value="True")
|
form.hidden(name="submitted", value="True")
|
||||||
@ -92,7 +92,7 @@ class register(FormPlugin, PagePlugin):
|
|||||||
if not password: msg.add = _("Must specify a password!")
|
if not password: msg.add = _("Must specify a password!")
|
||||||
|
|
||||||
if username and password:
|
if username and password:
|
||||||
output, error = privilegedaction_run("xmpp-register", [username, password])
|
output, error = superuser_run("xmpp-register", [username, password])
|
||||||
if error:
|
if error:
|
||||||
raise Exception("something is wrong: " + error)
|
raise Exception("something is wrong: " + error)
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from gettext import gettext as _
|
|||||||
from filedict import FileDict
|
from filedict import FileDict
|
||||||
from modules.auth import require
|
from modules.auth import require
|
||||||
from plugin_mount import PagePlugin, FormPlugin
|
from plugin_mount import PagePlugin, FormPlugin
|
||||||
from privilegedactions import privilegedaction_run
|
from actions import superuser_run
|
||||||
import cfg
|
import cfg
|
||||||
from forms import Form
|
from forms import Form
|
||||||
from model import User
|
from model import User
|
||||||
@ -55,7 +55,7 @@ def set_hostname(hostname):
|
|||||||
|
|
||||||
cfg.log.info("Changing hostname to '%s'" % hostname)
|
cfg.log.info("Changing hostname to '%s'" % hostname)
|
||||||
try:
|
try:
|
||||||
privilegedaction_run("hostname-change", hostname)
|
superuser_run("hostname-change", hostname)
|
||||||
# don't persist/cache change unless it was saved successfuly
|
# don't persist/cache change unless it was saved successfuly
|
||||||
sys_store = filedict_con(cfg.store_file, 'sys')
|
sys_store = filedict_con(cfg.store_file, 'sys')
|
||||||
sys_store['hostname'] = hostname
|
sys_store['hostname'] = hostname
|
||||||
@ -141,7 +141,7 @@ class general(FormPlugin, PagePlugin):
|
|||||||
time_zone = time_zone.strip()
|
time_zone = time_zone.strip()
|
||||||
if time_zone != sys_store['time_zone']:
|
if time_zone != sys_store['time_zone']:
|
||||||
cfg.log.info("Setting timezone to %s" % time_zone)
|
cfg.log.info("Setting timezone to %s" % time_zone)
|
||||||
privilegedaction_run("timezone-change", [time_zone])
|
superuser_run("timezone-change", [time_zone])
|
||||||
sys_store['time_zone'] = time_zone
|
sys_store['time_zone'] = time_zone
|
||||||
return message or "Settings updated."
|
return message or "Settings updated."
|
||||||
|
|
||||||
|
|||||||
@ -1,32 +1,16 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
|
# -*- mode: python; mode: auto-fill; fill-column: 80 -*-
|
||||||
|
|
||||||
|
from actions import superuser_run, run
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from privilegedactions import privilegedaction_run
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
class TestPrivileged(unittest.TestCase):
|
class TestPrivileged(unittest.TestCase):
|
||||||
"""Verify that privileged actions perform as expected:
|
"""Verify that privileged actions perform as expected.
|
||||||
|
|
||||||
1. Privileged actions run as root.
|
See actions.py for a full description of the expectations.
|
||||||
|
|
||||||
2. Only whitelisted privileged actions can run.
|
|
||||||
|
|
||||||
A. Actions can't be used to run other actions:
|
|
||||||
|
|
||||||
$ action="echo 'hi'; rm -rf /"
|
|
||||||
$ $action
|
|
||||||
|
|
||||||
B. Options can't be used to run other actions:
|
|
||||||
|
|
||||||
$ options="hi'; rm -rf /;'"
|
|
||||||
$ "echo " + "'$options'"
|
|
||||||
|
|
||||||
C. Scripts in a directory above the actions directory can't be run.
|
|
||||||
|
|
||||||
D. Scripts in a directory beneath the actions directory can't be run.
|
|
||||||
|
|
||||||
3. The actions directory can't be changed at run time.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def test_run_as_root(self):
|
def test_run_as_root(self):
|
||||||
@ -35,7 +19,7 @@ class TestPrivileged(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"0", # user 0 is root
|
"0", # user 0 is root
|
||||||
privilegedaction_run("id", "-ur")[0].strip())
|
superuser_run("id", "-ur")[0].strip())
|
||||||
|
|
||||||
def test_breakout_actions_dir(self):
|
def test_breakout_actions_dir(self):
|
||||||
"""2. The actions directory can't be changed at run time.
|
"""2. The actions directory can't be changed at run time.
|
||||||
@ -55,13 +39,13 @@ class TestPrivileged(unittest.TestCase):
|
|||||||
|
|
||||||
for arg in ("../echo", "/bin/echo"):
|
for arg in ("../echo", "/bin/echo"):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
privilegedaction_run(arg, options)
|
run(arg, options)
|
||||||
|
|
||||||
def test_breakout_down(self):
|
def test_breakout_down(self):
|
||||||
"""3B. Users can't call actions beneath the actions directory."""
|
"""3B. Users can't call actions beneath the actions directory."""
|
||||||
action="directory/echo"
|
action="directory/echo"
|
||||||
|
|
||||||
self.assertRaises(ValueError, privilegedaction_run, action)
|
self.assertRaises(ValueError, superuser_run, action)
|
||||||
|
|
||||||
def test_breakout_actions(self):
|
def test_breakout_actions(self):
|
||||||
"""3C. Actions can't be used to run other actions.
|
"""3C. Actions can't be used to run other actions.
|
||||||
@ -78,14 +62,14 @@ class TestPrivileged(unittest.TestCase):
|
|||||||
for action in actions:
|
for action in actions:
|
||||||
for option in options:
|
for option in options:
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
output = privilegedaction_run(action, option)
|
output = run(action, option)
|
||||||
|
|
||||||
# if it somewhow doesn't error, we'd better not evaluate the
|
# if it somewhow doesn't error, we'd better not evaluate the
|
||||||
# data.
|
# data.
|
||||||
self.assertFalse("2" in output[0])
|
self.assertFalse("2" in output[0])
|
||||||
|
|
||||||
def test_breakout_option_string(self):
|
def test_breakout_option_string(self):
|
||||||
"""3D. Options can't be used to run other actions.
|
"""3D. Option strings can't be used to run other actions.
|
||||||
|
|
||||||
Verify that shell control characters aren't interpreted.
|
Verify that shell control characters aren't interpreted.
|
||||||
|
|
||||||
@ -94,12 +78,12 @@ class TestPrivileged(unittest.TestCase):
|
|||||||
# counting is safer than actual badness.
|
# counting is safer than actual badness.
|
||||||
options = "good; echo $((1+1))"
|
options = "good; echo $((1+1))"
|
||||||
|
|
||||||
output, error = privilegedaction_run(action, options)
|
output, error = run(action, options)
|
||||||
|
|
||||||
self.assertFalse("2" in output)
|
self.assertFalse("2" in output)
|
||||||
|
|
||||||
def test_breakout_option_list(self):
|
def test_breakout_option_list(self):
|
||||||
"""3D. Options can't be used to run other actions.
|
"""3D. Option lists can't be used to run other actions.
|
||||||
|
|
||||||
Verify that only a string of options is accepted and that we can't just
|
Verify that only a string of options is accepted and that we can't just
|
||||||
tack additional shell control characters onto the list.
|
tack additional shell control characters onto the list.
|
||||||
@ -109,10 +93,18 @@ class TestPrivileged(unittest.TestCase):
|
|||||||
# counting is safer than actual badness.
|
# counting is safer than actual badness.
|
||||||
options = ["good", ";", "echo $((1+1))"]
|
options = ["good", ";", "echo $((1+1))"]
|
||||||
|
|
||||||
output, error = privilegedaction_run(action, options)
|
output, error = run(action, options)
|
||||||
|
|
||||||
# we'd better not evaluate the data.
|
# we'd better not evaluate the data.
|
||||||
self.assertFalse("2" in output)
|
self.assertFalse("2" in output)
|
||||||
|
|
||||||
|
def test_multiple_options(self):
|
||||||
|
"""4. Multiple options can be provided as a list.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.assertEqual(
|
||||||
|
subprocess.check_output(shlex.split("id -ur")).strip(),
|
||||||
|
run("id", ["-u" ,"-r"])[0].strip())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user