mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 09:29:13 +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 .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()) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 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 | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										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", | ||||
|         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 | ||||
|  | ||||
|     ############### | ||||
|   | ||||
		Reference in New Issue
	
	Block a user