Merge "Add fixups configuration / processing"
This commit is contained in:
commit
af129c1340
|
@ -235,6 +235,14 @@ Sample configuration for the default backend:
|
|||
|
||||
For more information, please refer to the documentation.
|
||||
|
||||
Fixups
|
||||
======
|
||||
|
||||
Anchor can modify the submitted CSRs in order to enforce some rules, remove
|
||||
deprecated elements, or just add information. Submitted CSR may be modified or
|
||||
entirely redone. Fixup are loaded from "anchor.fixups" namespace and can take
|
||||
parameters just like validators.
|
||||
|
||||
Reporting bugs and contributing
|
||||
===============================
|
||||
|
||||
|
|
|
@ -172,6 +172,18 @@ def validate_registration_authority_config(ra_name, conf):
|
|||
config_check_domains(ra_validators)
|
||||
logger.info("Validators OK for registration authority: %s", ra_name)
|
||||
|
||||
ra_fixups = ra_conf.get('fixups', {})
|
||||
|
||||
for step in ra_fixups.keys():
|
||||
try:
|
||||
jsonloader.conf.get_fixup(step)
|
||||
except KeyError:
|
||||
raise ConfigValidationException(
|
||||
"Unknown fixup <{}> found (for registration "
|
||||
"authority {})".format(step, ra_name))
|
||||
|
||||
logger.info("Fixups OK for registration authority: %s", ra_name)
|
||||
|
||||
|
||||
def load_config():
|
||||
"""Attempt to find and load a JSON configuration file.
|
||||
|
|
|
@ -176,6 +176,63 @@ def dispatch_sign(ra_name, csr):
|
|||
return cert_pem
|
||||
|
||||
|
||||
def _run_fixup(name, body, args):
|
||||
"""Parse the fixup tuple, call the fixup, and return the new csr.
|
||||
|
||||
:param name: the fixup name
|
||||
:param body: fixup body, directly from config
|
||||
:param args: additional arguments to pass to the fixup function
|
||||
:return: the fixed csr
|
||||
"""
|
||||
# careful to not modify the master copy of args with local params
|
||||
new_kwargs = args.copy()
|
||||
new_kwargs.update(body)
|
||||
|
||||
# perform the actual check
|
||||
logger.debug("_run_fixup: fixup <%s> with arguments: %s", name, body)
|
||||
try:
|
||||
fixup = jsonloader.conf.get_fixup(name)
|
||||
new_csr = fixup(**new_kwargs)
|
||||
logger.debug("_run_fixup: success: <%s> ", name)
|
||||
return new_csr
|
||||
except Exception:
|
||||
logger.exception("_run_fixup: FAILED: <%s>", name)
|
||||
return None
|
||||
|
||||
|
||||
def fixup_csr(ra_name, csr, request):
|
||||
"""Apply configured changes to the certificate.
|
||||
|
||||
:param ra_name: registration authority name
|
||||
:param csr: X509 certificate signing request
|
||||
:param request: pecan request
|
||||
"""
|
||||
ra_conf = jsonloader.config_for_registration_authority(ra_name)
|
||||
args = {'csr': csr,
|
||||
'conf': ra_conf,
|
||||
'request': request}
|
||||
|
||||
fixups = ra_conf.get('fixups', {})
|
||||
try:
|
||||
for fixup_name, fixup in fixups.items():
|
||||
new_csr = _run_fixup(fixup_name, fixup, args)
|
||||
if new_csr is None:
|
||||
pecan.abort(500, "Could not finish all required modifications")
|
||||
if not isinstance(new_csr, signing_request.X509Csr):
|
||||
logger.error("Fixup %s returned incorrect object", fixup_name)
|
||||
pecan.abort(500, "Could not finish all required modifications")
|
||||
args['csr'] = new_csr
|
||||
|
||||
except http_status.HTTPInternalServerError:
|
||||
raise
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to execute fixups")
|
||||
pecan.abort(500, "Could not finish all required modifications")
|
||||
|
||||
return args['csr']
|
||||
|
||||
|
||||
def sign(csr, ca_conf):
|
||||
"""Generate an X.509 certificate and sign it.
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ class SignInstanceController(GenericInstanceController):
|
|||
csr = certificate_ops.parse_csr(pecan.request.POST.get('csr'),
|
||||
pecan.request.POST.get('encoding'))
|
||||
certificate_ops.validate_csr(ra_name, auth_result, csr, pecan.request)
|
||||
csr = certificate_ops.fixup_csr(ra_name, csr, pecan.request)
|
||||
|
||||
return certificate_ops.dispatch_sign(ra_name, csr)
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ class AnchorConf():
|
|||
self._validators = stevedore.ExtensionManager("anchor.validators")
|
||||
self._authentication = stevedore.ExtensionManager(
|
||||
"anchor.authentication")
|
||||
self._fixups = stevedore.ExtensionManager("anchor.fixups")
|
||||
|
||||
def get_signing_backend(self, name):
|
||||
return self._signing_backends[name].plugin
|
||||
|
@ -73,6 +74,9 @@ class AnchorConf():
|
|||
def get_authentication(self, name):
|
||||
return self._authentication[name].plugin
|
||||
|
||||
def get_fixup(self, name):
|
||||
return self._fixups[name].plugin
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
'''Property to return the config dictionary
|
||||
|
|
|
@ -151,6 +151,8 @@ and the list of validators applied to each request.
|
|||
"source_cidrs": {
|
||||
"cidrs": [ "127.0.0.0/8" ]
|
||||
}
|
||||
},
|
||||
"fixups": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +164,8 @@ against two validators (``ca_status`` and ``source_cidrs``) and if they pass,
|
|||
the CSR will be signed by the previously defined signing ca called ``local``.
|
||||
|
||||
Each validator has its own set of parameters described separately in the
|
||||
:doc:`validators section </validators>`.
|
||||
:doc:`validators section </validators>`. Same for fixups described in
|
||||
:doc:`fixups section </fixups>`
|
||||
|
||||
|
||||
Example configuration
|
||||
|
@ -200,6 +203,8 @@ Example configuration
|
|||
"source_cidrs": {
|
||||
"cidrs": [ "127.0.0.0/8" ]
|
||||
}
|
||||
},
|
||||
"fixups": {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
Fixups
|
||||
======
|
||||
|
||||
Fixups can be used to modify submitted CSRs before sigining. That means for
|
||||
example adding extra name elements, or extensions. Each fixup is loaded from
|
||||
the "anchor.fixups" namespace using stevedore and gets access to the parsed CSR
|
||||
and the configuration.
|
||||
|
||||
Unlike validators, each fixup has to return either a new CSR structure or the
|
||||
modified original.
|
|
@ -17,6 +17,7 @@ Contents:
|
|||
signing_backends
|
||||
ephemeralPKI
|
||||
validators
|
||||
fixups
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
|
|
@ -55,11 +55,14 @@ class DefaultConfigMixin(object):
|
|||
"allowed_domains": [".test.com"]
|
||||
}
|
||||
}
|
||||
self.sample_conf_fixups = {
|
||||
}
|
||||
self.sample_conf_ra = {
|
||||
"default_ra": {
|
||||
"authentication": "default_auth",
|
||||
"signing_ca": "default_ca",
|
||||
"validators": self.sample_conf_validators
|
||||
"validators": self.sample_conf_validators,
|
||||
"fixups": self.sample_conf_fixups,
|
||||
}
|
||||
}
|
||||
self.sample_conf = {
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import webob
|
||||
|
||||
from anchor import certificate_ops
|
||||
from anchor import jsonloader
|
||||
from anchor.X509 import signing_request
|
||||
import tests
|
||||
|
||||
|
||||
class TestFixupFunctionality(tests.DefaultConfigMixin, unittest.TestCase):
|
||||
csr_data_with_cn = textwrap.dedent(u"""
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIDBTCCAe0CAQAwgb8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
|
||||
MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMSEwHwYDVQQKExhPcGVuU3RhY2sgU2Vj
|
||||
dXJpdHkgR3JvdXAxETAPBgNVBAsTCFNlY3VyaXR5MRYwFAYDVQQDEw1vc3NnLnRl
|
||||
c3QuY29tMTUwMwYJKoZIhvcNAQkBFiZvcGVuc3RhY2stc2VjdXJpdHlAbGlzdHMu
|
||||
b3BlbnN0YWNrLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJCw
|
||||
hIh3kwHGrGff7bHpY0x7ebXS8CfnwDx/wFSqlBeARL9f4riN172P4hkk7F+QQ2R9
|
||||
88osQX4dmbQZDX18y85TTQv9jmtzvTZtJM2UQ80XMIVLZjpK5966cmJKqn/s+IaL
|
||||
zh+kqyb7S6xV0590VarEFZ6JsXdxU9TtVHOWCfn/P8swr5DCTzsE/LUIuVdqgkGh
|
||||
g63E9iLYtAOUcQv6lpmrI8NHOMK2F7XnP64IEshpZ4POzc7m8nTEHHb0+xxxiive
|
||||
mwLTp6pyZ5wBx/Dvk2Dc7SF6x51wOxAxdWc3vxwA5Q2nbFK2RlBHCiIi+ZK3i5S/
|
||||
tOkcQydQ0Cl9escDrv0CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQA1dpxxTGFF
|
||||
TGFenVJlT2uecvXK4UePeaslRx2P1k3xwJK9ZEvKY297cqhK5Y8kWyzNUjGFLHPr
|
||||
RlgjFMYlUICNgCdcWD2b0avZ9q648+F3b9CWKg0kNMhxyQpXdSeLZOzpDVUyr6TN
|
||||
GcCZqcQQclixruXsIGQoZFIXazGju2UTtxwK/J87u2S0yR2bR48dPlNXAWKV+e4o
|
||||
Ua0RaDUUBypZNMMbY6KSB6C7oXGzA/WOnvNz9PzhXlqgWhOv5M6iG3sYDtKllXJT
|
||||
7lcLhUzNVdWaPveTqX/V8QX//53IkyNa+IBm+H84UE5M0GFunqFBYqrWw8S46tMQ
|
||||
JQxgjf65ujnn
|
||||
-----END CERTIFICATE REQUEST-----""")
|
||||
"""
|
||||
Subject:
|
||||
C=US, ST=California, L=San Francisco,
|
||||
O=OpenStack Security Group, OU=Security,
|
||||
CN=ossg.test.com/emailAddress=openstack-security@lists.openstack.org
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestFixupFunctionality, self).setUp()
|
||||
jsonloader.conf.load_extensions()
|
||||
self.csr = signing_request.X509Csr.from_buffer(
|
||||
TestFixupFunctionality.csr_data_with_cn)
|
||||
|
||||
def test_with_noop(self):
|
||||
"""Ensure single fixup is processed."""
|
||||
|
||||
self.sample_conf_ra['default_ra']['fixups'] = {'noop': {}}
|
||||
data = self.sample_conf
|
||||
|
||||
config = "anchor.jsonloader.conf._config"
|
||||
mock_noop = mock.MagicMock()
|
||||
mock_noop.name = "noop"
|
||||
mock_noop.plugin.return_value = self.csr
|
||||
|
||||
jsonloader.conf._fixups = jsonloader.conf._fixups.make_test_instance(
|
||||
[mock_noop], 'anchor.fixups')
|
||||
|
||||
with mock.patch.dict(config, data):
|
||||
certificate_ops.fixup_csr('default_ra', self.csr, None)
|
||||
|
||||
mock_noop.plugin.assert_called_with(
|
||||
csr=self.csr, conf=self.sample_conf_ra['default_ra'], request=None)
|
||||
|
||||
def test_with_no_fixups(self):
|
||||
"""Ensure no fixups is ok."""
|
||||
|
||||
self.sample_conf_ra['default_ra']['fixups'] = {}
|
||||
data = self.sample_conf
|
||||
|
||||
config = "anchor.jsonloader.conf._config"
|
||||
with mock.patch.dict(config, data):
|
||||
res = certificate_ops.fixup_csr('default_ra', self.csr, None)
|
||||
self.assertIs(res, self.csr)
|
||||
|
||||
def test_with_broken_fixup(self):
|
||||
"""Ensure broken fixups stop processing."""
|
||||
|
||||
self.sample_conf_ra['default_ra']['fixups'] = {'broken': {}}
|
||||
data = self.sample_conf
|
||||
|
||||
config = "anchor.jsonloader.conf._config"
|
||||
mock_noop = mock.MagicMock()
|
||||
mock_noop.name = "broken"
|
||||
mock_noop.plugin.side_effects = Exception("BOOM")
|
||||
|
||||
jsonloader.conf._fixups = jsonloader.conf._fixups.make_test_instance(
|
||||
[mock_noop], 'anchor.fixups')
|
||||
|
||||
with mock.patch.dict(config, data):
|
||||
with self.assertRaises(webob.exc.WSGIHTTPException):
|
||||
certificate_ops.fixup_csr('default_ra', self.csr, None)
|
Loading…
Reference in New Issue