From 925bc0ef9a4ce1fcf2b6230a5b44a87b5d395faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Sun, 27 Mar 2016 23:38:14 +0300 Subject: [PATCH] Refactor users, add OpenVPN and mailing support * Add abstraction for user objects * Mail authority admins about pending, revoked and signed certificates * Add NetworkManager's OpenVPN plugin support * Improve CRL support * Refactor CSRF protection * Update documentation --- README.rst | 117 ++++---- certidude/api/__init__.py | 41 +-- certidude/api/request.py | 9 +- certidude/api/signed.py | 9 +- certidude/api/tag.py | 5 +- certidude/auth.py | 235 ++++----------- certidude/authority.py | 19 +- certidude/cli.py | 273 ++++++++++-------- certidude/config.py | 18 +- certidude/constants.py | 1 + certidude/decorators.py | 14 +- certidude/helpers.py | 118 ++++++-- certidude/mailer.py | 12 +- certidude/static/js/templates.js | 173 +++++------ certidude/static/views/authority.html | 18 +- certidude/templates/certidude.conf | 2 +- .../templates/mail/certificate-revoked.md | 6 + certidude/templates/mail/request-stored.md | 5 + certidude/templates/nginx-https-site.conf | 1 + certidude/templates/nginx.conf | 5 +- .../templates/openvpn-client-to-site.ovpn | 5 +- .../templates/openvpn-site-to-client.ovpn | 5 +- certidude/templates/uwsgi.ini | 2 +- certidude/user.py | 165 +++++++++++ requirements.txt | 1 - 25 files changed, 695 insertions(+), 564 deletions(-) create mode 100644 certidude/templates/mail/certificate-revoked.md create mode 100644 certidude/templates/mail/request-stored.md create mode 100644 certidude/user.py diff --git a/README.rst b/README.rst index 435971f..a7fcf92 100644 --- a/README.rst +++ b/README.rst @@ -17,8 +17,9 @@ eventually support PKCS#11 and in far future WebCrypto. .. figure:: doc/usecase-diagram.png -Certidude is mainly designed for VPN gateway operators to make VPN adoption usage -as simple as possible. +Certidude is mainly designed for VPN gateway operators to make +desktop/laptop VPN setup as easy as possible. +User certificate management eg. for HTTPS is also made reasonably simple. For a full-blown CA you might want to take a look at `EJBCA `_ or `OpenCA `_. @@ -27,24 +28,29 @@ For a full-blown CA you might want to take a look at Features -------- +Common: + * Standard request, sign, revoke workflow via web interface. -* Colored command-line interface, check out ``certidude list``. -* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``. -* strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``. +* Kerberos and basic auth based web interface authentication. +* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. +* POSIX groups and Active Directory (LDAP) group membership based authorization. +* Command-line interface, check out ``certidude list``. * Privilege isolation, separate signer process is spawned per private key isolating private key use from the the web interface. -* Certificate numbering obfuscation, certificate serial numbers are intentionally - randomized to avoid leaking information about business practices. -* Server-side events support via for example nginx-push-stream-module. -* Kerberos based web interface authentication. -* File based whitelist authorization, easy to integrate with LDAP as shown below. +* Certificate serial numbers are intentionally randomized to avoid leaking information about business practices. +* Server-side events support via `nchan `_. +* E-mail notifications about pending, signed and revoked certificates. +Virtual private networking: -Coming soon ------------ +* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``. +* strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``. +* NetworkManager integration, check out ``certidude setup openvpn networkmanager`` and ``certidude setup strongswan networkmanager``. -* Refactor mailing subsystem and server-side events to use hooks. -* Notifications via e-mail. +HTTPS: + +* P12 bundle generation for web browsers, seems to work well with Android +* HTTPS server setup with client verification, check out ``certidude setup nginx`` TODO @@ -60,6 +66,7 @@ TODO * Cronjob for deleting expired certificates * Signer process logging. + Install ------- @@ -67,15 +74,15 @@ To install Certidude: .. code:: bash - apt-get install -y python python-pip python-dev cython \ + apt-get install -y python python-pip python-dev cython python-configparser \ python-pysqlite2 python-mysql.connector python-ldap \ build-essential libffi-dev libssl-dev libkrb5-dev \ ldap-utils krb5-user default-mta \ libsasl2-modules-gssapi-mit pip3 install certidude -Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, -not the outdated ones provided by APT. +Make sure you're running PyOpenSSL 0.15+ from PyPI, +not the outdated one provided by APT. Create a system user for ``certidude``: @@ -85,10 +92,10 @@ Create a system user for ``certidude``: mkdir /etc/certidude -Setting up CA --------------- +Setting up authority +-------------------- -First make sure the machine used for CA has fully qualified +First make sure the machine used for certificate authority has fully qualified domain name set up properly. You can check it with: @@ -96,10 +103,10 @@ You can check it with: hostname -f -The command should return ca.example.co +The command should return ca.example.com -Certidude can set up CA relatively easily, following will set up -CA in /var/lib/certidude/hostname.domain.tld: +Certidude can set up certificate authority relatively easily, +following will set up certificate authority in /var/lib/certidude/hostname.domain.tld: .. code:: bash @@ -176,6 +183,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl env = LANG=C.UTF-8 env = LC_ALL=C.UTF-8 env = KRB5_KTNAME=/etc/certidude/server.keytab + env = KRB5CCNAME=/run/certidude/krb5cc Also enable the application: @@ -183,7 +191,7 @@ Also enable the application: ln -s ../apps-available/certidude.ini /etc/uwsgi/apps-enabled/certidude.ini -We support `nginx-push-stream-module `_, +We support `nchan `_, configure the site in /etc/nginx/sites-available/certidude: .. code:: @@ -238,11 +246,9 @@ Also adjust ``/etc/nginx/nginx.conf``: events { worker_connections 768; - # multi_accept on; } http { - push_stream_shared_memory_size 32M; sendfile on; tcp_nopush on; tcp_nodelay on; @@ -254,10 +260,12 @@ Also adjust ``/etc/nginx/nginx.conf``: error_log /var/log/nginx/error.log; gzip on; gzip_disable "msie6"; + include /etc/nginx/conf.d/*; include /etc/nginx/sites-enabled/*; } -In your CA ssl.cnf make sure Certidude is aware of your nginx setup: +In your Certidude server's /etc/certidude/server.conf make sure Certidude +is aware of your nginx setup: .. code:: @@ -309,8 +317,6 @@ Reset Kerberos configuration in ``/etc/krb5.conf``: default_realm = EXAMPLE.LAN dns_lookup_realm = true dns_lookup_kdc = true - forwardable = true - proxiable = true Initialize Kerberos credentials: @@ -332,43 +338,25 @@ Set up Kerberos keytab for the web service: chown root:certidude /etc/certidude/server.keytab chmod 640 /etc/certidude/server.keytab +Reconfigure /etc/certidude/server.conf: -Setting up authorization ------------------------- +.. code:: ini -Obviously arbitrary Kerberos authenticated user should not have access to -the CA web interface. -You could either specify user name list -in ``/etc/ssl/openssl.cnf``: + [authentication] + backends = kerberos -.. code:: bash + [authorization] + backend = ldap + ldap gssapi credential cache = /run/certidude/krb5cc + ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s)) + ldap admin filter = (&(objectclass=user)(objectclass=person)(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com)(samaccountname=%s)) - admin_users=alice bob john kate - -Or alternatively specify file path: - -.. code:: bash - - admin_users=/run/certidude/user.whitelist - -Use following shell snippets eg in ``/etc/cron.hourly/update-certidude-user-whitelist`` -to generate user whitelist via LDAP: - -.. code:: bash - - ldapsearch -H ldap://dc1.example.com -s sub -x -LLL \ - -D 'cn=certidude,cn=Users,dc=example,dc=com' \ - -w 'certidudepass' \ - -b 'dc=example,dc=com' \ - '(&(objectClass=user)(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com))' sAMAccountName userPrincipalName givenName sn \ - | python3 -c "import ldif3; import sys; [sys.stdout.write('%s:%s:%s:%s\n' % (a.pop('sAMAccountName')[0], a.pop('userPrincipalName')[0], a.pop('givenName')[0], a.pop('sn')[0])) for _, a in ldif3.LDIFParser(sys.stdin.buffer).parse()]" \ - > /run/certidude/user.whitelist - -Set permissions: - -.. code:: bash - - chmod 700 /etc/cron.hourly/update-certidude-user-whitelist +User filter here specified which users can log in to Certidude web interface +at all eg. for generating user certificates for HTTPS. +Admin filter specifies which users are allowed to sign and revoke certificates. +Adjust admin filter according to your setup. +Also make sure there is cron.hourly job for creating GSSAPI credential cache - +that's necessary for querying LDAP using Certidude machine's credentials. Automating certificate setup @@ -384,7 +372,7 @@ Create ``/etc/NetworkManager/dispatcher.d/certidude`` with following content: case "$2" in up) - LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager ca.example.com gateway.example.com + LANG=C.UTF-8 /usr/local/bin/certidude request spawn -k ;; esac @@ -397,8 +385,7 @@ Finally make it executable: Whenever a wired or wireless connection is brought up, the dispatcher invokes ``certidude`` in order to generate RSA keys, submit CSR, fetch signed certificate, -create NetworkManager configuration for the VPN connection and -finally to bring up the VPN tunnel as well. +create NetworkManager configuration for the VPN connection. Development diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 547ab52..98619be 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -9,6 +9,7 @@ from datetime import datetime from time import sleep from certidude import authority, mailer from certidude.auth import login_required, authorize_admin +from certidude.user import User from certidude.decorators import serialize, event_source, csrf_protection from certidude.wrappers import Request, Certificate from certidude import constants, config @@ -33,34 +34,16 @@ class CertificateAuthorityResource(object): logger.info("Served CA certificate to %s", req.context.get("remote_addr")) resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") resp.append_header("Content-Type", "application/x-x509-ca-cert") - resp.append_header("Content-Disposition", "attachment; filename=ca.crt") + resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % + constants.HOSTNAME.encode("ascii")) class SessionResource(object): + @csrf_protection @serialize @login_required - @authorize_admin @event_source def on_get(self, req, resp): - if config.ACCOUNTS_BACKEND == "ldap": - import ldap - ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*") - r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, - ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"]) - - for dn,entry in r: - cn, = entry.get("cn") - break - else: - raise ValueError("Failed to look up group %s in LDAP" % repr(group_name)) - - admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")]) - elif config.ACCOUNTS_BACKEND == "posix": - import grp - _, _, gid, members = grp.getgrnam(config.ADMINS_GROUP) - admins = dict([(j, j) for j in members]) - else: - raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) return dict( user = dict( @@ -72,12 +55,6 @@ class SessionResource(object): request_submission_allowed = sum( # Dirty hack! [req.context.get("remote_addr") in j for j in config.REQUEST_SUBNETS]), - user_subnets = config.USER_SUBNETS, - autosign_subnets = config.AUTOSIGN_SUBNETS, - request_subnets = config.REQUEST_SUBNETS, - admin_subnets=config.ADMIN_SUBNETS, - admin_users = admins, - #admin_users=config.ADMIN_USERS, authority = dict( outbox = config.OUTBOX, certificate = authority.certificate, @@ -85,7 +62,12 @@ class SessionResource(object): requests=authority.list_requests(), signed=authority.list_signed(), revoked=authority.list_revoked(), - ) if config.ADMINS_GROUP in req.context.get("groups") else None, + admin_users = User.objects.filter_admins(), + user_subnets = config.USER_SUBNETS, + autosign_subnets = config.AUTOSIGN_SUBNETS, + request_subnets = config.REQUEST_SUBNETS, + admin_subnets=config.ADMIN_SUBNETS, + ) if req.context.get("user").is_admin() else None, features=dict( tagging=config.TAGGING_BACKEND, leases=False, #config.LEASES_BACKEND, @@ -124,7 +106,7 @@ class BundleResource(object): common_name = req.context["user"].mail logger.info("Signing bundle %s for %s", common_name, req.context.get("user")) resp.set_header("Content-Type", "application/x-pkcs12") - resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name) + resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) resp.body, cert = authority.generate_pkcs12_bundle(common_name, owner=req.context.get("user")) @@ -132,7 +114,6 @@ class BundleResource(object): import ipaddress class NormalizeMiddleware(object): - @csrf_protection def process_request(self, req, resp, *args): assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) diff --git a/certidude/api/request.py b/certidude/api/request.py index 6462b49..1188918 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -6,7 +6,7 @@ import ipaddress import os from certidude import config, authority, helpers, push, errors from certidude.auth import login_required, login_optional, authorize_admin -from certidude.decorators import serialize +from certidude.decorators import serialize, csrf_protection from certidude.wrappers import Request, Certificate from certidude.firewall import whitelist_subnets, whitelist_content_types @@ -19,6 +19,7 @@ class RequestListResource(object): def on_get(self, req, resp): return authority.list_requests() + @login_optional @whitelist_subnets(config.REQUEST_SUBNETS) @whitelist_content_types("application/pkcs10") @@ -53,7 +54,7 @@ class RequestListResource(object): # Process automatic signing if the IP address is whitelisted and autosigning was requested if req.get_param_as_bool("autosign"): for subnet in config.AUTOSIGN_SUBNETS: - if subnet.overlaps(req.context.get("remote_addr")): + if req.context.get("remote_addr") in subnet: try: resp.set_header("Content-Type", "application/x-x509-user-cert") resp.body = authority.sign(csr).dump() @@ -103,6 +104,8 @@ class RequestDetailResource(object): csr.common_name, req.context.get("remote_addr")) return csr + + @csrf_protection @login_required @authorize_admin def on_patch(self, req, resp, cn): @@ -118,6 +121,8 @@ class RequestDetailResource(object): logger.info("Signing request %s signed by %s from %s", csr.common_name, req.context.get("user"), req.context.get("remote_addr")) + + @csrf_protection @login_required @authorize_admin def on_delete(self, req, resp, cn): diff --git a/certidude/api/signed.py b/certidude/api/signed.py index 36adcb4..14319c3 100644 --- a/certidude/api/signed.py +++ b/certidude/api/signed.py @@ -3,7 +3,7 @@ import falcon import logging from certidude import authority from certidude.auth import login_required, authorize_admin -from certidude.decorators import serialize +from certidude.decorators import serialize, csrf_protection logger = logging.getLogger("api") @@ -24,20 +24,21 @@ class SignedCertificateDetailResource(object): try: cert = authority.get_signed(cn) except EnvironmentError: - logger.warning("Failed to serve non-existant certificate %s to %s", + logger.warning(u"Failed to serve non-existant certificate %s to %s", cn, req.context.get("remote_addr")) resp.body = "No certificate CN=%s found" % cn raise falcon.HTTPNotFound() else: - logger.debug("Served certificate %s to %s", + logger.debug(u"Served certificate %s to %s", cn, req.context.get("remote_addr")) return cert + @csrf_protection @login_required @authorize_admin def on_delete(self, req, resp, cn): - logger.info("Revoked certificate %s by %s from %s", + logger.info(u"Revoked certificate %s by %s from %s", cn, req.context.get("user"), req.context.get("remote_addr")) authority.revoke_certificate(cn) diff --git a/certidude/api/tag.py b/certidude/api/tag.py index 8009370..48b6ee7 100644 --- a/certidude/api/tag.py +++ b/certidude/api/tag.py @@ -3,7 +3,7 @@ import falcon import logging from certidude.relational import RelationalMixin from certidude.auth import login_required, authorize_admin -from certidude.decorators import serialize +from certidude.decorators import serialize, csrf_protection logger = logging.getLogger("api") @@ -17,6 +17,7 @@ class TagResource(RelationalMixin): return self.iterfetch("select * from tag") + @csrf_protection @serialize @login_required @authorize_admin @@ -51,6 +52,7 @@ class TagDetailResource(RelationalMixin): raise falcon.HTTPNotFound() + @csrf_protection @serialize @login_required @authorize_admin @@ -63,6 +65,7 @@ class TagDetailResource(RelationalMixin): push.publish("tag-updated", identifier) + @csrf_protection @serialize @login_required @authorize_admin diff --git a/certidude/auth.py b/certidude/auth.py index ea18622..20bd96b 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -6,14 +6,13 @@ import logging import os import re import socket +from certidude.user import User from certidude.firewall import whitelist_subnets from certidude import config, constants logger = logging.getLogger("api") -FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] - -if config.AUTHENTICATION_BACKENDS == {"kerberos"}: +if "kerberos" in config.AUTHENTICATION_BACKENDS: ktname = os.getenv("KRB5_KTNAME") if not ktname: @@ -24,139 +23,13 @@ if config.AUTHENTICATION_BACKENDS == {"kerberos"}: exit(248) try: - principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) + principal = kerberos.getServerPrincipalDetails("HTTP", constants.FQDN) except kerberos.KrbError as exc: - click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True) + click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % ( + constants.FQDN, exc), err=True) exit(249) else: - click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) - - -class User(object): - def __init__(self, name): - if "@" in name: - self.mail = name - self.name, self.domain = name.split("@") - else: - self.mail = None - self.name, self.domain = name, None - self.given_name, self.surname = None, None - - def __repr__(self): - if self.given_name and self.surname: - return u"%s %s <%s>" % (self.given_name, self.surname, self.mail) - else: - return self.mail - - -def member_of(group_name): - """ - Check if requesting user is member of an UNIX group - """ - - def wrapper(func): - def posix_check_group_membership(resource, req, resp, *args, **kwargs): - import grp - _, _, gid, members = grp.getgrnam(group_name) - if req.context.get("user").name not in members: - logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name) - raise falcon.HTTPForbidden("Forbidden", "User not member of designated group") - req.context.get("groups").add(group_name) - return func(resource, req, resp, *args, **kwargs) - - def ldap_check_group_membership(resource, req, resp, *args, **kwargs): - import ldap - - ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn) - r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, - ft.encode("utf-8"), - ["member"]) - - for dn,entry in r: - if not dn: continue - logger.debug("User %s is member of group %s" % ( - req.context.get("user"), repr(group_name))) - req.context.get("groups").add(group_name) - break - else: - raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name)) - - return func(resource, req, resp, *args, **kwargs) - - if config.AUTHORIZATION_BACKEND == "ldap": - return ldap_check_group_membership - elif config.AUTHORIZATION_BACKEND == "posix": - return posix_check_group_membership - else: - raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) - return wrapper - - -def account_info(func): - # TODO: Use Privilege Account Certificate for Kerberos - - def posix_account_info(resource, req, resp, *args, **kwargs): - import pwd - _, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name) - gecos = gecos.decode("utf-8").split(",") - full_name = gecos[0] - if full_name and " " in full_name: - req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1) - req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN - return func(resource, req, resp, *args, **kwargs) - - def ldap_account_info(resource, req, resp, *args, **kwargs): - import ldap - import ldap.sasl - - if "ldap_conn" not in req.context: - for server in config.LDAP_SERVERS: - conn = ldap.initialize(server) - conn.set_option(ldap.OPT_REFERRALS, 0) - if os.path.exists("/etc/krb5.keytab"): - ticket_cache = os.getenv("KRB5CCNAME") - if not ticket_cache: - raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!") - click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache)) - conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) - else: - raise NotImplementedError("LDAP simple bind not supported, use Kerberos") - req.context["ldap_conn"] = conn - break - else: - raise ValueError("No LDAP servers!") - - ft = config.LDAP_USER_FILTER % req.context.get("user").name - r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, - ft, - ["cn", "givenname", "sn", "mail", "userPrincipalName"]) - - for dn, entry in r: - if not dn: continue - if entry.get("givenname") and entry.get("sn"): - given_name, = entry.get("givenName") - surname, = entry.get("sn") - req.context["user"].given_name = given_name.decode("utf-8") - req.context["user"].surname = surname.decode("utf-8") - else: - cn, = entry.get("cn") - if " " in cn: - req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1) - - req.context["user"].dn = dn.decode("utf-8") - req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,) - retval = func(resource, req, resp, *args, **kwargs) - req.context.get("ldap_conn").unbind_s() - return retval - else: - raise ValueError("Failed to look up %s in LDAP" % req.context.get("user")) - - if config.ACCOUNTS_BACKEND == "ldap": - return ldap_account_info - elif config.ACCOUNTS_BACKEND == "posix": - return posix_account_info - else: - raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND) + click.echo("Kerberos enabled, service principal is HTTP/%s" % constants.FQDN) def authenticate(optional=False): @@ -167,7 +40,7 @@ def authenticate(optional=False): if not req.auth: resp.append_header("WWW-Authenticate", "Negotiate") - logger.debug("No Kerberos ticket offered while attempting to access %s from %s", + logger.debug(u"No Kerberos ticket offered while attempting to access %s from %s", req.env["PATH_INFO"], req.context.get("remote_addr")) raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?") @@ -175,7 +48,7 @@ def authenticate(optional=False): token = ''.join(req.auth.split()[1:]) try: - result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) + result, context = kerberos.authGSSServerInit("HTTP@" + constants.FQDN) except kerberos.GSSError as ex: # TODO: logger.error raise falcon.HTTPForbidden("Forbidden", @@ -185,34 +58,46 @@ def authenticate(optional=False): result = kerberos.authGSSServerStep(context, token) except kerberos.GSSError as ex: kerberos.authGSSServerClean(context) - # TODO: logger.error + logger.error(u"Kerberos authentication failed from %s. " + "Bad credentials: %s (%d)", + req.context.get("remote_addr"), + ex.args[0][0], ex.args[0][1]) raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1])) except kerberos.KrbError as ex: kerberos.authGSSServerClean(context) - # TODO: logger.error + logger.error(u"Kerberos authentication failed from %s. " + "Bad credentials: %s (%d)", + req.context.get("remote_addr"), + ex.args[0][0], ex.args[0][1]) raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex.args[0],)) user = kerberos.authGSSServerUserName(context) - req.context["user"] = User(user) - req.context["groups"] = set() + req.context["user"] = User.objects.get(user) try: kerberos.authGSSServerClean(context) except kerberos.GSSError as ex: - # TODO: logger.error + logger.error(u"Kerberos authentication failed for user %s from %s. " + "Authentication system failure: %s (%d)", + user, req.context.get("remote_addr"), + ex.args[0][0], ex.args[0][1]) raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0])) if result == kerberos.AUTH_GSS_COMPLETE: - logger.debug("Succesfully authenticated user %s for %s from %s", + logger.debug(u"Succesfully authenticated user %s for %s from %s", req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"]) - return account_info(func)(resource, req, resp, *args, **kwargs) + return func(resource, req, resp, *args, **kwargs) elif result == kerberos.AUTH_GSS_CONTINUE: - # TODO: logger.error + logger.error(u"Kerberos authentication failed for user %s from %s. " + "Unauthorized, tried GSSAPI.", + user, req.context.get("remote_addr")) raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") else: - # TODO: logger.error + logger.error(u"Kerberos authentication failed for user %s from %s. " + "Forbidden, tried GSSAPI.", + user, req.context.get("remote_addr")) raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") @@ -238,27 +123,26 @@ def authenticate(optional=False): basic, token = req.auth.split(" ", 1) user, passwd = b64decode(token).split(":", 1) - if "ldap_conn" not in req.context: - for server in config.LDAP_SERVERS: - click.echo("Connecting to %s as %s" % (server, user)) - conn = ldap.initialize(server) - conn.set_option(ldap.OPT_REFERRALS, 0) - try: - conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd) - except ldap.LDAPError, e: - resp.append_header("WWW-Authenticate", "Basic") - logger.debug("Failed to authenticate with user '%s'", user) - raise falcon.HTTPUnauthorized("Forbidden", - "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN) + for server in config.LDAP_SERVERS: + click.echo("Connecting to %s as %s" % (server, user)) + conn = ldap.initialize(server) + conn.set_option(ldap.OPT_REFERRALS, 0) + try: + conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd) + except ldap.LDAPError, e: + resp.append_header("WWW-Authenticate", "Basic") + logger.critical("LDAP bind authentication failed for user %s from %s", + repr(user), req.context.get("remote_addr")) + raise falcon.HTTPUnauthorized("Forbidden", + "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN) - req.context["ldap_conn"] = conn - break - else: - raise ValueError("No LDAP servers!") + req.context["ldap_conn"] = conn + break + else: + raise ValueError("No LDAP servers!") - req.context["user"] = User(user) - req.context["groups"] = set() - return account_info(func)(resource, req, resp, *args, **kwargs) + req.context["user"] = User.objects.get(user) + return func(resource, req, resp, *args, **kwargs) def pam_authenticate(resource, req, resp, *args, **kwargs): @@ -282,11 +166,12 @@ def authenticate(optional=False): import simplepam if not simplepam.authenticate(user, passwd, "sshd"): + logger.critical("Basic authentication failed for user %s from %s", + repr(user), req.context.get("remote_addr")) raise falcon.HTTPUnauthorized("Forbidden", "Invalid password") - req.context["user"] = User(user) - req.context["groups"] = set() - return account_info(func)(resource, req, resp, *args, **kwargs) + req.context["user"] = User.objects.get(user) + return func(resource, req, resp, *args, **kwargs) if config.AUTHENTICATION_BACKENDS == {"kerberos"}: return kerberos_authenticate @@ -302,14 +187,11 @@ def authenticate(optional=False): def login_required(func): return authenticate()(func) - def login_optional(func): return authenticate(optional=True)(func) - def authorize_admin(func): - - def whitelist_authorize(resource, req, resp, *args, **kwargs): + def whitelist_authorize_admin(resource, req, resp, *args, **kwargs): # Check for username whitelist if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST: logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", @@ -317,8 +199,13 @@ def authorize_admin(func): raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) return func(resource, req, resp, *args, **kwargs) - if config.AUTHORIZATION_BACKEND == "whitelist": - return whitelist_authorize - else: - return member_of(config.ADMINS_GROUP)(func) + def authorize_admin(resource, req, resp, *args, **kwargs): + if req.context.get("user").is_admin(): + req.context["admin_authorized"] = True + return func(resource, req, resp, *args, **kwargs) + logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name) + raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations") + if config.AUTHORIZATION_BACKEND == "whitelist": + return whitelist_authorize_admin + return authorize_admin diff --git a/certidude/authority.py b/certidude/authority.py index 815e8ea..2e51bee 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -26,12 +26,12 @@ def publish_certificate(func): cert = func(csr, *args, **kwargs) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) - if cert.email_address: - mailer.send( - "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address), - "certificate-signed.md", - attachments=(cert,), - certificate=cert) + mailer.send( + "certificate-signed.md", + to= "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) if + cert.given_name and cert.surname else cert.email_address, + attachments=(cert,), + certificate=cert) if config.PUSH_PUBLISH: url = config.PUSH_PUBLISH % csr.fingerprint() @@ -85,7 +85,9 @@ def store_request(buf, overwrite=False): fh.write(buf) os.rename(request_path + ".part", request_path) - return Request(open(request_path)) + req = Request(open(request_path)) + mailer.send("request-stored.md", attachments=(req,), request=req) + return req def signer_exec(cmd, *bits): @@ -110,6 +112,7 @@ def revoke_certificate(common_name): revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) os.rename(cert.path, revoked_filename) push.publish("certificate-revoked", cert.common_name) + mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert) def list_requests(directory=config.REQUESTS_DIR): @@ -184,7 +187,7 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): if owner.surname: csr.get_subject().SN = owner.surname csr.add_extensions([ - crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)]) + crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail.encode("ascii"))]) buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr) diff --git a/certidude/cli.py b/certidude/cli.py index b698fdd..7cf1f68 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -15,6 +15,7 @@ import subprocess import sys from configparser import ConfigParser from certidude import constants +from certidude.helpers import certidude_request_certificate from certidude.common import expand_paths, ip_address, ip_network from datetime import datetime from humanize import naturaltime @@ -63,8 +64,6 @@ if os.getuid() >= 1000: @click.command("spawn", help="Run processes for requesting certificates and configuring services") @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") def certidude_request_spawn(fork): - from certidude.helpers import certidude_request_certificate - clients = ConfigParser() clients.readfp(open("/etc/certidude/client.conf")) @@ -80,7 +79,7 @@ def certidude_request_spawn(fork): os.makedirs(run_dir) for server in clients.sections(): - if clients.get(server, "managed") != "true": + if clients.get(server, "trigger") != "interface up": continue pid_path = os.path.join(run_dir, server + ".pid") @@ -115,6 +114,7 @@ def certidude_request_spawn(fork): clients.get(server, "request_path"), clients.get(server, "certificate_path"), clients.get(server, "authority_path"), + clients.get(server, "revocations_path"), socket.gethostname(), None, autosign=True, @@ -133,9 +133,47 @@ def certidude_request_spawn(fork): csum = csummer.hexdigest() uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] + # Intranet HTTPS handled by PKCS#12 bundle generation, + # so it will not be implemented here + + if services.get(endpoint, "service") == "network-manager/openvpn": + config = ConfigParser() + config.add_section("connection") + config.add_section("vpn") + config.add_section("ipv4") + config.add_section("ipv6") + + config.set("connection", "id", endpoint) + config.set("connection", "uuid", uuid) + config.set("connection", "type", "vpn") + + config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn") + config.set("vpn", "connection-type", "tls") + config.set("vpn", "comp-lzo", "yes") + config.set("vpn", "cert-pass-flags", "0") + config.set("vpn", "tap-dev", "yes") + config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate + config.set("vpn", "remote", services.get(endpoint, "remote")) + config.set("vpn", "key", clients.get(server, "key_path")) + config.set("vpn", "cert", clients.get(server, "certificate_path")) + config.set("vpn", "ca", clients.get(server, "authority_path")) + + config.set("ipv6", "method", "auto") + + config.set("ipv4", "method", "auto") + config.set("ipv4", "never-default", "true") + + # Prevent creation of files with liberal permissions + os.umask(0o177) + + # Write keyfile + with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as configfile: + config.write(configfile) + continue + + # Set up IPsec via NetworkManager if services.get(endpoint, "service") == "network-manager/strongswan": - config = ConfigParser() config.add_section("connection") config.add_section("vpn") @@ -146,14 +184,14 @@ def certidude_request_spawn(fork): config.set("connection", "type", "vpn") config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan") - config.set("vpn", "userkey", clients.get(server, "key_path")) - config.set("vpn", "usercert", clients.get(server, "certificate_path")) config.set("vpn", "encap", "no") - config.set("vpn", "address", services.get(endpoint, "remote")) config.set("vpn", "virtual", "yes") config.set("vpn", "method", "key") - config.set("vpn", "certificate", clients.get(server, "authority_path")) config.set("vpn", "ipcomp", "no") + config.set("vpn", "address", services.get(endpoint, "remote")) + config.set("vpn", "userkey", clients.get(server, "key_path")) + config.set("vpn", "usercert", clients.get(server, "certificate_path")) + config.set("vpn", "certificate", clients.get(server, "authority_path")) config.set("ipv4", "method", "auto") @@ -203,9 +241,7 @@ def certidude_request_spawn(fork): os.system("ipsec start") continue - - - # TODO: OpenVPN, Puppet, OpenLDAP, intranet HTTPS, + # TODO: Puppet, OpenLDAP, os.unlink(pid_path) @@ -284,9 +320,8 @@ def certidude_signer_spawn(kill, no_interaction): asyncore.loop() - @click.command("client", help="Setup X.509 certificates for application") -@click.argument("url") #, help="Certidude authority endpoint URL") +@click.argument("server") @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, '%s' by default" % HOSTNAME) @click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) @@ -301,18 +336,18 @@ def certidude_signer_spawn(kill, no_interaction): @click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr by default" % HOSTNAME) @click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt by default" % HOSTNAME) @click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt by default") +@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default") def certidude_setup_client(quiet, **kwargs): - from certidude.helpers import certidude_request_certificate return certidude_request_certificate(**kwargs) @click.command("server", help="Set up OpenVPN server") -@click.argument("url") +@click.argument("server") @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) @click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) @click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default") -@click.option("--local", "-l", default="127.0.0.1", help="OpenVPN listening address, defaults to 127.0.0.1") +@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces") @click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default") @click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") @@ -321,15 +356,15 @@ def certidude_setup_client(quiet, **kwargs): type=click.File(mode="w", atomic=True, lazy=True), help="OpenVPN configuration file") @click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") -@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME) -@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME) -@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) -@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to --directory by default") +@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) +@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) +@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) +@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default") @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") +@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default") @expand_paths() -def certidude_setup_openvpn_server(url, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, local, proto, port): +def certidude_setup_openvpn_server(server, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, local, proto, port): # TODO: Intelligent way of getting last IP address in the subnet - from certidude.helpers import certidude_request_certificate subnet_first = None subnet_last = None subnet_second = None @@ -346,15 +381,9 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co click.echo("use following command to sign on Certidude server instead of web interface:") click.echo() click.echo(" certidude sign %s" % common_name) - retval = certidude_request_certificate( - url, - key_path, - request_path, - certificate_path, - authority_path, - common_name, - org_unit, - email_address, + retval = certidude_request_certificate(server, + key_path, request_path, certificate_path, authority_path, revocations_path, + common_name, org_unit, email_address, key_usage="digitalSignature,keyEncipherment", extended_key_usage="serverAuth", wait=True) @@ -378,7 +407,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co @click.command("nginx", help="Set up nginx as HTTPS server") -@click.argument("url") +@click.argument("server") @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) @click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--tls-config", @@ -392,14 +421,14 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co @click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default") @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) -@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) +@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) @click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default") @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default") +@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default") @click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off'])) @expand_paths() -def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client): +def certidude_setup_nginx(server, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client): # TODO: Intelligent way of getting last IP address in the subnet - from certidude.helpers import certidude_request_certificate if not os.path.exists(certificate_path): click.echo("As HTTPS server certificate needs specific key usage extensions please") @@ -407,8 +436,8 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d click.echo() click.echo(" certidude sign %s" % common_name) click.echo() - retval = certidude_request_certificate(url, key_path, request_path, - certificate_path, authority_path, common_name, org_unit, + retval = certidude_request_certificate(server, key_path, request_path, + certificate_path, authority_path, revocations_path, common_name, org_unit, key_usage="digitalSignature,keyEncipherment", extended_key_usage="serverAuth", dns = constants.FQDN, wait=True, bundle=True) @@ -446,33 +475,28 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d @click.command("client", help="Set up OpenVPN client") -@click.argument("url") +@click.argument("server") @click.argument("remote") @click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--org-unit", "-ou", help="Organizational unit") -@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) +@click.option("--email-address", "-m", help="E-mail associated with the request, none by default") @click.option("--config", "-o", default="/etc/openvpn/client-to-site.conf", type=click.File(mode="w", atomic=True, lazy=True), help="OpenVPN configuration file") @click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") -@click.option("--key-path", "-k", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME) -@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME) -@click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) -@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") +@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) +@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) +@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) +@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") +@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default") @expand_paths() -def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote): - from certidude.helpers import certidude_request_certificate - retval = certidude_request_certificate( - url, - key_path, - request_path, - certificate_path, - authority_path, - common_name, - org_unit, - email_address, +def certidude_setup_openvpn_client(server, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, proto, remote): + + retval = certidude_request_certificate(server, + key_path, request_path, certificate_path, authority_path, revocations_path, + common_name, org_unit, email_address, wait=True) if retval: @@ -490,7 +514,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ @click.command("server", help="Set up strongSwan server") -@click.argument("url") +@click.argument("server") @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) @click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") @@ -511,8 +535,9 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ @click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME) @click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME) @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") +@click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default") @expand_paths() -def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn): +def certidude_setup_strongswan_server(server, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, local, fqdn): if "." not in common_name: raise ValueError("Hostname has to be fully qualified!") if not local: @@ -523,19 +548,13 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email click.echo("use following command to sign on Certidude server instead of web interface:") click.echo() click.echo(" certidude sign %s" % common_name) - from certidude.helpers import certidude_request_certificate - retval = certidude_request_certificate( - url, - key_path, - request_path, - certificate_path, - authority_path, - common_name, - org_unit, - email_address, + click.echo() + + retval = certidude_request_certificate(server, + key_path, request_path, certificate_path, authority_path, revocations_path, + common_name, org_unit, email_address, key_usage="digitalSignature,keyEncipherment", extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", - ip_address=local, dns=fqdn, wait=True) @@ -555,7 +574,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email @click.command("client", help="Set up strongSwan client") -@click.argument("url") +@click.argument("server") @click.argument("remote") @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--org-unit", "-ou", help="Organizational unit") @@ -581,18 +600,12 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email @click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME) @click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME) @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") +@click.option("--revocations-path", "-crl", default="crls/ca.pemf", help="Certificate revocation list, ca.crl relative to -d by default") @expand_paths() -def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction): - from certidude.helpers import certidude_request_certificate - retval = certidude_request_certificate( - url, - key_path, - request_path, - certificate_path, - authority_path, - common_name, - org_unit, - email_address, +def certidude_setup_strongswan_client(server, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction): + retval = certidude_request_certificate(server, + key_path, request_path, certificate_path, authority_path, + common_name, org_unit, email_address, wait=True) if retval: @@ -612,8 +625,8 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo @click.command("networkmanager", help="Set up strongSwan client via NetworkManager") -@click.argument("url") -@click.argument("remote") +@click.argument("server") # Certidude server +@click.argument("remote") # StrongSwan gateway @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) @@ -622,63 +635,71 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo @click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME) @click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME) @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") +@click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default") @expand_paths() -def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote): - from certidude.helpers import certidude_request_certificate - retval = certidude_request_certificate( - url, - key_path, - request_path, - certificate_path, - authority_path, - common_name, - org_unit, - email_address, +def certidude_setup_strongswan_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote): + retval = certidude_request_certificate(server, + key_path, request_path, certificate_path, authority_path, revocations_path, + common_name, org_unit, email_address, wait=True) if retval: return retval - csummer = hashlib.sha1() - csummer.update(remote.encode("ascii")) - csum = csummer.hexdigest() - uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] + services = ConfigParser() + if os.path.exists("/etc/certidude/services.conf"): + services.readfp(open("/etc/certidude/services.conf")) - config = ConfigParser() - config.add_section("connection") - config.add_section("vpn") - config.add_section("ipv4") + endpoint = "IPSec to %s" % remote - config.set("connection", "id", remote) - config.set("connection", "uuid", uuid) - config.set("connection", "type", "vpn") - config.set("connection", "autoconnect", "true") + if services.has_section(endpoint): + click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint) + else: + click.echo("Section %s added to /etc/certidude/client.conf" % endpoint) + services.add_section(endpoint) + services.set(endpoint, "authority", server) + services.set(endpoint, "remote", remote) + services.set(endpoint, "service", "network-manager/strongswan") + services.write(open("/etc/certidude/services.conf", "w")) - config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan") - config.set("vpn", "userkey", key_path) - config.set("vpn", "usercert", certificate_path) - config.set("vpn", "encap", "no") - config.set("vpn", "address", remote) - config.set("vpn", "virtual", "yes") - config.set("vpn", "method", "key") - config.set("vpn", "certificate", authority_path) - config.set("vpn", "ipcomp", "no") - config.set("ipv4", "method", "auto") +@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager") +@click.argument("server") # Certidude server +@click.argument("remote") # OpenVPN gateway +@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) +@click.option("--org-unit", "-ou", help="Organizational unit") +@click.option("--email-address", "-m", help="E-mail associated with the request, none by default") +@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") +@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) +@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) +@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) +@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate path, ca.crt relative to -d by default") +@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default") +@expand_paths() +def certidude_setup_openvpn_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote): + retval = certidude_request_certificate(server, + key_path, request_path, certificate_path, authority_path, revocations_path, + common_name, org_unit, email_address, + wait=True) - # Prevent creation of files with liberal permissions - os.umask(0o277) + if retval: + return retval - # Write keyfile - with open(os.path.join("/etc/NetworkManager/system-connections", remote), "w") as configfile: - config.write(configfile) + services = ConfigParser() + if os.path.exists("/etc/certidude/services.conf"): + services.readfp(open("/etc/certidude/services.conf")) - # TODO: Avoid race condition here - sleep(3) - - # Tell NetworkManager to bring up the VPN connection - subprocess.call(("nmcli", "c", "up", "uuid", uuid)) + endpoint = "OpenVPN to %s" % remote + if services.has_section(endpoint): + click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint) + else: + click.echo("Section %s added to /etc/certidude/client.conf" % endpoint) + services.add_section(endpoint) + services.set(endpoint, "authority", server) + services.set(endpoint, "remote", remote) + services.set(endpoint, "service", "network-manager/openvpn") + services.write(open("/etc/certidude/services.conf", "w")) @click.command("production", help="Set up nginx, uwsgi and cron") @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") @@ -761,7 +782,8 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw @click.option("--push-server", default="", help="Streaming nginx push server") @click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA") @click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/ by default") -def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address): +@click.option("--outbox", default="smtp://smtp.%s" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN) +def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address, outbox): # Make sure common_name is valid if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name): @@ -1155,6 +1177,7 @@ certidude_setup_strongswan.add_command(certidude_setup_strongswan_client) certidude_setup_strongswan.add_command(certidude_setup_strongswan_networkmanager) certidude_setup_openvpn.add_command(certidude_setup_openvpn_server) certidude_setup_openvpn.add_command(certidude_setup_openvpn_client) +certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager) certidude_setup.add_command(certidude_setup_authority) certidude_setup.add_command(certidude_setup_openvpn) certidude_setup.add_command(certidude_setup_strongswan) diff --git a/certidude/config.py b/certidude/config.py index 7571a48..1af7512 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -57,7 +57,6 @@ except configparser.NoOptionError: PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" - TAGGING_BACKEND = cp.get("tagging", "backend") LOGGING_BACKEND = cp.get("logging", "backend") LEASES_BACKEND = cp.get("leases", "backend") @@ -68,18 +67,16 @@ if "whitelist" == AUTHORIZATION_BACKEND: ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j]) elif "posix" == AUTHORIZATION_BACKEND: USERS_GROUP = cp.get("authorization", "posix user group") - ADMINS_GROUP = cp.get("authorization", "posix admin group") + ADMIN_GROUP = cp.get("authorization", "posix admin group") elif "ldap" == AUTHORIZATION_BACKEND: - USERS_GROUP = cp.get("authorization", "ldap user group") - ADMINS_GROUP = cp.get("authorization", "ldap admin group") + LDAP_GSSAPI_CRED_CACHE = cp.get("authorization", "ldap gssapi credential cache") + LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") + LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") + if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") + if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") else: raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) -LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") -LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter") -LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter") -LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter") - for line in open("/etc/ldap/ldap.conf"): line = line.strip().lower() if "#" in line: @@ -92,6 +89,5 @@ for line in open("/etc/ldap/ldap.conf"): click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) elif key == "base": LDAP_BASE = value -else: - click.echo("No LDAP servers specified in /etc/ldap/ldap.conf") +# TODO: Check if we don't have base or servers diff --git a/certidude/constants.py b/certidude/constants.py index d04b661..86cbf04 100644 --- a/certidude/constants.py +++ b/certidude/constants.py @@ -1,4 +1,5 @@ +import click import socket FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] diff --git a/certidude/decorators.py b/certidude/decorators.py index 3aaac48..83ea9f6 100644 --- a/certidude/decorators.py +++ b/certidude/decorators.py @@ -6,6 +6,7 @@ import re import types from datetime import date, time, datetime from OpenSSL import crypto +from certidude.auth import User from certidude.wrappers import Request, Certificate from urllib.parse import urlparse @@ -76,6 +77,9 @@ class MyEncoder(json.JSONEncoder): if isinstance(obj, Certificate): return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \ if hasattr(obj, key) and getattr(obj, key)]) + if isinstance(obj, User): + return dict(name=obj.name, given_name=obj.given_name, + surname=obj.surname, mail=obj.mail) if hasattr(obj, "serialize"): return obj.serialize() return json.JSONEncoder.default(self, obj) @@ -95,16 +99,20 @@ def serialize(func): resp.set_header("Content-Type", "application/json") resp.set_header("Content-Disposition", "inline") resp.body = json.dumps(r, cls=MyEncoder) - elif hasattr(r, "content_type") and req.client_accepts(r.content_type): resp.set_header("Content-Type", r.content_type) resp.set_header("Content-Disposition", ("attachment; filename=%s" % r.suggested_filename).encode("ascii")) resp.body = r.dump() - else: - logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept)) + elif hasattr(r, "content_type"): + logger.debug("Client did not accept application/json or %s, " + "client expected %s", r.content_type, req.accept) raise falcon.HTTPUnsupportedMediaType( "Client did not accept application/json or %s" % r.content_type) + else: + logger.debug("Client did not accept application/json, client expected %s", req.accept) + raise falcon.HTTPUnsupportedMediaType( + "Client did not accept application/json") return r return wrapped diff --git a/certidude/helpers.py b/certidude/helpers.py index 35ba5fc..ac00b16 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -2,11 +2,14 @@ import click import os import requests +import subprocess +import tempfile from certidude import errors from certidude.wrappers import Certificate, Request +from configparser import ConfigParser from OpenSSL import crypto -def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False): +def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False): """ Exchange CSR for certificate using Certidude HTTP API server """ @@ -18,38 +21,80 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, if wait: request_params.add("wait=forever") - # Expand ca.example.com to http://ca.example.com/api/ - if not url.endswith("/"): - url += "/api/" - if "//" not in url: - url = "http://" + url - - authority_url = url + "certificate" - request_url = url + "request" + # Expand ca.example.com + authority_url = "http://%s/api/certificate/" % server + request_url = "http://%s/api/request/" % server + revoked_url = "http://%s/api/revoked/" % server if request_params: request_url = request_url + "?" + "&".join(request_params) - if os.path.exists(certificate_path): - click.echo("Found certificate: %s" % certificate_path) - # TODO: Check certificate validity, download CRL? - return - if os.path.exists(authority_path): - click.echo("Found CA certificate in: %s" % authority_path) + click.echo("Found authority certificate in: %s" % authority_path) else: - click.echo("Attempting to fetch CA certificate from %s" % authority_url) - + click.echo("Attempting to fetch authority certificate from %s" % authority_url) try: r = requests.get(authority_url, headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"}) cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) except crypto.Error: raise ValueError("Failed to parse PEM: %s" % r.text) - with open(authority_path + ".part", "w") as oh: + authority_partial = tempfile.mktemp(prefix=authority_path + ".part") + with open(authority_partial, "w") as oh: oh.write(r.text) - click.echo("Writing CA certificate to: %s" % authority_path) - os.rename(authority_path + ".part", authority_path) + click.echo("Writing authority certificate to: %s" % authority_path) + os.rename(authority_partial, authority_path) + + # Fetch certificate revocation list + r = requests.get(revoked_url, stream=True) + click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path)) + revocations_partial = tempfile.mktemp(prefix=revocations_path + ".part") + with open(revocations_partial, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + if subprocess.call(("openssl", "crl", "-CAfile", authority_path, "-in", revocations_partial, "-noout")): + raise ValueError("Failed to verify CRL in %s" % revocations_partial) + else: + # TODO: Check monotonically increasing CRL number + click.echo("Certificate revocation list passed verification") + os.rename(revocations_partial, revocations_path) + + # Check if we have been inserted into CRL + if os.path.exists(certificate_path): + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read()) + revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read()) + for revocation in revocation_list.get_revoked(): + if int(revocation.get_serial(), 16) == cert.get_serial_number(): + if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL' + # TODO: Disable service for time being + click.echo("Certificate put on hold, doing nothing for now") + break + + # Disable the client if operation has been ceased or + # the certificate has been superseded by other + if revocation.get_reason() in ("Cessation Of Operation", "Superseded"): + if os.path.exists("/etc/certidude/client.conf"): + clients.readfp(open("/etc/certidude/client.conf")) + if clients.has_section(server): + clients.set(server, "trigger", "operation ceased") + clients.write(open("/etc/certidude/client.conf", "w")) + click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf") + # TODO: Disable related services + if revocation.get_reason() in ("CA Compromise", "AA Compromise"): + if os.path.exists(authority_path): + os.remove(key_path) + + click.echo("Certificate has been revoked, wiping keys and certificates!") + if os.path.exists(key_path): + os.remove(key_path) + if os.path.exists(request_path): + os.remove(request_path) + if os.path.exists(certificate_path): + os.remove(certificate_path) + break + else: + click.echo("Certificate does not seem to be revoked. Good!") try: request = Request(open(request_path)) @@ -62,8 +107,9 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, key.generate_key(crypto.TYPE_RSA, 4096) # Dump private key + key_partial = tempfile.mktemp(prefix=key_path + ".part") os.umask(0o077) - with open(key_path + ".part", "wb") as fh: + with open(key_partial, "wb") as fh: fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) # Construct CSR @@ -107,10 +153,38 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, fh.write(request.dump()) click.echo("Writing private key to: %s" % key_path) - os.rename(key_path + ".part", key_path) + os.rename(key_partial, key_path) click.echo("Writing certificate signing request to: %s" % request_path) os.rename(request_path + ".part", request_path) + # We have CSR now, save the paths to client.conf so we could: + # Update CRL, renew certificate, maybe something extra? + + if not os.path.exists("/etc/certidude"): + os.makedirs("/etc/certidude") + + clients = ConfigParser() + if os.path.exists("/etc/certidude/client.conf"): + clients.readfp(open("/etc/certidude/client.conf")) + + if clients.has_section(server): + click.echo("Section %s already exists in /etc/certidude/client.conf, not reconfiguring" % server) + else: + clients.add_section(server) + clients.set(server, "trigger", "interface up") + clients.set(server, "key_path", key_path) + clients.set(server, "request_path", request_path) + clients.set(server, "certificate_path", certificate_path) + clients.set(server, "authority_path", authority_path) + clients.set(server, "key_path", key_path) + clients.set(server, "revocations_path", revocations_path) + clients.write(open("/etc/certidude/client.conf", "w")) + click.echo("Section %s added to /etc/certidude/client.conf" % repr(server)) + + if os.path.exists(certificate_path): + click.echo("Found certificate: %s" % certificate_path) + # TODO: Check certificate validity, download CRL? + return click.echo("Submitting to %s, waiting for response..." % request_url) submission = requests.post(request_url, diff --git a/certidude/mailer.py b/certidude/mailer.py index 6fd1b6e..2d92e8c 100644 --- a/certidude/mailer.py +++ b/certidude/mailer.py @@ -1,6 +1,8 @@ +import click import os import smtplib +from certidude.user import User from markdown import markdown from jinja2 import Environment, PackageLoader from email.mime.multipart import MIMEMultipart @@ -10,14 +12,18 @@ from urllib.parse import urlparse env = Environment(loader=PackageLoader("certidude", "templates/mail")) -def send(recipients, template, attachments=(), **context): +def send(template, to=None, attachments=(), **context): from certidude import authority, config if not config.OUTBOX: # Mailbox disabled, don't send e-mail return - if not recipients: - raise ValueError("No e-mail recipients specified!") + recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()]) + + if to: + recipients = to + u", " + recipients + + click.echo("Sending e-mail %s to %s" % (template, recipients)) scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX) scheme = scheme.lower() diff --git a/certidude/static/js/templates.js b/certidude/static/js/templates.js index b63863d..918cdfe 100644 --- a/certidude/static/js/templates.js +++ b/certidude/static/js/templates.js @@ -485,14 +485,14 @@ output += "\n E-mail disabled\n"; ; } output += "

\n\n

Authenticated users allowed from:\n\n"; -if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) { +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets"))) { output += "\n anywhere\n

\n"; ; } else { output += "\n

\n
    \n "; frame = frame.push(); -var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"); +var t_3 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets"); if(t_3) {var t_2 = t_3.length; for(var t_1=0; t_1 < t_3.length; t_1++) { var t_4 = t_3[t_1]; @@ -515,14 +515,14 @@ output += "\n
\n"; ; } output += "\n\n\n

Request submission is allowed from:\n\n"; -if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) { +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets"))) { output += "\n anywhere\n

\n"; ; } else { output += "\n

\n
    \n "; frame = frame.push(); -var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); +var t_7 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets"); if(t_7) {var t_6 = t_7.length; for(var t_5=0; t_5 < t_7.length; t_5++) { var t_8 = t_7[t_5]; @@ -545,7 +545,7 @@ output += "\n
\n"; ; } output += "\n\n

Autosign is allowed from:\n"; -if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) { +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"autosign_subnets"))) { output += "\n anywhere\n

\n"; ; } @@ -575,14 +575,14 @@ output += "\n \n"; ; } output += "\n\n

Authority administration is allowed from:\n"; -if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) { +if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets"))) { output += "\n anywhere\n

\n"; ; } else { output += "\n
    \n "; frame = frame.push(); -var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); +var t_15 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets"); if(t_15) {var t_14 = t_15.length; for(var t_13=0; t_13 < t_15.length; t_13++) { var t_16 = t_15[t_13]; @@ -606,15 +606,11 @@ output += "\n
\n"; } output += "\n\n

Authority administration allowed for:

\n\n
    \n"; frame = frame.push(); -var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); -if(t_19) {var t_17; -if(runtime.isArray(t_19)) { -var t_18 = t_19.length; -for(t_17=0; t_17 < t_19.length; t_17++) { -var t_20 = t_19[t_17][0] -frame.set("handle", t_19[t_17][0]); -var t_21 = t_19[t_17][1] -frame.set("full_name", t_19[t_17][1]); +var t_19 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_users"); +if(t_19) {var t_18 = t_19.length; +for(var t_17=0; t_17 < t_19.length; t_17++) { +var t_20 = t_19[t_17]; +frame.set("user", t_20); frame.set("loop.index", t_17 + 1); frame.set("loop.index0", t_17); frame.set("loop.revindex", t_18 - t_17); @@ -622,32 +618,15 @@ frame.set("loop.revindex0", t_18 - t_17 - 1); frame.set("loop.first", t_17 === 0); frame.set("loop.last", t_17 === t_18 - 1); frame.set("loop.length", t_18); -output += "\n
  • "; -output += runtime.suppressValue(t_21, env.opts.autoescape); -output += "
  • \n"; +output += "\n
  • "; +output += runtime.suppressValue(runtime.memberLookup((t_20),"given_name"), env.opts.autoescape); +output += " "; +output += runtime.suppressValue(runtime.memberLookup((t_20),"surname"), env.opts.autoescape); +output += "
  • \n"; ; } -} else { -t_17 = -1; -var t_18 = runtime.keys(t_19).length; -for(var t_22 in t_19) { -t_17++; -var t_23 = t_19[t_22]; -frame.set("handle", t_22); -frame.set("full_name", t_23); -frame.set("loop.index", t_17 + 1); -frame.set("loop.index0", t_17); -frame.set("loop.revindex", t_18 - t_17); -frame.set("loop.revindex0", t_18 - t_17 - 1); -frame.set("loop.first", t_17 === 0); -frame.set("loop.last", t_17 === t_18 - 1); -frame.set("loop.length", t_18); -output += "\n
  • "; -output += runtime.suppressValue(t_23, env.opts.autoescape); -output += "
  • \n"; -; -} -} } frame = frame.pop(); output += "\n
\n\n\n"; @@ -658,14 +637,14 @@ output += "\n

Here you can renew your certificates

\n\n"; ; } output += "\n\n"; -var t_24; -t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); -frame.set("s", t_24, true); +var t_21; +t_21 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); +frame.set("s", t_21, true); if(frame.topLevel) { -context.setVariable("s", t_24); +context.setVariable("s", t_21); } if(frame.topLevel) { -context.addExport("s", t_24); +context.addExport("s", t_21); } output += "\n\n\n"; if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { @@ -673,24 +652,24 @@ output += "\n
\n

Pending requests

\n\n

output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape); output += "\n\n

    \n "; frame = frame.push(); -var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); -if(t_27) {var t_26 = t_27.length; -for(var t_25=0; t_25 < t_27.length; t_25++) { -var t_28 = t_27[t_25]; -frame.set("request", t_28); -frame.set("loop.index", t_25 + 1); -frame.set("loop.index0", t_25); -frame.set("loop.revindex", t_26 - t_25); -frame.set("loop.revindex0", t_26 - t_25 - 1); -frame.set("loop.first", t_25 === 0); -frame.set("loop.last", t_25 === t_26 - 1); -frame.set("loop.length", t_26); +var t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); +if(t_24) {var t_23 = t_24.length; +for(var t_22=0; t_22 < t_24.length; t_22++) { +var t_25 = t_24[t_22]; +frame.set("request", t_25); +frame.set("loop.index", t_22 + 1); +frame.set("loop.index0", t_22); +frame.set("loop.revindex", t_23 - t_22); +frame.set("loop.revindex0", t_23 - t_22 - 1); +frame.set("loop.first", t_22 === 0); +frame.set("loop.last", t_22 === t_23 - 1); +frame.set("loop.length", t_23); output += "\n "; -env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) { -if(t_31) { cb(t_31); return; } -t_29.render(context.getVariables(), frame, function(t_32,t_30) { -if(t_32) { cb(t_32); return; } -output += t_30 +env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_28,t_26) { +if(t_28) { cb(t_28); return; } +t_26.render(context.getVariables(), frame, function(t_29,t_27) { +if(t_29) { cb(t_29); return; } +output += t_27 output += "\n\t "; })}); } @@ -698,24 +677,24 @@ output += "\n\t "; frame = frame.pop(); output += "\n
  • \n

    No certificate signing requests to sign!

    \n
  • \n
\n
\n\n
\n

Signed certificates

\n \n
    \n "; frame = frame.push(); -var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed"))); -if(t_35) {var t_34 = t_35.length; -for(var t_33=0; t_33 < t_35.length; t_33++) { -var t_36 = t_35[t_33]; -frame.set("certificate", t_36); -frame.set("loop.index", t_33 + 1); -frame.set("loop.index0", t_33); -frame.set("loop.revindex", t_34 - t_33); -frame.set("loop.revindex0", t_34 - t_33 - 1); -frame.set("loop.first", t_33 === 0); -frame.set("loop.last", t_33 === t_34 - 1); -frame.set("loop.length", t_34); +var t_32 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed"))); +if(t_32) {var t_31 = t_32.length; +for(var t_30=0; t_30 < t_32.length; t_30++) { +var t_33 = t_32[t_30]; +frame.set("certificate", t_33); +frame.set("loop.index", t_30 + 1); +frame.set("loop.index0", t_30); +frame.set("loop.revindex", t_31 - t_30); +frame.set("loop.revindex0", t_31 - t_30 - 1); +frame.set("loop.first", t_30 === 0); +frame.set("loop.last", t_30 === t_31 - 1); +frame.set("loop.length", t_31); output += "\n "; -env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) { -if(t_39) { cb(t_39); return; } -t_37.render(context.getVariables(), frame, function(t_40,t_38) { -if(t_40) { cb(t_40); return; } -output += t_38 +env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_36,t_34) { +if(t_36) { cb(t_36); return; } +t_34.render(context.getVariables(), frame, function(t_37,t_35) { +if(t_37) { cb(t_37); return; } +output += t_35 output += "\n\t "; })}); } @@ -729,31 +708,31 @@ output += "/certificate/ > session.pem\n openssl ocsp -issuer session.pem -CA output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape); output += "/ocsp/ -serial 0x\n \n -->\n
      \n "; frame = frame.push(); -var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); -if(t_43) {var t_42 = t_43.length; -for(var t_41=0; t_41 < t_43.length; t_41++) { -var t_44 = t_43[t_41]; -frame.set("j", t_44); -frame.set("loop.index", t_41 + 1); -frame.set("loop.index0", t_41); -frame.set("loop.revindex", t_42 - t_41); -frame.set("loop.revindex0", t_42 - t_41 - 1); -frame.set("loop.first", t_41 === 0); -frame.set("loop.last", t_41 === t_42 - 1); -frame.set("loop.length", t_42); +var t_40 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); +if(t_40) {var t_39 = t_40.length; +for(var t_38=0; t_38 < t_40.length; t_38++) { +var t_41 = t_40[t_38]; +frame.set("j", t_41); +frame.set("loop.index", t_38 + 1); +frame.set("loop.index0", t_38); +frame.set("loop.revindex", t_39 - t_38); +frame.set("loop.revindex0", t_39 - t_38 - 1); +frame.set("loop.first", t_38 === 0); +frame.set("loop.last", t_38 === t_39 - 1); +frame.set("loop.length", t_39); output += "\n
    • \n "; -output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((t_41),"changed"), env.opts.autoescape); output += "\n "; -output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((t_41),"serial_number"), env.opts.autoescape); output += " "; -output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape); +output += runtime.suppressValue(runtime.memberLookup((t_41),"identity"), env.opts.autoescape); output += "\n
    • \n "; ; } } -if (!t_42) { +if (!t_39) { output += "\n
    • Great job! No certificate signing requests to sign.
    • \n\t "; } frame = frame.pop(); @@ -1098,7 +1077,7 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook output += "\n "; })}); } -output += "\n \n "; +output += "\n\n "; if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) { output += "\n
      "; env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) { diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html index 8f50479..05bb553 100644 --- a/certidude/static/views/authority.html +++ b/certidude/static/views/authority.html @@ -31,13 +31,13 @@ as such require complete reset of X509 infrastructure if some of them needs to b

      Authenticated users allowed from: -{% if "0.0.0.0/0" in session.user_subnets %} +{% if "0.0.0.0/0" in session.authority.user_subnets %} anywhere

      {% else %}

        - {% for i in session.user_subnets %} + {% for i in session.authority.user_subnets %}
      • {{ i }}
      • {% endfor %}
      @@ -46,20 +46,20 @@ as such require complete reset of X509 infrastructure if some of them needs to b

      Request submission is allowed from: -{% if "0.0.0.0/0" in session.request_subnets %} +{% if "0.0.0.0/0" in session.authority.request_subnets %} anywhere

      {% else %}

        - {% for subnet in session.request_subnets %} + {% for subnet in session.authority.request_subnets %}
      • {{ subnet }}
      • {% endfor %}
      {% endif %}

      Autosign is allowed from: -{% if "0.0.0.0/0" in session.autosign_subnets %} +{% if "0.0.0.0/0" in session.authority.autosign_subnets %} anywhere

      {% else %} @@ -72,12 +72,12 @@ as such require complete reset of X509 infrastructure if some of them needs to b {% endif %}

      Authority administration is allowed from: -{% if "0.0.0.0/0" in session.admin_subnets %} +{% if "0.0.0.0/0" in session.authority.admin_subnets %} anywhere

      {% else %}
        - {% for subnet in session.admin_subnets %} + {% for subnet in session.authority.admin_subnets %}
      • {{ subnet }}
      • {% endfor %}
      @@ -86,8 +86,8 @@ as such require complete reset of X509 infrastructure if some of them needs to b

      Authority administration allowed for:

diff --git a/certidude/templates/certidude.conf b/certidude/templates/certidude.conf index a19c97b..18ddb9d 100644 --- a/certidude/templates/certidude.conf +++ b/certidude/templates/certidude.conf @@ -57,4 +57,4 @@ certificate path = {{ ca_crt }} requests dir = {{ directory }}/requests/ signed dir = {{ directory }}/signed/ revoked dir = {{ directory }}/revoked/ -outbox = smtp://localhost +outbox = {{ outbox }} diff --git a/certidude/templates/mail/certificate-revoked.md b/certidude/templates/mail/certificate-revoked.md new file mode 100644 index 0000000..4fe58a8 --- /dev/null +++ b/certidude/templates/mail/certificate-revoked.md @@ -0,0 +1,6 @@ +Certificate {{certificate.common_name}} ({{certificate.serial_number}}) revoked + +This is simply to notify that certificate {{ certificate.common_name }} +was revoked. + +Services making use of this certificates might become unavailable. diff --git a/certidude/templates/mail/request-stored.md b/certidude/templates/mail/request-stored.md new file mode 100644 index 0000000..4d8689f --- /dev/null +++ b/certidude/templates/mail/request-stored.md @@ -0,0 +1,5 @@ +Certificate signing request {{request.common_name}} stored + +This is simply to notify that certificate signing request for {{ request.common_name }} +was stored. You may log in with a certificate authority administration account to sign it. + diff --git a/certidude/templates/nginx-https-site.conf b/certidude/templates/nginx-https-site.conf index dc29bc2..1a063a0 100644 --- a/certidude/templates/nginx-https-site.conf +++ b/certidude/templates/nginx-https-site.conf @@ -15,6 +15,7 @@ server { ssl_certificate {{certificate_path}}; ssl_certificate_key {{key_path}}; ssl_client_certificate {{authority_path}}; + ssl_crl {{revocations_path}}; ssl_verify_client {{verify_client}}; } diff --git a/certidude/templates/nginx.conf b/certidude/templates/nginx.conf index cd42758..1c7e1c0 100644 --- a/certidude/templates/nginx.conf +++ b/certidude/templates/nginx.conf @@ -7,9 +7,6 @@ events { } http { - {% if not push_server %} - push_stream_shared_memory_size 32M; - {% endif %} include mime.types; default_type application/octet-stream; sendfile on; @@ -21,7 +18,7 @@ http { } server { - server_name {{hostname}}; + server_name {{hostname}}; # TODO: FQDN, SSL listen 80 default_server; listen [::]:80 default_server ipv6only=on; error_page 500 502 503 504 /50x.html; diff --git a/certidude/templates/openvpn-client-to-site.ovpn b/certidude/templates/openvpn-client-to-site.ovpn index 08759c2..0beff8f 100644 --- a/certidude/templates/openvpn-client-to-site.ovpn +++ b/certidude/templates/openvpn-client-to-site.ovpn @@ -2,7 +2,7 @@ client remote {{remote}} remote-cert-tls server proto {{proto}} -dev tap0 +dev tap nobind key {{key_path}} cert {{certificate_path}} @@ -10,4 +10,5 @@ ca {{authority_path}} comp-lzo user nobody group nogroup - +persist-tun +persist-key diff --git a/certidude/templates/openvpn-site-to-client.ovpn b/certidude/templates/openvpn-site-to-client.ovpn index 38c76cb..d5a6b78 100644 --- a/certidude/templates/openvpn-site-to-client.ovpn +++ b/certidude/templates/openvpn-site-to-client.ovpn @@ -2,15 +2,18 @@ mode server tls-server proto {{proto}} port {{port}} -dev tap0 +dev tap local {{local}} key {{key_path}} cert {{certificate_path}} ca {{authority_path}} +crl-verify {{revocations_path}} dh {{dhparam_path}} comp-lzo user nobody group nogroup +persist-tun +persist-key ifconfig-pool-persist /tmp/openvpn-leases.txt ifconfig {{subnet_first}} {{subnet.netmask}} server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}} diff --git a/certidude/templates/uwsgi.ini b/certidude/templates/uwsgi.ini index 97155a8..25e6737 100644 --- a/certidude/templates/uwsgi.ini +++ b/certidude/templates/uwsgi.ini @@ -5,7 +5,7 @@ processes = 1 vacuum = true uid = {{username}} gid = {{username}} -plugins = python34 +plugins = python27 chdir = /tmp module = certidude.wsgi callable = app diff --git a/certidude/user.py b/certidude/user.py new file mode 100644 index 0000000..ae24d80 --- /dev/null +++ b/certidude/user.py @@ -0,0 +1,165 @@ + +import click +import grp +import ldap +import ldap.sasl +import os +import pwd +from certidude import constants, config + +class User(object): + def __init__(self, username, mail, given_name="", surname=""): + if "@" not in mail: + raise ValueError("Invalid e-mail %s" % repr(mail)) + self.name = username + self.mail = mail + self.given_name = given_name + self.surname = surname + + def __unicode__(self): + if self.given_name and self.surname: + return u"%s %s <%s>" % (self.given_name, self.surname, self.mail) + else: + return self.mail + + def __hash__(self): + return hash(self.mail) + + def __eq__(self, other): + return self.mail == other.mail + + def __repr__(self): + return unicode(self).encode("utf-8") + + def is_admin(self): + if not hasattr(self, "_is_admin"): + self._is_admin = self.objects.is_admin(self) + return self._is_admin + + class DoesNotExist(StandardError): + pass + + +class PosixUserManager(object): + def get(self, username): + _, _, _, _, gecos, _, _ = pwd.getpwnam(username) + gecos = gecos.decode("utf-8").split(",") + full_name = gecos[0] + mail = username + "@" + constants.DOMAIN + if full_name and " " in full_name: + given_name, surname = full_name.split(" ", 1) + return User(username, mail, given_name, surname) + return User(username, mail) + + def filter_admins(self): + _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP) + for username in members: + yield self.get(username) + + def is_admin(self, username): + import grp + _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP) + return username in members + + +class DirectoryConnection(object): + def __enter__(self): + # TODO: Implement simple bind + if not os.path.exists(config.LDAP_GSSAPI_CRED_CACHE): + raise ValueError("Ticket cache not initialized, unable to " + "authenticate with computer account against LDAP server!") + os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE + for server in config.LDAP_SERVERS: + self.conn = ldap.initialize(server) + self.conn.set_option(ldap.OPT_REFERRALS, 0) + click.echo("Connecing to %s using Kerberos ticket cache from %s" % + (server, config.LDAP_GSSAPI_CRED_CACHE)) + self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) + return self.conn + raise ValueError("No LDAP servers specified!") + + def __exit__(self, type, value, traceback): + self.conn.unbind_s + + +class ActiveDirectoryUserManager(object): + def get(self, username): + # TODO: Sanitize username + if "@" in username: + username, _ = username.split("@", 1) + with DirectoryConnection() as conn: + ft = config.LDAP_USER_FILTER % username + attribs = "cn", "givenName", "sn", "mail", "userPrincipalName" + r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, + ft.encode("utf-8"), attribs) + for dn, entry in r: + if not dn: + continue + if entry.get("givenname") and entry.get("sn"): + given_name, = entry.get("givenName") + surname, = entry.get("sn") + else: + cn, = entry.get("cn") + if " " in cn: + given_name, surname = cn.split(" ", 1) + else: + given_name, surname = cn, "" + + mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,) + return User(username.decode("utf-8"), mail.decode("utf-8"), + given_name.decode("utf-8"), surname.decode("utf-8")) + raise User.DoesNotExist("User %s does not exist" % username) + + def filter(self, ft): + with DirectoryConnection() as conn: + attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName" + r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, + ft.encode("utf-8"), attribs) + for dn,entry in r: + if not dn: + continue + username, = entry.get("sAMAccountName") + cn, = entry.get("cn") + mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,) + if entry.get("givenName") and entry.get("sn"): + given_name, = entry.get("givenName") + surname, = entry.get("sn") + else: + cn, = entry.get("cn") + if " " in cn: + given_name, surname = cn.split(" ", 1) + else: + given_name, surname = cn, "" + yield User(username.decode("utf-8"), mail.decode("utf-8"), + given_name.decode("utf-8"), surname.decode("utf-8")) + + def filter_admins(self): + """ + Return admin User objects + """ + return self.filter(config.LDAP_ADMIN_FILTER % "*") + + def all(self): + """ + Return all valid User objects + """ + return self.filter(ft=config.LDAP_USER_FILTER % "*") + + def is_admin(self, user): + with DirectoryConnection() as conn: + ft = config.LDAP_ADMIN_FILTER % user.name + r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, + ft.encode("utf-8"), ["cn"]) + for dn, entry in r: + if not dn: + continue + return True + return False + +if config.ACCOUNTS_BACKEND == "ldap": + User.objects = ActiveDirectoryUserManager() +elif config.ACCOUNTS_BACKEND == "posix": + User.objects = PosixUserManager() +else: + raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) + diff --git a/requirements.txt b/requirements.txt index 897f147..b431ba4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ ipaddress==1.0.16 ipsecparse==0.1.0 Jinja2==2.8 Markdown==2.6.5 -MarkupSafe==0.23 pyasn1==0.1.8 pycrypto==2.6.1 pykerberos==1.1.8