mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 17:39:12 +00:00 
			
		
		
		
	Add API call for rendering scripts, bugfixes
This commit is contained in:
		| @@ -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 | ||||||
|  |  | ||||||
|     ############### |     ############### | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user