mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Add API call for rendering scripts, bugfixes
This commit is contained in:
parent
a75fb58cb5
commit
de1d182320
@ -172,7 +172,7 @@ def certidude_app(log_handlers=[]):
|
|||||||
from .signed import SignedCertificateDetailResource
|
from .signed import SignedCertificateDetailResource
|
||||||
from .request import RequestListResource, RequestDetailResource
|
from .request import RequestListResource, RequestDetailResource
|
||||||
from .lease import LeaseResource, LeaseDetailResource
|
from .lease import LeaseResource, LeaseDetailResource
|
||||||
from .cfg import ConfigResource, ScriptResource
|
from .script import ScriptResource
|
||||||
from .tag import TagResource, TagDetailResource
|
from .tag import TagResource, TagDetailResource
|
||||||
from .attrib import AttributeResource
|
from .attrib import AttributeResource
|
||||||
from .bootstrap import BootstrapResource
|
from .bootstrap import BootstrapResource
|
||||||
@ -194,6 +194,7 @@ def certidude_app(log_handlers=[]):
|
|||||||
|
|
||||||
# Extended attributes for scripting etc.
|
# Extended attributes for scripting etc.
|
||||||
app.add_route("/api/signed/{cn}/attr/", AttributeResource())
|
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
|
# API calls used by pushed events on the JS end
|
||||||
app.add_route("/api/signed/{cn}/tag/", TagResource())
|
app.add_route("/api/signed/{cn}/tag/", TagResource())
|
||||||
|
@ -4,7 +4,6 @@ from ipaddress import ip_address
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from certidude import config, authority
|
from certidude import config, authority
|
||||||
from certidude.decorators import serialize
|
from certidude.decorators import serialize
|
||||||
from xattr import getxattr, listxattr
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -18,24 +17,10 @@ class AttributeResource(object):
|
|||||||
Results made available only to lease IP address.
|
Results made available only to lease IP address.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
path, buf, cert = authority.get_signed(cn)
|
path, buf, cert, attribs = authority.get_attributes(cn)
|
||||||
except IOError:
|
except IOError:
|
||||||
raise falcon.HTTPNotFound()
|
raise falcon.HTTPNotFound()
|
||||||
else:
|
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:
|
try:
|
||||||
whitelist = ip_address(attribs.get("user").get("lease").get("address").decode("ascii"))
|
whitelist = ip_address(attribs.get("user").get("lease").get("address").decode("ascii"))
|
||||||
except AttributeError: # TODO: probably race condition
|
except AttributeError: # TODO: probably race condition
|
||||||
|
@ -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
21
certidude/api/script.py
Normal 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
|
@ -15,6 +15,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
|||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude import errors
|
from certidude import errors
|
||||||
from jinja2 import Template
|
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]))?$"
|
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()
|
buf = fh.read()
|
||||||
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
|
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):
|
def store_request(buf, overwrite=False):
|
||||||
"""
|
"""
|
||||||
Store CSR for later processing
|
Store CSR for later processing
|
||||||
|
@ -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("-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")
|
@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):
|
def certidude_request(fork, renew, no_wait):
|
||||||
rpm("openssl")
|
# Here let's try to avoid compiling packages from scratch
|
||||||
apt("openssl")
|
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
|
import requests
|
||||||
from jinja2 import Environment, PackageLoader
|
from jinja2 import Environment, PackageLoader
|
||||||
env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
|
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("proto %s\n" % proto)
|
||||||
config.write("dev tun-%s\n" % remote.split(".")[0])
|
config.write("dev tun-%s\n" % remote.split(".")[0])
|
||||||
config.write("nobind\n")
|
config.write("nobind\n")
|
||||||
config.write("key %s\n" % paths.get("key path"))
|
config.write("key %s\n" % paths.get("key_path"))
|
||||||
config.write("cert %s\n" % paths.get("certificate path"))
|
config.write("cert %s\n" % paths.get("certificate_path"))
|
||||||
config.write("ca %s\n" % paths.get("authority path"))
|
config.write("ca %s\n" % paths.get("authority_path"))
|
||||||
config.write("crl-verify %s\n" % paths.get("revocations path"))
|
config.write("crl-verify %s\n" % paths.get("revocations_path"))
|
||||||
config.write("comp-lzo\n")
|
config.write("comp-lzo\n")
|
||||||
config.write("user nobody\n")
|
config.write("user nobody\n")
|
||||||
config.write("group nogroup\n")
|
config.write("group nogroup\n")
|
||||||
|
@ -102,3 +102,7 @@ TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to second
|
|||||||
TOKEN_SECRET = cp.get("token", "secret")
|
TOKEN_SECRET = cp.get("token", "secret")
|
||||||
|
|
||||||
# TODO: Check if we don't have base or servers
|
# 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"
|
||||||
|
@ -21,7 +21,7 @@ def certidude_request_certificate(server, system_keytab_required, key_path, requ
|
|||||||
Exchange CSR for certificate using Certidude HTTP API server
|
Exchange CSR for certificate using Certidude HTTP API server
|
||||||
"""
|
"""
|
||||||
import requests
|
import requests
|
||||||
from certidude import errors, const, config
|
from certidude import errors, const
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
38
certidude/templates/script/openwrt.sh
Normal file
38
certidude/templates/script/openwrt.sh
Normal 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
|
||||||
|
|
@ -389,6 +389,10 @@ def test_cli_setup_authority():
|
|||||||
query_string = "client=test&address=127.0.0.1",
|
query_string = "client=test&address=127.0.0.1",
|
||||||
headers={"Authorization":admintoken})
|
headers={"Authorization":admintoken})
|
||||||
assert r.status_code == 200, r.text # lease update ok
|
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/",
|
r = client().simulate_post("/api/lease/",
|
||||||
query_string = "client=test&address=127.0.0.1&serial=0",
|
query_string = "client=test&address=127.0.0.1&serial=0",
|
||||||
headers={"Authorization":admintoken})
|
headers={"Authorization":admintoken})
|
||||||
@ -646,6 +650,7 @@ def test_cli_setup_authority():
|
|||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert "Writing certificate to:" in result.output, 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
|
# TODO: test client verification with curl
|
||||||
|
|
||||||
###############
|
###############
|
||||||
|
Loading…
Reference in New Issue
Block a user