From 0987ee1cba78f7a13b26085da93426619d6fd071 Mon Sep 17 00:00:00 2001
From: Alain Takoudjou <alain.takoudjou@nexedi.com>
Date: Wed, 28 Jun 2017 11:55:50 +0200
Subject: [PATCH] stack.erp5: Intergrate caucase and client-certificate-based
 authentication

Allows requesting a caucase partition and reusing an existing caucase instance.
For client-certificate-based authentication, client must be able to access backend
directly (frontend is not possible).
---
 component/apache/apache-backend.conf.in       | 20 ++++-
 component/apache/buildout.cfg                 |  2 +-
 software/erp5/instance-erp5-input-schema.json | 28 +++++++
 .../erp5/instance-erp5-output-schema.json     |  5 ++
 stack/erp5/buildout.cfg                       |  5 ++
 stack/erp5/buildout.hash.cfg                  |  6 +-
 stack/erp5/instance-balancer.cfg.in           | 79 ++++++++++++++++++-
 stack/erp5/instance-erp5.cfg.in               | 22 +++++-
 stack/erp5/instance.cfg.in                    |  6 +-
 9 files changed, 162 insertions(+), 11 deletions(-)

diff --git a/component/apache/apache-backend.conf.in b/component/apache/apache-backend.conf.in
index 02bfe3877..65daaa8c3 100644
--- a/component/apache/apache-backend.conf.in
+++ b/component/apache/apache-backend.conf.in
@@ -108,12 +108,16 @@ SSLProxyEngine On
 
 # As backend is trusting REMOTE_USER header unset it always
 RequestHeader unset REMOTE_USER
+RequestHeader unset SSL_CLIENT_SERIAL
 {% if parameter_dict['ca-cert'] -%}
-SSLVerifyClient require
+SSLVerifyClient optional
 RequestHeader set REMOTE_USER %{SSL_CLIENT_S_DN_CN}s
+RequestHeader set SSL_CLIENT_SERIAL "%{SSL_CLIENT_M_SERIAL}s"
 SSLCACertificateFile {{ parameter_dict['ca-cert'] }}
+{%   if parameter_dict['crl'] -%}
 SSLCARevocationCheck chain
 SSLCARevocationFile {{ parameter_dict['crl'] }}
+{%-   endif %}
 {%- endif %}
 
 ErrorLog "{{ parameter_dict['error-log'] }}"
@@ -128,12 +132,24 @@ CustomLog "{{ parameter_dict['access-log'] }}" combined
 </Directory>
 
 RewriteEngine On
-{% for port, _, backend in parameter_dict['backend-list'] -%}
+{% for port, _, backend, enable_authentication in parameter_dict['backend-list'] -%}
 {%   for ip in parameter_dict['ip-list'] -%}
 Listen {{ ip }}:{{ port }}
 {%   endfor -%}
 <VirtualHost *:{{ port }}>
   SSLEngine on
+{% if enable_authentication and parameter_dict['ca-cert'] and parameter_dict['crl'] -%}
+  SSLVerifyClient require
+  SSLCACertificateFile {{ parameter_dict['ca-cert'] }}
+  SSLCARevocationCheck chain
+  SSLCARevocationFile {{ parameter_dict['crl'] }}
+
+  LogFormat "%h %l %{REMOTE_USER}i %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" combined
+
+  # We would like to separate the the authentificated logs.
+  ErrorLog "{{ parameter_dict['log-dir'] }}/apache-service-error.log"
+  CustomLog "{{ parameter_dict['log-dir'] }}/apache-service-access.log" combined
+{% endif -%}
   RewriteRule ^/(.*) {{ backend }}/$1 [L,P]
 </VirtualHost>
 {% endfor -%}
diff --git a/component/apache/buildout.cfg b/component/apache/buildout.cfg
index 7c01e7ec4..fa7238829 100644
--- a/component/apache/buildout.cfg
+++ b/component/apache/buildout.cfg
@@ -190,5 +190,5 @@ make-targets =
 [template-apache-backend-conf]
 recipe = slapos.recipe.build:download
 url = ${:_profile_base_location_}/apache-backend.conf.in
-md5sum = feef079241bda3407b7ceed5876cb61f
+md5sum = 8f1f92ad308ab6bad973c790ee583428
 mode = 640
diff --git a/software/erp5/instance-erp5-input-schema.json b/software/erp5/instance-erp5-input-schema.json
index a0b410f81..9f873be66 100644
--- a/software/erp5/instance-erp5-input-schema.json
+++ b/software/erp5/instance-erp5-input-schema.json
@@ -111,6 +111,12 @@
               "default": 5,
               "type": "integer"
             },
+            "ssl-authentication": {
+              "title": "Enable SSL Client authentication on this zope instance.",
+              "description": "If set to true, will set SSL Client verification to required on apache VirtualHost which allow to access this zope instance.",
+              "type": "boolean",
+              "default": false
+            },
             "webdav": {
               "description": "Serve webdav queries, implies timerserver-interval=0 (disabled). Mixing webdav and non-webdav nodes in a single family will give unspecified results.",
               "default": false,
@@ -263,6 +269,28 @@
       "description": "In wendelin.core there are 2 formats for storing data, so called ZBlk0 and ZBlk1. See https://lab.nexedi.com/nexedi/wendelin.core/blob/2e5e1d3d/bigfile/file_zodb.py#L19 for more details.",
       "default": "",
       "type": "string"
+    },
+    "caucase": {
+      "description": "Caucase certificate authority parameters",
+      "properties": {
+        "url": {
+          "title": "Caucase URL",
+          "description": "URL of existing caucase instance to use. If empty, a new caucase instance will be deployed. If not empty, other properties in this section will be ignored.",
+          "default": "",
+          "type": "string",
+          "format": "uri"
+        },
+        "crl-update-periodicity": {
+            "title": "Periodicity of CRL update",
+            "description": "Periodicity of CRL update, in cron format. The CRL will be downloaded from caucase URL and the new content will be saved if there was a change. Everytime a new CRL is writen, Apache reload will be called.",
+            "type": "string",
+            "default": "0 0 * * *"
+        }
+      },
+      "additionalProperties": {
+        "$ref": "../caucase/instance-caucase-input-schema.json#/properties"
+      },
+      "type": "object"
     }
   }
 }
diff --git a/software/erp5/instance-erp5-output-schema.json b/software/erp5/instance-erp5-output-schema.json
index 20a9d720d..22e60374c 100644
--- a/software/erp5/instance-erp5-output-schema.json
+++ b/software/erp5/instance-erp5-output-schema.json
@@ -72,6 +72,11 @@
       "description": "Jupyter notebook web UI access information",
       "pattern": "^https://",
       "type": "string"
+    },
+    "caucase-http-url": {
+      "description": "Caucase url on HTTP. For HTTPS URL, uses https scheme, if port is explicitely specified in http URL, take that port and add 1 and use it as https port. If it is not specified.",
+      "pattern": "^http://",
+      "type": "string"
     }
   },
   "patternProperties": {
diff --git a/stack/erp5/buildout.cfg b/stack/erp5/buildout.cfg
index 4c150488f..0aac734c3 100644
--- a/stack/erp5/buildout.cfg
+++ b/stack/erp5/buildout.cfg
@@ -62,6 +62,7 @@ extends =
   ../../component/postfix/buildout.cfg
   ../monitor/buildout.cfg
   ../../software/ipython_notebook/software.cfg
+  ../../software/caucase/software.cfg
   ../../software/neoppod/software-common.cfg
 # keep neoppod extends last
 
@@ -149,6 +150,9 @@ extra-ldflags = -Wl,-rpath=${gcc:location}/lib -Wl,-rpath=${gcc:location}/lib64
 [instance-jupyter]
 rendered = ${buildout:directory}/template-jupyter.cfg
 
+[instance-caucase]
+rendered = ${buildout:directory}/instance-caucase.cfg
+
 [download-base]
 <= download-base-neo
 url = ${:_profile_base_location_}/${:filename}
@@ -231,6 +235,7 @@ context =
     key bin_directory buildout:bin-directory
     key buildout_bin_directory buildout:bin-directory
     key cairo_location cairo:location
+    key caucase_template instance-caucase:rendered
     key coreutils_location coreutils:location
     key cups_location cups:location
     key curl_location curl:location
diff --git a/stack/erp5/buildout.hash.cfg b/stack/erp5/buildout.hash.cfg
index 5287c23c6..7166a8ef4 100644
--- a/stack/erp5/buildout.hash.cfg
+++ b/stack/erp5/buildout.hash.cfg
@@ -75,7 +75,7 @@ md5sum = 0969fbb25b05c02ef3c2d437b2f4e1a0
 
 [template]
 filename = instance.cfg.in
-md5sum = 09862389b2bf3da3f9387a6424329da9
+md5sum = e364ea67bfe786b6b6ebd6c4f0cd628a
 
 [monitor-template-dummy]
 filename = dummy.cfg
@@ -83,7 +83,7 @@ md5sum = d41d8cd98f00b204e9800998ecf8427e
 
 [template-erp5]
 filename = instance-erp5.cfg.in
-md5sum = 929b87c01aaf6ea1bbf6ff906ce9c0d9
+md5sum = 13638031b6b6c9ad9c0a9c4e6d9a202a
 
 [template-zeo]
 filename = instance-zeo.cfg.in
@@ -95,7 +95,7 @@ md5sum = 6a64d1615c3ef9f6311c863d5aa0c58f
 
 [template-balancer]
 filename = instance-balancer.cfg.in
-md5sum = a3ad32c46bb56076895441edbd66018d
+md5sum = f2fb0c537c124622fe8e89afe0188519
 
 [template-haproxy-cfg]
 filename = haproxy.cfg.in
diff --git a/stack/erp5/instance-balancer.cfg.in b/stack/erp5/instance-balancer.cfg.in
index 1aab5c5fe..25bb53ce0 100644
--- a/stack/erp5/instance-balancer.cfg.in
+++ b/stack/erp5/instance-balancer.cfg.in
@@ -1,5 +1,6 @@
 {% set part_list = [] -%}
 {% set ssl_parameter_dict = slapparameter_dict.get('ssl', {}) %}
+{% set caucase_url = slapparameter_dict.get('caucase-url', '') -%}
 {% macro section(name) %}{% do part_list.append(name) %}{{ name }}{% endmacro -%}
 {% set use_ipv6 = slapparameter_dict.get('use-ipv6', False) -%}
 {#
@@ -36,6 +37,56 @@ context = key content {{content_section_name}}:content
 mode = {{ mode }}
 {%- endmacro %}
 
+[certificate-request-base]
+recipe = slapos.cookbook:wrapper
+wrapper-path = ${directory:bin}/request-instance-certificate
+parameters-extra = true
+command-line = {{ parameter_dict['bin-directory'] }}/caucase-cliweb
+  --crt-file ${apache-conf-ssl:cert}
+  --key-file ${apache-conf-ssl:key}
+  --crl-file ${apache-conf-ssl:crl}
+  --ca-url {{ caucase_url }}
+  --ca-crt-file ${apache-conf-ssl:ca-cert}
+
+{% macro request_cert(name, common_name) -%}
+{% set get_crl_periodicity = slapparameter_dict.get('crl-update-periodicity', 'daily') -%}
+
+[{{ section(name ~ '-certificate-request') }}]
+recipe = slapos.cookbook:wrapper
+wrapper-path = ${directory:services}/request-{{ name }}-certificate
+command-line =
+  ${certificate-request-base:wrapper-path}
+  --cn {{ common_name }}
+  --request
+
+[{{ section(name ~ '-renew-cron-entry') }}]
+recipe = slapos.cookbook:cron.d
+cron-entries = ${cron:cron-entries}
+name = {{ name }}-certificate-auto-renew
+time = weekly
+# 2592000 = 30*24*60*60  equivalent to one month in seconds
+command = ${certificate-request-base:wrapper-path} --renew --threshold 2592000 --on-renew="${apache-graceful:output}"
+
+[{{ section(name ~ '-download-crl') }}]
+# download the crl for the first time
+recipe = plone.recipe.command
+command = 
+  if [ ! -s "${apache-conf-ssl:crl}" ]; then
+    ${certificate-request-base:wrapper-path} --update-crl
+  fi
+update-command = ${:command}
+stop-on-error = true
+
+[{{ section(name ~ '-update-crl-cron-entry') }}]
+recipe = slapos.cookbook:cron.d
+cron-entries = ${cron:cron-entries}
+name = {{ name }}-update-crl
+time = {{ get_crl_periodicity }}
+# XXX - Update crl call apache graceful restart, it's not recommended to check crl too often, Apache
+# has an issue with reload and can be frozen and stop responding. Default periodicity time = daily
+command = ${certificate-request-base:wrapper-path} --update-crl --on-crl-update="${apache-graceful:output}"
+{%- endmacro %}
+
 {% if use_ipv6 -%}
 [zope-tunnel-base]
 recipe = slapos.cookbook:ipv4toipv6
@@ -81,6 +132,7 @@ ipv6 = {{ zope_address.split(']:')[0][1:] }}
 -#}
 {%   do zope_family_address_list[0][0] -%}
 {%   set haproxy_port = next_port() -%}
+{%   set backend_path = slapparameter_dict['backend-path-dict'][family_name] -%}
 {%   do haproxy_dict.__setitem__(family_name, (haproxy_port, zope_family_address_list)) -%}
 {%   if has_webdav -%}
 {%     set internal_scheme = 'http' -%}{# mod_rewrite does not recognise webdav scheme -#}
@@ -89,7 +141,8 @@ ipv6 = {{ zope_address.split(']:')[0][1:] }}
 {%     set internal_scheme = 'http' -%}
 {%     set external_scheme = 'https' -%}
 {%   endif -%}
-{%   do apache_dict.__setitem__(family_name, (next_port(), external_scheme, internal_scheme ~ '://' ~ ipv4 ~ ':' ~ haproxy_port ~ slapparameter_dict['backend-path'])) -%}
+{%   set ssl_authentication = slapparameter_dict['ssl-authentication-dict'].get(family_name, False) -%}
+{%   do apache_dict.__setitem__(family_name, (next_port(), external_scheme, internal_scheme ~ '://' ~ ipv4 ~ ':' ~ haproxy_port ~ backend_path, ssl_authentication)) -%}
 {% endfor -%}
 
 [haproxy-cfg-parameter-dict]
@@ -122,6 +175,7 @@ crl = ${directory:apache-conf}/crl.pem
 backend-list = {{ dumps(apache_dict.values()) }}
 ip-list = {{ dumps(apache_ip_list) }}
 pid-file = ${directory:run}/apache.pid
+log-dir = ${directory:log}
 error-log = ${directory:log}/apache-error.log
 access-log = ${directory:log}/apache-access.log
 # Apache 2.4's default value (60 seconds) can be a bit too short
@@ -145,6 +199,18 @@ context = section parameter_dict apache-conf-parameter-dict
 recipe = slapos.cookbook:wrapper
 wrapper-path = ${directory:services}/apache
 command-line = "{{ parameter_dict['apache'] }}/bin/httpd" -f "${apache-conf:rendered}" -DFOREGROUND
+wait-for-files =
+  ${apache-conf-ssl:cert}
+  ${apache-conf-ssl:key}
+
+[apache-graceful]
+recipe = collective.recipe.template
+input = inline:
+  #!/bin/sh
+  kill -USR1 "$(cat '${apache-conf-parameter-dict:pid-file}')"
+
+output = ${directory:bin}/apache-httpd-graceful
+mode = 700
 
 [{{ section('apache-promise') }}]
 # Check any apache port in ipv4, expect other ports and ipv6 to behave consistently
@@ -155,7 +221,7 @@ port = {{ apache_dict.values()[0][0] }}
 
 [publish]
 recipe = slapos.cookbook:publish.serialised
-{% for family_name, (apache_port, scheme, _) in apache_dict.items() -%}
+{% for family_name, (apache_port, scheme, _, _) in apache_dict.items() -%}
 {{   family_name ~ '-v6' }} = {% if ipv6_set %}{{ scheme ~ '://[' ~ ipv6 ~ ']:' ~ apache_port }}{% endif %}
 {{   family_name }} = {{ scheme ~ '://' ~ ipv4 ~ ':' ~ apache_port }}
 {% endfor -%}
@@ -167,6 +233,11 @@ key = ${apache-ssl-key:rendered}
 cert = ${apache-ssl-cert:rendered}
 {{ simplefile('apache-ssl-key', '${apache-conf-ssl:key}', ssl_parameter_dict['key']) }}
 {{ simplefile('apache-ssl-cert', '${apache-conf-ssl:cert}', ssl_parameter_dict['cert']) }}
+{% elif caucase_url -%}
+key = ${apache-conf-ssl:key}
+cert = ${apache-conf-ssl:cert}
+
+{{ request_cert('erp5', 'instance.apache@erp5') }}
 {% else %}
 recipe = plone.recipe.command
 command = "{{ parameter_dict['openssl'] }}/bin/openssl" req -newkey rsa -batch -new -x509 -days 3650 -nodes -keyout "${:key}" -out "${:cert}"
@@ -180,6 +251,10 @@ cert = ${apache-ssl-ca:rendered}
 crl = ${apache-ssl-crl:rendered}
 {{ simplefile('apache-ssl-ca', '${apache-conf-ssl:ca-cert}', ssl_parameter_dict['ca-cert']) }}
 {{ simplefile('apache-ssl-crl', '${apache-conf-ssl:crl}', ssl_parameter_dict['crl']) }}
+{% elif caucase_url -%}
+cert = ${apache-conf-ssl:ca-cert}
+crl = ${apache-conf-ssl:crl}
+
 {% else %}
 cert =
 crl =
diff --git a/stack/erp5/instance-erp5.cfg.in b/stack/erp5/instance-erp5.cfg.in
index ecdaa0abd..c5fe2e971 100644
--- a/stack/erp5/instance-erp5.cfg.in
+++ b/stack/erp5/instance-erp5.cfg.in
@@ -9,6 +9,8 @@
 {% set has_jupyter = jupyter_dict.get('enable', jupyter_enable_default.lower() in ('true', 'yes')) -%}
 {% set jupyter_zope_family = jupyter_dict.get('zope-family', '') -%}
 {% set monitor_base_url_dict = {} -%}
+{% set caucase_url = slapparameter_dict.get('caucase', {}).pop('url', '') -%}
+{% set crl_update_period = slapparameter_dict.get('caucase', {}).pop('crl-update-periodicity', 'daily') -%}
 [request-common]
 <= request-common-base
 config-use-ipv6 = {{ dumps(slapparameter_dict.get('use-ipv6', False)) }}
@@ -50,6 +52,14 @@ config-{{ k }} = {{ '${' ~ v ~ '}' }}
 connection-url = smtp://127.0.0.2:0/
 {%- endif %}
 
+{% if caucase_url -%}
+{%   do publish_dict.__setitem__('caucase-http-url', caucase_url) -%}
+[request-caucase]
+connection-http-url = {{ caucase_url }}
+{%- else %}
+{{ request('caucase', 'caucase', 'caucase', {'server-port': 8890, 'server-https-port': 8891}, {'http-url': True, 'https-url': False}) }}
+{% endif -%}
+
 {# ZODB -#}
 {% set zodb_dict = {} -%}
 {% set storage_dict = {} -%}
@@ -137,6 +147,7 @@ return =
 {% endif -%}
 config-bt5 = {{ dumps(slapparameter_dict.get('bt5', bt5_default_list)) }}
 config-bt5-repository-url = {{ dumps(slapparameter_dict.get('bt5-repository-url', local_bt5_repository)) }}
+config-caucase-url = ${request-caucase:connection-http-url}
 config-cloudooo-url = ${request-cloudooo:connection-url}
 config-deadlock-debugger-password = ${publish-early:deadlock-debugger-password}
 config-developer-list = {{ dumps(slapparameter_dict.get('developer-list', [inituser_login])) }}
@@ -168,17 +179,22 @@ config-tidstorage-port = ${request-zodb:connection-tidstorage-port}
 software-type = zope
 
 {% set zope_family_dict = {} -%}
+{% set zope_backend_path_dict = {} -%}
+{% set ssl_authentication_dict = {} -%}
 {% set jupyter_zope_family_default = [] -%}
 {% for custom_name, zope_parameter_dict in zope_partition_dict.items() -%}
 {%   set partition_name = 'zope-' ~ custom_name -%}
 {%   set section_name = 'request-' ~ partition_name -%}
 {%   set zope_family = zope_parameter_dict.get('family', 'default') -%}
+{%   set backend_path = zope_parameter_dict.get('backend-path', '/') % {'site-id': site_id} %}
 {#   # default jupyter zope family is first zope family. -#}
 {#   # use list.append() to update it, because in jinja2 set changes only local scope. -#}
 {%   if not jupyter_zope_family_default -%}
 {%     do jupyter_zope_family_default.append(zope_family) -%}
 {%   endif -%}
 {%   do zope_family_dict.setdefault(zope_family, []).append(section_name) -%}
+{%   do zope_backend_path_dict.__setitem__(zope_family, backend_path) -%}
+{%   do ssl_authentication_dict.__setitem__(zope_family, zope_parameter_dict.get('ssl-authentication', False)) -%}
 [{{ section_name }}]
 <= request-zope-base
 name = {{ partition_name }}
@@ -256,10 +272,12 @@ config-{{ name }} = {{ ' ${' ~ zope_section_id ~ ':connection-zope-address-list}
 {% endfor -%}
 # XXX: should those really be same for all families ?
 config-haproxy-server-check-path = {{ dumps(balancer_dict.get('haproxy-server-check-path', '/') % {'site-id': site_id}) }}
-config-backend-path = {{ dumps(balancer_dict.get('apache-backend-path', '/') % {'site-id': site_id}) }}
 config-ssl = {{ dumps(balancer_dict.get('ssl', {})) }}
 config-monitor-passwd = ${monitor-htpasswd:passwd}
-
+config-caucase-url = ${request-caucase:connection-http-url}
+config-crl-update-periodicity = {{ crl_update_period }}
+config-backend-path-dict = {{ dumps(zope_backend_path_dict) }}
+config-ssl-authentication-dict = {{ dumps(ssl_authentication_dict) }}
 
 [request-frontend-base]
 {% if has_frontend -%}
diff --git a/stack/erp5/instance.cfg.in b/stack/erp5/instance.cfg.in
index 04de68fb1..18d678a20 100644
--- a/stack/erp5/instance.cfg.in
+++ b/stack/erp5/instance.cfg.in
@@ -1,5 +1,7 @@
 [buildout]
-extends = {{ instance_common_cfg }}
+extends =
+  {{ instance_common_cfg }}
+  {{ caucase_template }}
 
 [jinja2-template-base]
 mode = 644
@@ -97,6 +99,7 @@ bin-directory = {{ bin_directory }}
 apachedex-location = {{ bin_directory }}/apachedex
 run-apachedex-location = {{ bin_directory }}/runApacheDex
 6tunnel = {{ sixtunnel_location }}
+curl-location = {{ curl_location }}
 dash = {{ dash_location }}
 template-haproxy-cfg = {{ template_haproxy_cfg }}
 template-apache-conf = {{ template_apache_conf }}
@@ -216,6 +219,7 @@ create-erp5-site = dynamic-template-create-erp5-site:rendered
 RootSoftwareInstance = ${:default}
 # Internal software types
 kumofs = dynamic-template-kumofs:rendered
+caucase = dynamic-template-caucase:rendered
 cloudooo = dynamic-template-cloudooo:rendered
 mariadb = dynamic-template-mariadb:rendered
 balancer = dynamic-template-balancer:rendered
-- 
2.30.9