diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index fb905a9..8945e16 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -172,7 +172,7 @@ def certidude_app(log_handlers=[]): from .signed import SignedCertificateDetailResource from .request import RequestListResource, RequestDetailResource from .lease import LeaseResource, LeaseDetailResource - from .cfg import ConfigResource, ScriptResource + from .script import ScriptResource from .tag import TagResource, TagDetailResource from .attrib import AttributeResource from .bootstrap import BootstrapResource @@ -194,6 +194,7 @@ def certidude_app(log_handlers=[]): # Extended attributes for scripting etc. app.add_route("/api/signed/{cn}/attr/", AttributeResource()) + app.add_route("/api/signed/{cn}/script/", ScriptResource()) # API calls used by pushed events on the JS end app.add_route("/api/signed/{cn}/tag/", TagResource()) diff --git a/certidude/api/attrib.py b/certidude/api/attrib.py index 53465a8..3b611c6 100644 --- a/certidude/api/attrib.py +++ b/certidude/api/attrib.py @@ -4,7 +4,6 @@ from ipaddress import ip_address from datetime import datetime from certidude import config, authority from certidude.decorators import serialize -from xattr import getxattr, listxattr logger = logging.getLogger(__name__) @@ -18,24 +17,10 @@ class AttributeResource(object): Results made available only to lease IP address. """ try: - path, buf, cert = authority.get_signed(cn) + path, buf, cert, attribs = authority.get_attributes(cn) except IOError: raise falcon.HTTPNotFound() else: - attribs = dict() - for key in listxattr(path): - if not key.startswith("user."): - continue - value = getxattr(path, key) - current = attribs - if "." in key: - namespace, key = key.rsplit(".", 1) - for component in namespace.split("."): - if component not in current: - current[component] = dict() - current = current[component] - current[key] = value - try: whitelist = ip_address(attribs.get("user").get("lease").get("address").decode("ascii")) except AttributeError: # TODO: probably race condition diff --git a/certidude/api/cfg.py b/certidude/api/cfg.py deleted file mode 100644 index 092e265..0000000 --- a/certidude/api/cfg.py +++ /dev/null @@ -1,110 +0,0 @@ -import falcon -import logging -import ipaddress -import string -from random import choice -from certidude import config -from certidude.auth import login_required, authorize_admin -from certidude.decorators import serialize -from certidude.relational import RelationalMixin -from jinja2 import Environment, FileSystemLoader - -logger = logging.getLogger(__name__) -env = Environment(loader=FileSystemLoader("/etc/certidude/scripts"), trim_blocks=True) - -SQL_SELECT_INHERITED = """ -select `key`, `value` from tag_inheritance where tag_id in (select - tag.id -from - device_tag -join - tag on device_tag.tag_id = tag.id -join - device on device_tag.device_id = device.id -where - device.cn = %s) -""" - -SQL_SELECT_TAGS = """ -select - tag.`key` as `key`, - tag.`value` as `value` -from - device_tag -join - tag on device_tag.tag_id = tag.id -join - device on device_tag.device_id = device.id -""" - - -SQL_SELECT_RULES = """ -select - tag.cn as `cn`, - tag.key as `tag_key`, - tag.value as `tag_value`, - tag_properties.property_key as `property_key`, - tag_properties.property_value as `property_value` -from - tag_properties -join - tag -on - tag.key = tag_properties.tag_key and - tag.value = tag_properties.tag_value -""" - - -class ConfigResource(RelationalMixin): - @serialize - @login_required - @authorize_admin - def on_get(self, req, resp): - return self.iterfetch(SQL_SELECT_TAGS) - - -class ScriptResource(RelationalMixin): - def on_get(self, req, resp): - from certidude.api.whois import address_to_identity - - node = address_to_identity( - self.connect(), - req.context.get("remote_addr") - ) - if not node: - resp.body = "Could not map IP address: %s" % req.context.get("remote_addr") - resp.status = falcon.HTTP_404 - return - - address, acquired, identity = node - - key, common_name = identity.split("=") - assert "=" not in common_name - - conn = self.connect() - cursor = conn.cursor() - - resp.set_header("Content-Type", "text/x-shellscript") - - args = common_name, - ctx = dict() - - for query in SQL_SELECT_INHERITED, SQL_SELECT_TAGS: - cursor.execute(query, args) - - for key, value in cursor: - current = ctx - if "." in key: - path, key = key.rsplit(".", 1) - - for component in path.split("."): - if component not in current: - current[component] = dict() - current = current[component] - current[key] = value - cursor.close() - conn.close() - - resp.body = env.get_template("uci.sh").render(ctx) - - # TODO: Assert time is within reasonable range diff --git a/certidude/api/script.py b/certidude/api/script.py new file mode 100644 index 0000000..fefa8c2 --- /dev/null +++ b/certidude/api/script.py @@ -0,0 +1,21 @@ +import falcon +import logging +from certidude import config, authority +from certidude.decorators import serialize +from jinja2 import Environment, FileSystemLoader + +logger = logging.getLogger(__name__) +env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) + +class ScriptResource(): + def on_get(self, req, resp, cn): + + try: + path, buf, cert, attribs = authority.get_attributes(cn) + except IOError: + raise falcon.HTTPNotFound() + else: + resp.set_header("Content-Type", "text/x-shellscript") + resp.body = env.get_template(config.SCRIPT_DEFAULT).render(attributes=attribs) + + # TODO: Assert time is within reasonable range diff --git a/certidude/authority.py b/certidude/authority.py index bdaa1b9..5b1354b 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -15,6 +15,7 @@ from cryptography.hazmat.primitives import hashes, serialization from certidude import config, push, mailer, const from certidude import errors from jinja2 import Template +from xattr import getxattr, listxattr RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" @@ -50,6 +51,25 @@ def get_revoked(serial): buf = fh.read() return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) + +def get_attributes(cn): + path, buf, cert = get_signed(cn) + attribs = dict() + for key in listxattr(path): + if not key.startswith("user."): + continue + value = getxattr(path, key) + current = attribs + if "." in key: + namespace, key = key.rsplit(".", 1) + for component in namespace.split("."): + if component not in current: + current[component] = dict() + current = current[component] + current[key] = value + return path, buf, cert, attribs + + def store_request(buf, overwrite=False): """ Store CSR for later processing diff --git a/certidude/cli.py b/certidude/cli.py index 5483f6c..7ba657c 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -91,8 +91,10 @@ def setup_client(prefix="client_", dh=False): @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") @click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign") def certidude_request(fork, renew, no_wait): - rpm("openssl") - apt("openssl") + # Here let's try to avoid compiling packages from scratch + rpm("openssl") # TODO + apt("openssl python-cryptography python-jinja2") # Native packages on Ubuntu 16.04 + pip("cryptography jinja2") # Mac OS X, should be skipped on Ubuntu import requests from jinja2 import Environment, PackageLoader env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) @@ -529,10 +531,10 @@ def certidude_setup_openvpn_client(authority, remote, common_name, config, proto config.write("proto %s\n" % proto) config.write("dev tun-%s\n" % remote.split(".")[0]) config.write("nobind\n") - config.write("key %s\n" % paths.get("key path")) - config.write("cert %s\n" % paths.get("certificate path")) - config.write("ca %s\n" % paths.get("authority path")) - config.write("crl-verify %s\n" % paths.get("revocations path")) + config.write("key %s\n" % paths.get("key_path")) + config.write("cert %s\n" % paths.get("certificate_path")) + config.write("ca %s\n" % paths.get("authority_path")) + config.write("crl-verify %s\n" % paths.get("revocations_path")) config.write("comp-lzo\n") config.write("user nobody\n") config.write("group nogroup\n") diff --git a/certidude/config.py b/certidude/config.py index b7dea21..526a797 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -102,3 +102,7 @@ TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to second TOKEN_SECRET = cp.get("token", "secret") # TODO: Check if we don't have base or servers + +# The API call for looking up scripts uses following directory as root +SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script") +SCRIPT_DEFAULT = "openwrt.sh" diff --git a/certidude/helpers.py b/certidude/helpers.py index 6241812..b3b4a0c 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -21,7 +21,7 @@ def certidude_request_certificate(server, system_keytab_required, key_path, requ Exchange CSR for certificate using Certidude HTTP API server """ import requests - from certidude import errors, const, config + from certidude import errors, const from cryptography import x509 from cryptography.hazmat.primitives.asymmetric import rsa, padding from cryptography.hazmat.backends import default_backend diff --git a/certidude/templates/script/openwrt.sh b/certidude/templates/script/openwrt.sh new file mode 100644 index 0000000..78045c7 --- /dev/null +++ b/certidude/templates/script/openwrt.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# This script can executed on a preconfigured OpenWrt box +# https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html + +# Password protected wireless area +for band in 2ghz 5ghz; do + uci set wireless.lan$band=wifi-iface + uci set wireless.lan$band.network=lan + uci set wireless.lan$band.mode=ap + uci set wireless.lan$band.device=radio$band + uci set wireless.lan$band.encryption=psk2 + {% if attributes.protected and attributes.protected.ssid %} + uci set wireless.lan$band.ssid={{ attrbutes.protected.ssid }} + {% else %} + uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected + {% endif %} + {% if attributes.protected and attributes.protected.psk %} + uci set wireless.lan$band.key={{ attributes.protected.psk }} + {% else %} + uci set wireless.lan$band.key=salakala + {% endif %} +done + +# Public wireless area +for band in 2ghz 5ghz; do + uci set wireless.guest$band=wifi-iface + uci set wireless.guest$band.network=guest + uci set wireless.guest$band.mode=ap + uci set wireless.guest$band.device=radio$band + uci set wireless.guest$band.encryption=none + {% if attributes.public and attributes.public.ssid %} + uci set wireless.guest$band.ssid={{ attrbutes.public.ssid }} + {% else %} + uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public + {% endif %} +done + diff --git a/tests/test_cli.py b/tests/test_cli.py index 9cc8502..d2e52fd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -389,6 +389,10 @@ def test_cli_setup_authority(): query_string = "client=test&address=127.0.0.1", headers={"Authorization":admintoken}) assert r.status_code == 200, r.text # lease update ok + r = client().simulate_get("/api/signed/test/script/") + assert r.status_code == 200, r.text # script render ok + assert "uci set " in r.text, r.text + r = client().simulate_post("/api/lease/", query_string = "client=test&address=127.0.0.1&serial=0", headers={"Authorization":admintoken}) @@ -646,6 +650,7 @@ def test_cli_setup_authority(): assert not result.exception, result.output assert "Writing certificate to:" in result.output, result.output + # TODO: assert key, req, cert paths were included correctly in OpenVPN config # TODO: test client verification with curl ###############