Add API call for rendering scripts, bugfixes

This commit is contained in:
Lauri Võsandi 2017-05-04 17:56:53 +00:00
parent a75fb58cb5
commit de1d182320
10 changed files with 100 additions and 134 deletions

View File

@ -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())

View File

@ -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

View File

@ -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

21
certidude/api/script.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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
###############