summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/development/cfg.txt16
-rw-r--r--doc/exts/xmlschema.py37
-rw-r--r--doc/server/plugins/generators/cfg.txt215
-rw-r--r--schemas/authorizedkeys.xsd105
-rw-r--r--schemas/privkey.xsd138
-rw-r--r--schemas/pubkey.xsd16
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py4
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py101
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py258
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py63
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py95
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py51
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgAuthorizedKeysGenerator.py176
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py435
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPublicKeyCreator.py76
-rw-r--r--testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py61
16 files changed, 1766 insertions, 81 deletions
diff --git a/doc/development/cfg.txt b/doc/development/cfg.txt
index ba71232e7..6533e0d7a 100644
--- a/doc/development/cfg.txt
+++ b/doc/development/cfg.txt
@@ -20,10 +20,11 @@ implement more than one handler type.
Cfg Handler Types
=================
-There are four different types of Cfg handlers. A new handler must
+There are several different types of Cfg handlers. A new handler must
inherit either from one of these classes, or from an existing handler.
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgGenerator
+.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgCreator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgFilter
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgInfo
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgVerifier
@@ -43,6 +44,7 @@ Cfg Exceptions
Cfg handlers may produce the following exceptions:
.. autoexception:: Bcfg2.Server.Plugins.Cfg.CfgVerificationError
+.. autoexception:: Bcfg2.Server.Plugins.Cfg.CfgCreationError
In addition, Cfg handlers may produce the following base plugin
exceptions:
@@ -53,10 +55,11 @@ exceptions:
.. autoexception:: Bcfg2.Server.Plugin.exceptions.PluginInitError
:noindex:
-Accessing Configuration Options
-===============================
+Global Variables
+================
.. autodata:: Bcfg2.Server.Plugins.Cfg.SETUP
+.. autodata:: Bcfg2.Server.Plugins.Cfg.CFG
Existing Cfg Handlers
=====================
@@ -70,6 +73,13 @@ Generators
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenerator.CfgEncryptedGenerator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenshiGenerator.CfgEncryptedGenshiGenerator
.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgEncryptedCheetahGenerator.CfgEncryptedCheetahGenerator
+.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.CfgAuthorizedKeysGenerator
+
+Creators
+--------
+
+.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.CfgPrivateKeyCreator
+.. autoclass:: Bcfg2.Server.Plugins.Cfg.CfgPublicKeyCreator.CfgPublicKeyCreator
Filters
-------
diff --git a/doc/exts/xmlschema.py b/doc/exts/xmlschema.py
index fc5788107..81affc610 100644
--- a/doc/exts/xmlschema.py
+++ b/doc/exts/xmlschema.py
@@ -299,15 +299,24 @@ class XMLDocumentor(object):
def document_complexType(self):
rv = nodes.definition_list()
+ content = self.entity.find("xs:simpleContent", namespaces=NSMAP)
+ if content is not None:
+ base = content.xpath("xs:extension|xs:restriction",
+ namespaces=NSMAP)[0]
+ attr_container = base
+ else:
+ base = None
+ attr_container = self.entity
+
##### ATTRIBUTES #####
table, tbody = self.get_attr_table()
- attrs = self.get_attrs(self.entity)
+ attrs = self.get_attrs(attr_container)
if attrs:
tbody.extend(attrs)
foreign_attr_groups = nodes.bullet_list()
- for agroup in self.entity.xpath("xs:attributeGroup",
- namespaces=NSMAP):
+ for agroup in attr_container.xpath("xs:attributeGroup",
+ namespaces=NSMAP):
# if the attribute group is in another namespace, just
# link to it
ns, name = self.split_ns(agroup.get('ref'))
@@ -349,11 +358,17 @@ class XMLDocumentor(object):
append_node(rv, nodes.definition, *groups)
##### TEXT CONTENT #####
- if (self.include['text'] and
- self.entity.get("mixed", "false").lower() == "true"):
- append_node(rv, nodes.term, text("Text content:"))
- append_node(rv, nodes.definition,
- build_paragraph(self.get_values_from_simpletype()))
+ if self.include['text']:
+ if self.entity.get("mixed", "false").lower() == "true":
+ append_node(rv, nodes.term, text("Text content:"))
+ append_node(rv, nodes.definition,
+ build_paragraph(self.get_values_from_simpletype()))
+ elif base is not None:
+ append_node(rv, nodes.term, text("Text content:"))
+ append_node(
+ rv, nodes.definition,
+ build_paragraph(self.get_values_from_simpletype(content)))
+
return [rv]
def document_attributeGroup(self):
@@ -544,8 +559,10 @@ class XMLDocumentor(object):
if entity is None:
entity = self.entity
# todo: xs:union, xs:list
- restriction = entity.find("xs:restriction", namespaces=NSMAP)
- if restriction is None:
+ try:
+ restriction = entity.xpath("xs:restriction|xs:extension",
+ namespaces=NSMAP)[0]
+ except IndexError:
return "Any"
doc = self.get_doc(restriction)
if len(doc) == 1 and len(doc[0]) == 0:
diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt
index 94394f98f..a33028a13 100644
--- a/doc/server/plugins/generators/cfg.txt
+++ b/doc/server/plugins/generators/cfg.txt
@@ -29,7 +29,7 @@ in ``Cfg/etc/passwd/passwd``, while the ssh pam module config file,
``/etc/pam.d/sshd``, goes in ``Cfg/etc/pam.d/sshd/sshd``. The reason for
the like-name directory is to allow multiple versions of each file to
exist, as described below. Note that these files are exact copies of what
-will appear on the client machine (except when using genshi or cheetah
+will appear on the client machine (except when using Genshi or Cheetah
templating -- see below).
Group-Specific Files
@@ -355,6 +355,219 @@ either order, e.g.::
To encrypt or decrypt a file, use :ref:`bcfg2-crypt`.
+.. _server-plugins-generators-cfg-sshkeys:
+
+SSH Keys
+========
+
+.. versionadded:: 1.3.0
+
+Cfg can also be used to automatically create and distribute SSH key
+pairs and the ``authorized_keys`` file.
+
+Keys can be created one of two ways:
+
+* Host-specific keys, where each client has its own key pair. This is
+ the default.
+* Group-specific keys. To do this, you must set ``category`` in
+ either ``bcfg2.conf`` (see "Configuration" below) or in
+ ``privkey.xml``. Keys created for a given client will be specific
+ to that client's group in the specified category.
+
+Group-specific keys are useful if, for instance, you have multiple
+distinct environments (development, testing, production, for example)
+and want to maintain separate keys for each environment.
+
+This feature actually creates static keys, much like the
+:ref:`server-plugins-generators-sshbase` plugin creates SSH
+certificates. It doesn't generate them on the fly for each request;
+it generates the key once, then saves it to the filesystem.
+
+Creating key pairs
+------------------
+
+To create an SSH key pair, you need to define how the private key will
+be created in ``privkey.xml``. For instance, to create
+``/home/foo/.ssh/id_rsa``, you would create
+``/var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa/privkey.xml``.
+
+This will create *both* the private key and the public key; the latter
+is created by appending ``.pub`` to the private key filename. It is
+not possible to change the public key filename.
+
+You may *optionally* also create a corresponding ``pubkey.xml``, which
+will allow the key pair to be created when the public key is
+requested. (For the example above, you'd create
+``/var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa.pub/pubkey.xml``. This can
+speed up the propagation of SSH keys throughout your managed systems,
+particularly if you use the ``authorized_keys`` generation feature.
+
+``privkey.xml``
+~~~~~~~~~~~~~~~
+
+``privkey.xml`` contains a top-level ``PrivateKey`` element, and is
+structured as follows:
+
+.. xml:element:: PrivateKey
+ :linktotype:
+
+``pubkey.xml``
+~~~~~~~~~~~~~~~
+
+``pubkey.xml`` only ever contains a single line:
+
+.. code-block:: xml
+
+ <PublicKey/>
+
+.. xml:element:: PublicKey
+
+It acts only as a flag to Bcfg2 that a key pair should be generated, if
+none exists, using the associated ``privkey.xml`` file. The path to
+``privkey.xml`` is determined by removing ``.pub`` from the directory
+containing ``pubkey.xml``. I.e., if you create
+``/var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa.pub/pubkey.xml``, then Bcfg2
+will use ``/var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa/privkey.xml`` to
+create the key pair.
+
+Use of ``pubkey.xml`` is optional, but is recommended. If you do not
+use ``pubkey.xml`` files, you may encounter two problems:
+
+* On the first Bcfg2 client run on a given client, the private keys
+ may be present but the public keys may not be. This will be fixed
+ by running ``bcfg2`` again.
+* If you are including an automatically created public key in
+ ``authorized_keys``, it will not be created until the client the key
+ is for requests the key pair.
+
+As an example of this latter scenario, suppose that your
+``authorized_keys.xml`` allows access to foo.example.com from
+``/root/.ssh/id_rsa.pub`` for bar.example.com. If bar.example.com has
+not run the Bcfg2 client, then no key pair will have been generated,
+and generating the foo.example.com ``authorized_keys`` file will
+create a warning. But if you create
+``Cfg/root/.ssh/id_rsa.pub/pubkey.xml``, then building
+``authorized_keys`` for foo.example.com will create root's keypair for
+bar.example.com.
+
+.. note::
+
+ In order to use ``pubkey.xml``, there *must* be a corresponding
+ ``privkey.xml``. You cannot, for instance, populate a directory
+ with manually-generated private SSH keys, drop ``pubkey.xml`` in
+ the related public key directory, and expect Bcfg2 to generate the
+ public keys. It will not.
+
+Examples
+~~~~~~~~
+
+``privkey.xml`` can, at its simplest, be very simple indeed:
+
+.. code-block:: xml
+
+ <PrivateKey/>
+
+This will create a private key with all defaults. Or it can be more
+complex:
+
+.. code-block:: xml
+
+ <PrivateKey category="environment"/>
+ <Params bits="1024" type="dsa"/>
+ <Group name="secure">
+ <Passphrase encrypted="secure">U2FsdGVkX19xACol83uyPELP94s4CmngD12oU6PLLuE=</Passphrase>
+ </Group>
+ </PrivateKey>
+
+This creates a 1024-bit DSA key for each group in the ``environment``
+category, and keys for clients in the ``secure`` group will be
+protected with the given (encrypted) passphrase.
+
+To complete the example, assume that this file was saved at
+``/var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa/privkey.xml``. If a client
+in the ``development`` group, which is a group in the ``environment``
+category, requests the private key, then the following files would be
+created::
+
+ /var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa/id_rsa.G50_development
+ /var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa.pub/id_rsa.pub.G50_development
+
+``/var/lib/bcfg2/Cfg/home/foo/.ssh/id_rsa.pub`` would be created if it
+did not exist.
+
+Subsequent clients that were also members of the ``development``
+environment would get the keys that have already been generated.
+
+``pubkey.xml`` always contains a single empty tag:
+
+.. code-block:: xml
+
+ <PublicKey/>
+
+Generating ``authorized_keys``
+------------------------------
+
+``authorized_keys`` can be automatically generated from public SSH
+keys that exist in the Cfg tree. The keys in question can be
+generated from ``privkey.xml``, or they can be manually created.
+
+If a key doesn't exist when ``authorized_keys`` is generated, the key
+will only be created if ``pubkey.xml`` exists. If that is not the
+case, a warning will be produced.
+
+To generate ``authorized_keys``, create ``authorized_keys.xml``, e.g.:
+``/var/lib/bcfg2/Cfg/root/.ssh/authorized_keys/authorized_keys.xml``.
+
+``authorized_keys.xml``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+``authorized_keys.xml`` is structured as follows:
+
+.. xml:element:: AuthorizedKeys
+ :linktotype:
+
+Example
+~~~~~~~
+
+.. code-block:: xml
+
+ <AuthorizedKeys>
+ <Group name="some_group">
+ <Allow from="/root/.ssh/id_rsa.pub"/>
+ <Allow from="/root/.ssh/id_rsa.pub" group="test"/>
+ </Group>
+ <Allow from="/root/.ssh/id_rsa.pub" host="foo.example.com"/>
+ <Allow from="/home/foo_user/.ssh/id_rsa.pub">
+ <Params command="/home/foo_user/.ssh/ssh_command_filter"/>
+ </Allow>
+ <Allow>
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDw/rgKQeARRAHK5bQQhAAe1b+gzdtqBXWrZIQ6cIaLgxqj76TwZ3DY4A6aW9RgC4zzd0p4a9MfsScUIB4+UeZsx9GopUj4U6H8Vz7S3pXxrr4E9logVLuSfOLFbI/wMWNRuOANqquLYQ+JYWKeP4kagkVp0aAWp7mH5IOI0rp0A6qE2you4ep9N/nKvHDrtypwhYBWprsgTUXXMHnAWGmyuHGYWxNYBV9AARPdAvZfb8ggtuwibcOULlyK4DdVNbDTAN1/BDBE1ve6WZDcrc386KhqUGj/yoRyPjNZ46uZiOjRr3cdY6yUZoCwzzxvm5vle6mEbLjHgjGEMQMArzM9 vendor@example.com
+ </Allow>
+ </AuthorizedKeys>
+
+Configuration
+-------------
+
+In addition to ``privkey.xml`` and ``authorized_keys.xml``, described
+above, the behavior of the SSH key generation feature can be
+influenced by several options in ``bcfg2.conf``:
+
++----------------+---------------------------------------------------------+-----------------------+------------+
+| Option | Description | Values | Default |
++================+=========================================================+=======================+============+
+| ``passphrase`` | Use the named passphrase to encrypt private keys on the | String | None |
+| | filesystem. The passphrase must be defined in the | | |
+| | ``[encryption]`` section. See :ref:`server-encryption` | | |
+| | for more details on encryption in Bcfg2 in general. | | |
++----------------+---------------------------------------------------------+-----------------------+------------+
+| ``category`` | Generate keys specific to groups in the given category. | String | None |
+| | It is best to pick a category that all clients have a | | |
+| | group from. | | |
++----------------+---------------------------------------------------------+-----------------------+------------+
+| ``decrypt`` | If decrypt is set to ``lax``, then a key that cannot be | ``strict`` or ``lax`` | ``strict`` |
+| | decrypted will produce a warning instead of an error. | | |
++----------------+---------------------------------------------------------+-----------------------+------------+
+
Deltas
======
diff --git a/schemas/authorizedkeys.xsd b/schemas/authorizedkeys.xsd
new file mode 100644
index 000000000..848f99bae
--- /dev/null
+++ b/schemas/authorizedkeys.xsd
@@ -0,0 +1,105 @@
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xml:lang="en">
+ <xsd:annotation>
+ <xsd:documentation>
+ Schema for :ref:`server-plugins-generators-cfg-sshkeys`
+ ``authorizedkeys.xml``
+ </xsd:documentation>
+ </xsd:annotation>
+
+ <xsd:complexType name="AuthorizedKeysGroupType">
+ <xsd:annotation>
+ <xsd:documentation>
+ An **AuthorizedKeysGroupType** is a tag used to provide logic.
+ Child entries of an AuthorizedKeysGroupType tag only apply to
+ machines that match the condition specified -- either
+ membership in a group, or a matching client name.
+ :xml:attribute:`AuthorizedKeysGroupType:negate` can be set to
+ negate the sense of the match.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:choice minOccurs="1" maxOccurs="unbounded">
+ <xsd:element name="Allow" type="AllowType"/>
+ <xsd:element name="Group" type="AuthorizedKeysGroupType"/>
+ <xsd:element name="Client" type="AuthorizedKeysGroupType"/>
+ </xsd:choice>
+ <xsd:attribute name='name' type='xsd:string'>
+ <xsd:annotation>
+ <xsd:documentation>
+ The name of the client or group to match on. Child entries
+ will only apply to this client or group (unless
+ :xml:attribute:`AuthorizedKeysGroupType:negate` is set).
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name='negate' type='xsd:boolean'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Negate the sense of the match, so that child entries only
+ apply to a client if it is not a member of the given group
+ or does not have the given name.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:complexType>
+
+ <xsd:complexType name="AllowType" mixed="true">
+ <xsd:annotation>
+ <xsd:documentation>
+ Allow access from a public key, given either as text content,
+ or described by the attributes.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:choice minOccurs="0" maxOccurs="unbounded">
+ <xsd:element name="Params" type="AuthorizedKeysParamsType"/>
+ </xsd:choice>
+ <xsd:attribute name="from" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>
+ The path of the public key to allow.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="group" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>
+ Use a public key specific to the given group, instead of the
+ public key specific to the appropriate category group of the
+ current client.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="host" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>
+ Use a public key specific to the given host.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:complexType>
+
+ <xsd:complexType name="AuthorizedKeysParamsType">
+ <xsd:annotation>
+ <xsd:documentation>
+ Specify parameters for public key authentication and
+ connection. See :manpage:`sshd(8)` for details on allowable
+ parameters.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:anyAttribute processContents="lax"/>
+ </xsd:complexType>
+
+ <xsd:element name="AuthorizedKeys">
+ <xsd:annotation>
+ <xsd:documentation>
+ Top-level tag for describing a generated SSH key pair.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:choice minOccurs="1" maxOccurs="unbounded">
+ <xsd:element name="Allow" type="AllowType"/>
+ <xsd:element name="Group" type="AuthorizedKeysGroupType"/>
+ <xsd:element name="Client" type="AuthorizedKeysGroupType"/>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+</xsd:schema>
diff --git a/schemas/privkey.xsd b/schemas/privkey.xsd
new file mode 100644
index 000000000..b8d9e317d
--- /dev/null
+++ b/schemas/privkey.xsd
@@ -0,0 +1,138 @@
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xml:lang="en">
+ <xsd:annotation>
+ <xsd:documentation>
+ Schema for :ref:`server-plugins-generators-cfg-sshkeys` ``privkey.xml``
+ </xsd:documentation>
+ </xsd:annotation>
+
+ <xsd:complexType name="PrivateKeyGroupType">
+ <xsd:annotation>
+ <xsd:documentation>
+ An **PrivateKeyGroupType** is a tag used to provide logic.
+ Child entries of a PrivateKeyGroupType tag only apply to
+ machines that match the condition specified -- either
+ membership in a group, or a matching client name.
+ :xml:attribute:`PrivateKeyGroupType:negate` can be set to
+ negate the sense of the match.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:choice minOccurs="1" maxOccurs="unbounded">
+ <xsd:element name="Passphrase" type="PassphraseType"/>
+ <xsd:element name="Params" type="PrivateKeyParamsType"/>
+ <xsd:element name="Group" type="PrivateKeyGroupType"/>
+ <xsd:element name="Client" type="PrivateKeyGroupType"/>
+ </xsd:choice>
+ <xsd:attribute name='name' type='xsd:string'>
+ <xsd:annotation>
+ <xsd:documentation>
+ The name of the client or group to match on. Child entries
+ will only apply to this client or group (unless
+ :xml:attribute:`PrivateKeyGroupType:negate` is set).
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name='negate' type='xsd:boolean'>
+ <xsd:annotation>
+ <xsd:documentation>
+ Negate the sense of the match, so that child entries only
+ apply to a client if it is not a member of the given group
+ or does not have the given name.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:complexType>
+
+ <xsd:simpleType name="PrivateKeyTypeEnum">
+ <xsd:annotation>
+ <xsd:documentation>
+ Available private key formats
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:restriction base="xsd:string">
+ <xsd:enumeration value="rsa"/>
+ <xsd:enumeration value="dsa"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+
+ <xsd:complexType name="PassphraseType">
+ <xsd:annotation>
+ <xsd:documentation>
+ Specify the private key passphrase.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:simpleContent>
+ <xsd:extension base="xsd:string">
+ <xsd:attribute name="encrypted" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>
+ The name of the passphrase to use to encrypt this
+ private key on the filesystem (in Bcfg2).
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:extension>
+ </xsd:simpleContent>
+ </xsd:complexType>
+
+ <xsd:complexType name="PrivateKeyParamsType">
+ <xsd:annotation>
+ <xsd:documentation>
+ Specify parameters for creating the private key
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:attribute name="bits" type="xsd:positiveInteger">
+ <xsd:annotation>
+ <xsd:documentation>
+ Number of bits in the key. See :manpage:`ssh-keygen(1)` for
+ defaults.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="type" type="PrivateKeyTypeEnum" default="rsa">
+ <xsd:annotation>
+ <xsd:documentation>
+ Key type to create.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:complexType>
+
+ <xsd:element name="PrivateKey">
+ <xsd:annotation>
+ <xsd:documentation>
+ Top-level tag for describing a generated SSH key pair.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType>
+ <xsd:choice minOccurs="0" maxOccurs="unbounded">
+ <xsd:element name="Passphrase" type="PassphraseType"/>
+ <xsd:element name="Params" type="PrivateKeyParamsType"/>
+ <xsd:element name="Group" type="PrivateKeyGroupType"/>
+ <xsd:element name="Client" type="PrivateKeyGroupType"/>
+ </xsd:choice>
+ <xsd:attribute name="perhost" type="xsd:boolean">
+ <xsd:annotation>
+ <xsd:documentation>
+ Create keys on a per-host basis (rather than on a per-group
+ basis).
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="category" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation>
+ Create keys specific to the given category, instead of
+ specific to the category given in ``bcfg2.conf``.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ <xsd:attribute name="priority" type="xsd:positiveInteger" default="50">
+ <xsd:annotation>
+ <xsd:documentation>
+ Create group-specific keys with the given priority.
+ </xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+ </xsd:complexType>
+ </xsd:element>
+</xsd:schema>
diff --git a/schemas/pubkey.xsd b/schemas/pubkey.xsd
new file mode 100644
index 000000000..5671a818d
--- /dev/null
+++ b/schemas/pubkey.xsd
@@ -0,0 +1,16 @@
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xml:lang="en">
+ <xsd:annotation>
+ <xsd:documentation>
+ Schema for :ref:`server-plugins-generators-cfg-sshkeys` ``pubkey.xml``
+ </xsd:documentation>
+ </xsd:annotation>
+
+ <xsd:element name="PublicKey">
+ <xsd:annotation>
+ <xsd:documentation>
+ Top-level tag for flagging a generated SSH public key.
+ </xsd:documentation>
+ </xsd:annotation>
+ <xsd:complexType/>
+ </xsd:element>
+</xsd:schema>
diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py
index e93338ae4..37bc230d1 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -18,6 +18,10 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
{"Metadata/groups.xml": "metadata.xsd",
"Metadata/clients.xml": "clients.xsd",
"Cfg/**/info.xml": "info.xsd",
+ "Cfg/**/privkey.xml": "privkey.xsd",
+ "Cfg/**/pubkey.xml": "pubkey.xsd",
+ "Cfg/**/authorizedkeys.xml": "authorizedkeys.xsd",
+ "Cfg/**/authorized_keys.xml": "authorizedkeys.xsd",
"SSHbase/**/info.xml": "info.xsd",
"SSLCA/**/info.xml": "info.xsd",
"TGenshi/**/info.xml": "info.xsd",
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py
new file mode 100644
index 000000000..824d01023
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py
@@ -0,0 +1,101 @@
+""" The CfgAuthorizedKeysGenerator generates ``authorized_keys`` files
+based on an XML specification of which SSH keypairs should granted
+access. """
+
+import lxml.etree
+from Bcfg2.Server.Plugin import StructFile, PluginExecutionError
+from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP, CFG
+from Bcfg2.Server.Plugins.Metadata import ClientMetadata
+
+
+class CfgAuthorizedKeysGenerator(CfgGenerator, StructFile):
+ """ The CfgAuthorizedKeysGenerator generates authorized_keys files
+ based on an XML specification of which SSH keypairs should granted
+ access. """
+
+ #: Different configurations for different clients/groups can be
+ #: handled with Client and Group tags within authorizedkeys.xml
+ __specific__ = False
+
+ #: Handle authorized keys XML files
+ __basenames__ = ['authorizedkeys.xml', 'authorized_keys.xml']
+
+ #: This handler is experimental, in part because it depends upon
+ #: the (experimental) CfgPrivateKeyCreator handler
+ experimental = True
+
+ def __init__(self, fname):
+ CfgGenerator.__init__(self, fname, None, None)
+ StructFile.__init__(self, fname)
+ self.cache = dict()
+ self.core = CFG.core
+ __init__.__doc__ = CfgGenerator.__init__.__doc__
+
+ @property
+ def category(self):
+ """ The name of the metadata category that generated keys are
+ specific to """
+ if (SETUP.cfp.has_section("sshkeys") and
+ SETUP.cfp.has_option("sshkeys", "category")):
+ return SETUP.cfp.get("sshkeys", "category")
+ return None
+
+ def handle_event(self, event):
+ CfgGenerator.handle_event(self, event)
+ StructFile.HandleEvent(self, event)
+ self.cache = dict()
+ handle_event.__doc__ = CfgGenerator.handle_event.__doc__
+
+ def get_data(self, entry, metadata):
+ spec = self.XMLMatch(metadata)
+ rv = []
+ for allow in spec.findall("Allow"):
+ params = ''
+ if allow.find("Params") is not None:
+ params = ",".join("=".join(p)
+ for p in allow.find("Params").attrib.items())
+
+ pubkey_name = allow.get("from")
+ if pubkey_name:
+ host = allow.get("host")
+ group = allow.get("group")
+ if host:
+ key_md = self.core.build_metadata(host)
+ elif group:
+ key_md = ClientMetadata("dummy", group, [group], [],
+ set(), set(), dict(), None,
+ None, None, None)
+ elif (self.category and
+ not metadata.group_in_category(self.category)):
+ self.logger.warning("Cfg: %s ignoring Allow from %s: "
+ "No group in category %s" %
+ (metadata.hostname, pubkey_name,
+ self.category))
+ continue
+ else:
+ key_md = metadata
+
+ key_entry = lxml.etree.Element("Path", name=pubkey_name)
+ try:
+ self.core.Bind(key_entry, key_md)
+ except PluginExecutionError:
+ self.logger.info("Cfg: %s skipping Allow from %s: "
+ "No key found" % (metadata.hostname,
+ pubkey_name))
+ continue
+ if not key_entry.text:
+ self.logger.warning("Cfg: %s skipping Allow from %s: "
+ "Empty public key" %
+ (metadata.hostname, pubkey_name))
+ continue
+ pubkey = key_entry.text
+ elif allow.text:
+ pubkey = allow.text.strip()
+ else:
+ self.logger.warning("Cfg: %s ignoring empty Allow tag: %s" %
+ (metadata.hostname,
+ lxml.etree.tostring(allow)))
+ continue
+ rv.append(" ".join([params, pubkey]).strip())
+ return "\n".join(rv)
+ get_data.__doc__ = CfgGenerator.get_data.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
new file mode 100644
index 000000000..bb54c6faa
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py
@@ -0,0 +1,258 @@
+""" The CfgPrivateKeyCreator creates SSH keys on the fly. """
+
+import os
+import shutil
+import tempfile
+import subprocess
+from Bcfg2.Server.Plugin import PluginExecutionError, StructFile
+from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, SETUP
+from Bcfg2.Server.Plugins.Cfg.CfgPublicKeyCreator import CfgPublicKeyCreator
+try:
+ import Bcfg2.Encryption
+ HAS_CRYPTO = True
+except ImportError:
+ HAS_CRYPTO = False
+
+
+class CfgPrivateKeyCreator(CfgCreator, StructFile):
+ """The CfgPrivateKeyCreator creates SSH keys on the fly. """
+
+ #: Different configurations for different clients/groups can be
+ #: handled with Client and Group tags within privkey.xml
+ __specific__ = False
+
+ #: Handle XML specifications of private keys
+ __basenames__ = ['privkey.xml']
+
+ def __init__(self, fname):
+ CfgCreator.__init__(self, fname)
+ StructFile.__init__(self, fname)
+
+ pubkey_path = os.path.dirname(self.name) + ".pub"
+ pubkey_name = os.path.join(pubkey_path, os.path.basename(pubkey_path))
+ self.pubkey_creator = CfgPublicKeyCreator(pubkey_name)
+ __init__.__doc__ = CfgCreator.__init__.__doc__
+
+ @property
+ def category(self):
+ """ The name of the metadata category that generated keys are
+ specific to """
+ if (SETUP.cfp.has_section("sshkeys") and
+ SETUP.cfp.has_option("sshkeys", "category")):
+ return SETUP.cfp.get("sshkeys", "category")
+ return None
+
+ @property
+ def passphrase(self):
+ """ The passphrase used to encrypt private keys """
+ if (HAS_CRYPTO and
+ SETUP.cfp.has_section("sshkeys") and
+ SETUP.cfp.has_option("sshkeys", "passphrase")):
+ return Bcfg2.Encryption.get_passphrases(SETUP)[SETUP.cfp.get(
+ "sshkeys",
+ "passphrase")]
+ return None
+
+ def handle_event(self, event):
+ CfgCreator.handle_event(self, event)
+ StructFile.HandleEvent(self, event)
+ handle_event.__doc__ = CfgCreator.handle_event.__doc__
+
+ def _gen_keypair(self, metadata, spec=None):
+ """ Generate a keypair according to the given client medata
+ and key specification.
+
+ :param metadata: The client metadata to generate keys for
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param spec: The key specification to follow when creating the
+ keys. This should be an XML document that only
+ contains key specification data that applies to
+ the given client metadata, and may be obtained by
+ doing ``self.XMLMatch(metadata)``
+ :type spec: lxml.etree._Element
+ :returns: None
+ """
+ if spec is None:
+ spec = self.XMLMatch(metadata)
+
+ # set key parameters
+ ktype = "rsa"
+ bits = None
+ params = spec.find("Params")
+ if params is not None:
+ bits = params.get("bits")
+ ktype = params.get("type", ktype)
+ try:
+ passphrase = spec.find("Passphrase").text
+ except AttributeError:
+ passphrase = ''
+ tempdir = tempfile.mkdtemp()
+ try:
+ filename = os.path.join(tempdir, "privkey")
+
+ # generate key pair
+ cmd = ["ssh-keygen", "-f", filename, "-t", ktype]
+ if bits:
+ cmd.extend(["-b", bits])
+ cmd.append("-N")
+ log_cmd = cmd[:]
+ cmd.append(passphrase)
+ if passphrase:
+ log_cmd.append("******")
+ else:
+ log_cmd.append("''")
+ self.debug_log("Cfg: Generating new SSH key pair: %s" %
+ " ".join(log_cmd))
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ err = proc.communicate()[1]
+ if proc.wait():
+ raise CfgCreationError("Cfg: Failed to generate SSH key pair "
+ "at %s for %s: %s" %
+ (filename, metadata.hostname, err))
+ elif err:
+ self.logger.warning("Cfg: Generated SSH key pair at %s for %s "
+ "with errors: %s" % (filename,
+ metadata.hostname,
+ err))
+ return filename
+ except:
+ shutil.rmtree(tempdir)
+ raise
+
+ def get_specificity(self, metadata, spec=None):
+ """ Get config settings for key generation specificity
+ (per-host or per-group).
+
+ :param metadata: The client metadata to create data for
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param spec: The key specification to follow when creating the
+ keys. This should be an XML document that only
+ contains key specification data that applies to
+ the given client metadata, and may be obtained by
+ doing ``self.XMLMatch(metadata)``
+ :type spec: lxml.etree._Element
+ :returns: dict - A dict of specificity arguments suitable for
+ passing to
+ :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.write_data`
+ or
+ :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.get_filename`
+ """
+ if spec is None:
+ spec = self.XMLMatch(metadata)
+ category = spec.get("category", self.category)
+ print("category=%s" % category)
+ if category is None:
+ per_host_default = "true"
+ else:
+ per_host_default = "false"
+ per_host = spec.get("perhost", per_host_default).lower() == "true"
+
+ specificity = dict(host=metadata.hostname)
+ if category and not per_host:
+ group = metadata.group_in_category(category)
+ if group:
+ specificity = dict(group=group,
+ prio=int(spec.get("priority", 50)))
+ else:
+ self.logger.info("Cfg: %s has no group in category %s, "
+ "creating host-specific key" %
+ (metadata.hostname, category))
+ return specificity
+
+ # pylint: disable=W0221
+ def create_data(self, entry, metadata, return_pair=False):
+ """ Create data for the given entry on the given client
+
+ :param entry: The abstract entry to create data for. This
+ will not be modified
+ :type entry: lxml.etree._Element
+ :param metadata: The client metadata to create data for
+ :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
+ :param return_pair: Return a tuple of ``(public key, private
+ key)`` instead of just the private key.
+ This is used by
+ :class:`Bcfg2.Server.Plugins.Cfg.CfgPublicKeyCreator.CfgPublicKeyCreator`
+ to create public keys as requested.
+ :type return_pair: bool
+ :returns: string - The private key data
+ :returns: tuple - Tuple of ``(public key, private key)``, if
+ ``return_pair`` is set to True
+ """
+ spec = self.XMLMatch(metadata)
+ specificity = self.get_specificity(metadata, spec)
+ filename = self._gen_keypair(metadata, spec)
+
+ try:
+ # write the public key, stripping the comment and
+ # replacing it with a comment that specifies the filename.
+ kdata = open(filename + ".pub").read().split()[:2]
+ kdata.append(self.pubkey_creator.get_filename(**specificity))
+ pubkey = " ".join(kdata) + "\n"
+ self.pubkey_creator.write_data(pubkey, **specificity)
+
+ # encrypt the private key, write to the proper place, and
+ # return it
+ privkey = open(filename).read()
+ if HAS_CRYPTO and self.passphrase:
+ self.debug_log("Cfg: Encrypting key data at %s" % filename)
+ privkey = Bcfg2.Encryption.ssl_encrypt(
+ privkey,
+ self.passphrase,
+ algorithm=Bcfg2.Encryption.get_algorithm(SETUP))
+ specificity['ext'] = '.crypt'
+
+ self.write_data(privkey, **specificity)
+
+ if return_pair:
+ return (pubkey, privkey)
+ else:
+ return privkey
+ finally:
+ shutil.rmtree(os.path.dirname(filename))
+ # pylint: enable=W0221
+
+ def Index(self):
+ StructFile.Index(self)
+ if HAS_CRYPTO:
+ strict = SETUP.cfp.get("sshkeys", "decrypt",
+ default="strict") == "strict"
+ for el in self.xdata.xpath("//*[@encrypted]"):
+ try:
+ el.text = self._decrypt(el).encode('ascii',
+ 'xmlcharrefreplace')
+ except UnicodeDecodeError:
+ self.logger.info("Cfg: Decrypted %s to gibberish, skipping"
+ % el.tag)
+ except Bcfg2.Encryption.EVPError:
+ msg = "Cfg: Failed to decrypt %s element in %s" % \
+ (el.tag, self.name)
+ if strict:
+ raise PluginExecutionError(msg)
+ else:
+ self.logger.warning(msg)
+ Index.__doc__ = StructFile.Index.__doc__
+
+ def _decrypt(self, element):
+ """ Decrypt a single encrypted element """
+ if not element.text.strip():
+ return
+ passes = Bcfg2.Encryption.get_passphrases(SETUP)
+ try:
+ passphrase = passes[element.get("encrypted")]
+ try:
+ return Bcfg2.Encryption.ssl_decrypt(
+ element.text,
+ passphrase,
+ algorithm=Bcfg2.Encryption.get_algorithm(SETUP))
+ except Bcfg2.Encryption.EVPError:
+ # error is raised below
+ pass
+ except KeyError:
+ # bruteforce_decrypt raises an EVPError with a sensible
+ # error message, so we just let it propagate up the stack
+ return Bcfg2.Encryption.bruteforce_decrypt(
+ element.text,
+ passphrases=passes.values(),
+ algorithm=Bcfg2.Encryption.get_algorithm(SETUP))
+ raise Bcfg2.Encryption.EVPError("Failed to decrypt")
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py
new file mode 100644
index 000000000..6be438462
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py
@@ -0,0 +1,63 @@
+""" The CfgPublicKeyCreator invokes
+:class:`Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.CfgPrivateKeyCreator`
+to create SSH keys on the fly. """
+
+import lxml.etree
+from Bcfg2.Server.Plugin import StructFile, PluginExecutionError
+from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, CFG
+
+
+class CfgPublicKeyCreator(CfgCreator, StructFile):
+ """ .. currentmodule:: Bcfg2.Server.Plugins.Cfg
+
+ The CfgPublicKeyCreator creates SSH public keys on the fly. It is
+ invoked by :class:`CfgPrivateKeyCreator.CfgPrivateKeyCreator` to
+ handle the creation of the public key, and can also call
+ :class:`CfgPrivateKeyCreator.CfgPrivateKeyCreator` to trigger the
+ creation of a keypair when a public key is created. """
+
+ #: Different configurations for different clients/groups can be
+ #: handled with Client and Group tags within privkey.xml
+ __specific__ = False
+
+ #: Handle XML specifications of private keys
+ __basenames__ = ['pubkey.xml']
+
+ def __init__(self, fname):
+ CfgCreator.__init__(self, fname)
+ StructFile.__init__(self, fname)
+ self.cfg = CFG
+ __init__.__doc__ = CfgCreator.__init__.__doc__
+
+ def create_data(self, entry, metadata):
+ if entry.get("name").endswith(".pub"):
+ privkey = entry.get("name")[:-4]
+ else:
+ raise CfgCreationError("Cfg: Could not determine private key for "
+ "%s: Filename does not end in .pub" %
+ entry.get("name"))
+
+ if privkey not in self.cfg.entries:
+ raise CfgCreationError("Cfg: Could not find Cfg entry for %s "
+ "(private key for %s)" % (privkey,
+ self.name))
+ eset = self.cfg.entries[privkey]
+ try:
+ creator = eset.best_matching(metadata,
+ eset.get_handlers(metadata,
+ CfgCreator))
+ except PluginExecutionError:
+ raise CfgCreationError("Cfg: No privkey.xml defined for %s "
+ "(private key for %s)" % (privkey,
+ self.name))
+
+ privkey_entry = lxml.etree.Element("Path", name=privkey)
+ pubkey = creator.create_data(privkey_entry, metadata,
+ return_pair=True)[0]
+ return pubkey
+ create_data.__doc__ = CfgCreator.create_data.__doc__
+
+ def handle_event(self, event):
+ CfgCreator.handle_event(self, event)
+ StructFile.HandleEvent(self, event)
+ handle_event.__doc__ = CfgCreator.handle_event.__doc__
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
index 2466d68a2..ea4a4263b 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py
@@ -12,7 +12,7 @@ import Bcfg2.Server.Plugin
import Bcfg2.Server.Lint
from Bcfg2.Server.Plugin import PluginExecutionError
# pylint: disable=W0622
-from Bcfg2.Compat import u_str, unicode, b64encode, b64decode, walk_packages, \
+from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, \
any, oct_mode
# pylint: enable=W0622
@@ -27,6 +27,14 @@ from Bcfg2.Compat import u_str, unicode, b64encode, b64decode, walk_packages, \
#: the EntrySet children.
SETUP = None
+#: CFG is a reference to the :class:`Bcfg2.Server.Plugins.Cfg.Cfg`
+#: plugin object created by the Bcfg2 core. This is provided so that
+#: the handler objects can access it as necessary, since the existing
+#: :class:`Bcfg2.Server.Plugin.helpers.GroupSpool` and
+#: :class:`Bcfg2.Server.Plugin.helpers.EntrySet` classes have no
+#: facility for passing it otherwise.
+CFG = None
+
class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
Bcfg2.Server.Plugin.Debuggable):
@@ -62,8 +70,8 @@ class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData,
#: if they handle a given event. If this explicit priority is not
#: set, then :class:`CfgPlaintextGenerator.CfgPlaintextGenerator`
#: would match against nearly every other sort of generator file
- #: if it comes first. It's not necessary to set ``__priority`` on
- #: handlers where :attr:`CfgBaseFileMatcher.__specific__` is
+ #: if it comes first. It's not necessary to set ``__priority__``
+ #: on handlers where :attr:`CfgBaseFileMatcher.__specific__` is
#: False, since they don't have a potentially open-ended regex
__priority__ = 0
@@ -304,6 +312,23 @@ class CfgCreator(CfgBaseFileMatcher):
client, writes its data to disk as a static file, and is not
called on subsequent runs by the same client. """
+ #: CfgCreators generally store their configuration in a single XML
+ #: file, and are thus not specific
+ __specific__ = False
+
+ #: The CfgCreator interface is experimental at this time
+ experimental = True
+
+ def __init__(self, fname):
+ """
+ :param name: The full path to the file
+ :type name: string
+
+ .. -----
+ .. autoattribute:: Bcfg2.Server.Plugins.Cfg.CfgCreator.__specific__
+ """
+ CfgBaseFileMatcher.__init__(self, fname, None, None)
+
def create_data(self, entry, metadata):
""" Create new data for the given entry and write it to disk
using :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.write_data`.
@@ -312,11 +337,43 @@ class CfgCreator(CfgBaseFileMatcher):
:type entry: lxml.etree._Element
:param metadata: The client metadata to create data for.
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
- :returns: string - the contents of the entry
+ :returns: string - The contents of the entry
+ :raises: :exc:`Bcfg2.Server.Plugins.Cfg.CfgCreationError`
"""
raise NotImplementedError
- def write_data(self, data, host=None, group=None, prio=0):
+ def get_filename(self, host=None, group=None, prio=0, ext=''):
+ """ Get the filename where the new data will be written. If
+ ``host`` is given, it will be host-specific. It will be
+ group-specific if ``group`` and ``prio`` are given. If
+ neither ``host`` nor ``group`` is given, the filename will be
+ non-specific.
+
+ :param host: The file applies to the given host
+ :type host: bool
+ :param group: The file applies to the given group
+ :type group: string
+ :param prio: The file has the given priority relative to other
+ objects that also apply to the same group.
+ ``group`` must also be specified.
+ :type prio: int
+ :param ext: An extension to add after the specificity (e.g.,
+ '.crypt', to signal that an encrypted file has
+ been created)
+ :type prio: string
+ :returns: string - the filename
+ """
+ basefilename = \
+ os.path.join(os.path.dirname(self.name),
+ os.path.basename(os.path.dirname(self.name)))
+ if group:
+ return "%s.G%02d_%s%s" % (basefilename, prio, group, ext)
+ elif host:
+ return "%s.H_%s%s" % (basefilename, host, ext)
+ else:
+ return "%s%s" % (basefilename, ext)
+
+ def write_data(self, data, host=None, group=None, prio=0, ext=''):
""" Write the new data to disk. If ``host`` is given, it is
written as a host-specific file, or as a group-specific file
if ``group`` and ``prio`` are given. If neither ``host`` nor
@@ -332,19 +389,14 @@ class CfgCreator(CfgBaseFileMatcher):
objects that also apply to the same group.
``group`` must also be specified.
:type prio: int
+ :param ext: An extension to add after the specificity (e.g.,
+ '.crypt', to signal that an encrypted file has
+ been created)
+ :type prio: string
:returns: None
:raises: :exc:`Bcfg2.Server.Plugins.Cfg.CfgCreationError`
"""
- basefilename = \
- os.path.join(os.path.dirname(self.name),
- os.path.basename(os.path.dirname(self.name)))
- if group:
- fileloc = "%s.G%02d_%s" % (basefilename, prio, group)
- elif host:
- fileloc = "%s.H_%s" % (basefilename, host)
- else:
- fileloc = basefilename
-
+ fileloc = self.get_filename(host=host, group=group, prio=prio, ext=ext)
self.debug_log("%s: Writing new file %s" % (self.name, fileloc))
try:
os.makedirs(os.path.dirname(fileloc))
@@ -369,8 +421,9 @@ class CfgVerificationError(Exception):
class CfgCreationError(Exception):
- """ Raised by :class:`Bcfg2.Server.Plugins.Cfg.CfgCreator` when
- various stages of data creation fail """
+ """ Raised by
+ :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.create_data` when data
+ creation fails """
pass
@@ -607,8 +660,8 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet,
:type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata
:returns: string - the data for the entry
"""
- creator = self.best_matching(metadata, self.get_handlers(metadata,
- CfgCreator))
+ creator = self.best_matching(metadata,
+ self.get_handlers(metadata, CfgCreator))
try:
return creator.create_data(entry, metadata)
@@ -766,10 +819,12 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool,
es_child_cls = Bcfg2.Server.Plugin.SpecificData
def __init__(self, core, datastore):
- global SETUP # pylint: disable=W0603
+ global SETUP, CFG # pylint: disable=W0603
Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore)
Bcfg2.Server.Plugin.PullTarget.__init__(self)
+ CFG = self
+
SETUP = core.setup
if 'validate' not in SETUP:
SETUP.add_option('validate', Bcfg2.Options.CFG_VALIDATION)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
index 559742d00..6dbdc7667 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py
@@ -433,9 +433,12 @@ class TestXMLFileBacked(TestFileBacked):
xdata = dict()
mock_parse.side_effect = lambda p: xdata[p]
+ base = os.path.dirname(self.path)
+
# basic functionality
- xdata['/test/test2.xml'] = lxml.etree.Element("Test").getroottree()
- xfb._follow_xincludes(xdata=xdata['/test/test2.xml'])
+ test2 = os.path.join(base, 'test2.xml')
+ xdata[test2] = lxml.etree.Element("Test").getroottree()
+ xfb._follow_xincludes(xdata=xdata[test2])
self.assertFalse(xfb.add_monitor.called)
if (not hasattr(self.test_obj, "xdata") or
@@ -443,56 +446,56 @@ class TestXMLFileBacked(TestFileBacked):
# if xdata is settable, test that method of getting data
# to _follow_xincludes
reset()
- xfb.xdata = xdata['/test/test2.xml'].getroot()
+ xfb.xdata = xdata[test2].getroot()
xfb._follow_xincludes()
self.assertFalse(xfb.add_monitor.called)
xfb.xdata = None
reset()
- xfb._follow_xincludes(fname="/test/test2.xml")
+ xfb._follow_xincludes(fname=test2)
self.assertFalse(xfb.add_monitor.called)
# test one level of xinclude
xdata[self.path] = lxml.etree.Element("Test").getroottree()
lxml.etree.SubElement(xdata[self.path].getroot(),
Bcfg2.Server.XI_NAMESPACE + "include",
- href="/test/test2.xml")
+ href=test2)
reset()
xfb._follow_xincludes(fname=self.path)
- xfb.add_monitor.assert_called_with("/test/test2.xml")
+ xfb.add_monitor.assert_called_with(test2)
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()])
- mock_exists.assert_called_with("/test/test2.xml")
+ mock_exists.assert_called_with(test2)
reset()
xfb._follow_xincludes(fname=self.path, xdata=xdata[self.path])
- xfb.add_monitor.assert_called_with("/test/test2.xml")
+ xfb.add_monitor.assert_called_with(test2)
self.assertItemsEqual(mock_parse.call_args_list,
[call(f) for f in xdata.keys()
if f != self.path])
- mock_exists.assert_called_with("/test/test2.xml")
+ mock_exists.assert_called_with(test2)
# test two-deep level of xinclude, with some files in another
# directory
- xdata["/test/test3.xml"] = \
- lxml.etree.Element("Test").getroottree()
- lxml.etree.SubElement(xdata["/test/test3.xml"].getroot(),
+ test3 = os.path.join(base, "test3.xml")
+ test4 = os.path.join(base, "test_dir", "test4.xml")
+ test5 = os.path.join(base, "test_dir", "test5.xml")
+ test6 = os.path.join(base, "test_dir", "test6.xml")
+ xdata[test3] = lxml.etree.Element("Test").getroottree()
+ lxml.etree.SubElement(xdata[test3].getroot(),
Bcfg2.Server.XI_NAMESPACE + "include",
- href="/test/test_dir/test4.xml")
- xdata["/test/test_dir/test4.xml"] = \
- lxml.etree.Element("Test").getroottree()
- lxml.etree.SubElement(xdata["/test/test_dir/test4.xml"].getroot(),
+ href=test4)
+ xdata[test4] = lxml.etree.Element("Test").getroottree()
+ lxml.etree.SubElement(xdata[test4].getroot(),
Bcfg2.Server.XI_NAMESPACE + "include",
- href="/test/test_dir/test5.xml")
- xdata['/test/test_dir/test5.xml'] = \
- lxml.etree.Element("Test").getroottree()
- xdata['/test/test_dir/test6.xml'] = \
- lxml.etree.Element("Test").getroottree()
+ href=test5)
+ xdata[test5] = lxml.etree.Element("Test").getroottree()
+ xdata[test6] = lxml.etree.Element("Test").getroottree()
# relative includes
lxml.etree.SubElement(xdata[self.path].getroot(),
Bcfg2.Server.XI_NAMESPACE + "include",
href="test3.xml")
- lxml.etree.SubElement(xdata["/test/test3.xml"].getroot(),
+ lxml.etree.SubElement(xdata[test3].getroot(),
Bcfg2.Server.XI_NAMESPACE + "include",
href="test_dir/test6.xml")
@@ -526,10 +529,6 @@ class TestXMLFileBacked(TestFileBacked):
xfb.extras = []
xfb.xdata = None
- # syntax error
- xfb.data = "<"
- self.assertRaises(PluginInitError, xfb.Index)
-
# no xinclude
reset()
xdata = lxml.etree.Element("Test", name="test")
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgAuthorizedKeysGenerator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgAuthorizedKeysGenerator.py
new file mode 100644
index 000000000..23a77d1e5
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgAuthorizedKeysGenerator.py
@@ -0,0 +1,176 @@
+import os
+import sys
+import lxml.etree
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator import *
+from Bcfg2.Server.Plugin import PluginExecutionError
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import *
+from TestServer.TestPlugins.TestCfg.Test_init import TestCfgGenerator
+from TestServer.TestPlugin.Testhelpers import TestStructFile
+
+
+class TestCfgAuthorizedKeysGenerator(TestCfgGenerator, TestStructFile):
+ test_obj = CfgAuthorizedKeysGenerator
+ should_monitor = False
+
+ def get_obj(self, name=None, core=None, fam=None):
+ if name is None:
+ name = self.path
+ Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.CFG = Mock()
+ if core is not None:
+ Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.CFG.core = core
+ return self.test_obj(name)
+
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgGenerator.handle_event")
+ @patch("Bcfg2.Server.Plugin.helpers.StructFile.HandleEvent")
+ def test_handle_event(self, mock_HandleEvent, mock_handle_event):
+ akg = self.get_obj()
+ evt = Mock()
+ akg.handle_event(evt)
+ mock_HandleEvent.assert_called_with(akg, evt)
+ mock_handle_event.assert_called_with(akg, evt)
+
+ def test_category(self):
+ akg = self.get_obj()
+ cfp = Mock()
+ cfp.has_section.return_value = False
+ cfp.has_option.return_value = False
+ Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.SETUP = Mock()
+ Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.SETUP.cfp = cfp
+
+ self.assertIsNone(akg.category)
+ cfp.has_section.assert_called_with("sshkeys")
+
+ cfp.reset_mock()
+ cfp.has_section.return_value = True
+ self.assertIsNone(akg.category)
+ cfp.has_section.assert_called_with("sshkeys")
+ cfp.has_option.assert_called_with("sshkeys", "category")
+
+ cfp.reset_mock()
+ cfp.has_option.return_value = True
+ self.assertEqual(akg.category, cfp.get.return_value)
+ cfp.has_section.assert_called_with("sshkeys")
+ cfp.has_option.assert_called_with("sshkeys", "category")
+ cfp.get.assert_called_with("sshkeys", "category")
+
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.ClientMetadata")
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgAuthorizedKeysGenerator.CfgAuthorizedKeysGenerator.category", "category")
+ def test_get_data(self, mock_ClientMetadata):
+ akg = self.get_obj()
+ akg.XMLMatch = Mock()
+
+ def ClientMetadata(host, profile, groups, *args):
+ rv = Mock()
+ rv.hostname = host
+ rv.profile = profile
+ rv.groups = groups
+ return rv
+
+ mock_ClientMetadata.side_effect = ClientMetadata
+
+ def build_metadata(host):
+ rv = Mock()
+ rv.hostname = host
+ rv.profile = host
+ return rv
+
+ akg.core.build_metadata = Mock()
+ akg.core.build_metadata.side_effect = build_metadata
+
+ def Bind(ent, md):
+ ent.text = "%s %s" % (md.profile, ent.get("name"))
+ return ent
+
+ akg.core.Bind = Mock()
+ akg.core.Bind.side_effect = Bind
+ metadata = Mock()
+ metadata.profile = "profile"
+ metadata.group_in_category.return_value = "profile"
+ entry = lxml.etree.Element("Path", name="/root/.ssh/authorized_keys")
+
+ def reset():
+ mock_ClientMetadata.reset_mock()
+ akg.XMLMatch.reset_mock()
+ akg.core.build_metadata.reset_mock()
+ akg.core.Bind.reset_mock()
+ metadata.reset_mock()
+
+ pubkey = "/home/foo/.ssh/id_rsa.pub"
+ spec = lxml.etree.Element("AuthorizedKeys")
+ lxml.etree.SubElement(spec, "Allow", attrib={"from": pubkey})
+ akg.XMLMatch.return_value = spec
+ self.assertEqual(akg.get_data(entry, metadata), "profile %s" % pubkey)
+ akg.XMLMatch.assert_called_with(metadata)
+ self.assertEqual(akg.core.Bind.call_args[0][0].get("name"), pubkey)
+ self.assertEqual(akg.core.Bind.call_args[0][1], metadata)
+
+ reset()
+ group = "somegroup"
+ spec = lxml.etree.Element("AuthorizedKeys")
+ lxml.etree.SubElement(spec, "Allow",
+ attrib={"from": pubkey, "group": group})
+ akg.XMLMatch.return_value = spec
+ self.assertEqual(akg.get_data(entry, metadata),
+ "%s %s" % (group, pubkey))
+ akg.XMLMatch.assert_called_with(metadata)
+ self.assertItemsEqual(mock_ClientMetadata.call_args[0][2], [group])
+ self.assertEqual(akg.core.Bind.call_args[0][0].get("name"), pubkey)
+ self.assertIn(group, akg.core.Bind.call_args[0][1].groups)
+
+ reset()
+ host = "baz.example.com"
+ spec = lxml.etree.Element("AuthorizedKeys")
+ lxml.etree.SubElement(
+ lxml.etree.SubElement(spec,
+ "Allow",
+ attrib={"from": pubkey, "host": host}),
+ "Params", foo="foo", bar="bar=bar")
+ akg.XMLMatch.return_value = spec
+ self.assertEqual(akg.get_data(entry, metadata),
+ "foo=foo,bar=bar=bar %s %s" % (host, pubkey))
+ akg.XMLMatch.assert_called_with(metadata)
+ akg.core.build_metadata.assert_called_with(host)
+ self.assertEqual(akg.core.Bind.call_args[0][0].get("name"), pubkey)
+ self.assertEqual(akg.core.Bind.call_args[0][1].hostname, host)
+
+ reset()
+ spec = lxml.etree.Element("AuthorizedKeys")
+ text = lxml.etree.SubElement(spec, "Allow")
+ text.text = "ssh-rsa publickey /foo/bar\n"
+ lxml.etree.SubElement(text, "Params", foo="foo")
+ akg.XMLMatch.return_value = spec
+ self.assertEqual(akg.get_data(entry, metadata),
+ "foo=foo %s" % text.text.strip())
+ akg.XMLMatch.assert_called_with(metadata)
+ self.assertFalse(akg.core.build_metadata.called)
+ self.assertFalse(akg.core.Bind.called)
+
+ reset()
+ lxml.etree.SubElement(spec, "Allow", attrib={"from": pubkey})
+ akg.XMLMatch.return_value = spec
+ self.assertItemsEqual(akg.get_data(entry, metadata).splitlines(),
+ ["foo=foo %s" % text.text.strip(),
+ "profile %s" % pubkey])
+ akg.XMLMatch.assert_called_with(metadata)
+
+ reset()
+ metadata.group_in_category.return_value = ''
+ spec = lxml.etree.Element("AuthorizedKeys")
+ lxml.etree.SubElement(spec, "Allow", attrib={"from": pubkey})
+ akg.XMLMatch.return_value = spec
+ self.assertEqual(akg.get_data(entry, metadata), '')
+ akg.XMLMatch.assert_called_with(metadata)
+ self.assertFalse(akg.core.build_metadata.called)
+ self.assertFalse(akg.core.Bind.called)
+ self.assertFalse(mock_ClientMetadata.called)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py
new file mode 100644
index 000000000..dd18306cb
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py
@@ -0,0 +1,435 @@
+import os
+import sys
+import lxml.etree
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugins.Cfg import CfgCreationError
+from Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator import *
+from Bcfg2.Server.Plugin import PluginExecutionError
+try:
+ from Bcfg2.Encryption import EVPError
+ HAS_CRYPTO = True
+except:
+ HAS_CRYPTO = False
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import *
+from TestServer.TestPlugins.TestCfg.Test_init import TestCfgCreator
+from TestServer.TestPlugin.Testhelpers import TestStructFile
+
+
+class TestCfgPrivateKeyCreator(TestCfgCreator, TestStructFile):
+ test_obj = CfgPrivateKeyCreator
+ should_monitor = False
+
+ def get_obj(self, name=None, fam=None):
+ return TestCfgCreator.get_obj(self, name=name)
+
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgCreator.handle_event")
+ @patch("Bcfg2.Server.Plugin.helpers.StructFile.HandleEvent")
+ def test_handle_event(self, mock_HandleEvent, mock_handle_event):
+ pkc = self.get_obj()
+ evt = Mock()
+ pkc.handle_event(evt)
+ mock_HandleEvent.assert_called_with(pkc, evt)
+ mock_handle_event.assert_called_with(pkc, evt)
+
+ def test_category(self):
+ pkc = self.get_obj()
+ cfp = Mock()
+ cfp.has_section.return_value = False
+ cfp.has_option.return_value = False
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP = Mock()
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP.cfp = cfp
+
+ self.assertIsNone(pkc.category)
+ cfp.has_section.assert_called_with("sshkeys")
+
+ cfp.reset_mock()
+ cfp.has_section.return_value = True
+ self.assertIsNone(pkc.category)
+ cfp.has_section.assert_called_with("sshkeys")
+ cfp.has_option.assert_called_with("sshkeys", "category")
+
+ cfp.reset_mock()
+ cfp.has_option.return_value = True
+ self.assertEqual(pkc.category, cfp.get.return_value)
+ cfp.has_section.assert_called_with("sshkeys")
+ cfp.has_option.assert_called_with("sshkeys", "category")
+ cfp.get.assert_called_with("sshkeys", "category")
+
+ @skipUnless(HAS_CRYPTO, "No crypto libraries found, skipping")
+ def test_passphrase(self):
+ @patch("Bcfg2.Encryption.get_passphrases")
+ def inner(mock_get_passphrases):
+ pkc = self.get_obj()
+ cfp = Mock()
+ cfp.has_section.return_value = False
+ cfp.has_option.return_value = False
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP = Mock()
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP.cfp = cfp
+
+ self.assertIsNone(pkc.passphrase)
+ cfp.has_section.assert_called_with("sshkeys")
+
+ cfp.reset_mock()
+ cfp.has_section.return_value = True
+ self.assertIsNone(pkc.passphrase)
+ cfp.has_section.assert_called_with("sshkeys")
+ cfp.has_option.assert_called_with("sshkeys", "passphrase")
+
+ cfp.reset_mock()
+ cfp.get.return_value = "test"
+ mock_get_passphrases.return_value = dict(test="foo", test2="bar")
+ cfp.has_option.return_value = True
+ self.assertEqual(pkc.passphrase, "foo")
+ cfp.has_section.assert_called_with("sshkeys")
+ cfp.has_option.assert_called_with("sshkeys", "passphrase")
+ cfp.get.assert_called_with("sshkeys", "passphrase")
+ mock_get_passphrases.assert_called_with(Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP)
+
+ inner()
+
+ @patch("shutil.rmtree")
+ @patch("tempfile.mkdtemp")
+ @patch("subprocess.Popen")
+ def test__gen_keypair(self, mock_Popen, mock_mkdtemp, mock_rmtree):
+ pkc = self.get_obj()
+ pkc.XMLMatch = Mock()
+ mock_mkdtemp.return_value = datastore
+ metadata = Mock()
+
+ proc = Mock()
+ proc.wait.return_value = 0
+ proc.communicate.return_value = MagicMock()
+ mock_Popen.return_value = proc
+
+ spec = lxml.etree.Element("PrivateKey")
+ pkc.XMLMatch.return_value = spec
+
+ def reset():
+ pkc.XMLMatch.reset_mock()
+ mock_Popen.reset_mock()
+ mock_mkdtemp.reset_mock()
+ mock_rmtree.reset_mock()
+
+ self.assertEqual(pkc._gen_keypair(metadata),
+ os.path.join(datastore, "privkey"))
+ pkc.XMLMatch.assert_called_with(metadata)
+ mock_mkdtemp.assert_called_with()
+ self.assertItemsEqual(mock_Popen.call_args[0][0],
+ ["ssh-keygen", "-f",
+ os.path.join(datastore, "privkey"),
+ "-t", "rsa", "-N", ""])
+
+ reset()
+ lxml.etree.SubElement(spec, "Params", bits="768", type="dsa")
+ passphrase = lxml.etree.SubElement(spec, "Passphrase")
+ passphrase.text = "foo"
+
+ self.assertEqual(pkc._gen_keypair(metadata),
+ os.path.join(datastore, "privkey"))
+ pkc.XMLMatch.assert_called_with(metadata)
+ mock_mkdtemp.assert_called_with()
+ self.assertItemsEqual(mock_Popen.call_args[0][0],
+ ["ssh-keygen", "-f",
+ os.path.join(datastore, "privkey"),
+ "-t", "dsa", "-b", "768", "-N", "foo"])
+
+ reset()
+ proc.wait.return_value = 1
+ self.assertRaises(CfgCreationError, pkc._gen_keypair, metadata)
+ mock_rmtree.assert_called_with(datastore)
+
+ def test_get_specificity(self):
+ pkc = self.get_obj()
+ pkc.XMLMatch = Mock()
+
+ metadata = Mock()
+
+ def reset():
+ pkc.XMLMatch.reset_mock()
+ metadata.group_in_category.reset_mock()
+
+ category = "Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.CfgPrivateKeyCreator.category"
+ @patch(category, None)
+ def inner():
+ pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey")
+ self.assertItemsEqual(pkc.get_specificity(metadata),
+ dict(host=metadata.hostname))
+ inner()
+
+ @patch(category, "foo")
+ def inner2():
+ pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey")
+ self.assertItemsEqual(pkc.get_specificity(metadata),
+ dict(group=metadata.group_in_category.return_value,
+ prio=50))
+ metadata.group_in_category.assert_called_with("foo")
+
+ reset()
+ pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey",
+ perhost="true")
+ self.assertItemsEqual(pkc.get_specificity(metadata),
+ dict(host=metadata.hostname))
+
+ reset()
+ pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey",
+ category="bar")
+ self.assertItemsEqual(pkc.get_specificity(metadata),
+ dict(group=metadata.group_in_category.return_value,
+ prio=50))
+ metadata.group_in_category.assert_called_with("bar")
+
+ reset()
+ pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey",
+ prio="10")
+ self.assertItemsEqual(pkc.get_specificity(metadata),
+ dict(group=metadata.group_in_category.return_value,
+ prio=10))
+ metadata.group_in_category.assert_called_with("foo")
+
+ reset()
+ pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey")
+ metadata.group_in_category.return_value = ''
+ self.assertItemsEqual(pkc.get_specificity(metadata),
+ dict(host=metadata.hostname))
+ metadata.group_in_category.assert_called_with("foo")
+
+ inner2()
+
+ @patch("shutil.rmtree")
+ @patch("%s.open" % builtins)
+ def test_create_data(self, mock_open, mock_rmtree):
+ pkc = self.get_obj()
+ pkc.XMLMatch = Mock()
+ pkc.get_specificity = MagicMock()
+ pkc._gen_keypair = Mock()
+ privkey = os.path.join(datastore, "privkey")
+ pkc._gen_keypair.return_value = privkey
+ pkc.pubkey_creator = Mock()
+ pkc.pubkey_creator.get_filename.return_value = "pubkey.filename"
+ pkc.write_data = Mock()
+
+ entry = lxml.etree.Element("Path", name="/home/foo/.ssh/id_rsa")
+ metadata = Mock()
+
+ def open_read_rv():
+ mock_open.return_value.read.side_effect = lambda: "privatekey"
+ return "ssh-rsa publickey foo@bar.com"
+
+ def reset():
+ mock_open.reset_mock()
+ mock_rmtree.reset_mock()
+ pkc.XMLMatch.reset_mock()
+ pkc.get_specificity.reset_mock()
+ pkc._gen_keypair.reset_mock()
+ pkc.pubkey_creator.reset_mock()
+ pkc.write_data.reset_mock()
+ mock_open.return_value.read.side_effect = open_read_rv
+
+ reset()
+ passphrase = "Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.CfgPrivateKeyCreator.passphrase"
+
+ @patch(passphrase, None)
+ def inner():
+ self.assertEqual(pkc.create_data(entry, metadata), "privatekey")
+ pkc.XMLMatch.assert_called_with(metadata)
+ pkc.get_specificity.assert_called_with(metadata,
+ pkc.XMLMatch.return_value)
+ pkc._gen_keypair.assert_called_with(metadata,
+ pkc.XMLMatch.return_value)
+ self.assertItemsEqual(mock_open.call_args_list,
+ [call(privkey + ".pub"), call(privkey)])
+ pkc.pubkey_creator.get_filename.assert_called_with(
+ **pkc.get_specificity.return_value)
+ pkc.pubkey_creator.write_data.assert_called_with(
+ "ssh-rsa publickey pubkey.filename\n",
+ **pkc.get_specificity.return_value)
+ pkc.write_data.assert_called_with(
+ "privatekey",
+ **pkc.get_specificity.return_value)
+ mock_rmtree.assert_called_with(datastore)
+
+ reset()
+ self.assertEqual(pkc.create_data(entry, metadata, return_pair=True),
+ ("ssh-rsa publickey pubkey.filename\n",
+ "privatekey"))
+ pkc.XMLMatch.assert_called_with(metadata)
+ pkc.get_specificity.assert_called_with(metadata,
+ pkc.XMLMatch.return_value)
+ pkc._gen_keypair.assert_called_with(metadata,
+ pkc.XMLMatch.return_value)
+ self.assertItemsEqual(mock_open.call_args_list,
+ [call(privkey + ".pub"), call(privkey)])
+ pkc.pubkey_creator.get_filename.assert_called_with(
+ **pkc.get_specificity.return_value)
+ pkc.pubkey_creator.write_data.assert_called_with(
+ "ssh-rsa publickey pubkey.filename\n",
+ **pkc.get_specificity.return_value)
+ pkc.write_data.assert_called_with(
+ "privatekey",
+ **pkc.get_specificity.return_value)
+ mock_rmtree.assert_called_with(datastore)
+
+ inner()
+
+ if HAS_CRYPTO:
+ @patch(passphrase, "foo")
+ @patch("Bcfg2.Encryption.ssl_encrypt")
+ @patch("Bcfg2.Encryption.get_algorithm")
+ def inner2(mock_get_algorithm, mock_ssl_encrypt):
+ reset()
+ mock_ssl_encrypt.return_value = "encryptedprivatekey"
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.HAS_CRYPTO = True
+ self.assertEqual(pkc.create_data(entry, metadata),
+ "encryptedprivatekey")
+ pkc.XMLMatch.assert_called_with(metadata)
+ pkc.get_specificity.assert_called_with(
+ metadata,
+ pkc.XMLMatch.return_value)
+ pkc._gen_keypair.assert_called_with(metadata,
+ pkc.XMLMatch.return_value)
+ self.assertItemsEqual(mock_open.call_args_list,
+ [call(privkey + ".pub"), call(privkey)])
+ pkc.pubkey_creator.get_filename.assert_called_with(
+ **pkc.get_specificity.return_value)
+ pkc.pubkey_creator.write_data.assert_called_with(
+ "ssh-rsa publickey pubkey.filename\n",
+ **pkc.get_specificity.return_value)
+ pkc.write_data.assert_called_with(
+ "encryptedprivatekey",
+ **pkc.get_specificity.return_value)
+ mock_ssl_encrypt.assert_called_with(
+ "privatekey", "foo",
+ algorithm=mock_get_algorithm.return_value)
+ mock_rmtree.assert_called_with(datastore)
+
+ inner2()
+
+ def test_Index(self):
+ has_crypto = Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.HAS_CRYPTO
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.HAS_CRYPTO = False
+ TestStructFile.test_Index(self)
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.HAS_CRYPTO = has_crypto
+
+ @skipUnless(HAS_CRYPTO, "No crypto libraries found, skipping")
+ def test_Index_crypto(self):
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP = Mock()
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP.cfp.get.return_value = "strict"
+
+ pkc = self.get_obj()
+ pkc._decrypt = Mock()
+ pkc._decrypt.return_value = 'plaintext'
+ pkc.data = '''
+<PrivateKey>
+ <Group name="test">
+ <Passphrase encrypted="foo">crypted</Passphrase>
+ </Group>
+ <Group name="test" negate="true">
+ <Passphrase>plain</Passphrase>
+ </Group>
+</PrivateKey>'''
+
+ # test successful decryption
+ pkc.Index()
+ self.assertItemsEqual(
+ pkc._decrypt.call_args_list,
+ [call(el)
+ for el in pkc.xdata.xpath("//Passphrase[@encrypted]")])
+ for el in pkc.xdata.xpath("//Crypted"):
+ self.assertEqual(el.text, pkc._decrypt.return_value)
+
+ # test failed decryption, strict
+ pkc._decrypt.reset_mock()
+ pkc._decrypt.side_effect = EVPError
+ self.assertRaises(PluginExecutionError, pkc.Index)
+
+ # test failed decryption, lax
+ Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.SETUP.cfp.get.return_value = "lax"
+ pkc._decrypt.reset_mock()
+ pkc.Index()
+ self.assertItemsEqual(
+ pkc._decrypt.call_args_list,
+ [call(el)
+ for el in pkc.xdata.xpath("//Passphrase[@encrypted]")])
+
+ @skipUnless(HAS_CRYPTO, "No crypto libraries found, skipping")
+ def test_decrypt(self):
+
+ @patch("Bcfg2.Encryption.ssl_decrypt")
+ @patch("Bcfg2.Encryption.get_algorithm")
+ @patch("Bcfg2.Encryption.get_passphrases")
+ @patch("Bcfg2.Encryption.bruteforce_decrypt")
+ def inner(mock_bruteforce, mock_get_passphrases, mock_get_algorithm,
+ mock_ssl):
+ pkc = self.get_obj()
+
+ def reset():
+ mock_bruteforce.reset_mock()
+ mock_get_algorithm.reset_mock()
+ mock_get_passphrases.reset_mock()
+ mock_ssl.reset_mock()
+
+ # test element without text contents
+ self.assertIsNone(pkc._decrypt(lxml.etree.Element("Test")))
+ self.assertFalse(mock_bruteforce.called)
+ self.assertFalse(mock_get_passphrases.called)
+ self.assertFalse(mock_ssl.called)
+
+ # test element with a passphrase in the config file
+ reset()
+ el = lxml.etree.Element("Test", encrypted="foo")
+ el.text = "crypted"
+ mock_get_passphrases.return_value = dict(foo="foopass",
+ bar="barpass")
+ mock_get_algorithm.return_value = "bf_cbc"
+ mock_ssl.return_value = "decrypted with ssl"
+ self.assertEqual(pkc._decrypt(el), mock_ssl.return_value)
+ mock_get_passphrases.assert_called_with(SETUP)
+ mock_get_algorithm.assert_called_with(SETUP)
+ mock_ssl.assert_called_with(el.text, "foopass",
+ algorithm="bf_cbc")
+ self.assertFalse(mock_bruteforce.called)
+
+ # test failure to decrypt element with a passphrase in the config
+ reset()
+ mock_ssl.side_effect = EVPError
+ self.assertRaises(EVPError, pkc._decrypt, el)
+ mock_get_passphrases.assert_called_with(SETUP)
+ mock_get_algorithm.assert_called_with(SETUP)
+ mock_ssl.assert_called_with(el.text, "foopass",
+ algorithm="bf_cbc")
+ self.assertFalse(mock_bruteforce.called)
+
+ # test element without valid passphrase
+ reset()
+ el.set("encrypted", "true")
+ mock_bruteforce.return_value = "decrypted with bruteforce"
+ self.assertEqual(pkc._decrypt(el), mock_bruteforce.return_value)
+ mock_get_passphrases.assert_called_with(SETUP)
+ mock_get_algorithm.assert_called_with(SETUP)
+ mock_bruteforce.assert_called_with(el.text,
+ passphrases=["foopass",
+ "barpass"],
+ algorithm="bf_cbc")
+ self.assertFalse(mock_ssl.called)
+
+ # test failure to decrypt element without valid passphrase
+ reset()
+ mock_bruteforce.side_effect = EVPError
+ self.assertRaises(EVPError, pkc._decrypt, el)
+ mock_get_passphrases.assert_called_with(SETUP)
+ mock_get_algorithm.assert_called_with(SETUP)
+ mock_bruteforce.assert_called_with(el.text,
+ passphrases=["foopass",
+ "barpass"],
+ algorithm="bf_cbc")
+ self.assertFalse(mock_ssl.called)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPublicKeyCreator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPublicKeyCreator.py
new file mode 100644
index 000000000..2e7b6eef4
--- /dev/null
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPublicKeyCreator.py
@@ -0,0 +1,76 @@
+import os
+import sys
+import lxml.etree
+from mock import Mock, MagicMock, patch
+from Bcfg2.Server.Plugins.Cfg import CfgCreationError, CfgCreator
+from Bcfg2.Server.Plugins.Cfg.CfgPublicKeyCreator import *
+from Bcfg2.Server.Plugin import StructFile, PluginExecutionError
+
+# add all parent testsuite directories to sys.path to allow (most)
+# relative imports in python 2.4
+path = os.path.dirname(__file__)
+while path != "/":
+ if os.path.basename(path).lower().startswith("test"):
+ sys.path.append(path)
+ if os.path.basename(path) == "testsuite":
+ break
+ path = os.path.dirname(path)
+from common import *
+from TestServer.TestPlugins.TestCfg.Test_init import TestCfgCreator
+from TestServer.TestPlugin.Testhelpers import TestStructFile
+
+
+class TestCfgPublicKeyCreator(TestCfgCreator, TestStructFile):
+ test_obj = CfgPublicKeyCreator
+ should_monitor = False
+
+ def get_obj(self, name=None, fam=None):
+ return TestCfgCreator.get_obj(self, name=name)
+
+ @patch("Bcfg2.Server.Plugins.Cfg.CfgCreator.handle_event")
+ @patch("Bcfg2.Server.Plugin.helpers.StructFile.HandleEvent")
+ def test_handle_event(self, mock_HandleEvent, mock_handle_event):
+ pkc = self.get_obj()
+ evt = Mock()
+ pkc.handle_event(evt)
+ mock_HandleEvent.assert_called_with(pkc, evt)
+ mock_handle_event.assert_called_with(pkc, evt)
+
+ def test_create_data(self):
+ metadata = Mock()
+ pkc = self.get_obj()
+ pkc.cfg = Mock()
+
+ privkey_entryset = Mock()
+ privkey_creator = Mock()
+ pubkey = Mock()
+ privkey = Mock()
+ privkey_creator.create_data.return_value = (pubkey, privkey)
+ privkey_entryset.best_matching.return_value = privkey_creator
+ pkc.cfg.entries = {"/home/foo/.ssh/id_rsa": privkey_entryset}
+
+ # public key doesn't end in .pub
+ entry = lxml.etree.Element("Path", name="/home/bar/.ssh/bogus")
+ self.assertRaises(CfgCreationError,
+ pkc.create_data, entry, metadata)
+
+ # private key not in cfg.entries
+ entry = lxml.etree.Element("Path", name="/home/bar/.ssh/id_rsa.pub")
+ self.assertRaises(CfgCreationError,
+ pkc.create_data, entry, metadata)
+
+ # successful operation
+ entry = lxml.etree.Element("Path", name="/home/foo/.ssh/id_rsa.pub")
+ self.assertEqual(pkc.create_data(entry, metadata), pubkey)
+ privkey_entryset.get_handlers.assert_called_with(metadata, CfgCreator)
+ privkey_entryset.best_matching.assert_called_with(metadata,
+ privkey_entryset.get_handlers.return_value)
+ self.assertXMLEqual(privkey_creator.create_data.call_args[0][0],
+ lxml.etree.Element("Path",
+ name="/home/foo/.ssh/id_rsa"))
+ self.assertEqual(privkey_creator.create_data.call_args[0][1], metadata)
+
+ # no privkey.xml
+ privkey_entryset.best_matching.side_effect = PluginExecutionError
+ self.assertRaises(CfgCreationError,
+ pkc.create_data, entry, metadata)
diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
index 6412480f0..55fbb7446 100644
--- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
+++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py
@@ -82,9 +82,10 @@ class TestCfgBaseFileMatcher(TestSpecificData):
self.assertFalse(self.test_obj.handles(evt))
mock_get_regex.assert_called_with(
[b for b in self.test_obj.__basenames__])
- self.assertItemsEqual(match.call_args_list,
- [call(evt.filename)
+ print("match calls: %s" % match.call_args_list)
+ print("expected: %s" % [call(evt.filename)
for b in self.test_obj.__basenames__])
+ match.assert_called_with(evt.filename)
mock_get_regex.reset_mock()
match.reset_mock()
@@ -186,47 +187,61 @@ class TestCfgCreator(TestCfgBaseFileMatcher):
test_obj = CfgCreator
path = "/foo/bar/test.txt"
+ def get_obj(self, name=None):
+ if name is None:
+ name = self.path
+ return self.test_obj(name)
+
def test_create_data(self):
cc = self.get_obj()
self.assertRaises(NotImplementedError,
cc.create_data, Mock(), Mock())
+ def test_get_filename(self):
+ cc = self.get_obj()
+
+ # tuples of (args to get_filename(), expected result)
+ cases = [(dict(), "/foo/bar/bar"),
+ (dict(prio=50), "/foo/bar/bar"),
+ (dict(ext=".crypt"), "/foo/bar/bar.crypt"),
+ (dict(ext="bar"), "/foo/bar/barbar"),
+ (dict(host="foo.bar.example.com"),
+ "/foo/bar/bar.H_foo.bar.example.com"),
+ (dict(host="foo.bar.example.com", prio=50, ext=".crypt"),
+ "/foo/bar/bar.H_foo.bar.example.com.crypt"),
+ (dict(group="group", prio=1), "/foo/bar/bar.G01_group"),
+ (dict(group="group", prio=50), "/foo/bar/bar.G50_group"),
+ (dict(group="group", prio=50, ext=".crypt"),
+ "/foo/bar/bar.G50_group.crypt")]
+
+ for args, expected in cases:
+ self.assertEqual(cc.get_filename(**args), expected)
+
@patch("os.makedirs")
@patch("%s.open" % builtins)
def test_write_data(self, mock_open, mock_makedirs):
cc = self.get_obj()
data = "test\ntest"
+ parent = os.path.dirname(self.path)
def reset():
mock_open.reset_mock()
mock_makedirs.reset_mock()
- # test writing non-specific file
- cc.write_data(data)
- mock_makedirs.assert_called_with("/foo/bar")
- mock_open.assert_called_with("/foo/bar/bar", "wb")
- mock_open.return_value.write.assert_called_with(data)
-
- # test writing group-specific file
- reset()
- cc.write_data(data, group="foogroup", prio=9)
- mock_makedirs.assert_called_with("/foo/bar")
- mock_open.assert_called_with("/foo/bar/bar.G09_foogroup", "wb")
- mock_open.return_value.write.assert_called_with(data)
-
- # test writing host-specific file
+ # test writing file
reset()
- cc.write_data(data, host="foo.example.com")
- mock_makedirs.assert_called_with("/foo/bar")
- mock_open.assert_called_with("/foo/bar/bar.H_foo.example.com", "wb")
+ spec = dict(group="foogroup", prio=9)
+ cc.write_data(data, **spec)
+ mock_makedirs.assert_called_with(parent)
+ mock_open.assert_called_with(cc.get_filename(**spec), "wb")
mock_open.return_value.write.assert_called_with(data)
# test already-exists error from makedirs
reset()
mock_makedirs.side_effect = OSError(errno.EEXIST, self.path)
cc.write_data(data)
- mock_makedirs.assert_called_with("/foo/bar")
- mock_open.assert_called_with("/foo/bar/bar", "wb")
+ mock_makedirs.assert_called_with(parent)
+ mock_open.assert_called_with(cc.get_filename(), "wb")
mock_open.return_value.write.assert_called_with(data)
# test error from open
@@ -391,6 +406,10 @@ class TestCfgEntrySet(TestEntrySet):
evt = Mock()
evt.filename = "test.txt"
handler = Mock()
+ handler.__basenames__ = []
+ handler.__extensions__ = []
+ handler.deprecated = False
+ handler.experimental = False
handler.__specific__ = True
# test handling an event with the parent entry_init