diff --git a/.fixtures.yml b/.fixtures.yml index 2d45c82..da3c52f 100644 --- a/.fixtures.yml +++ b/.fixtures.yml @@ -1,8 +1,16 @@ --- fixtures: repositories: - stdlib: https://github.com/simp/puppetlabs-stdlib.git + ds389: https://github.com/simp/pupmod-simp-ds389.git + pki: https://github.com/simp/pupmod-simp-pki.git + selinux: https://github.com/simp/pupmod-simp-selinux.git simplib: https://github.com/simp/pupmod-simp-simplib.git + stdlib: https://github.com/simp/puppetlabs-stdlib.git + systemd: https://github.com/simp/puppet-systemd.git + vox_selinux: + repo: https://github.com/simp/pupmod-voxpupuli-selinux.git + branch: simp-master + # This needs to be in place for the rspec-puppet Hiera 5 hook to work # No idea why, it may be because Puppet sees a custom backend and loads all # of the global parts. diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eca7bf6..beb40bc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -332,21 +332,34 @@ pup7.x-unit: # Repo-specific content # ============================================================================== +# default suite: Exercises the file plugin with file plugin-specific +# validation and special cases +# ldap_suite: Exercises the ldap_plugin with ldap plugin-specific +# validation and special cases +# multiple_plugins: Exercises both the file and ldap plugins simultaneously with +# their respective plugin-specific validation: +# - Test configures the simpkv backend configuration to have +# both plugins +# - Test ensures both plugins are used in individual catalog +# runs +# - Test does NOT include plugin special cases found in the +# individual plugin suites +# +#---- default suite ---- +# Nodesets: default -> CentOS8 +# oel -> OEL8 +# centos7 -> CentOS7 +# oel7 -> OEL7 pup6.x: <<: *pup_6_x <<: *acceptance_base script: - 'bundle exec rake beaker:suites[default,default]' -pup6.x-fips: - <<: *pup_6_x - <<: *acceptance_base - script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites[default,default]' - pup6.pe: <<: *pup_6_pe <<: *acceptance_base + <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - 'bundle exec rake beaker:suites[default,default]' @@ -357,62 +370,98 @@ pup6.pe-fips: script: - 'BEAKER_fips=yes bundle exec rake beaker:suites[default,default]' -pup6.x.centos7: - <<: *pup_6_x +pup6.pe-centos7: + <<: *pup_6_pe <<: *acceptance_base script: - 'bundle exec rake beaker:suites[default,centos7]' -pup6.x.centos7-fips: - <<: *pup_6_x +pup6.pe-oel: + <<: *pup_6_pe <<: *acceptance_base - <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites[default,centos7]' + - 'bundle exec rake beaker:suites[default,oel]' -pup6.pe.centos7: +pup6.pe-oel7: <<: *pup_6_pe <<: *acceptance_base <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 + script: + - 'bundle exec rake beaker:suites[default,oel7]' + +pup7.x: + <<: *pup_7_x + <<: *acceptance_base + script: + - 'bundle exec rake beaker:suites[default,default]' + +pup7.x-centos7: + <<: *pup_7_x + <<: *acceptance_base + <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - 'bundle exec rake beaker:suites[default,centos7]' -pup6.pe-oel: - <<: *pup_6_pe +#---- ldap_plugin suite ---- +# Nodesets: default -> CentOS8 + CentOS7 +# oel -> OEL8 + OEL7 +pup6.x-ldap_plugin: + <<: *pup_6_x <<: *acceptance_base script: - - 'bundle exec rake beaker:suites[default,oel]' + - 'bundle exec rake beaker:suites[ldap_plugin,default]' -pup6.pe-oel-fips: +pup6.pe-ldap_plugin: <<: *pup_6_pe <<: *acceptance_base <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites[default,oel]' + - 'bundle exec rake beaker:suites[ldap_plugin,default]' -pup6.pe.centos7-oel: +pup6.pe-ldap_plugin-fips: <<: *pup_6_pe <<: *acceptance_base <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - - 'bundle exec rake beaker:suites[default,oel7]' + - 'BEAKER_fips=yes bundle exec rake beaker:suites[ldap_plugin,default]' -pup6.pe.centos7-oel-fips: +pup6.pe-ldap_plugin-oel: <<: *pup_6_pe <<: *acceptance_base - <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - - 'BEAKER_fips=yes bundle exec rake beaker:suites[default,oel7]' + - 'bundle exec rake beaker:suites[ldap_plugin,oel]' -pup7.x: +pup7.x-ldap_plugin: <<: *pup_7_x <<: *acceptance_base script: - - 'bundle exec rake beaker:suites[default,default]' + - 'bundle exec rake beaker:suites[ldap_plugin,default]' -pup7.x.centos7: - <<: *pup_7_x +#---- multiple_plugins suite ---- +# Nodesets: default -> CentOS8 +# oel -> OEL8 +pup6.x-multiple_plugin: + <<: *pup_6_x <<: *acceptance_base <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 script: - - 'bundle exec rake beaker:suites[default,centos7]' + - 'bundle exec rake beaker:suites[multiple_plugin,default]' + +pup6.pe-multiple_plugins: + <<: *pup_6_pe + <<: *acceptance_base + script: + - 'bundle exec rake beaker:suites[multiple_plugins,default]' + +pup6.pe.multiple_plugins-fips: + <<: *pup_6_pe + <<: *acceptance_base + script: + - 'BEAKER_fips=yes bundle exec rake beaker:suites[multiple_plugins,default]' + +pup7.x.multiple_plugins: + <<: *pup_7_x + <<: *acceptance_base + script: + - 'bundle exec rake beaker:suites[multiple_plugins,default]' + diff --git a/CHANGELOG b/CHANGELOG index 0e26479..5ceb5ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -* Mon Jun 21 2021 Liz Nemsick - 0.8.0 +* Tue Sep 28 2021 Liz Nemsick - 0.8.0 - BREAKING CHANGES: - Added 'globals' and 'environments' root directories for global and Puppet-environment keys, respectively, in the normalized key paths @@ -27,11 +27,17 @@ - Plugin type is now determined from its filename. - Previous mechanism did not work when when multiple plugins were used. - Added + - LDAP plugin + - Acceptance test that demonstrates its use and integration with + a 389-DS instance configured with the SIMP data schema - More detailed plugin exception reporting in order to pinpoint plugin logic problems. - Now prints out the useful portion of the backtrace when an exception is raised. - Especially useful during plugin development. + - More background information for users + - More background information for plugin developers, which has now + been split out into its own document. * Wed Jun 16 2021 Chris Tessmer - 0.8.0 - Removed support for Puppet 5 diff --git a/README.md b/README.md index e57a934..d7096e3 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ * [Overview](#overview) * [This is a SIMP module](#this-is-a-simp-module) * [Module Description](#module-description) +* [Setup](#setup) + * [What simpkv Affects](#what-simpkv-affects) + * [Setup Requirements](#setup-requirements) * [Terminology](#terminology) * [Usage](#usage) * [Single Backend Example](#single-backend-example) @@ -18,13 +21,24 @@ * [Binary Value Example](#binary-value-example) * [Global Key Example](#global-key-example) * [Auto-Default Backend](#auto-default-backend) - * [Backend Folder Layout](#backend-folder-layout) +* [Reference](#reference) + * [simpkv Function Reference](#simpkv-function-reference) * [simpkv Configuration Reference](#simpkv-configuration-reference) -* [File Store and Plugin](#file-store-and-plugin) + * [Backend Configuration Entries](#backend-configuration-entries) + * [Backend Selection](#backend-selection) + * [Backend Basic](#backend-basics) + * [Backend Folder Layout](#backend-folder-layout) + * [Backend Data Format](#backend-data-format) + * [File Plugin](#file-plugin) + * [File Plugin Requirements](#file-plugin-requirements) + * [File Plugin Configuration](#file-plugin-configuration) + * [LDAP Plugin](#ldap-plugin) + * [LDAP Plugin Requirements](#ldap-plugin-requirements) + * [LDAP Plugin Configuration](#ldap-plugin-configuration) + * [Debugging LDAP Integration Issues](#debugging-ldap-integration-issues) + * [Other Resources](#other-resources) * [Limitations](#limitations) * [Plugin Development](#plugin-development) - * [Plugin Loading](#plugin-loading) - * [Implementing the Store Interface API](#implementing-the-store-interface-api) * [simpkv Development](#simpkv-development) * [Unit tests](#unit-tests) * [Acceptance tests](#acceptance-tests) @@ -40,8 +54,6 @@ a compliance-management framework built on Puppet. If you find any issues, please submit them via [JIRA](https://simp-project.atlassian.net/). -Please read our [Contribution Guide](https://simp.readthedocs.io/en/stable/contributors_guide/index.html). - ## Module Description Provides an abstract library that allows Puppet to access one or more key/value @@ -59,50 +71,87 @@ This module provides of different key/value store instances * adapter software that loads and uses store-specific interface software provided by the simpkv module itself and other modules -* a Ruby API for the store interface software that developers can implement - to provide their own store interface -* a file-based store on the local filesystem and its interface software. +* interfaces for 2 types of stores + + * **local filesystem store**: Useful for module acceptance tests or small + sites that are not subject to high-performance and/or high-availability + requirements. + * **LDAP store**: Useful for large sites that are subject to high-performance + and/or high-availability requirements. + +* support for creating custom store interfaces + + * a Ruby API for the store interface software that developers can implement + to provide their own store interface + * a standard acceptance testing framework developers can use to verify their + store interfaces work as expected + * a developers guide. + +## Setup + +### What simpkv Affects + +simpkv manages the contents of one or more key/value stores via Puppet functions +which affect store, retrieve, and modify operations. + +### Setup Requirements + +simpkv has 3 main setup requirements: + +1. simpkv requires hieradata configuration (a `simpkv::options` Hash), when it + is used to manage the contents of key/value stores. + + * simpkv will operate without any configuration by internally configuring + itself to use the [auto-default local filesystem store](#auto-default-backend), + however, this key/value store is really only useful in Puppet module tests. + +2. simpkv expects each configured key/value store to be managed elsewhere. + + * You can manage LDAP servers with the `simp/ds389` module. - * Future versions of this module will provide a distributed key/value store. +3. simpkv expects any store-specific interface requirements to be addressed + elsewhere. -If you find any issues, they may be submitted to our -[bug tracker](https://simp-project.atlassian.net/). + * Some stores may impose other store-specific requirements in order for their + simpkv interface software to operate. For example, a store may require + packages to be installed on a server compiling the Puppet manifests. + * Any store-specific requirements will be listed in that store's interface + documentation. ## Terminology -The following terminology will be used throughout this document: +The following terminology will be used throughout the remainder of this document: -* backend - A specific key/value store, e.g., files on a local filesystem, - Consul, Etcd, Zookeeper. -* plugin - Ruby software that interfaces with a specific backend to - affect the operations requested in simpkv Puppet functions. -* plugin instance - Instance of the plugin that handles a unique backend - configuration. -* plugin adapter - Ruby software that loads, selects, and executes the - appropriate plugin software for a simpkv function call. +* **backend**- A specific key/value store that has unique configuration, (e.g., + directory of files on a local filesystem, LDAP server, Consul server, Etcd + server, Zookeeper server). + +* **plugin** - Ruby software that interfaces with a type of backend to affect + the operations requested in simpkv Puppet functions. For example, the 'ldap' + plugin manages simpkv data stored in an external LDAP server. ## Usage Using `simpkv` is simple: -* Use `simpkv` functions to store and retrieve key/value pairs in your Puppet +* Use `simpkv` functions to store or retrieve key/value pairs in your Puppet code. -* Configure the backend(s) to use in Hieradata. -* Reconfigure the backend(s) in Hieradata, as your needs change. +* Configure the backend(s) to use in hieradata. +* Reconfigure the backend(s) in hieradata, as your needs change. * No changes to your Puppet code will be required. * Just transfer your data from the old key/value store to the new one. The backend configuration of `simpkv` can be as simple as you want (one backend) -or complex (multiple backends servicing different applications). Examples of -both scenarios will be shown in this section, along with a configuration -reference. +or complex (multiple backends of different types servicing different +applications). Examples of both scenarios will be shown in this section. ### Single Backend Example -This example will store and retrieve host information using simpkv function -signatures that assume the default backend and hieradata that only configures -the default backend. +This example will store and retrieve host information using + +* simpkv function signatures that assume the default backend +* hieradata that only configures the default backend. To store a node's hostname and IP address: @@ -121,7 +170,7 @@ $result['keys'].each |$host, $info | { } ``` -In hieradata, configure the default backend in the ``simpkv::options`` Hash. This +In hieradata, configure the default backend in the ``simpkv::options`` Hash. This example, will configure simpkv's file backend. ```yaml @@ -132,9 +181,8 @@ simpkv::options: # all simpkv calls. backends: default: - # This is the advertised type for simpkv's file plugin. + # The plugin type and id must be specified. type: file - # This is a unique id for this configuration of the 'file' plugin. id: file # plugin-specific configuration @@ -144,10 +192,14 @@ simpkv::options: ### Multiple Backends Example -This example will store and retrieve host information using simpkv function -signatures that request a backend based on an application id and multi-backend -hieradata that supports the request. The function signatures and hieradata are -a little more complicated, but still relatively straightforward to understand. +This example will store and retrieve host information using + +* simpkv function signatures that request a backend based on an application + identifier +* multi-backend hieradata that supports the request. + +The function signatures and hieradata are a little more complicated, but still +relatively straightforward to understand. To store a node's hostname and IP address using the backend servicing `myapp1`: @@ -171,26 +223,34 @@ $result['keys'].each |$host, $info | { ``` In hieradata, configure multiple backends in the ``simpkv::options`` Hash. -This example will configure multiple instances of simpkv's file backend. +This example will configure a single instance of a LDAP backend and two +instances of file backend, assuming we are using simpkv's LDAP and file plugins, +respectively. It is artificially complex, but illustrates the flexibility you +have when configuring backends: + +* You can use any mix of backend types. +* You can use multiple instances of a backend type. + + * Each 'type' and 'id' specifies a unique configuration. + +* You can map different applications to the same backend. ```yaml # The backend configurations here will be inserted into simpkv::options # below via the alias function. -simpkv::backend::file_default: - type: file +simpkv::backend::ldap_default: + type: ldap id: default - root_path: "/var/simp/simpkv/file" + ldap_uri: ldapi://%2fvar%2frun%2fslapd-simp_data.socket simpkv::backend::file_myapp: type: file id: myapp - root_path: "/path/to/myapp" simpkv::backend::file_yourapp: type: file id: yourapp - root_path: "/path/to/yourapp" simpkv::options: @@ -198,27 +258,24 @@ simpkv::options: # * Includes application-specific backends and the required default backend. # * simpkv will use the appropriate backend for each simpkv function call. backends: - # backend for specific myapp application - "myapp_special_snowflake": "%{alias('simpkv::backend::file_default')}" + # Backend for specific myapp_special_snowflake* applications + "myapp_special_snowflake": "%{alias('simpkv::backend::file_myapp')}" - # backend for remaining myapp* applications, including myapp1 - "myapp": "%{alias('simpkv::backend::file_myapp')}" + # Backend for remaining myapp* applications, including myapp1 + "myapp": "%{alias('simpkv::backend::ldap_default')}" - # backend for all yourapp* applications + # Backend for all yourapp* applications "yourapp": "%{alias('simpkv::backend::file_yourapp')}" - # required default backend - "default": "%{alias('simpkv::backend::file_default')}" + # Required default backend for everything else, including simpkv + # function calls with no application identifier + "default": "%{alias('simpkv::backend::ldap_default')}" ``` In this example, we are setting the application identifier to `myapp1` in -our simpkv function calls. simpkv selects `myapp` as the backend to use for -`myapp1` using the following simple search algorithm: - -* First, it looks for a backend named for the application id. -* Next, it looks for the longest backend name matching the start of the - application id. -* Finally, if no match is found, it defaults to a backend named `default`. +our simpkv function calls. simpkv will select `myapp` as the backend to +use for `myapp1`, based on the simple matching algorithm described in +[Backend Selection](#backend-selection). ### Binary Value Example @@ -230,7 +287,7 @@ Below is an example of using simpkv for a binary value. To store the content of a generated keytab file: ```puppet -# Load in the binary content from a file. Returns a Binary Puppet type. +# Load in the binary content from a file. Returns a Binary Puppet type. $original_binary_content = binary_file('/path/to/keytabs/app.keytab') # Set a key/value pair with the binary content @@ -284,12 +341,13 @@ $result['keys'].each |$host, $info | { ### Auto-Default Backend -simpkv is intended to be configured via ``simpkv::options`` and any +simpkv is intended to be configured via `simpkv::options` and any application-specific configuration passed to the simpkv Puppet functions. -However, to facilitate rollout of simpkv capabilities, (specifically -use of simpkv internally in ``simplib::passgen``), when ``simpkv::options`` -is not set in hieradata, simpkv will automatically use the simpkv file store with -the configuration that is equivalent to the following hieradata: +However, to facilitate Puppet manifest testing and the rollout of simpkv +capabilities (specifically, the use of simpkv internally in `simplib::passgen`), +when `simpkv::options` is not set in hieradata, simpkv will automatically use +the 'file' plugin to store key/value data on the local filesystem. This is +equivalent to the following `simpkv::options` hieradata: ```yaml simpkv::options: @@ -302,83 +360,38 @@ simpkv::options: id: auto_default ``` -### Backend Folder Layout - -The storage in a simpkv backend can be notionally represented as a folder -tree with key files at terminal nodes. simpkv automatically sets up the -folder layout at the top level and the user specifies key files below that. -Specifically, - -* simpkv stores global keys in a `globals` sub-folder of the root folder. +The 'file' plugin id described [here](#file-plugin). - * Global keys are not tied to any specific Puppet environment. - * You must specify `'global' => true` in the options passed to - simpkv functions in order to access global keys. +## Reference -* simpkv stores all other keys in sub-folders named for the Puppet - environment in which each key was created. - - * The parent directory for all environment folders is - `/environments`. +### simpkv Function Reference -* Further sub-folder trees are allowed for global or environment-specific keys. - - * A relative paths in a key name indicates a sub-folder tree (e.g. - 'app1/keya'). - -* The actual representation of the root folder is backend specific. - - * For the 'file' backend, the root folder is a directory on the local file - system of the Puppet server. - -Below is an example of a folder tree for the `file` backend configured -with an `id` of `default`: - -``` -/var/simp/simpkv/file/default -│ -├── globals/ .............. Global keys parent -│ ├── app1/ ............. Folder for 'app1' global keys -│ │ └── global_keyq .. simpkv::put('app1/global_keyq', { 'global' => true }) -│ └── global_keyr ....... simpkv::put('global_keyr', { 'global'=> true }) -│ -├── environments/.......... Environment keys parent -│ ├── dev/ .............. Folder for 'dev' Puppet environment keys -│ │ └── app1/ -│ │ └── keya ...... simpkv::put('app1/keya') for a 'dev' env node -│ │ -│ └── production/ ....... Folder for 'production' Puppet environment keys -│ ├── app1/ -│ │ └── keya ...... simpkv::put('app1/keya') for a 'production' env node -│ ├── app2/ -│ │ ├── groupx/ -│ │ │ └── keyb -│ │ └── groupy/ -│ │ └── keyc .. simpkv::put('app2/groupy/keyc') in a 'production' node -│ └── keyd .......... simpkv::put('keyd') in a 'production' env node -└── -``` +See [REFERENCE.md](./REFERENCE.md) for the module's function reference. ### simpkv Configuration Reference The simpkv configuration used for each simpkv function call is comprised of -a merge of a function-provided options Hash, Hiera configuration specified -by the `simpkv::options` Hash, and global configuration defaults. The merge -is executed in a fashion to ensure the function-provided options take -precedence over the `simpkv::options` Hiera values. +a merge of + +* function-provided options Hash +* hieradata configuration specified by the `simpkv::options` Hash +* global configuration defaults. + +The merge is executed in a fashion to ensure the function-provided options take +precedence over the `simpkv::options` hieradata values and global defaults. The merged simpkv configuration contains global and backend-specific -configurations, along with an optional application identifier. The primary -keys in this Hash are as follows: +configurations, along with an optional, function-provided application +identifier. The primary keys in this Hash are as follows: -* `app_id`: Optional String in simpkv function calls, only. Specifies an +* `app_id`: Optional String from simpkv function calls, only. Specifies an application name that can be used to identify which backend configuration to use via fuzzy name matching, in the absence of the `backend` option. - (See [Backend Selection](#backend-selection)). * More flexible option than `backend`. * Useful for grouping together simpkv function calls found in different catalog resources. + * See [Backend Selection](#backend-selection). * `backend`: Optional String. Specifies a definitive backend configuration to use. @@ -390,9 +403,9 @@ keys in this Hash are as follows: * When absent, the backend configuration will be selected from the set of entries in `backends`, using the `app_id` option if specified. - (See [Backend Selection](#backend-selection)). + * See [Backend Selection](#backend-selection). -* `backends`: Required Hash. Specifies backend configurations. Each key +* `backends`: Required Hash. Specifies backend configurations. Each key is the name of a backend configuration and its value contains the corresponding configuration Hash. @@ -418,13 +431,16 @@ keys in this Hash are as follows: #### Backend Configuration Entries -Each backend configuration entry in `backends` is a Hash. The Hash must +Each backend configuration entry in `backends` is a Hash. The Hash must contain `type` and `id` keys, where the (`type`,`id`) pair defines a unique -configuration. +configuration (i.e., unique plugin instance). + +* `type` is the type of plugin, e.g., 'ldap'. + + * 'file' and 'ldap' are provided by the simpkv module. + * Other modules may provide their own plugins. -* `type` must be unique across all backend plugins, including those - provided by other modules. -* `id` must be unique for a each distinct configuration for a `type`. +* `id` is the instance identifier for a `type`. * Other keys for configuration specific to the backend may also be present. #### Backend Selection @@ -450,68 +466,411 @@ the merged simpkv options Hash as follows: * Otherwise, if the `app_id` option does not match any key in in the `backends` Hash or is not present, the `default` backend will be selected. -## File Store and Plugin +To clarify this backend selection algorithm, consider a site in which simpkv is +configured for multiple backends in the following ``simpkv::options`` Hash. + +```yaml +simpkv::options: + # Hash of backend configurations. + # * Includes application-specific backends and the required default backend. + # * simpkv will use the appropriate backend for each simpkv function call. + backends: + # Backend for specific myapp_special_snowflake* applications + "myapp_special_snowflake": + type: file + id: myapp_special_snowflake + + # Backend for remaining myapp* applications + "myapp": + type: file + id: myapp + + # Backend for all yourapp* applications + "yourapp": + type: file + id: yourapp + + # required default backend for everything else, including simpkv + # function calls with no application identifier + "default": + type: file + id: default +``` + +The following table shows the backend that simpkv will select make given the +options specified by the simpkv function call. + +| simpkv Function Call | Backend Selected | Comments | +|----------------------------------------------- | ------------------- | --------------------------------| +| `simpkv::get('key')` | `default` | No special handling requested | +| `simpkv::get('key', {'backend' => 'yourapp'})` | `yourapp` | Specific backend requested | +| `simpkv::get('key', {'backend' => 'oops'})` | N/A | Function will fail since 'oops' backend does not exist | +| `simpkv::get('key', {'app_id' => 'myapp'})` | `myapp` | Exact match | +| `simpkv::get('key', {'app_id' => 'myapp1'})` | `myapp` | Starts with match | +| `simpkv::get('key', {'app_id' => 'myapp_special'})` | `myapp` | Starts with match | +| `simpkv::get('key', {'app_id' => 'myapp_special_snowflake_for_bob'})` | `myapp_special_snowflake` | Starts with match | +| `simpkv::get('key', {'app_id' => 'otherapp'})` | `default` | No match so fallback to default | + +### Backend Basics + +This section describes details about the folder layout and format of data +stored by simpkv that you may find useful when you are inspecting data in +a backend. + +#### Backend Folder Layout + +The storage in a simpkv backend can be notionally represented as a folder +tree with key files at terminal nodes. simpkv automatically sets up the +folder layout at the top level and the user specifies key files below that. +Specifically, + +* simpkv stores global keys in a `globals` sub-folder of the root folder. + + * Global keys are not tied to any specific Puppet environment. + * You must specify `'global' => true` in the options passed to + simpkv functions in order to access global keys. + +* simpkv stores all other keys in sub-folders named for the Puppet + environment in which each key was created. + + * The parent directory for all environment folders is + `/environments`. + +* Further sub-folder trees are allowed for global or environment-specific keys. + + * A relative paths in a key name indicates a sub-folder tree (e.g. + `'app1/keya'`). + +For example, + +![simpkv Key/Value Tree](docs/assets/simpkv_Key_Value_Tree.png) + +The *actual* representation of the root folder and 'key file' is backend specific. + +### Backend Data Format + +Internally, simpkv automatically serializes each key's value and optional +metadata into a string for backend storage, and then deserializes it upon +retrieval. The string is a JSON representation of a Hash with at least 2 +attributes: + +* `value`: Always present. Key's value +* `metadata`: Always present. Key's metadata +* `encoding` and `original_encoding`: Only present when the key's value is a + Puppet Binary type. Indicates simpkv's internal encoding of the binary data + into a representation suitable for JSON. + + * simpkv uses strict Base64 encoding. + +This serialization simplifies plugin development, but does limit the types of +data that can be stored in the value and metadata. simpkv works with the +following types: + +* *Simple type*: String, boolean, or numeric values +* *Standard complex type*: Hash, Array, or nested Hash/Array structure whose + terminal nodes are simple types +* *Binary type*: Puppet Binary type when it is the key's value, only; not in + any complex values or metadata -simpkv provides a file-based key/value store and its plugin. This file store -maintains individual key files on a **local** filesystem, has a backend type `file`, -and supports the following plugin-specific configuration parameters. +The table below shows a few examples of the serialization for clarification. -* `root_path`: Root directory path for the key files + +| Value Type | Serialized String | +| ---------- | --------------------- | +| Simple without metadata | `{"value":10,"metadata":{}}` | +| Simple with metadata | `{"value":true,"metadata":{"verified":true,"user":"vsmith"}}` | +| Array with metadata | `{"value":[1,2,3],"metadata":{"originator":"njones","location":{"room":"29B","rack":10}}}` | +| Hash without metadata | `{"value":{"attr1":"hello","attr2":{"part1":9.898,"part2":[1,2,3]}},"metadata":{}` | +| Binary transformed by simpkv without metadata | `{"value":"","encoding":"base64","original_encoding":"ASCII-8BIT","metadata":{}"}` | + + +## File Plugin + +simpkv provides a plugin that maintains a file-based key/value store on the +local filesystem on the server on which the Puppet manifests are being compiled. +It effectively uses system commands to affect local filesystems changes. + +* This plugin is appropriate for Puppet module tests. +* This plugin can be used for small-sites in which there is only a single server + compiling Puppet manifests. + +### File Plugin Requirements + +* The file plugin must be configured for a local filesystem on the server + compiling the Puppet manifests. + + * The file locking mechanism the plugin uses to ensure the integrity of the + key files is only guaranteed to work on a **local** filesystem, and is + **not** suitable for shared filesystems such as NFS. + +* The file plugin must have write privileges for the configured `root_path` for + the user compiling the manifests. +* The file plugin must **not** be used for sites using distributed Puppet servers. + + * The file plugin has no mechanisms to distribute the key/store to other + compile servers. + +### File Plugin Configuration + +The plugin has a backend type `file`, and supports the following plugin-specific +configuration parameters. + +* `root_path`: Optional. Root directory path for the key files * Defaults to `/var/simp/simpkv/file/` when that directory can be created - or '/simp/simpkv/' otherwise. + or `/simp/simpkv/` otherwise. -* `lock_timeout_seconds`: Maximum number of seconds to wait for an exclusive - file lock on a file modifying operation before failing the operation. +* `lock_timeout_seconds`: Optional. Maximum number of seconds to wait for an + exclusive file lock on a file modifying operation before failing the + operation. * Defaults to 5 seconds. +Here is an example configuration for the file plugin: + +```yaml +simpkv::options: + backends: + default: + type: file + id: default +``` + +## LDAP Plugin + +simpkv provides a plugin to interface with a LDAP key/value store configured +with a simpkv LDAP schema. If your site is large and requires a +high-availability, distributed key/value store, LDAP is the appropriate +backend to use! The benefits of using LDAP are as follows: + +* LDAP supports the storage of any data, not just accounts data. +* LDAP is well defined protocol implemented by a wide variety of client and + server implementations. +* LDAP is secure. +* LDAP server implementations are highly performant. +* LDAP server implementations support replication. + +### LDAP Plugin Requirements + +The LDAP plugin has 4 main requirements: + +1. The package providing `ldapadd`, `ldapdelete`, `ldapmodify` and `ldapsearch` + (i.e. `openldap-clients`) must be installed on the system on which the Puppet + manifests will be compiled. + +2. The LDAP server must be loaded with the + [simpkv LDAP schema](docs/simpkv_LDAP_DIT_and_schema.md). + +3. The root Directory Name (DN) for the simpkv tree in LDAP must already + exist. + + * This corresponds to the LDAP plugin's `base_dn` configuration parameter. + +4. The supplied LDAP configuration must permit the user compiling the Puppet + manifests to modify the LDAP tree below that 'simpkv' root DN. + +**In addition**, if LDAPI is not the protocol being being used to communicate +with the LDAP server, the configured password file used for simple +authentication with the LDAP server **MUST** already exist before the catalog +is compiled. + +* Catalog compilation will fail if you attempt to create the file using a `file` + Puppet resource in the same catalog as simpkv function calls. This is because + the simpkv functions will be evaluated before the `file` resource in the + compilation. + +### LDAP Plugin Configuration + +The simpkv LDAP plugin is of type `ldap` and supports the following +plugin-specific configuration parameters. + +* `ldap_uri`: Required. The LDAP server URI. + + * This can be a LDAPI socket path or an ldap/ldaps URI specifying host and, + optionally, port. + * When using an 'ldap://' URI with StartTLS, `enable_tls` must be true and + `tls_cert`, `tls_key`, and `tls_cacert` must be configured. + * When using an 'ldaps://' URI, `tls_cert`, `tls_key`, and `tls_cacert` must + be configured. + +* `base_dn`: Optional. The root DN for the 'simpkv' tree in LDAP. + + * Defaults to 'ou=simpkv,o=puppet,dc=simp' + * Must already exist + +* `admin_dn`: Optional. The bind DN for simpkv administration. + + * Defaults to 'cn=Directory_Manager' + * This identity must have permission to modify the LDAP tree below `base_dn`. + +* `admin_pw_file`: Required for all but LDAPI. A file containing the simpkv + adminstration password. + + * Will be used for authentication when set, even with LDAPI. + * When unset for LDAPI, the `admin_dn` is assumed to be properly configured + for external EXTERNAL SASL authentication for the user compiling the manifest + (e.g., 'puppet' for 'puppet agent', 'root' for 'puppet apply' and the Bolt + user for Bolt plans). + +* `enable_tls`: Optional. Whether to enable TLS. + + * Defaults to true when `ldap_uri` is an 'ldaps://' URI, otherwise defaults + to false. + * Must be set to true to enable StartTLS when using an 'ldap://' URI. + * When true `tls_cert`, `tls_key` and `tls_cacert` must be set. + +* `tls_cert`: Required for StartTLS or TLS. The certificate file. +* `tls_key`: Required for StartTLS or TLS. The key file. +* `tls_cacert`: Required for StartTLS or TLS. The cacert file. +* `retries`: Optional. Number of times to retry an LDAP operation if the + server reports it is busy. + + * Defaults to 1. + +Here are some example configurations: + +* To use LDAPI (best option if the Puppet manifest compilation is being done on + the LDAP server), configure the LDAP plugin as follows: + +```yaml + simpkv::options: + backends: + default: + type: ldap + id: default + + # Set this to your LDAP server's LDAPI URI + ldap_uri: ldapi://%2fvar%2frun%2fslapd-simp_data.socket +``` + +* To use LDAPS, configure the LDAP plugin as follows: + +```yaml + simpkv::options: + backends: + default: + type: ldap + id: default + + # Set this to your LDAP server's LDAPS URI + ldap_uri: ldaps://ldap.example.com + + # Set this to the location of your administration DN password file + admin_pw_file: /etc/simp/simpkv_pw.txt + + # Set these to the appropriate certs for your server + tls_cert: /etc/pki/simp_apps/openldap/public/puppetserver.example.com.pub + tls_key: /etc/pki/simp_apps/openldap/private/puppetserver.example.com.pem + tls_cacert: /etc/pki/simp_apps/openldap/cacerts/cacerts.pem +``` + +* To use LDAP with StartTLS, configure the LDAP plugin as follows: + +```yaml + simpkv::options: + backends: + default: + type: ldap + id: default + + # Set this to your LDAP server's LDAP URI + ldap_uri: ldap://ldap.example.com + + # Set this to the location of your administration DN password file + admin_pw_file: /etc/simp/simpkv_pw.txt + + # Use StartTLS with the LDAP URI + enable_tls: true + + # Set these to the appropriate certs for your server + tls_cert: /etc/pki/simp_apps/openldap/public/puppetserver.example.com.pub + tls_key: /etc/pki/simp_apps/openldap/private/puppetserver.example.com.pem + tls_cacert: /etc/pki/simp_apps/openldap/cacerts/cacerts.pem +``` + + +The LDAP plugin creates the key/value store for each instance under its own +tree on the LDAP server. The tree's DN is *ou=,ou=instances,*, +where the instance id is the 'ldap' plugin's `id` configuration parameter. So, +you can actually use the same LDAP server for different plugin instances. + +Here is an example configuration of multiple instances with the same LDAPI +configuration: + +```yaml + simpkv::options: + backends: + # stored under the 'myapp' instance tree of the simp_data LDAP server + myapp: + type: ldap + id: myapp + ldap_uri: ldapi://%2fvar%2frun%2fslapd-simp_data.socket + + # stored under the 'default' instance tree of the simp_data LDAP server + default: + type: ldap + id: default + ldap_uri: ldapi://%2fvar%2frun%2fslapd-simp_data.socket +``` + +### Debugging LDAP Integration Issues + +When you are trying to debug LDAP integration issues, use the `--debug` +option during manifest compilation to see more details about what the +LDAP plugin is doing. For example, + +```shell +puppet apply -t --debug my_manifests.pp +``` + +Using this option will log the exact LDAP commands used during simpkv +function calls. + +### Other Resources + +The `ldap_plugin` acceptance test suite is complete example on how to use the +LDAP plugin. It demonstrates the following: + +* How to configure simpkv to use the LDAP plugin for LDAPI unencrypted LDAP, + LDAP + StartTLS, and LDAPS protocols. +* How to stand up simple 389-DS LDAP instances for those protocol options. + + Each 389-DS instance is + + * Created by the `simp/ds389` module. + * Bootstrapped with a LDIF that corresponds to the simpkv leg of the SIMP Data + Root Directory Information Tree (DIT) shown below. + ![SIMP Data Root DIT](docs/assets/LDAP_DIT_root.png) + + * Explicitly configured with non-standard ports, so as to not be confused + with LDAP servers used for accounts management. + * Loaded with the [simpkv LDAP schema](docs/simpkv_LDAP_DIT_and_schema.md). + ## Limitations * SIMP Puppet modules are generally intended to be used on a Red Hat Enterprise Linux-compatible distribution such as EL7 and EL8. -* simpkv's file plugin is only guaranteed to work on local filesystems. It may not - work on shared filesystems, such as NFS. - * `simpkv` only supports the use of binary data for the value when that data is a Puppet `Binary`. It does not support binary data which is a sub-element of - a more complex value type (e.g. `Array[Binary]` or `Hash` that has a key or + a more complex value type (e.g. `Array[Binary]` or `Hash` that has a key or value that is a `Binary`). +* `simpkv` does not support custom data types that cannot be serialized to JSON + by the Ruby libraries provided by Puppet. + +* simpkv's file plugin is only guaranteed to work on local filesystems. It may not + work on shared filesystems, such as NFS. + +* SIMP does not yet manage the LDAP server configured for simpkv data. + + * See the `ldap_plugin` acceptance test suite for an example of how to set up + a simple 389-DS LDAP instance for simpkv data. + ## Plugin Development -### Plugin Loading - -Each plugin (store interface) is written in pure Ruby and, to prevent -cross-environment contamination, is implemented as an anonymous class -that is automatically loaded by the simpkv adapter with each Puppet compile. -You do not have to do anything special to have your plugin loaded, provided -you follow the instructions in the next section. - -### Implementing the Store Interface API - -To create your own plugin - -* Create a `lib/puppet_x/simpkv` directory within your store plugin module. -* Copy `lib/puppet_x/simpkv/plugin_template.rb` from the simpkv module into that - directory with a name `_plugin.rb`. For example, - `nfs_file_plugin.rb`. -* **READ** all the documentation in your plugin skeleton, paying close attention - the `IMPORTANT NOTES` discussion. -* Implement the body of each method as identified by a `FIXME`. Be sure to - conform to the API for the method. -* Write unit tests for your plugin, using the unit tests for simpkv's file - plugin, `spec/unit/puppet_x/simpkv/file_plugin_spec.rb` as an example. That - test shows you how to instantiate an object of your plugin for testing - purposes. -* Write acceptance tests for your plugin, using the acceptance tests for - simpkv's file plugin in `spec/acceptances/suites/default/`, - as an example. That test uses a test module, `spec/support/simpkv_test` - and a plugin-specific validator to exercise the the simpkv API and verify its - operation with the file plugin. -* Document your plugin's type and configuration parameters in the README.md for - your store plugin module. +Please see [simpkv Plugin Development Guide](docs/simpkv_plugin_development_guide.md) +for details on how to write your own custom plugin. ## simpkv Development @@ -522,28 +881,53 @@ Please read our [Contribution Guide] (https://simp.readthedocs.io/en/stable/cont Unit tests, written in ``rspec-puppet`` can be run by calling: ```shell +bundle install bundle exec rake spec ``` ### Acceptance tests -To run the system tests, you need [Vagrant](https://www.vagrantup.com/) installed. Then, run: +This module includes [Beaker](https://github.com/voxpupuli/beaker) acceptance +tests using the SIMP [Beaker Helpers](https://github.com/simp/rubygem-simp-beaker-helpers). +By default the tests use [Vagrant](https://www.vagrantup.com/) with +[VirtualBox](https://www.virtualbox.org) as a back-end; Vagrant and VirtualBox +must both be installed to run these tests without modification. + +To execute all of the tests run the following: ```shell +bundle install bundle exec rake beaker:suites ``` -Some environment variables may be useful: +To execute an individual suite for a specific nodeset, add the suite and the +nodeset to the `beaker:suites` task. For example, + +```shell +bundle exec rake beaker:suites[ldap_plugin,default] +``` + +Some environment variables that may be useful: ```shell BEAKER_debug=true -BEAKER_provision=no BEAKER_destroy=no -BEAKER_use_fixtures_dir_for_modules=yes +BEAKER_fips=yes +BEAKER_provision=no ``` -* `BEAKER_debug`: show the commands being run on the STU and their output. -* `BEAKER_destroy=no`: prevent the machine destruction after the tests finish so you can inspect the state. -* `BEAKER_provision=no`: prevent the machine from being recreated. This can save a lot of time while you're writing the tests. -* `BEAKER_use_fixtures_dir_for_modules=yes`: cause all module dependencies to be loaded from the `spec/fixtures/modules` directory, based on the contents of `.fixtures.yml`. The contents of this directory are usually populated by `bundle exec rake spec_prep`. This can be used to run acceptance tests to run on isolated networks. +* `BEAKER_debug`: Show the commands being run on the virtual machines and their + output. +* `BEAKER_destroy=no`: Prevent the virtual machine destruction after the tests + finish so you can inspect the state. +* `BEAKER_fips=yes`: Enable FIPS-mode on the virtual machines. + + * This can take a long time, because it must enable FIPS in the kernel + command-line, rebuild the initramfs, then reboot. + +* `BEAKER_provision=no`: Prevent the virtual machine from being recreated. + + * This can save a lot of time while you're writing the tests. +Please refer to the [SIMP Beaker Helpers documentation](https://github.com/simp/rubygem-simp-beaker-helpers/blob/master/README.md) +for more information. diff --git a/docs/Design_Prototype2.md b/docs/Design_Prototype2.md index 2c6b38e..b6ba8d6 100644 --- a/docs/Design_Prototype2.md +++ b/docs/Design_Prototype2.md @@ -17,6 +17,7 @@ * [Rollout Considerations](#rollout-considerations) * [Design](#design) + * [Changes from Version 0.7.X](#changes-from-version-0.7.x) * [Changes from Version 0.6.X](#changes-from-version-0.6.x) * [simpkv Configuration](#simpkv-configuration) @@ -293,14 +294,17 @@ general requirements: * Getting this to work on specific shared file system types is deferred to a future requirement. -* simpkv may provide a Consul-based plugin +* simpkv must provide a plugin to interface with a high-availability, distributed + key/store + + * The first high-availability, distributed key/store to which simpkv will + interface will be LDAP. ### Future Requirements This is a placeholder for miscellaneous, additional simpkv requirements to be addressed, once it moves beyond the prototype stage. -* simpkv must provide a plugin for a remote key/value store, such as LDAP * simpkv must support audit operations on a key/value store * Auditing information to be provided must include: @@ -399,6 +403,48 @@ This section discusses at a high level the design to meet the second prototype requirements. For indepth understanding of the design, please refer to the prototype software and is tests. +### Changes from Version 0.7.X + +This section lists the changes that have been made to the simpkv function and +plugin APIs to address deficiencies found when developing the LDAP plugin. + +#### simpkv function API changes + +* The confusing 'environment' backend option in each simpkv Puppet + function has been replaced with a 'global' Boolean option. + + * Global keys are now specified by setting 'global' to true in lieu of + setting 'environment' to ''. + +* The key and folder name specification now restricts the allowed letter + characters to lowercase. + + +#### plugin API changes + +* 'globals' and 'environments' root directories have been added for global + and Puppet-environment keys, respectively, in the normalized key paths + in the backend. + + * This change makes the top-level organization of keys in the backend + explicit, and thus more understandable. + * The prefix used for global keys was changed from `` to + `/globals`. + * The prefix used for environment keys was changed from + `/` to + `/environments/`. + +* Plugin configuration has been split out into its own method, instead of being + done in the plugin constructor. + + * This minimal change allows the use of mock objects in the unit tests for + complex plugins, such as those that require connections to external servers. + +* Fixed the mechanism a plugin uses to advertise its type. + + * Plugin type is now determined from its filename. + * Previous mechanism did not work when when multiple plugins were used. + ### Changes from Version 0.6.X Major design/API changes since version 0.6.X are as follows: @@ -562,19 +608,10 @@ are specified and mapped to different application identifiers. type: file root_path: "/some/other/path" - simpkv::backend::consul: - id: consul - type: consul - - request_timeout_seconds: 15 - num_retries: 1 - uris: - - "consul+ssl+verify://1.2.3.4:8501/puppet" - - "consul+ssl+verify://1.2.3.5:8501/puppet" - auth: - ca_file: "/path/to/ca.crt" - cert_file: "/path/to/server.crt" - key_file: "/path/to/server.key" + simpkv::backend::ldap: + id: ldap + type: ldap + ldap_uri: ldapi://%2fvar%2frun%2fslapd-simp_data.socket simpkv::options: # global options @@ -592,10 +629,10 @@ are specified and mapped to different application identifiers. "myapp": "%{alias('simpkv::backend::file')}" # backend for all yourapp* applications - "yourapp": "%{alias('simpkv::backend::consul')}" + "yourapp": "%{alias('simpkv::backend::ldap')}" # required default backend - "default": "%{alias('simpkv::backend::consul')}" + "default": "%{alias('simpkv::backend::ldap')}" ``` diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv.md b/docs/Use_of_LDAP_as_backend_for_simpkv.md new file mode 100644 index 0000000..019ce03 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv.md @@ -0,0 +1,549 @@ +The text below is a copy of the SIMP Project Confluence page of the same title +(created by Liz Nemsick and published on May 11, 2021). It has been preserved +here, so that it is available for interested users/developers who do not access +to that page. + +# Use of LDAP as backend for simpkv + +This page documents how 389DS can be used as a LDAP key/value store for simpkv. +It provides a basic overview of simpkv and then discusses Directory Information +Trees (DITs) that can be used to organize the data, an Object Identifier (OID) +tree and custom schemas to support those DITs, and the technology choices for +the implementation of the simpkv plugin interface to an 389DS instance. + +#### Table of Contents + + + +* [simpkv Overview](#simpkv-overview) + * [Operations supported](#operations-supported) + * [Backend logical structure](#backend-logical-structure) + * [Backend selection](#backend-selection) + * [simpkv plugin internals (10,000 foot view)](#simpkv-plugin-internals-10000-foot-view) + * [Value normalization](#value-normalization) +* [LDAP Directory Information Tree design](#ldap-directory-information-tree-design) + * [Requirements](#requirements) + * [Design Considerations](#design-considerations) + * [Root Tree](#root-tree) + * [simpkv Subtree Option 1](#simpkv-subtree-option-1) + * [simpkv Subtree Option 2](#simpkv-subtree-option-2) + * [Recommendation](#recommendation) +* [OID Subtree Design and Custom LDAP Schema](#oid-subtree-design-and-custom-ldap-schema) + * [SIMP OID Subtree](#simp-oid-subtree) + * [LDAP Custom Schema](#ldap-custom-schema) + * [simpkv DIT Option 1](#simpkv-dit-options-1) + * [simpkv DIT Option 2](#simpkv-dit-options-2) +* [Technologies for Plugin Implementation](#technologies-for-plugin-implementation) + * [Requirements](#requirements-1) + * [Options Considered](#options-considered) + * [Recommendation](#recommendaiton-1) + +## simpkv Overview + +The simp/simpkv module provides a library that allows Puppet to access one or +more key/value stores (aka backends), each of which, can be used to store +global keys and keys specific to Puppet environments. This section will +present an overview of simpkv. Please refer to the module +[design documentation](Design_Prototype2.md), [README](../README.md), and +[library documentation](../REFERENCE.md) for more details. + +### Operations supported + +The operations simpkv supports are as follows: + +| Function Name | Description | +| -------------------- | ----------------------------------------------------- | +| `simpkv::delete` | Deletes a key from a backend. | +| `simpkv::deletetree` | Deletes an entire folder from a backend. | +| `simpkv::exists` | Returns whether a key or key folder exists in a backend. | +| `simpkv::get` | Retrieves a key’s value and any user-provided metadata from a backend. | +| `simpkv::list` | Returns a listing of all keys and sub-folders in a folder in a backend. The list operation does *not* recurse through any sub-folders. Only information about the specified key folder is returned. | +| `simpkv::put` | Sets a key’s value and optional, user-provided metadata in a backend. | + +### Backend logical structure + +Logically, keys for a specific backend are organized within global and environment +directory trees below that backend's root directory. You can visualize this tree +as a filesystem in which a leaf node is a file named for the key and whose +contents contains the value for that key. For example, + +![simpkv Key/Value Tree](Use_of_LDAP_as_backend_for_simpkv/simpkv%20Key_Value%20Tree.png) + + +To facilitate implementations of this tree, key and folder names are restricted +to sequences of alphanumeric, `'.'`, `'_'`, `'-'`, and `'/'` characters, where + `'/'` is used as the path separator. Furthermore, when a folder or key +pecification contains a path, the path cannot contain relative path subsequences + (e.g., `'/./'` or `'/../'`). + +### Backend selection + +simpkv allows the user to select and configure one or more backends to be used +when `simpkv::*` functions are called in Puppet manifests during catalog +compilation. The configuration is largely made via hieradata. + +* Each backend has its own configuration. +* Each backend configuration block must specify simpkv plugin type (e.g., + ‘file’, ‘ldap’) and a user-provided instance identifier. + + * A plugin is a backend interface that actually affects the keystore operation + when a `simpkv::*` function is called during a Puppet catalog compilation. + For the ‘ldap' plugin, this will be the software that modifies key/value + pairs stored in an LDAP server. + * The same plugin can be used for multiple backend instances. + * The combination of plugin type and instance identifier uniquely identifies + a backend instance. + +* Each backend configuration block may specify additional, plugin-specific + configuration (such as LDAP server URL and port, TLS configuration,...). + +### simpkv plugin internals (10,000 foot view) + +Internally, simpkv constructs a plugin object for each unique backend, and uses +the plugin object to interface with it corresponding backend. When a +`simpkv::*` function is called, an internal adapter calls the plugin’s +corresponding API method with normalized arguments to affect the operation. +The adapter then (de)normalizes the results of the operation and reports them +back to the calling `simpkv::*` function. + +For example, for a `simpkv::put` operation using a LDAP plugin, the sequence of +operations is notionally as follows: + +![simpkv Store Operation for Non-global Key](Use_of_LDAP_as_backend_for_simpkv/simpkv%20store%20operation.png) + +Then, for a `simpkv::get` operation using a LDAP plugin, the sequence of operations is notionally as follows: + +![simpkv Retrieve Operation for Non-global Key](Use_of_LDAP_as_backend_for_simpkv/simpkv%20retrieve%20operation.png) + +#### Value normalization + +One of the normalizations done by the simpkv adapter involves the value and +optional, user-provided metadata associated with a key. In a `simpkv::put` +operation, the simpkv adapter serializes a key’s value and optional metadata +into a single JSON string and then sends that to the plugin for storage in the +backend. Then, after a key’s information has been retrieved by a plugin during +a `simpkv::get` or `simpkv::list` operation, the simpkv adapter deserializes +each JSON string back into the key’s value and metadata objects before serving +the results back to the calling function. This encoding of a key’s value an +metadata into a single string with a known, parsable format is intended to +simplify backend operations. + +The table below shows a few examples of the serialization for clarification. + + + +| Value Type | Serialization Example | +| ---------- | --------------------- | +| Basic value[1](#footnote1) without metadata | `{"value":"the value","metadata":{}}`

`{"value":10,"metadata":{}}` | +| Basic value with user-provided metadata[3](#footnote3) | `{"value":true,"metadata":{"optional":"user","extra":"data"}}` | +| Complex value[2](#footnote2) with basic sub-elements with no user-provided metadata | `{"value":[1,2,3],"metadata":{}` | +| Binary value[4](#footnote4) transformed by simpkv with no user-provided metadata | `{"value":"","encoding":"base64","original_encoding":"ASCII-8BIT","metadata":{}"}` | + +1: *Basic value* refers to a string, boolean, or numeric +value.[↵](#origin1) + +2: *Complex value* refers to an array or hash constructed +from basic values.[↵](#origin2) + +3: simpkv currently only supports metadata hashes +comprised of basic values.[↵](#origin3) + +4: simpkv currently provides limited support for binary +data. + * simpkv attempts to detect when the value is Puppet Binary type, transforms it into Base64 and records the transformation with ‘encoding' and 'original_encoding' attributes in the JSON. It then uses those attributes to properly deserialize back to the binary on a retrieval operation. + + * simpkv does does not support binary data in arrays, hashes, or the metadata. + [↵](#origin4) + +## LDAP Directory Information Tree design + +### Requirements + +* There must be one LDAP backend DIT for all SIMP application data. + + * This is distinct from the DIT containing user accounts data. + * Data to be stored must include simpkv data. + + * Data to be stored may in the future include other application data, (e.g., + IP firewall data). + +* The simpkv data must be a subtree of the DIT. +* The simpkv subtree must support partitioning the data into LDAP backend + instances. +* The simpkv subtree must allow storage of per-LDAP-backend-instance global and + environment-specific key/value entries. + + * Entries may be stored in subtrees within the LDAP instance subtree. + * Each key/value entry must be a leaf node in the LDAP instance subtree. + * The Distinguished Name (DN) to each key/value entry throughout the entire + DIT must be unique. + +* The JSON value of the key/value entry must be stored in some form in the + key/value entry. + + * The key/value entry may have a single attribute containing the JSON-encoded + value. + + * The key/value entry may have multiple attributes that map to the value’s + JSON attributes. + +* The tree must support efficient `simpkv::get`, `simpkv::exists`, and + `simpkv::list` operations. + + * Folder and/or key objects may store data in attributes to leverage LDAP + search capabilities. + + * The simpkv LDAP plugin should not have to retrieve the entire tree or + subtree in order to fulfill any of these operations. + +* Any custom schema attributeType or objectClass will be specified with an + Object Identifier (OID) below the official + [SIMP Object Identifier (OID)](http://www.oid-info.com/get/1.3.6.1.4.1.47012). + +### Design Considerations + +At first blush, the mapping of the logical simpkv tree structure into a LDAP +DIT appears to be straight forward, because LDAP is fundamentally a tree whose +leaf nodes hold data. For example, we could design a tree as follows: + +* Use Organizations or Organizational Units to represent folders in a key path + and other grouping (e.g., environments). + +* Create a custom schema element with key name and value attributes to + represent a key/value entry. + +* Construct the DN for a key/value node using each part of the key path as a + relative DN (RDN). + +So, for a key path `production/app1/key1` the key/value pair could be found at +the DN `simpkvKey=key1,ou=app1,ou=production,ou=environments,`, where `simpkvKey` is an attribute of a `simpkvEntry` LDAP +object used to store the key/value pair. Visually, this subtree in the DIT +would look something like the following: + +![LDAP DIT snippet](Use_of_LDAP_as_backend_for_simpkv/LDAP%20DIT%20snippet.png) + +Unfortunately, there is a nuance in 389DS that complicates that simple mapping: + +**__389DS instances treat DNs as case invariant strings.__** + +So, the key paths `production/app1/key1` and `production/App1/Key1` both +resolve to the same DN inside of 389DS, even though from simpkv’s perspective, +they were intended to be distinct. This unexpected collision in the backend +needs to be addressed either by simpkv or within the DIT itself. + +### Root Tree + +The proposed root tree to hold all SIMP data in LDAP is as follows: + +![LDAP DIT root](Use_of_LDAP_as_backend_for_simpkv/LDAP%20DIT%20root.png) + +This trivial root tree can be expanded in the future to hold data for other +Puppet applications or even site-specific data not associated with Puppet, +if necessary. + +### simpkv Subtree Option 1 + +The simplest design option enforces DN case invariance by requiring all the +values of all attributes used in a DN for a key/value pair to be lowercase. In +other words, change the experimental simpkv API to only allow lowercase letters, +numerals, and `'.'`, `'_'`, `'-'`, and `'/'` characters for all key names, +folder names, and plugin instance identifiers. Then, because each key’s DN is +unique and case invariant, the simple mapping scheme described in +[Design Considerations](#design-considerations) can be used. + +With this simple mapping, the proposed simpkv LDAP subtree will look nearly +like that of the logical key/value tree. It just inserts a few extra "folders" +into the tree in order to clarify the roles of the nodes beneath it. The new +"folders" are + +* 'instances' under which you will find an individual subtree for each backend + instance + +* 'globals' under which you will find a subtree for global keys for a backend + instance + +* 'environments' under which you will find individual subtrees for each Puppet + environment for a backend instance. + +Below is an example of the Option 1 DIT in which simpkvEntry is a custom LDAP +object class with `simpkvKey` and `simpkvJsonValue` attributes holding the key +and value, respectively: + +![Option 1 LDAP DIT](Use_of_LDAP_as_backend_for_simpkv/Option%201%20LDAP%20DIT.png) + +### simpkv Subtree Option 2 + +The second design option enforces DN case invariance without impacting the +existing simpkv API. Its simpkv subtree has the same essential layout as that of +Option 1, including the use of the 'instances', 'globals', and .environments' +grouping "folders". However, in this design + +* The LDAP plugin transforms any problematic attributes that are to be used in a + DN for a key/value pair to an encoded representation (e.g., hexadecimal, + Base 64) . For example, with a hexadecimal transformation, all backend + instance identifiers, key names, and folder names would be represented in hex, + minus the '0x' or '0X' preface. (The Puppet environment does not require + transformation, as Puppet environment names must be lowercase.) So, key paths + `production/app1/key1` and `production/App1/Key1` would be mapped to + `simpkvHexId=61707031,simpkvHexId=6b657931,ou=production,ou=environments,...` + and + `simpkvHexId=41707031,simpkvHexId=4b657931,ou=production,ou=environments,...` + respectively, where `simpkvHexId` is an attribute of both an LDAP object used + to represent backend identifiers/folders and an LDAP object used to store the + key/value pair. + +* Each node with an encoded identifier RDN includes an attribute with the raw + identifier. Although this means a little more data must be stored in the DIT, + this extra information will support external searches of the LDAP tree using + the raw backend instance identifiers, key names, and folder names. In other + words, users can search the LDAP tree without being forced to mimic the + transformations done in `simpkv::*` functions. + +Below is an example of the Option 2 DIT in which + +* `simpkvFolder` is a custom LDAP object class with `simpkvHexId` and `simpkvId` + attributes holding the transformed backend identifier/folder and raw + identifier/folder, respectively + +* `simpkvEntry` is a custom LDAP object class with `simpkvHexId`, `simpkvId` + and `simpkvJsonValue` attributes holding the transformed key, raw key and + JSON-formatted value, respectively. + +![Option 2 LDAP DIT](Use_of_LDAP_as_backend_for_simpkv/Option%202%20LDAP%20DIT.png) + +### Recommendation + +Option 1 is the recommended solution for the following reasons: + +* It yields a DIT that is simple to understand and navigate. +* An API change is not unexpected for `simp/simpkv`, since it is still + experimental (version < 1.0.0) and not enabled by default. +* SIMP can help users with the transition to lowercase key names for any + existing simpkv key paths or `simplib::passgen` password names (whether using + legacy mode or simpkv mode). + + * Any SIMP-provided module that uses simplib::passgen can be modified to + ensure the password names are downcased. + * The `simplib::passgen` function that uses simpkv can be modified to downcase + existing password names that have any uppercase letters and then to emit a + warning. + * The script SIMP will provide to import any existing simpkv key entries or + `simplib::passgen` passwords into a simpkv LDAP backend can check for + uppercase letters in the destination key paths and either skip the import + of the problematic entries, or convert to lowercase and warn the user of + the conversion. Then, it would be up to the user to make any adjustments to + the corresponding manifests. + +## OID Subtree Design and Custom LDAP Schema + +Either option for the LDAP DIT for SIMP data requires at least one custom LDAP +object class. The LDAP object class, in turn, must be specified by a unique OID. +This section proposes a SIMP OID subtree design to support LDAP OIDs and then +uses the OIDs in schemas for the two DIT options discussed above. + +### SIMP OID Subtree + +SIMP has an officially registered OID, 1.3.6.1.4.1.47012, under which all OIDs +for Puppet, SNMP, etc should reside. Once an OID is in use, its definition is +not supposed to change. In other words, an OID can be deprecated, but not +removed or reassigned a different name. So, the OID tree must be designed to +allow future expansion. + +Below is the proposed SIMP OID subtree showing the parent OIDs for attributes +and class objects needed for the SIMP DIT. + +![SIMP OID Tree](Use_of_LDAP_as_backend_for_simpkv/SIMP%20OID%20Tree.png) + +### LDAP Custom Schema + +#### simpkv DIT Option 1 + +The proposed custom schema for the simpkv DIT option 1 is shown below. It has a +custom object class, `simpkvEntry`, that is comprised of two custom attributes, +`simpkvKey` and `simpkvJsonValue`. + +* `simpkvKey` is a case-invariant string for the key (excluding path) + + * This is used as the final RDN of the DN for a key/value node. + +* `simpkvJsonValue` is a case-sensitive string for the JSON-formatted value. + + * In the future, we could write a custom syntax validator for this attribute. + +``` +################################################################################ +# +dn: cn=schema +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.1 + NAME 'simpkvKey' + DESC 'key' + SUP name + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.2 + NAME 'simpkvJsonValue' + DESC 'JSON-formatted value' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +objectClasses: ( + 1.3.6.1.4.1.47012.1.1.1.1.2.1 + NAME 'simpkvEntry' + DESC 'simpkv entry' + SUP top + STRUCTURAL + MUST ( simpkvKey $ simpkvJsonValue ) + X-ORIGIN 'SIMP simpkv' + ) +``` + +The corresponding SIMP OID subtree is as follows: + +![SIMP OID subtree option 1](Use_of_LDAP_as_backend_for_simpkv/SIMP%20OID%20subtree%20option%201.png) + +#### simpkv DIT Option 2 + +The proposed custom schema for the simpkv DIT option 2 is shown below. It has +two custom object classes and three custom attributes. + +* Classes: + + * `simpkvFolder` is an object class for a node representing a backend + identifier or folder. + * `simpkvEntry` is an object class for a key/value node. + +* Attributes: + + * `simpkvHexId` is an attribute that is a case-invariant, hex-encoded string + for the backend identifier, folder or key (excluding path) + + * This is used as the final RDN of the DN for a node. + * In the future, we could write a custom syntax validator for this + attribute. + + * `simpkvId` is an attribute that is the raw, case-sensitive string for a + backend identifier, folder or key (excluding path) + + * `simpkvJsonValue` is an attribute that is a case-sensitive string for a + JSON-formatted value in a key/value node. + + * In the future, we could write a custom syntax validator for this + attribute. + +``` +################################################################################ +# +dn: cn=schema +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.1 + NAME 'simpkvHexId' + DESC 'hex-encoded backend instance, folder, or key name' + SUP name + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.2 + NAME 'simpkvId' + DESC 'backend instance, key or folder name' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.3 + NAME 'simpkvJsonValue' + DESC 'JSON-formatted value' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +objectClasses: ( + 1.3.6.1.4.1.47012.1.1.1.1.2.1 + NAME 'simpkvEntry' + DESC 'simpkv entry' + SUP top + STRUCTURAL + MUST ( simpkvHexId $ simpkvId $ simpkvJsonValue ) + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +objectClasses: ( + 1.3.6.1.4.1.47012.1.1.1.1.2.2 + NAME 'simpkvFolder' + DESC 'simpkv folder in which simpKvHexId represents the relative folder name in hex in the DN' + SUP top + STRUCTURAL + MUST ( simpkvHexId $ simpkvId ) + X-ORIGIN 'SIMP simpkv' + ) +``` + +The corresponding SIMP OID subtree is as follows: + +![SIMP OID subtree option 2](Use_of_LDAP_as_backend_for_simpkv/SIMP%20OID%20subtree%20option%202.png) + + +## Technologies for Plugin Implementation + +### Requirements + +* Plugins are written in Ruby and implement the simpkv plugin API. +* Plugins must be multi-thread safe. +* Plugins must be written to provide Puppet-environment isolation when executed + on the puppetserver. +* Manifests that use `simpkv::*` functions must be able to be compiled with + puppet agent, puppet apply or Bolt commands. This means the plugin code will + run in JRuby in the puppetserver, run in the Ruby installed with puppet-agent, + or run using the Bolt user’s Ruby into which the puppet gem is installed. + +### Options Considered + +| Option | PROs | CONs | +| ------- | ---- | ---- | +| Tools provided by openldap-utils RPM |
  • Existing, signed, vendor RPM.
  • Package will already be installed on host operating as the simpkv LDAP server.
  • Supports ldapi interface, which is faster than ldap/ldaps, while still being secure.
|
  • Requires openldap-utils RPM to be installed on host executing Bolt compiles.
  • To take advantage of ldapi either have to educate user on when ldapi should be configured OR create internal auto-ldapi-detection logic to use the ldapi interface when it is available <--> complexity.
| +| net-ldap Ruby gem | User can install gem without sysadmin support, when not on isolated network. |
  • Requires gem RPM packaging for use on isolated networks (e.g., simp-vendored-net-ldap RPM)
  • Requires gem installation into the puppetserver
  • Does not support ldapi.
| +| Support both tools provided by openldap-utils and net-ldap Ruby gem, using whichever it discovers is available | More installation flexibility when not on isolated networks. |
  • Increased code+test complexity.
  • Still has gem packaging issues on isolated systems for Bolt users.
  • User still needs to know when ldapi can be used, unless auto-discovery mechanism is built.
| + +### Recommendation + +Option 1 without the auto-discovery mechanism is recommended for the following reasons: + +* Options 2 and 3 require additional packaging in order to work on isolated + networks for Bolt users. So, if you are going to require a Bolt user to + install a package, anyways, might as well be an existing vendor package. +* The auto-discovery mechanism can be added after the initial implementation, + because it is not required for the LDAP plugin to function. diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT root.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT root.drawio new file mode 100644 index 0000000..2fd3ae1 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT root.drawio @@ -0,0 +1 @@ +5Vptc9o4EP41fISRLNvgjwkk7d2lvbRp75pPHYEVcDGWT5YD9NefZMvYsozjACGZCZkJ1uoN7T6PdldyD41Xmw8Mx4tP1CdhzwL+pocmPcuCYGSJLynZ5hJniHLBnAW+alQK7oLfRBemgU8STcQpDXkQ68IZjSIy45oMM0bXerMHGuqTxnhODMHdDIeFdOCU8n8Dny+UHLpeWfGRBPOFmnxkDfOKKZ4t54ymkZqxZ6GH7JNXr3AxFsgFyQL7dF0RoaseGjNKef602oxJKJVb6C3vd72ndrceRiLepQP+vLhcrZf3fvwlTD+vg+HfH6/6apRHHKakWEb2Y/m2UJEYRVhDFC7Xi4CTuxjPZM1a4EHIFnwVihIUjw9BGI5pSJkoRzSSPRLO6JIUQqEhL/uIGkY55gGNhLhvAyFQP4QwTjZ7Vwh3ehOAJHRFONuKJqoDsuFgqKyzLexoDzw7F60r5vXcXLaoWBYC0VbJscLVfDdJqVnxoJS7x27k65ebD98n1389er/u+eSf6+Wyj1xTsb6AoipSxhd0TiMcXpXSywxdRA4rNVS2uaE0Vkr/RTjfKlrhlFPdJGQT8MlG9c8KW1WIaGWirG05vBLsUb9YA03ZjLQsFipScczmhLc1VFqRquhiYct2BwA4moldtN9k2UAXjOFtpUFMg4gnlXlupUCHUX2SEdK51aELhKiGmvyHlBjare9wWMGRQWCaiu8kWMXLRwNyz2CwIOs1GI9d0IYEg677LYcaNKS0WmXmCJjEROClSDlsJSUJp3Rd5eNJGSh0ybZlVVYq6gwuljsBLBr/kG2F+1LF+8oclSpRui96yeWIZ2m0YCaHPobibleKjzpS/Bjr/nlHFhsGkvnP+c/g+8X0R+pM+qiZGkH8EDCyxnKAN0MPNHxdejQqcPQ8dugYPSlXDD7sDyx8nCy6EaWFf69IojYoH8ShZo/VR547cC0dcDbUkZQvQPVscX8OQIPRSB9raOlj5Ws0xjqZI3QMtvsz5QjfEs+ByXP3lb1ggcLqRim+4jSOCX/juoPW+ZTn+j9uUncaLrffpl+36Kp/t076lqG7nuWGXGqCZhQpVef+l9Kiop9km+CFaACdeFNWiqe5/L6ZXNyKyq8yPbTAN0ZIlt4y8f/uj0+yaoI5LuYSPz2fLu9sGE1omOt20jdQla41ZHA4DOYySZsJuxEhvyw2vgtVsQp8P9yXHeoJjNzx83W35xfdIQE9VHOZBhoszzPhYL0UHEwqjWn0EMxThqdCS5aLV1Iv0TSJ35OdbEf3Dg40WWuZZoLeC5nJa2BtPdSJ/At5wCR1GuIkCWa1YKUILXaFez3O2BNaGJHLCMg/2e7J84BZyh7L2KaIUSoRShmvNMdRnUKVNmA/He47HUOViumdhg27kDESYh486od43cMcuxaWeM5hEQ4qdpFinNoGcrroplH7EBiAvVXuGeA4DgXRs8M0C/iZV3gn2wpEdi0acIx9BaKmo76X2ligGQ8oF5777uJc5p3ZCdbcdJEcVOxUiLSo7cXMZPrpwx0A0B3A8OweAJ7FAxR70OkOdc/lAlwdfMWm8VwfYANP5h+g+EB9WGd3l3QupzB8GsQ6Xp5AdAswD0Da02A+AovWm4LYzu8oLFjOgRCzUA1U3vC8kGoKjPMU0w+k2yr8TJmwhuSBm+nrLRXwyjIeQDYxjpIsOslHmrIyT92lr9nw7yx7tevZq2ceZkBonzF8scw489l+8XTurTy8HQFPd7IjoaonjnBl6ZawQGhGmv5Y7+eZ3q/tRv2N7EyOU4dYLXHp7Pwcp8X5WQC9nPNrVLN5ufNsoGo3BfsvFFruAjr5PuOepOEK4dB7ikasne7OvlHz9qviueZpUX2IzlcWth4VerUtdQ98n/sugW3X+AesGhuOeylgv4WM8/yI8DVlS4MmRx3oH/W+jm05A88724G+KJZvW+W6Lt9pQ1f/Aw== \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT root.png b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT root.png new file mode 100644 index 0000000..186f1c9 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT root.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT snippet.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT snippet.drawio new file mode 100644 index 0000000..90e1097 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT snippet.drawio @@ -0,0 +1 @@ +7Vjbcts2EP0aPtbDu6THkLLdaZRMYk2T+hEiIRIWCDAgqEu+vgsQFElRlp2pk6oz1fBBe3YBELvnLCBZXlzs7wUq8w88xdRy7XRveXPLdR3fdS312OmhQSZ20ACZIKkJ6oAl+Y6HYE1SXA0gyTmVpByCCWcMJ3KAISH4bhi25nS4aIkyPAKWCaItehN0+FeSytzgTjjrHL9jkuVm8ak7aRwrlGwywWtmVrRcb60/jbtA7Vx2A1Q5SvmuB3m3lhcLzmXzrdjHmKrktnlrxt094z3uR2AmXzPAxg+fF/d/zu/eb2dPj3L+5W6z+c3MskW0NhmpSFFutu+xqucGH0xAJQ9tyqodKShiYEVrzuTSeGywk5zQdIEOvFZvVEnIUGtFORfkO8QjCi4HAHALaQjhhoOIpRpp5hS4gphP7TadE+gD2g8CF6iS7dtwSlFZkZV+PzWwQCIjLOJS8sIErQmlMadc6M15vj8LdOg4u22qsJB434NMtu8xL7AUBwgx3rbwRhmeM23sXY9mgZk17zPMEAwZbmfHmY+LPYAWEMtgY8fV/OlgNSf07PFyzmS8HOR+sByiEguGJI4Ut6s+p+BLb6MdpJn2A6xzR6zjqyfYUkxRBW/xTneB0lJv5jmqSiN3w9JbptNyDOtxVwU19D3x/lFx9qVZWMWA/1utJBhZk+jm5saazHtQN3gkAyCB1CwWfIN7DPJCb+alY2YlCQ6gN3gRoiRjgFG8VhMoPhFoSO8MrDYO05YoISxb6Ji53yEPpnAK4jB2TXVPyUmaYqZUwCWSaHWUZMkJk7qyQQQP7De2oecF8Eox2E5nw6PChYw5g10hoomPQU87rDQVoVryqtHreYVc7DAvy+Yw5ONLMgmd52UyIOyPstO3R8XGKZwaxoQM5TzjDNHbDo30QYBTk/UuZsFVPXXCnrCUB9PwVC5Vy5NF2w7xnsi/1HCoRmM99jzztstp42CMZ5tUxWuR4EsKNBKEHpzhS+Vz/SZQZeBi9QSmSJLt8LR989q449YBIkWF0gyVug6gXNeef9S3AaG2mGNzWmM4qmF+YDdikJ12XGakrsevYEhokBMS7HIi8RJkqOwdXIeGBTzR+50dx+HFIr3+JPGP15S2u9vuWCR22Eb1ZeKHP6kU/vhqcKUyweqY6A1S5mPf1w3T1hvIy3+tvMLrkpc/PplrdQqwLRGcFVidJVekC/tUF+GZO5Zjj1XRXo7eXhXu/6p4iewvq2J6XaoIz6uiFDytE0k4uyJNBKeacM/87pj9Ukl4/xVJ/ANqT19Jbee6mD09z2xUluPT/V/kdHDK6TN9fvo2nAaz+0+m+cHb/fPl3f4N \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT snippet.png b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT snippet.png new file mode 100644 index 0000000..d428195 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/LDAP DIT snippet.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/Option 1 LDAP DIT.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 1 LDAP DIT.drawio new file mode 100644 index 0000000..27d4bbb --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 1 LDAP DIT.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/Option 1 LDAP DIT.png b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 1 LDAP DIT.png new file mode 100644 index 0000000..6afbf96 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 1 LDAP DIT.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/Option 2 LDAP DIT.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 2 LDAP DIT.drawio new file mode 100644 index 0000000..630cf5a --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 2 LDAP DIT.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/Option 2 LDAP DIT.png b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 2 LDAP DIT.png new file mode 100644 index 0000000..9c31ed4 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/Option 2 LDAP DIT.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID Tree.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID Tree.drawio new file mode 100644 index 0000000..30c114e --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID Tree.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID Tree.png b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID Tree.png new file mode 100644 index 0000000..d9d9413 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID Tree.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 1.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 1.drawio new file mode 100644 index 0000000..25eb3d9 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 1.drawio @@ -0,0 +1 @@ +3VrZcqM4FP0aV2UeTElICHhM0kmmZyY1qXg66X7qwka2SWNEYXnrrx9hJAMWNniNE78YXUks55y7SNBCt6P5Q+LFw0fm07BlAn/eQl9apglNDMVfallkFmK6mWGQBL4clBs6wW9aNk4Cn45LJs5YyIO4bOyxKKI9XrJ5ScJm5WF9FpYvGnsDqhk6PS/Ura+Bz4eZ1bFAbv+TBoOhujIEsmfkqcHSMB56PpsVTOiuhW4Txnh2NJrf0jDFTsGSzbvf0Lu6sYRGvMkE+37Uf/3mPzx2RvOnn3z6/NN/bSMzO83UCyfyiTtfH5/kHfOFgmE2DDjtxF4vbc8E0y10M+SjULSgOOwHYXjLQpYsR6N+v2/2esI+5gn7tQIuHTmlCQ8EvNdhMIiErcs4Z6P0FCzikn3orKYWTuqTLrGI6NEfXGKRnpvOCyYJxANlI8qThRiieoEUoRQlAhKHWc4xBiizDQv8YmxJbUldDVbnzqEXBxL9HZgwicaEx3kSdCec/reI6RX840BWfIs6Pq6C1jG7iBwLWoeUoCXA0aBdOUkRWgROhaytIashSX3h9bLJEj5kAxZ54V1uvcmtEl06D/h3cQwMS7Z+FHq+pHAA1VioRiQeqDApbf4o9uXTli01L2GTyKe+bG1kacwmSY9ug0I+OfeSAeUN1Jji0oR1ZJdIb0PbPZzNZPr89rX/0v02uufPN7/j4eDle9vSuCuDk/P0D2OxZOSNcr6Q0cWbcFb2k31Z2UsBBaHRsMtmRY0tDaJDBckV12s07Ek+0cmvhNispjqhoceDaflGqviVU59YIG5xJRFThU4VGDApnyJ7ADlrTSWr29hfOGQPpz9cWmcMEseUitNQKuhAqRwW2B0tsPdCbzz+t/smSsEr82MkTOQ6xppnVKRM96wZ0z1NxiwI23GL0m4DAyBUF27T1hNNAvGQNDkgBp8/mzoXkE0r7wy7W4mNWHTCMAi2slRcEFQ6IVn+tjFYi/eBKc2yXMPFBLnAciEmsEwbxsRADiLAxcCxkIMbpbvjXiST5sly6nb1aPXNu+hoUzqFNeFmLLDj1+kWgjAs80rQU+b7IMyDmq8GSW8RFtkPjlnTVRKgtkSa1fNnSNSbq4RSOhkHo/jX9C4F/IwLWy0pV4C+MShjAAx7LVHbQEvUjqWy+bFTdSW0Dda2A+F28UYA5MaZ11XDwa7AmK69VsCYxDKQUCIhlg0IsPTNlcpyBgKD2LaJEDZd5GLnRDkPVu95XWEbwPepGbcyW19IrsrGEnyqZnivIhJiDWYvjkMRA3nAonfazjoO0hBalwW1VR8F6uv1iBVaS8S1Cn43QGsLaOWJtelLaalpAd1GaD0mtTFxq4LS8cnQN3JD34s/puBVDVqCEtqVgbqYBM+6WoUa4B9D/RX7cdUPCHZVv7W+pdDGwnQO9SvstVrvE+lfyPui9K+X13vo/6LfbxzkZ8p96v3M/EB+BjTS40kcU/6Z/My0LsnPYIO11uf1IlWC1ddqO746rKzVmntR9abZ2rsmaOFtS8vjvXmqRkT3VWgggxjQwAbUNLSbt167t/ju5vQ+WeN16656qnc+8CjJ7h2KvaZJSK2JDnIf4BgmcYBYp5o2sm3rCB+tVG8/6srW2Dj19hM0tfdnFdtyJjznrpy+IsmK4L/pBW13bmFUB/sMSXfztwhrAiOhuOqNH0zF4SA9zMD9a8yil3Tc8s1vNkhcszDuIwK/+mINapqGKicfyIRo5p8hZkkv/5YT3f0P \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 1.png b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 1.png new file mode 100644 index 0000000..d914325 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 1.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 2.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 2.drawio new file mode 100644 index 0000000..005cd48 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 2.drawio @@ -0,0 +1 @@ +3Vvfc6I6FP5rnOk+yBASfj3WVru9czu3d73bdu/LDkpUukgYjFb3r98giYJERAVs24cOOSQhfOc7J18SbMGb6fIucsLJA3Gx39JUd9mCty1Ns3SD/Y8Nq8SAIEoM48hzExPYGvreb5w1zj0XzzImSohPvTBrHJIgwEOasTlRRN6y1UbEzz40dMY4Z+gPHT9vffZcOhFvpW7tX7E3nognA5XfmTqiMjfMJo5L3lIm2G3Bm4gQmlxNlzfYj6ETsCTtenvubgYW4YCWaWD2pqPn7+7dQ3+6fPxJF99+us9tqCXdLBx/zt+4f//wyEdMVwKGt4lHcT90hnH5jTm6BTsTOvVZCbDLkef7N8Qn0bo2HI1G2nDI7DMakV8b4OKaCxxRj8F77XvjgNkGhFIyjbsgAeXeB9amaapT1xgYjEywk39xjkXcN16mTByIO0ymmEYrVoXfNZMGnJIa4pR823rYUPXENkl5VweQM4uzarzpeQs8u+DYy/0Q9UI4/vd29L/23O3ftxfR9cuojXKIY5fxkBexPyBv3a2hE5F54OK4Q5WVSEQnZEwCx/+bkJAD/YopXXFAnTklzLTuhhWFD7I+xEuPvsQ9Kjov/UjduV3yh60LK15IRh0PdS8puWlG5tEQFzBR43mCOtEY0wKooNyzEfYd6i2yAznHT4WjTMWLQ2nkDeYU/7cK8RX4cmbsuDq2XCQLAEsbQKOiAAAiK60E3a1cCGxSWToEoFoXsmYO2cKY2JI+HRhb6xmsZrBGq1SjuPgjfW/bbF0S7bJReUYoaPlQKGTjTgDu9TrM5r02MO0K8tni2+v96Gnwfdqj3zq/w8n46aWt53x3WspK56cTvXISA4qSrySNVpz2ZIhqJV1dOg/ypo/EY2PeUERDejYxICPbRfJGvNUOSzbDOJ04xglBfz61GkwSZ1DFKkkVWAtVrqPIWaUqhDEFZvuZhGwtyyRdz+rT3fpChcnrs4tkBCfTTSYmiueYinTXuUzL55vzyFeNXpOxUQbxRcinix5rIlMhJCkJM/Sd2eyfwStbml5pH0MaImQoO3OARBzajWpDux5t2KCiaF4bWu9AG0pHhuxC5wUkqDHVqoVeSm9CSAPNWP8VebBugabrtmIjA9qqbgNkgKzb4uCFFjRUG6mWDi1USrxV+5CEmrUpxGL2NDVlF/NoX2YBBzLLjGFHr+NtS2ZYzx3eUJh7nr9NXK6oxKOFWfj9PYqhVMaR4i12XStbjFa+UyMddonthDHjRlh+xtxsMjsD0YMqf0OxlrLNnZmU6RAFMvwMQzdVQ9W1cvMqUBXDNDUIkWZDG1k1JWYg3wy+QqYKLiNeCj17WNFs9EsGPjGxXUrNAJSD2QlDnwUq9UhwoR3EapAGQH9fUOtVCMeApEprxHNS8jhAD6o8EYkHk67gUlmV14ZwNye1kWHLklL1zsjvnfuuE35MwguhlIESmNJEnWK/1eiyCeQA/xjsl+yJyl9QPZb9upVjPzM1wX6BfcoZM28a/lp8Iv4zer8r/sMq+P+uj5TOijMRPofjTPtAcabmnB7OwxDTzxRnmv6e4gyUWGt93igSEuywVjvytFaq1cpH0Z5Tlh3FoqOipWV1h31yRPKxChSoGApQkAJyHDouWq/tG9Tt1B+TB6JuN1Tr+jIFVDLZXUDslZ2ExJrorPBRLUUzLJWtUzUTmiY/Far+e678UifnjNTuU+mtJslG1f7dp51DHFPN8RWCzTRd9UQh30rMx3sRLBmOFaF89k4dUJGSD2cNNApODptkfXDvXujksNiFh9NkA3Kk6EuZDMkMnz2143oLdjmOLxNs/5qR4CmudwW/iErsmal6HxF4fteGOUYDIVYqd8TeLzUljP6Kl4zUzclxSX4ol1qKOW03COVxpxm1zCdIVRXz8JwCbEPRmsyb+1jWjZcAl1r0FfnwuMyp1zZD7/1YRwJmj/gujhqciE6JWbM4HZqKRLLb1UQxK25/yZEsj7a/hoHdPw== \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 2.png b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 2.png new file mode 100644 index 0000000..d05254e Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/SIMP OID subtree option 2.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv Key_Value Tree.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv Key_Value Tree.drawio new file mode 100644 index 0000000..a2a077d --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv Key_Value Tree.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv Key_Value Tree.png b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv Key_Value Tree.png new file mode 100644 index 0000000..d9b6f21 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv Key_Value Tree.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv retrieve operation.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv retrieve operation.drawio new file mode 100644 index 0000000..a3f6d57 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv retrieve operation.drawio @@ -0,0 +1 @@ +7R1rc5u49td4svdDGL2Q4GPqNL3bzbbZm5m7t/3SITax2WBDMXn1118JEAZJYLDBcXeW6aQg0JE4Ou9zhCd4unr5kHjx8vdo7ocTBOYvE3w5QQgCCvl/ouU1b6EY5w2LJJgXD20bboMfvuxZtD4Gc39TezCNojAN4nrjLFqv/Vlaa/OSJHquP3YfhfVRY2/haw23My+UrZa9bf8zmKfLop0AsL3xbz9YLIvBHbu4cefNHhZJ9LguRpwgfJ8d+e2VJ2EVz2+W3jx6rjTh9xM8TaIozc9WL1M/FNiViMv7XTXcLd8n8ddplw6s6PHkhY/F22+CVfzwxNv+46dJ4D/5/PRz7CdeGkTrDJ0J//spWp8vwujOE9B+81+L10lfJRIzJPhiGDjB7+6jdXpb3MyugzCcRiEHJR7GV1eXLqW8fZMm0YMv76yjNe/wzntMo01OJaKvl8wKmuFYL0AXdwUIHQXyHf0k9V8qTQVKPvjRir8qfwVQ3CXMsVBBA6912nzekgNmRduyQglyYb2CBhcl8O0q8JNiIcyLQrQ1WXnr4N7fpFYcTxDlCHu5EG86RZML8MssWsVBmC3Pv7RlKAnMgPULNCVTtwnrVbySYfAKiWvV0Up1tEJg62i1B0Ar1dCqYUtB0HRKIKcDFUH8DqUAbIXBASjBmCkogbYJJzpKKBwAJ6yR/UsqC4O7xEt0Bj8pykIueDO6cnbTlT/nCqa4LJDgr+cXQldtW+beZlkIzCaEbKLHZObXli71koUvSSJvEqN1Qdq5bUCSgdLKRhOWCug3UcDnuiVrgrgEdRyGiU2ZC2CdxFXSzd+rgFFVVwawNqPQtV2HUZuQOlhq18HmuNHAZgtaoqPTGrvaGnPAv5x5cfzCuz74r/BMl73VVffDu+j5/bbhXdbAbwgaD7jtwZvk6bV354c3XOVlGhdfplFcuXsRBgvRehelabQq2OnKWwWhQMOUYzLwMw3tPwtlWTwd+vdpG6spNFMnNdRKRmw0KkKQ1RfY3Zdu8A5ADZSiATq3ab1jdH+/8Q+lLmjvFsPe3ItTsbCnLIZtQi3kYNfmPgDjJ86bCWXYQdv3lcotKKpyi1zNqmSWhNxVNNvcCQEEOYgAJv7V8IiYCZHDsBwXrJbdODB0Lbd6sP3YsX0Q3D7IcEId6taPRiPHtwhtF1uszjhHNQlhB103QRwMOMsekVcT9i77eykuM89Ec1XKp8789VOQROsVR9OZaBXD0EWuocBZnETzx5nQfjlsAxh5ciY8bs61Z/qt4qQGOhu+BdwWbvoa+4aphVwM75zUFkqgT6wOL1hvUm89k1jsAtSyrPYHxRKYMF9tfjNrRTFI/Jcg/V8hXcX5F34OLLu4uhQ8AeTFq7xYc5IWnc6BhV0qW/KuBJcN297ZVa37jZ8EnDX8pGhsNaK6Wk07Le2Cn88JHU2AY6I6lNgm+4ppLomYJohLsNjqZm9zjeq9Vh6LxQObdjsLwjEMLaSHu05A4ru47rweVdxLDNRQQr8/igjku1I6bZuqkQEuC8W7h4+LYK3h8aQMU8dBbxh5Qng32Y0RIpDDVC3RvqLLYcQiDnMIQ9mhuNz8rslYG0GwuZhY1EZMHoqJtG9IoR1sSSbDW5+GMO/15cWNWEI/eTp1T4/zivt2/KT7y8fhJ2Lgp55BNwgwtTDmHrJLsoPW6ZiOF5SD/KhG5YZhoAxqNShnm19oBA46yYg+RyTdN6QPnAGYQ/dqM/9t61RxCH/bwKW+LNLmlh1GYCwK6utN4J4Wt4tAO6DeFnbDhBXml+NsSSyH2GCcqyGZYWxzQx6lwVvHF4m/eQzTieqeGztU/2ad8xGQfqfmmVeDGztB8vf05l7qGWfU4o+bXX584SfJt9VmoYFbB2G1RwHlQPathTibeLnkWpWdcyY/Di9LEWtrEpai0bjbdrSstRytf/5qJ6iBOJxA2DBSRx6HQJnYIEyOdaN3w80y3nJ9+evVWymigwnSgYrcxsBimMLyGI04XaKMTIEFMSAAoqqR19+oA1SFeyCdHkI1ur2fkwvHKEcoAtHdX6IQjA8r/lx+OgGReDBJqaaAA01+6ihugrL2tkzf9zZnFOLkgCxAGMgqAjh90kOtmwNoSk6tQlNhFD08iohWkVMQhBWLtd6IwcV1qXlvHuNYCEPgr580YuOOepy7namva7/nZZD6t7GXOZXPiSe1p3TQkfBOinM2jLOOqaotDf6IY6AeMoCzTjrEXMdw1uXcq846dvsxIVeCdcyV2bExIvcuskQ+1CaEi26EoXnk/qK86R2G98htNPxaV/I9wMp89zLbU95syPQcLIQxw1yPQ84bdnYoIS9CLeoySIpyBzIeaRBkwbpQdtRYQZ8aKgUWGy/KKTmulSLCMIg3vhLVzChhCNnHuapyKFwFXAu7BLmFvaRr1XoxguNQ04IeLiVv/vp4jW7vv/z+8PXrw9Xqm/3ng3/udkFehZ2iJF1Gi2jthVXzZpmuwoJZFFW04VooWC+us6tLVOfEuzCaPfCmbSF3LfcKq4w4CLNBu6k6uFf2U1lf3IlL+rpXmJrH6eZcabNEyiyH4z8jXUGgu2Hn/LI0gGRKD8gEYMX0EQ/GiR/nBtKDKP3nq56Z4YU1fiTjyBnGOKJQSZcD3Tgqy/prmQwlQz0Y25d5k2ObTO3EUjWkGp7smwtBDNdwT8B4+UNKQIt5VY7c27zCTe9wNGbuUuo4poqlCLSoWMzoCatYCDoUAf6jY9uyHZh1Cw/0TlIg8zjddKw2S1eZ5XBsufxGCP34/dPLh/h5+uX+4r/nfzjnzKxiV1nJTE1rptEkj1nVVOws8b1UZCmKSFf21Mb3ktnSlDPId+UZIl8np29dW5EP2OBSE0PlABtABpiXqou6HVOAujZrEaBcdJ6GADUjr2eh/c8tP7l2V6sr3WbENkVF1fXtli3uKz9dah6nm/zUZkmUWY4tP4+4ra6z9qQQqbX5xFj4NFAkHgBiEeY6rgttRBxbtVn3DAmKvdY1QBgdd231uIbUbMdQZANF1SFU6nZJ1+AwHksYO3pSYzSmqTqOratc9RvNs+7LhswlihQmzngV9ZCzvWVXPEfzyL0dR6f5LY7EhxLxb2b4cBZSyxTIqbiLZoyhfgz2j7UDOftoS9yt1LSvwcOpqWmobjaPaa4ddwoPxpKGrQK7quAMBWxneadiz0ZR8FZu4cAXyr3a/o7p9mJb1lbtm5e0scvGLWud69d+wuI1uWFN7D2bVPesUcYmrbvW+IW666wrhypl6A1cO4Luc4itGrw27ZZSMUVkdsIaqr6Vsqahupa/jbLR38zyhvAD25agGjahlqWpP0f5Z4VlgF3jGe7ZjMgztW0aR2MZLJ0pGWDlZh7a01GDDtVguXqgqO9XLBRPfjzSdkfwR96yHoRy45N74Y31IM6R6kHEBxRQXb7tXQ9igDViPYiZTHpa1adOJi6lFmopG2JHIpN8b2PTBu69KaYd7NGJZ4TNtW9JPBASVC06o29EPNm+PlhZZncY6tkBd0TyIV+vv39OP7yC6W9fP/yKX5arj4/nhi9g8sXbbDNvld02ClUNG5E0G1YTc0TFQGHN0ki1hLEeK3MM+TbVaNmr+NeI8hHE/W6vqXX1q6ai+UHcF+2QKYUhNsGWTMlU+RUZcL+PsHeg5RDu5ziAYGgr396qDr5HpHI3sLHZtIuU3xGqPIyLFC1uY2wB0BanNO3wHSIwacaPXm7QylMnEJjUuLX7YnD33LZYPVLncsUIm32jLtaSq37110ZOV67pn50lLaN1zNEaZkz0GY/Nmnqmdu5vuNfOSeiH0KMfbz9/Ku///PqUio+Q1kU7NdWwMGbJOt9a8q+FMA8TAT2rxcdUq05HtSoTlj3UKhFfTFbQLzZijKdZBXjMmjRrdfDemtWmOi0Z4I3MwZ3SxmMqV4qwzlL0lPRrp1Tp30e/Ehda8g3LLQ5Ao8suxaLa1ywphjVNPVb1qGMeuId+NU3ebp382IzawXMSPz8RN/Jk8ZsZ3p18vL8Alj6T3Eza9eM29hAft9n9/gd/8acd87trG47x1T7cM+jVmAdaRknwg1tRWSaoXx1QK55qJoJuDCCjiGqkOVzfYADqqsK4nXmobE7ryFr1fPddI8D8CscqM+hiKx4tQtpKSZ1phNsLbBtzd2RoVFau2MfarKvsr1DFXue8TB2MKjX2JhB+uf19ofzx7c844ff/Bw== \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv retrieve operation.png b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv retrieve operation.png new file mode 100644 index 0000000..c13d4cd Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv retrieve operation.png differ diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv store operation.drawio b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv store operation.drawio new file mode 100644 index 0000000..3bdc0c0 --- /dev/null +++ b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv store operation.drawio @@ -0,0 +1 @@ +7Vxbb9s4Fv41RmYfIvAmknpMnaa73U4niwA723kpFJu2tZEtjSzn0l8/pO4iaVt2JMfB1ChSiyIPxcPvXHnkER4vnz8lfrz4NZqKcITA9HmEr0cIQeAC+Z9qeclbKGR5wzwJpkWnuuEu+CHKkUXrJpiKdatjGkVhGsTtxkm0WolJ2mrzkyR6anebRWF71tifC6PhbuKHZavj1u2/B9N0UbQTAOob/xTBfFFMzssl3/uTh3kSbVbFjCOEZ9knv730S1pF//XCn0ZPjSb8cYTHSRSl+bfl81iEirsl4/JxN1vuVutJxCrtMoAVIx79cFOsfh0s44dH2XaXRomQ//8Wi8RPg2iV8TKRf79Gq8t5GN37itS/xUuxlvSl5GDGAaHmgCP8YRat0rviZnYdhOE4CiUp1Rnf3Fx7lMr2dZpED6K8s4pWcsAHf5NG6xwiaqyfTArASJYXpIu7ioS5/nKBIknFc6Op4McnES1FmsglgOIu8bCDCgC8tIH5VGMBk6Jt0YBBuat+AcB5RbzeAvml2AX7jiy+E0I///n1+VP8NP42u/rv5X/4pUeMbTJ4rmAXd19/JT7+fUkB7OQLdD2nzRZqsgUCwMpeTc5QwF/PGrSfCRq0xmMC5V7q0JJ3KAWgluaaXbvZ3x1ETOOVZ+OVySfCeoCQCZalvwpmYp06sUQIlct/vlLCMkajK/DLJFrGQZhJ+D8MhlYKyiK4V2hMxt42wW2KJrGK5mt53eKlBXRuD7yk7wdzkPE25KiDudf4dEMgxQ6Fr2cc22pYKvyFwX3iJ6b1OGfMlXcxKAX8FCjk+1EoptKHKS4LbojV9Eq5Q3XL1F8vCrN8GGfW0SaZiNbOpn4yF6V6z5vUM3Th3iWkRNOPl3ZjYrKzarTxs5jxNgrkqurN4shhLucME5cyD0DcmlrfoHytBY2m72SQxQ5gFHquxxl1CWlRrbBQks35ZZDNtr5iUSc0eAYa4k36y4Ufx89y6IN4gRcjNDYUvRxykY2BF6aeb8JHhPfR08e64UPWIG8o+Qiknyybyq9f/HsR3koPLXMQ8XUaxY27V2EwV633UZpGy0JAb/xlECoujSWjA5E5lOJJ+XZF71DM0sOFtzv4XHRC8Lm6JyBVx5F4Y3tJbcGYQerSpe2B0Wy2Fq/FJXT363x/6sep2vP3p/Mxpw7j2HOBDGgRx5yczgDADn7IoRbgYFY1jUC5100rUIb5XSURUxlpE8QRAUz9a2HbFnT1JZMEew7YNrGKc5pek8eOE9fdk7Ddk/RnLKDpiBnIORcPlmDiIM2JPWHcBLsa1sqO5lcj9iH7e60uC1Nr2t6814VYPQZJtFpK3l2oVjUNnef2DlzESTTdTJQt3WbCa1uuck1SwC/MW8WXFuls+h3karrpSywsjxZKzb33oWoqgflgbXrBap36q4nY6qyYXxzH2d1RbYGN883mN/N9NPdGPAfp/wpFrL5/k99VUJFfXSuRAOXFS3mxkpBWgy6Bgz1atuRDCa4a6tHZVWv4rUgCKRoiKRp3umSD+WBmAAABHUzhQ97WKqh84MOVutRQrKG2XY0udrq5/dIs+y+NbrHqsN7ttEE4hNeGzBTw2RoIyvUc5CntQ8mYFqfonxuVrP9QqbO6qZnqkMpTsSTczIOVwd534PxSgiyMHsjbRXg/JE+X7ygnb7q6pdB0VXgUY8fjjBOGso/mDUJi9QYH0ITUI44MgFn5aevFY1Mhu6ki/TH7824tue4v11e3agdF8vg+40wO+Slzi8iM2t9S2IhF2A5MLzLPdTiWYbpHsg9tCxuwMbcf6eIMtDONvUgXZ7CVaNQPJwdLNL6n0w9O29t8UsfAjLGzwLGO5iSFfenZizxCK3yHnFTtSuAr7V7LzxjXF/Lh/amf+u2xkrQMgtj13zzze9qogxBXmw4zTVY7G1jD8TVIHRxobMlXw20z1cKQ09wWpZRKot8oxXIElqdd8FUi1pswHekZizRREjSWHUSSfF+u50aPVRCW6YFXCkUrrbpNQipZ0IUkF523CMQRRK29Jtjm5veVddXP3Y49CPH2EOpJFJDrWuexCsIRmMYHhjlbgbiIkuCHRE8GxYEcM5tfdhjW1PEVaHzam1h5RwMgb/fM+OgDYONAznDx+vPFsBnprOVWy5Yv1/+6eSurPpBacqlhhDzoMExh9RkMLNTT5yYIOhADAiDK/frj4MKp7g4QBDvBZX+y8BhAWU5rN5OJWK/PwBoOBCtmbK0LHGomLoYIDc3d10OP7tkWYxky7gTyTxZzSpBSLb19SlyVhbENXIVR9KBqU0FxZCW/RbGCwVpNrq6rWOh2E8fKWQRi9WjgcL3w4zwFkQozanhaBKm4i/3MYj0lfulTlUkbpKLS4jsbMoGDgL7VFV6awOK2cLSP8s8OCf3TJXTKFTUdB+wdJrem7EDXVs3Yj6QijByXEeASItU9woDbpz5UbL3tq+jfXXBR/yBonD8CJ0vyVKeP1c0tJ48DKXQEiYMwlKLkZh+vrRUJdajHICkqdmz1Ov1VGOb5LauHaVS/dy8x3EnX1UpVBksElgK7E01hGMRroaXVMxQNp2hRy7Fvp1sh8RzsEeQVfptp29vc45zawPB6hXz7/89f0N3s268Pf/zxcLP87v7+IC69LixtCGiUpItoHq38sOl8LdJlWIifZg3X0hAGq/mX7OoatWX7PowmD7Kpfk+nVV0Am6I9hPgmQr1v8NjsdJi4GZWQBHeSsINTAGDbTN2yYZYnpdqT9ielVpxBYAaOl/Ky8snKM2lQnmA3vDHVMU5EnPtsD+pNL4kCKT5KjLL/2v5aa+BaJIEE5Q9RTV44e6DOUYM0kn8+3/321QB/f84eH9LZI65u1SuXp6FpqlfDWqd3WjlHb7qlOhI6DxdwNy6bjuGWngee/0EIsZ4+IMMdqRMOWv4itM98sO1H21dxMt3Rpez49HafULDD7mNIz9juQ9ChIPen4e98ogY1Ij0ZfmIEa+VM3Qy/5Ulxt2TfEcJr3RNmt/vLrBCtZcozG3z9VTPfk0T4qbLZeX4577WMpsHsxVZkW1n4NE2C+002MpoVhLPB5pjc7kvgJxKw79UBYBDoO23L9pRVG00l00ftgX3vu9j/0+ttJjXzDr3tgfPQ23aWHvgGzk+13dh3YIgI71YIcajaZmjbTN3UtvmkZLjyKjvO3vxd385uNmZIf12nOtEa4hSXYsdjHvc86Eqn29W0x7GnKQwZltobLES3b7mZCsoKZmw2tTKibdt6MlM66MEJ58ZWcIspLSWidXDSw++LWFfBzeOsE8tjM5TeiZ9mJG1fy4FV69B124XqZMAw2kOqynVbGE2ODqPptjWcSLpLpp+ZL8Y9jcGYnbH3xdFhUvjT+2rsNKPaTg8TMnPPPk83z8t4Sve04TK3FOP9LDB9tZNo5lFI+dMBQ9RucarPxrsdDFkSOHtJ9VVxDfiWmfoqNLUD3swR5ID+myL1Epbv/9flvieqDYNMR4Cr/5RgV9i6eymdshbMDrwDT77fZ2kKx47L6soU7QX005WmlIVAry1F0ei4tBus+gKNN0AYdn6gUXWc2G2ghr4RagjmjvYW79HIsdA6WeGSfVcODCbeJ5ZUXoudQW1c/m648W77a1G1m+wbA2yA3y84P4B5ADWLL99KVal3rmkDCF4/+NpNdih8ycv6x7/z7vVvrOOPfwE= \ No newline at end of file diff --git a/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv store operation.png b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv store operation.png new file mode 100644 index 0000000..8968b00 Binary files /dev/null and b/docs/Use_of_LDAP_as_backend_for_simpkv/simpkv store operation.png differ diff --git a/docs/assets/LDAP_DIT_root.drawio b/docs/assets/LDAP_DIT_root.drawio new file mode 100644 index 0000000..22f8de5 --- /dev/null +++ b/docs/assets/LDAP_DIT_root.drawio @@ -0,0 +1 @@ +5Vlbd5s4EP41frSPQIDh0bGTdNu0m8btbvLUoxgFU2PECuFLf/1KIBkEhOBcGp9T+xwbzehizXzfaEYewOl6d0lRsvxMfBwNTODvBnA2ME3XtfinEOwLgQNAIQho6BeiimAe/sKF0JDCLPRxqokYIRELE124IHGMF0yTIUrJVu/2QCJfEyQowA3BfIEiJR3Zpfzf0GdLKTccr1R8wGGwlIu75rhQ3KPFKqAki+WKAxM+5K9CvUZqLrn/dIl8sq2I4PkATikhrHha76Y4ErbV7XbxiPawH4pj1mcAwDdfry6/zy4+bbyfd2z2z8VqNYSO/HFsr0yCfW4h2SSULUlAYhSdl9KzfNNYTAt4q+xzRUjChQYX/sSM7aW3UcYIFy3ZOpJavAvZbCfH5429bMSkslDet5xeCpr7lqZISUYXuGOzhvQ1QzTArKujtIowRWUJaddLTNaY0T3vIFlgWs4IADm7IgKUKJUIDQ6j5EQTStG+0iEhYczSyjrXQlAuAi2jsYgLdZf3GGIY2hj+UPwQ1arsrxTlUDoCVoZbrLlBUSYtSDL+nYbrZLVpQG67DBmeJyj33pbHGR0uD2EUTUlEaN4bXoDp1AFdSNhgyvCul+dgi4WkVcG2jAeuJPGyEgkgeNzDFfMebz047iQlju7JtsrHV2UgtyXdl6q8pXQNLpaRwFCdb0VfHlVl866yRkXFW3dqlNgOfxZOCxdi6pdQ3OlLcbcnxV/i3Y9zvNxRkAY/gh/h98n9bWbPhrCdGmHyEFK8RWKCk6EHHL8vPVoN6B7HDh2jr8qVBh86GdBBrHdkRxdGn0WO9qNoCD1n5Jg6kixDh0ixATmy41yzARy5rj7X2NTnKvbYmOvVTji7QWN/IU+4UyIwaBLYeefjTaGwGgH5V5IlCWYnbjvD/H3Gc/zbq8y5j1b7b/c3e3g+nG/Todmw3cB0IiYsQXKKlKZz/suIUgzTPLpNeAfDTnalkj8F4vtqNrnmyhtRjpjgG8U4L6co/5z/9VmoZoghtRb/6cVyxeCG07iFme6nlFGywspTMYlxzXlShKIwiHlzwf2GufxMBb6JVKxD38/jehsw9MpEhPJi392FQ39IGB6snYUNNJie14SD+VZwMEADD9eSRwAlScQtx0ISiwI9d9+f4ido1WhrNxxlqNq36ijDeytHNYkruVaQTFVGf5ifjBqf1Cle8ZMSaeH1zdzUPJua2WbsT8TVk7BqhNI0XNTyxUMSqKWAo/EhJXwkC9R9yk8zF4i36Pfkncwio5uyEJPppHq+01LL9ly2V1bZGYNe71ql4n275XBVMoojHt02+gVf/5TUch0dfCpoHJuOWsATiQJQL0Of1j5cMr5+btrukPHTINbx8gSiO4D5DKQ9DeYXYNE8KYgdzh2JBdN+JsRMWAOVN34rSKEvy7P1dnXnJ1+j7Ms2HP/94bzl0uL4sPh4jdsLKo3CvqU0fm793Qtwve+UWy1onRQw7RowYX2K3qW4pQdRr3YyP4LLYy+/LauWKACzBvOX3WJ3uaxWp6Y8TVqjUyryLdMeed5vK1R5s/zXqrB1+dcgPP8f \ No newline at end of file diff --git a/docs/assets/LDAP_DIT_root.png b/docs/assets/LDAP_DIT_root.png new file mode 100644 index 0000000..8ab2a94 Binary files /dev/null and b/docs/assets/LDAP_DIT_root.png differ diff --git a/docs/assets/Logical_to_simpkv_DIT_mapping.drawio b/docs/assets/Logical_to_simpkv_DIT_mapping.drawio new file mode 100644 index 0000000..b804b3b --- /dev/null +++ b/docs/assets/Logical_to_simpkv_DIT_mapping.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/assets/Logical_to_simpkv_DIT_mapping.png b/docs/assets/Logical_to_simpkv_DIT_mapping.png new file mode 100644 index 0000000..d604c14 Binary files /dev/null and b/docs/assets/Logical_to_simpkv_DIT_mapping.png differ diff --git a/docs/assets/simpkv_Key_Value_Tree.drawio b/docs/assets/simpkv_Key_Value_Tree.drawio new file mode 100644 index 0000000..2ec8f0e --- /dev/null +++ b/docs/assets/simpkv_Key_Value_Tree.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/assets/simpkv_Key_Value_Tree.png b/docs/assets/simpkv_Key_Value_Tree.png new file mode 100644 index 0000000..d677235 Binary files /dev/null and b/docs/assets/simpkv_Key_Value_Tree.png differ diff --git a/docs/assets/simpkv_OID_tree.drawio b/docs/assets/simpkv_OID_tree.drawio new file mode 100644 index 0000000..0c33ad2 --- /dev/null +++ b/docs/assets/simpkv_OID_tree.drawio @@ -0,0 +1 @@ +3VrbcqM4EP0aV2UfQkkIBDwmmSQ7u5vaVLKTzDxNYSNjMoAoId/m61cYyYCFbeJbxpOXoNYFOOeou9W4h26S2T3zs9EDDUjcM0Ew66FPPdOE0ALiX2GZlxZsWqUhZFEgB1WG5+gnaRrHUUDyholTGvMoaxoHNE3JgDdsPmN02hw2pHHzppkfEs3wPPBj3foaBXxUWl0bVPY/SRSO1J0hkD2JrwZLQz7yAzqtmdBtD90wSnl5lcxuSFxgp2Ap592t6V0+GCMp7zLBuUuGr1+C+4fnZPb4nU+evgevl8gsl5n48Vi+8fPnh0f5xHyuYJiOIk6eM39QtKeC6R66HvEkFi0oLodRHN/QmLLFaDQcDs3BQNhzzuiPJXDFyAlhPBLwXsVRmApbn3JOk2IJmnLJPnSXU2uLBriPbSx69BeXWBRrk1nNJIG4JzQhnM3FENULvHKKFCUCEodpxbEFUGkb1fi1LFtqS+oqXK5dQS8uJPrvYMLEGhM+5yzqjzn5b56RC/jHnqwENnEDqw1a1+wjfChoXdyAFgNXg3a5SerQInAsZB0NWQ1JEohdL5uU8RENaerHt5X1urJKdMks4l/FNTBs2fpW6/lUwAFUY64aqXih2qSi+a3eV01btNQ8RsdpQALZWstSTsdsQDZBId+c+ywkvIMaC1y6sI6cBumX0PH2Z5NNnt4+D1/6X5I7/nT9MxuFL18vbY27JjgVT/9QmklG3gjnc+ld/DGnzX2yKys7KaAmNBL36bSusYVBdCgnuR/XWOe6FVGzI9WMxD6PJs3A2MavnPpII/HMS4mYynUqx2Dh5hLlG8lZKypZPsbuwsE7bPr9pXVCJ7GHVNyOUkGHlsp+jt3VHPsg9vP83/6bSAUvzPMImMhzjZWd0RIyvZNGTO84EbMmbNerS/sSGAChbe62aD0SFomXJGwPH3z6aOr+AtG09cksbyOxKU2P6AbBRpbqB4LWTYgXf5sYPHZIs23P8CyMPGB70MKwSZtlYQO5CAPPAq6NXKtTuDvsTUppHi2mblaPlt98iI7WhVO4xd3kAjt+VZQQhGERV6KBMt9FceXUAjVI7hZhkf1gj5yuFW9VATlY+n7wQL0pbaiFkzxKsh+T2wLwcznYWgAYzkqgdoAWqF1bRfNDh+p2RYDtoToU+y7rjsCymOb31QpgIzLQ1DKYFmBMeFJc1kjub/JRgttM33YZ1jV2pGRw0/GwITAci7teB9FEXIbFZQnuXzlNX4pxi9y7HCTuWRt3jsAva4ZQ0zSE1nGYaPXqsL1Ye2E5AJ7JYafmHlXpVcWqjzrsKArrhdcsi0Ws5hFNzyU6tQDr6no9LbB6RTsO/OxsEPXACqIW0qV6LFe88cCpRbXzxdS2PhpTPYHKxllG+Pliann6zj8ppi1JaZRTg7LQCGhgiMMoYSnhRsaiic+JQQqDaOREZQ19VmUM2E8KnNN+Xvy7gAYysAENy9iboCvvxrq9PhAN5ioNpu1pNCy/Bjd4MI/Fg63xoCG2vY6X0lprAaNW2VuL39Yamsppth5pVZjuWkOD7iofWChHLI6x7QAM7KOVpWGH742/xddERcl27t75NVHnzjE7c7emioZRUwwiX2qs2FzwcN+i2oOOfjz95TZmy+fDjQG0M7m2tzu5+4V6vQ61A+q/xdZVpG1n1zwXduFB2D3unuqKOnwv6prDhKaA3XEE3BgiF5mu827YRbP6dVrp+aqf+KHb/wE= \ No newline at end of file diff --git a/docs/assets/simpkv_OID_tree.png b/docs/assets/simpkv_OID_tree.png new file mode 100644 index 0000000..08d69e8 Binary files /dev/null and b/docs/assets/simpkv_OID_tree.png differ diff --git a/docs/assets/simpkv_put_operation.drawio b/docs/assets/simpkv_put_operation.drawio new file mode 100644 index 0000000..f0eccbf --- /dev/null +++ b/docs/assets/simpkv_put_operation.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/assets/simpkv_put_operation.png b/docs/assets/simpkv_put_operation.png new file mode 100644 index 0000000..fcee793 Binary files /dev/null and b/docs/assets/simpkv_put_operation.png differ diff --git a/docs/simpkv_LDAP_DIT_and_schema.md b/docs/simpkv_LDAP_DIT_and_schema.md new file mode 100644 index 0000000..9131c32 --- /dev/null +++ b/docs/simpkv_LDAP_DIT_and_schema.md @@ -0,0 +1,123 @@ +#### Table of Contents + + +* [Introduction](#introduction) +* [simpkv DIT](#simpkv-dit) +* [simpkv OID Subtree and Custom LDAP Schema](#simpkv-oid-subtree-and-custom-ldap-schema) + * [simpkv OIDs](#simpkv-oids) + * [simpkv Custom LDAP Schema](#simpkv-custom-ldap-schema) + +## Introduction + +This document describes the simpkv subtree of SIMP Data LDAP Directory Information +Tree (DIT) and its custom LDAP schema. + +Readers are assumed to already have a basic understanding of simpkv and LDAP. + +## simpkv DIT + +SIMP has designed a LDAP DIT to store non-accounts, site data such as simpkv +data. The root of this DIT is depicted below and uses standard LDAP object +classes and attributes: + +![SIMP Data Root DIT](assets/LDAP_DIT_root.png) + +The subtree beneath the *simpkv* root Directory Name (DN), +*ou=simpkv,o=puppet,dc=simp*, has been designed explicitly to support simpkv +data. It is a simple mapping of the notional, file directory tree representation +of backend storage to a LDAP DIT, but it adds a tree for LDAP backend instances. + +A pictoral representation of this mapping is as follows: + +![Logical to simpkv DIT mapping](assets/Logical_to_simpkv_DIT_mapping.png) + +The simpkv subtree of the SIMP Data DIT uses both standard and custom LDAP +object classes and attributes. Specifically, + +* Folders in a key path, LDAP plugin instance identifiers and other grouping + added by either the simpkv adapter or the LDAP plugin (i.e., grouping for + types of keys) are represented by standard organizational units (`ou`). + +* Key/value entries are represented by a custom object class, `simpkvEntry` + with custom key name and value attributes, `simpkvKey` and `simpkvJson Value`. + +* The DN for a key/value node is constructed using each part of the key path + as a Relative DN (RDN). + + +## simpkv OID Subtree and Custom LDAP Schema + +The simpkv DIT data requires at a custom LDAP object class to hold the key +and JSON serialized value attributes. This custom LDAP object class and its +attributes must be specified by unique OIDs. This section describes the SIMP +OID subtree that was designed to support these custom LDAP OIDs and then +uses the OIDs in schemas for the simpkv DIT discussed above. + +### simpkv OIDs + +SIMP has an officially registered OID, +[1.3.6.1.4.1.47012](http://www.oid-info.com/get/1.3.6.1.4.1.47012), under which +all OIDs for Puppet, SNMP, etc. reside. Best practices for OID assignment +dictate that once an OID is in use, its definition is not supposed to change. +In other words, an OID can be deprecated, but not removed or reassigned a +different name. So, SIMP's OID tree has been designed to allow future expansion. + +Below is the (to be published) SIMP OID subtree showing the OIDs required for +simpkv's LDAP custom schema. It organizes the OIDs for attributes and class +objects under corresponding parent OIDs. + +![SIMP OID Tree](assets/simpkv_OID_tree.png) + +### simpkv Custom LDAP Schema + +The custom schema for the simpkv DIT uses the simpkv OIDs and is shown below. +It has a custom object class, `simpkvEntry`, that is comprised of two custom +attributes, `simpkvKey` and `simpkvJsonValue`. + +* `simpkvKey` is a case-invariant string for the key (excluding path) + + * This is used as the final RDN for a key/value node. + +* `simpkvJsonValue` is a case-sensitive string for the JSON-formatted value. + +``` +################################################################################ +# +dn: cn=schema +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.1 + NAME 'simpkvKey' + DESC 'key' + SUP name + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.2 + NAME 'simpkvJsonValue' + DESC 'JSON-formatted value' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +objectClasses: ( + 1.3.6.1.4.1.47012.1.1.1.1.2.1 + NAME 'simpkvEntry' + DESC 'simpkv entry' + SUP top + STRUCTURAL + MUST ( simpkvKey $ simpkvJsonValue ) + X-ORIGIN 'SIMP simpkv' + ) +``` diff --git a/docs/simpkv_plugin_development_guide.md b/docs/simpkv_plugin_development_guide.md new file mode 100644 index 0000000..1992d47 --- /dev/null +++ b/docs/simpkv_plugin_development_guide.md @@ -0,0 +1,256 @@ +#### Table of Contents + + + +* [Introduction](#introduction) +* [Terminology](#terminology) +* [Plugin Overview](#plugin-overview) + * [Plugin Instance Lifecycle](#plugin-instance-lifecycle) + * [Plugin Loading ](#plugin-loading) + * [Why worry about cross contamination](#why-worry-about-cross-contamination) +* [Implementing the Plugin API](#implementing-the-plugin-api) + + + +## Introduction + +This is a developers guide for writing custom simpkv plugins. simpkv plugins +are the software interfaces used by simpkv functions to affect the requested +store, retrieve, and modify operations on the key/value stores. + +This guide assumes the plugin writer has the following prequisite knowledge: + +* A basic understanding of what [simpkv provides and how it used and configured](../README.md). +* Ruby +* rspec for unit testing (aka 'spec' testing). + +In addition, a basic understanding of how to acceptance test Puppet modules +using [Beaker](https://github.com/voxpupuli/beaker) is helpful. + +## Terminology + +The following terminology will be used throughout this document: + +* **backend**- A specific key/value store that has unique configuration, (e.g., + directory of files on a local filesystem, LDAP server, Consul server, Etcd + server, Zookeeper server). + +* **plugin** - Ruby anonymous class to interface with a type of backend to + affect the operations requested in simpkv Puppet functions. + + * The class implements the simpkv plugin API for a specific backend type. + * The class will be instantiated and configured for each backend configuration + of that type. + +* **plugin instance** - Instance of the plugin class that is configured for + specific backend configuration + + * Uniquely identified by configured plugin (`'type'`, `'id'`) pair. + +* **plugin adapter** - Internal simpkv Ruby software that loads plugin software, + instantiates plugin objects, and then executes a plugin API call on the appropriate + plugin object to affect a simpkv function call. It also normalizes key paths + and data to be stored (the key's value and metadata), so that plugins simply + manage storage of strings at specified paths. + +## Plugin Overview + +Before implementing a plugin, it is useful to have a basic understanding of the +simpkv internal framework for loading plugin software, instantiating plugin +objects, and then using those objects to affect store/retrieve/modify +operations. + +Conceptionally, you can divide the simpkv internals into 3 pieces: + +* *simpkv library*: A set of simpkv Puppet library functions that provide the + abstract key/value store API to Puppet manifests and does the following with + each simpkv API function call + + * Creates merged simpkv options for the plugin adapter and plugins using + configuration provided by simpkv function signature, `simpkv::options` + hieradata, internal defaults, and the Puppet environment for which the + compilation is being done. + + * Ensures the 'backend' attribute is set to identify the backend to use. + * Adds the 'environment' attribute with the name of the Puppet environment. + + * Ensures the simpkv plugin adapter is loaded and available for the duration + of the catalog run. + * Delegates the function to the corresponding plugin adapter function and + waits for the result. + * Returns the plugin adapter's function result to the requesting Puppet + manifest or fails, per `simpkv::options::softfail` configuration. + +* *simpkv adapter*: A Ruby plugin adapter that loads all available plugin Ruby + files when it is first loaded and then does the following for each function + that corresponds to a simpkv Puppet function call: + + * Ensures the appropriate plugin instance required for the simpkv function + call is constructed, configured, and then cached for the duration of the + catalog run. + + * Adds the appropriate prefix to the key path + + * `environments//` for Puppet environment keys. + * `globals/` for global keys. + + * Serializes the a key's value and metadata to a single string on `simpkv::put` + operation. + + * The stored format is a JSON string described in the [simpkv README](../README.md) + + * Delegates the function to the corresponding plugin adapter function and + waits for the result. + * Deserializes key value and metadata strings on `simpkv::get` or `simpkv::list` + operations back to value and metadata objects. + * Removes key path prefixes on a `simpkv::list` operation. + * Returns the result. + +* *instance plugin*: An instance of the Ruby plugin code for a plugin type that + affects the store operation, after being configured. + +The relationship among the simpkv internal pieces and the key/value store during +manifest compilation will be clarified by the following notional sequence +diagram for an example `simpkv::put` operation. These diagrams assumes we are +using the LDAP plugin and that the simpkv adapter has been loaded and has +already constructed and configured the 'default' LDAP plugin instance. + +![simpkv::put Internal Operation](assets/simpkv_put_operation.png) + +### Plugin Instance Lifecycle + + +All plugin files are loaded the first time any simpkv API function is evaluated +in a catalog compile (similar to using `require` for the each plugin file). +However, each plugin instance is only constructed and configured the first time +it is required for a simpkv function. Each plugin instance is then persisted +until the catalog compile finishes, so it can be used for any subsequent simpkv +function calls that require that backend configuration. + +The plugin instance caching during a catalog compile allows each plugin type +to use the most efficient mechanism available for affecting one or more +transactions with the backend. Just be aware there is not 'destructor' in Ruby. +The plugin objects will go out of scope when the catalog compile finishes, but +the object removal is subject to garbage collection. + +### Plugin Loading + +Each plugin is written in pure Ruby and, to prevent [cross-Puppet-environment +contamination](#why-worry-about-cross-contamination), is implemented as an +anonymous class that is automatically loaded by the simpkv adapter with each +Puppet compile. Specifically, the simpkv adapter will load any module file +whose + +* path is `/lib/puppet_x/simpkv` +* filename ends with `_plugin.rb` +* content contains valid Ruby and is an anonymous class with the Class + definition line that is **exactly** as shown in + [simpkv's plugin_template.rb](../lib/puppet_x/simpkv/plugin_template.rb). + +The simpkv adapter parses each plugin's filename to determine its type, +internally stores the anonymous class for that plugin in a Hash, and then +creates an instance from the stored anonymous class when needed. + + * It assumes the filename format is `_plugin.rb`. + * It only loads the first plugin for any type. + * It emits warnings for duplicate plugin names and malformed plugins. + +If this convoluted but necessary loading mechanism seems a bit complex, it is, +but don't worry! If you create an appropriately named plugin file in your +module, ensure your plugin filename is unique across all Puppet modules, and +follow the instructions provided for implementing and testing your plugin, +you will not have problems with plugin loading or +cross-Puppet-environment-contamination. + +#### Why worry about cross contamination + +[Puppet-environment cross contamination](https://tickets.puppetlabs.com/browse/SERVER-94) +occurs when Ruby code from Puppet modules in one Puppet environment is +erroneously used in a Puppet manifest compile for a different Puppet +environment. Puppet has done an excellent job of solving this contamination +problem for standard Ruby Puppet code (Puppet Ruby API functions, facts, custom +types and providers). Unfortunately, the infrastructure required for simpkv +falls outside the bounds of standard Puppet code. So, we have to take steps to +ensure cross-contamination does not occur in any of the simpkv internals, +including simpkv plugins. Specifically, we need to make sure that any Ruby code +simpkv internally loads in any given Puppet compiler instance is only from the +Puppet code for that Puppet environment. + +This code loading requirement means that simpkv cannot use load its adapter or +the software for a particular plugin using a normal Ruby `require`, such as +`require 'ldap_plugin'`. Other more oblique methods are required. + +When simpkv was developed, two primary solutions for ensuring that +cross-contamination does not occur were considered: + +* Generate plugin class names dynamically and ensure the Puppet environment is + part of the each class name. +* Use anonymous classes. + +Both are painful. Using the anonymous class method seemed the least painful of +the two. + +## Implementing Plugin API + +To create your own plugin + +1. Create a `lib/puppet_x/simpkv` directory within your plugin module. +2. Copy [simpkv's lib/puppet_x/simpkv/plugin_template.rb](../lib/puppet_x/simpkv/plugin_template.rb) + into that directory with a name `_plugin.rb`. For example, + `my_module_consul_plugin.rb`. +3. **READ all the documentation in your plugin skeleton**, paying close attention + the *SIMPKV PLUGIN REQUIREMENTS* discussion. + + * You may find it helpful to peruse simpkv's LDAP and file plugin + implementations if you have any questions about the API. + +4. Implement the body of each method as identified by a `FIXME`. + + * Make sure your implementation handles any exceptions, locally, and returns + exactly what the API dictates. + + +5. Write unit tests for your plugin, using the unit tests for simpkv's LDAP + and file plugins as examples. + + * Your plugin's tests can be written using the same unit test infrastructure + you use for Puppet module spec testing. + * The unit tests for simpkv's plugins can be found at + [spec/unit/puppet_x/simpkv/](../spec/unit/puppet_x/simpkv). + * Both plugin tests shows you how to instantiate an object of your plugin + for unit testing purposes. + + * This is simple, but something you normally don't have to worry about for + Puppet module testing. + + * The LDAP plugin test demonstrates how to using rspec mocking for simulating + external interactions. + + +6. Write acceptance tests for your plugin, using the acceptance tests for + simpkv's LDAP and file plugins in + [spec/acceptance/suites/ldap_plugin/](../spec/acceptances/suites/ldap_plugin), + and [spec/acceptance/suites/default/](../spec/acceptances/suites/default), + as examples. These tests use the + ['simpkv plugin test' shared_examples](../spec/acceptance/shared_examples/simpkv_plugin.rb) + to exercise and validatate plugin operation via a standard set of simpkv + function calls operations. + + * Use the *'simpkv plugin' shared_examples* if you want to ensure your + plugin works in the same fashion as the two plugins provided by simpkv. + * You will have to ensure [simpkv's simpkv_test module](../spec/support/modules/simpkv_test) + is in the module path for your test. + * You will have to provide a plugin-specific validator for these tests. + (See the comments within the *shared_examples* for details.) + * This *shared_examples* may use methods provided the + [SIMP Beaker Helpers Ruby Gem](https://github.com/simp/rubygem-simp-beaker-helpers). + + * Be sure to add simp-beaker-helpers to your gem file and add the appropriate + `include` directives in your `spec/spec_helper_acceptance.rb`. + +7. Document your plugin's type, configuration parameters and requirements in + the README.md for your plugin module. + + * Be sure to list any RPM packages or Ruby Gems that need to be installed + in order for your plugin to operate. diff --git a/lib/puppet_x/simpkv/consul_provider.rb b/lib/puppet_x/simpkv/consul_provider.rb deleted file mode 100644 index 587606f..0000000 --- a/lib/puppet_x/simpkv/consul_provider.rb +++ /dev/null @@ -1,340 +0,0 @@ -# vim: set expandtab ts=2 sw=2: -provider_class = Class.new do - require 'net/http' - require 'uri' - require 'base64' - - def self.name - 'consul' - end - - def initialize(url, auth) - @uri = URI.parse(url) - @resturi = URI.parse(url) - scheme_split = @uri.scheme.split("+") - # defaults - @resturi.scheme = "http" - @verifyssl = true - scheme_split.each do |modifier| - case modifier - when "ssl" - @resturi.scheme = "https" - when "nossl" - @resturi.scheme = "http" - when "verify" - @verifyssl = true - when "noverify" - @verifyssl = false - end - end - @auth = auth - @basepath = @uri.path.chomp("/") - # XXX: Todo: break out the rest client into a mixin - # - # self.extend($SIMPKV.restclient); - # - - end - - # Begin REST Client - - def rest_request(params = {}) - unless params.key?(:method) - params[:method] = 'GET' - end - http = Net::HTTP.new(@uri.host, @uri.port) - http.use_ssl = @resturi.scheme == 'https' - if (@resturi.scheme == 'https') - if (@auth != nil) - if (@auth.key?("ca_file")) - http.ca_file = @auth["ca_file"] - end - if (@auth.key?("cert_file")) - http.cert = OpenSSL::X509::Certificate.new(File.read(@auth["cert_file"])) - end - if (@auth.key?("key_file")) - http.key = OpenSSL::PKey::RSA.new(File.read(@auth["key_file"])) - end - end - if (@verifyssl == true) - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - else - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - end - case params[:method] - when 'GET' - request = Net::HTTP::Get.new(params[:path]) - when 'DELETE' - request = Net::HTTP::Delete.new(params[:path]) - when 'PUT' - request = Net::HTTP::Put.new(params[:path]) - request.body = params[:body].to_s - end - if (params.key?("headers")) - params["headers"].each do |key, value| - request[key] = value - end - end - response = http.request(request) - end - - # End REST Client - def consul_request(params) - headers = {} - if (@auth != nil) - headers['X-Consul-Token'] = @auth["token"] - end - params["headers"] = headers - rest_request(params) - end - def supports(params) - [ - "delete", - "deletetree", - 'get', - 'put', - 'exists', - 'list', - - 'atomic_create', - 'atomic_delete', - 'atomic_get', - 'atomic_put', - 'atomic_list', - - 'empty_value', - 'info', - 'provider', - 'supports', - ] - end - def provider(params) - "consul" - end - def get(params) - key = params['key'] - if (key == nil) - throw Exception - end - begin - response = consul_request(path: "/v1/kv" + @basepath + key) - if (response.class == Net::HTTPOK) - json = response.body - parsed = JSON.parse(json)[0]; - value = Base64.decode64(parsed['Value']); - elsif (response.class == Net::HTTPNotFound) - self.empty_value({})['value'] - else - end - rescue - nil - end - end - - def put(params) - retval = {} - debug = params['debug'] - key = params['key'] - value = params['value'] - - if (key == nil) - raise "Put requires 'key' to be specified" - end - - if (value == nil) - raise "Put requires 'value' to be specified" - end - response = consul_request(path: "/v1/kv" + @basepath + key, method: 'PUT', body: value) - if (debug == true) - retval["response_class"] = response.class - retval["response_body"] = response.body - end - if (response.class == Net::HTTPOK) - if (response.body == "true\n") - retval["result"] = true - else - retval["result"] = false - end - else - retval["result"] = false - end - unless (params["debug"] == true) - retval = retval["result"] - end - return retval - end - - def atomic_get(params) - key = params['key'] - if (key == nil) - throw Exception - end - begin - response = consul_request(path: "/v1/kv" + @basepath + key) - if (response.class == Net::HTTPOK) - json = response.body - parsed = JSON.parse(json)[0]; - parsed['value'] = Base64.decode64(parsed['Value']); - parsed - elsif (response.class == Net::HTTPNotFound) - self.empty_value({}) - else - throw Exception - end - rescue - throw Exception - end - end - def atomic_create(params) - empty = empty_value() - atomic_put(params.merge({ 'previous' => empty})) - end - def atomic_put(params) - key = params['key'] - value = params['value'] - previous = params['previous'] - - if (key == nil) - throw Exception - end - - if (value == nil) - throw Exception - end - if (previous == nil) - throw Exception - end - previndex=previous["ModifyIndex"] - path = "/v1/kv" + @basepath + key + "?cas=" + previndex.to_s - response = consul_request(path: path, method: 'PUT', body: value) - if (response.class == Net::HTTPOK) - if (response.body =~ /true/) - true - else - false - end - elsif(response.class == Net::HTTPInternalServerError) - false - else - false - end - end - def atomic_delete(params) - key = params['key'] - previous = params['previous'] - - if (key == nil) - throw Exception - end - if (previous == nil) - throw Exception - end - previndex=previous["ModifyIndex"] - response = consul_request(path: "/v1/kv" + @basepath + key + "?cas=" + previndex.to_s, method: 'DELETE') - if (response.class == Net::HTTPOK) - if (response.body =~ /true/) - true - else - false - end - else - false - end - end - def delete(params) - key = params['key'] - if (key == nil) - throw Exception - end - # Get the value of key first. This is the only way to tell if we try to delete a key - response = consul_request(path: "/v1/kv" + @basepath + key, method: 'DELETE') - if (response.class == Net::HTTPOK) - if (response.body =~ /true/) - true - else - false - end - else - false - end - end - def deletetree(params) - end - def info(params) - end - def atomic_list(params) - key = params['key'] - last_char = key.slice(key.size - 1,1) - if (last_char != "/") - key = key + "/" - end - if (key == nil) - throw Exception - end - retval = {} - begin - response = consul_request(path: "/v1/kv" + @basepath + key + "?recurse") - if (response.class == Net::HTTPOK) - json = response.body - value = JSON.parse(json) - else - return retval - end - rescue - return retval - end - - last_char = key.slice(key.size - 1,1) - if (last_char != "/") - key = key + "/" - end - reg = Regexp.new("^" + @basepath.gsub(/^\//, "") + key) - unless (value == nil) - value.each do |entry| - nkey = entry["Key"].gsub(reg,"") - retval[nkey] = entry - unless (entry["Value"] == nil) - retval[nkey]["value"] = Base64.decode64(entry["Value"]) - else - retval[nkey]["value"] = nil - end - retval[nkey].delete("Value") - retval[nkey].delete("Key") - end - end - retval - end - def list(params) - list = atomic_list(params) - retval = {} - unless (list == nil) - list.each do |key, entry| - retval[key] = entry["value"] - end - end - retval - end - def exists(params) - key = params['key'] - if (key == nil) - throw Exception - end - # Get the value of key first. This is the only way to tell if we try to delete a key - response = consul_request(path: "/v1/kv" + @basepath + key + "?keys", method: 'GET') - if (response.class == Net::HTTPOK) - true - elsif(response.class == Net::HTTPNotFound) - false - else - false - end - - end - def empty_value(params = {}) - { - "ModifyIndex" => 0, - "value" => nil - } - end -end diff --git a/lib/puppet_x/simpkv/file_plugin.rb b/lib/puppet_x/simpkv/file_plugin.rb index 08a8e38..dec7132 100644 --- a/lib/puppet_x/simpkv/file_plugin.rb +++ b/lib/puppet_x/simpkv/file_plugin.rb @@ -37,12 +37,13 @@ def initialize(name) # The plugin-specific configuration will be found in # `options['backends'][ options['backend'] ]`: # - # * `root_path`: root directory path; defaults to '/var/simp/simpkv/' when - # that directory can be created or '/simp/simpkv/' - # otherwise - # * `lock_timeout_seconds`: max seconds to wait for an exclusive file lock - # on a file modifying operation before failing the operation; defaults - # to 5 seconds + # * `root_path`: Optional. Root directory path + # - Defaults to '/var/simp/simpkv/' when that directory can be created + # or '/simp/simpkv/' otherwise + # + # * `lock_timeout_seconds`: Optional. Max seconds to wait for an exclusive file lock + # on a file modifying operation before failing the operation + # - Defaults to 5 seconds # # @param options Hash of global simpkv and backend-specific options # @raise RuntimeError if any required configuration is missing from options, @@ -59,8 +60,6 @@ def configure(options) options['backends'].has_key?(options['backend']) && options['backends'][ options['backend'] ].has_key?('id') && options['backends'][ options['backend'] ].has_key?('type') && - # self is not available to an anonymous class and can't use constants, - # so have to repeat what is already in self.type (options['backends'][ options['backend'] ]['type'] == 'file') ) raise("Plugin misconfigured: #{options}") diff --git a/lib/puppet_x/simpkv/ldap_plugin.rb b/lib/puppet_x/simpkv/ldap_plugin.rb new file mode 100644 index 0000000..1c75e7f --- /dev/null +++ b/lib/puppet_x/simpkv/ldap_plugin.rb @@ -0,0 +1,1158 @@ +# Plugin implementation of an interface to an LDAP key/value store +# +# Each plugin **MUST** be an anonymous class accessible only through +# a `plugin_class` local variable. +# DO NOT CHANGE THE LINE BELOW!!!! +plugin_class = Class.new do + require 'facter' + require 'pathname' + require 'set' + attr_accessor :existing_folders + + # NOTES FOR MAINTAINERS: + # - See simpkv/lib/puppet_x/simpkv/plugin_template.rb for important + # information about plugin responsibilties and restrictions. + # - One OBTW that will drive you crazy are limitations on anonymous classes. + # In typical Ruby code, using constants and class methods is quite normal. + # Unfortunately, you cannot use constants or class methods in an anonymous + # class, as they will be added to the Class Object, itself, and will not be + # available to the anonymous class. In other words, you will be tearing your + # hair out trying to figure out why normal Ruby code does not work here! + + ###### Public Plugin API ###### + + # Construct an instance of this plugin setting its instance name + # + # @param name Name to ascribe to this plugin instance + # + def initialize(name) + @name = name + + # Whether configuration required for public API has been set + @configured = false + + # Path to root of the key/value tree for this plugin instance + # - Relative to simpkv root tree + # - Don't need the the 'ldap/' prefix the simpkv adapter adds to @name... + # just want the configured id + @instance_path = File.join('instances', @name.gsub(%r{^ldap/},'')) + + # Maintain a list of folders that already exist to reduce the number of + # unnecessary ldap add operations over the lifetime of this plugin instance + @existing_folders = Set.new + + # Configuration to be set in configure() + # - Base DN of the simpkv tree + # - Number of times to retry an LDAP operation if the server reports it + # is busy + # - Base LDAP commands, each of which includes any environment variables, + # general standard options and any command-specific options + @base_dn = nil + @retries = nil + @ldapadd = nil + @ldapdelete = nil + @ldapmodify = nil + @ldapsearch = nil + + Puppet.debug("#{@name} simpkv plugin constructed") + end + + # Configure this plugin instance using global and plugin-specific + # configuration found in options + # + # The plugin-specific configuration will be found in + # `options['backends'][ options['backend'] ]`: + # + # * `ldap_uri`: Required. The LDAP server URI. + # - This can be a LDAPI socket path or an ldap/ldaps URI + # specifying host and port. + # - When using an 'ldap://' URI with StartTLS, `enable_tls` + # must be true and `tls_cert`, `tls_key`, and `tls_cacert` + # must be configured. + # - When using an 'ldaps://' URI, `tls_cert`, `tls_key`, and + # `tls_cacert` must be configured. + # + # * `base_dn`: Optional. The root DN for the 'simpkv' tree in LDAP. + # - Defaults to 'ou=simpkv,o=puppet,dc=simp' + # - Must already exist + # + # * `admin_dn`: Optional. The bind DN for simpkv administration. + # - Defaults to 'cn=Directory_Manager'. + # - This identity must have permission to modify the LDAP tree + # below `base_dn`. + # + # * `admin_pw_file`: Required for all but LDAPI. A file containing the simpkv + # adminstration password. + # - Will be used for authentication when set, even with + # LDAPI. + # - When unset for LDAPI, the admin_dn is assumed to + # be properly configured for external EXTERNAL SASL + # authentication for the user compiling the manifest + # (e.g., 'puppet' for 'puppet agent', 'root' for + # 'puppet apply' and the Bolt user for Bolt plans). + # + # * `enable_tls`: Optional. Whether to enable TLS. + # - Defaults to true when `ldap_uri` is an 'ldaps://' URI, + # otherwise defaults to false. + # - Must be set to true to enable StartTLS when using an + # 'ldap://' URI. + # - When true `tls_cert`, `tls_key` and `tls_cacert` must + # be set. + # + # * `tls_cert`: Required for StartTLS or TLS. The certificate file. + # * `tls_key`: Required for StartTLS or TLS. The key file. + # * `tls_cacert`: Required for StartTLS or TLS. The cacert file. + # * `retries`: Optional. Number of times to retry an LDAP operation if the + # server reports it is busy. + # - Defaults to 1. + # + # @param options Hash of global simpkv and backend-specific options + # + # @raise RuntimeError if ldap_uri is malformed, any required configuration is + # missing from options, or cannot connect to the LDAP server + # + def configure(options) + # backend config should already have been verified by simpkv adapter, but + # just in case... + unless ( + options.is_a?(Hash) && + options.has_key?('backend') && + options.has_key?('backends') && + options['backends'].is_a?(Hash) && + options['backends'].has_key?(options['backend']) && + options['backends'][ options['backend'] ].has_key?('id') && + options['backends'][ options['backend'] ].has_key?('type') && + (options['backends'][ options['backend'] ]['type'] == 'ldap') + ) + raise("Plugin misconfigured: #{options}") + end + + # parse and validate backend config options and then set variables needed + # for LDAP operations + opts = parse_config(options['backends'][options['backend']]) + @base_dn = opts[:base_dn] + @retries = opts[:retries] + set_base_ldap_commands(opts[:cmd_env], opts[:base_opts]) + + # verify LDAP config allows access and then ensure the base tree with + # 'globals' and 'environments' sub-folders is in place + verify_ldap_access + ensure_instance_tree + + @configured = true + Puppet.debug("#{@name} simpkv plugin configured") + end + + # @return unique identifier assigned to this plugin instance + def name + @name + end + + # Deletes a `key` from the configured backend. + # + # @param key String key + # + # @return results Hash + # * :result - Boolean indicating whether operation succeeded + # * :err_msg - String. Explanatory text upon failure; nil otherwise. + # + def delete(key) + Puppet.debug("#{@name} delete(#{key})") + unless @configured + return { + :result => false, + :err_msg => 'Internal error: delete called before configure' + } + end + + full_key_path = File.join(@instance_path, key) + cmd = %Q{#{@ldapdelete} "#{path_to_dn(full_key_path)}"} + deleted = false + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + deleted = true + done = true + when ldap_code_no_such_object + deleted = true + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + done = true + end + else + err_msg = result[:stderr] + done = true + end + retries -= 1 + end + + { :result => deleted, :err_msg => err_msg } + end + + # Deletes a whole folder from the configured backend. + # + # @param keydir String key folder path + # + # @return results Hash + # * :result - Boolean indicating whether operation succeeded + # * :err_msg - String. Explanatory text upon failure; nil otherwise. + # + def deletetree(keydir) + Puppet.debug("#{@name} deletetree(#{keydir})") + unless @configured + return { + :result => false, + :err_msg => 'Internal error: deletetree called before configure' + } + end + + full_keydir_path = File.join(@instance_path, keydir) + cmd = %Q{#{@ldapdelete} -r "#{path_to_dn(full_keydir_path, false)}"} + deleted = false + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + deleted = true + done = true + when ldap_code_no_such_object + deleted = true + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + done = true + end + else + err_msg = result[:stderr] + done = true + end + retries -= 1 + end + + if deleted + existing_folders.delete(full_keydir_path) + parent_path = full_keydir_path + "/" + existing_folders.delete_if { |path| path.start_with?(parent_path) } + end + + { :result => deleted, :err_msg => err_msg } + end + + # Returns whether key or key folder exists in the configured backend. + # + # @param key String key or key folder to check + # + # @return results Hash + # * :result - Boolean indicating whether key/key folder exists; + # nil if could not be determined + # * :err_msg - String. Explanatory text when status could not be + # determined; nil otherwise. + # + def exists(key) + Puppet.debug("#{@name} exists(#{key})") + unless @configured + return { + :result => nil, + :err_msg => 'Internal error: exists called before configure' + } + end + + # don't know if the key path is to a key or a folder so need to create a + # search filter for both an RDN of ou= or an RDN simpkvKey=. + full_key_path = File.join(@instance_path, key) + dn = path_to_dn(File.dirname(full_key_path), false) + leaf = File.basename(key) + search_filter = "(|(ou=#{leaf})(simpkvKey=#{leaf}))" + cmd = [ + @ldapsearch, + '-b', %Q{"#{dn}"}, + '-s one', + %Q{"#{search_filter}"}, + '1.1' # only print out the dn, no attributes + ].join(' ') + + found = false + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + # Parent DN exists, but search may or may not have returned a result + # (i.e. search may have returned no matches). Have to parse console + # output to see if a dn was returned. + found = true if result[:stdout].match(%r{^dn: (ou=#{leaf})|(simpkvKey=#{leaf}),#{dn}}) + done = true + when ldap_code_no_such_object + # Some part of the parent DN does not exist, so it does not exist! + done = true + when ldap_code_server_is_busy + if (retries == 0) + found = nil + err_msg = result[:stderr] + done = true + end + else + found = nil + err_msg = result[:stderr] + done = true + end + retries -= 1 + end + + { :result => found, :err_msg => err_msg } + end + + # Retrieves the value stored at `key` from the configured backend. + # + # @param key String key + # + # @return results Hash + # * :result - String. Retrieved value for the key; nil if could not + # be retrieved + # * :err_msg - String. Explanatory text upon failure; nil otherwise. + # + def get(key) + Puppet.debug("#{@name} get(#{key})") + unless @configured + return { + :result => nil, + :err_msg => 'Internal error: get called before configure' + } + end + + full_key_path = File.join(@instance_path, key) + cmd = %Q{#{@ldapsearch} -b "#{path_to_dn(full_key_path)}"} + value = nil + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + match = result[:stdout].match(/^simpkvJsonValue: (.*?)$/) + if match + value = match[1] + else + err_msg = "Key retrieval did not return key/value entry:" + err_msg += "\n#{result[:stdout]}" + end + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + done = true + end + else + err_msg = result[:stderr] + done = true + end + retries -= 1 + end + + { :result => value, :err_msg => err_msg } + end + + # Returns a listing of all keys/info pairs and sub-folders in a folder + # + # The list operation does not recurse through any sub-folders. Only + # information about the specified key folder is returned. + # + # This implementation is best effort. It will attempt to retrieve the + # information in a folder and only fail if the folder itself cannot be + # accessed. Individual key retrieval failures will be ignored. + # + # @return results Hash + # * :result - Hash of retrieved key and sub-folder info; nil if the + # retrieval operation failed + # + # * :keys - Hash of the key/value pairs for keys in the folder + # * :folders - Array of sub-folder names + # + # * :err_msg - String. Explanatory text upon failure; nil otherwise. + # + def list(keydir) + Puppet.debug("#{@name} list(#{keydir})") + unless @configured + return { + :result => nil, + :err_msg => 'Internal error: list called before configure' + } + end + full_keydir_path = File.join(@instance_path, keydir) + + cmd = [ + @ldapsearch, + '-b', %Q{"#{path_to_dn(full_keydir_path, false)}"}, + '-s', 'one', + ].join(' ') + + ldif_out = nil + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + ldif_out = result[:stdout] + done = true + when ldap_code_no_such_object + err_msg = result[:stderr] + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + done = true + end + else + err_msg = result[:stderr] + done = true + end + retries -= 1 + end + + list = nil + unless ldif_out.nil? + if ldif_out.empty? + list = { :keys => {}, :folders => [] } + else + list = parse_list_ldif(ldif_out) + end + end + + { :result => list, :err_msg => err_msg } + end + + # Sets the data at `key` to a `value` in the configured backend. + # + # @param key String key + # @param value String value + # + # @return results Hash + # * :result - Boolean indicating whether operation succeeded + # * :err_msg - String. Explanatory text upon failure; nil otherwise. + # + def put(key, value) + Puppet.debug("#{@name} put(#{key},...)") + unless @configured + return { + :result => false, + :err_msg => 'Internal error: put called before configure' + } + end + + full_key_path = File.join(@instance_path, key) + + # We want to add the key/value entry if it does not exist, but only modify + # the value if its current value does not match the desired value. + # The modification restriction ensures that we do not update LDAP's + # modifyTimestamp unnecessarily. Accurate timestamps are important for + # keystore auditing! + # + # The tricky part is this add/update logic is that, at any point in this + # process, something else could be modifying the database at the same time. + # So, there is no point in checking for the existence of the key's folders + # or its key/value entry, because that info may not be accurate at the time + # we request our changes. Instead, try to add each folder/key node + # individually, and handle any "Already exists" failures appropriately for + # each node. + + results = nil + ldap_results = ensure_folder_path( File.dirname(full_key_path) ) + if ldap_results[:success] + # first try ldapadd for the key/value entry + ldif = entry_add_ldif(full_key_path, value) + + Puppet.debug("#{@name} Attempting add for #{full_key_path}") + ldap_results = ldap_add(ldif, false) + + if ldap_results[:success] + results = { :result => true, :err_msg => nil } + elsif (ldap_results[:exitstatus] == ldap_code_already_exists) + Puppet.debug("#{@name} #{full_key_path} already exists") + # ldapmodify only if necessary + results = update_value_if_changed(key, value) + else + results = { :result => false, :err_msg => ldap_results[:err_msg] } + end + else + results = { :result => false, :err_msg => ldap_results[:err_msg] } + end + + results + end + + ###### Internal Methods ###### + + # Ensure all folders in a folder path are present. + # + # Adds any folder not in @existing_folders + # + # @param folder_path the folder path to ensure + # + # @return results Hash + # * :success - Whether all folders are now present + # * :exitstatus - 0 when all folders are now present or the exit code of + # the first folder add operation that failed + # * :err_msg - nil when all folders are now present or the error message + # of the first folder add operation that failed + # + def ensure_folder_path(folder_path) + Puppet.debug("#{@name} ensure_folder_path(#{folder_path})") + # Handle each folder separately instead of all at once, so we don't have to + # use log scraping to understand what happened...log scraping is fragile! + ldif_file = nil + folders_ensured = true + results = nil + Pathname.new(folder_path).descend do |folder| + folder_str = folder.to_s + next if existing_folders.include?(folder_str) + ldif = folder_add_ldif(folder_str) + ldap_results = ldap_add(ldif, true) + if ldap_results[:success] + existing_folders.add(folder_str) + else + folders_ensured = false + results = ldap_results + break + end + end + + if folders_ensured + results = { :success => true, :exitstatus => 0, :err_msg => nil } + end + + results + end + + # Ensures the basic tree for this instance is created below the base DN + # base DN + # | - instances + # | | - + # | | | - globals + # | | | - environments + # | | -- + # | -- + # -- + def ensure_instance_tree + [ + File.join(@instance_path, 'globals'), + File.join(@instance_path, 'environments') + ].each do | folder| + # Have already verified access to the base DN, so going to *assume* any + # failures here are transient and will ignore them for now. If there is + # a persistent problem, it will be caught in the first key storage + # operation. + ensure_folder_path(folder) + end + end + + # @return LDIF to add a simpkvEntry containing a key/value pair + def entry_add_ldif(key, value) + <<~EOM + dn: #{path_to_dn(key)} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{File.basename(key)} + simpkvJsonValue: #{value} + EOM + end + + # @return LDIF to modify the value (simpkvJsonValue) of a + # a simpkvEntry containing a key/value pair + def entry_modify_ldif(key, value) + <<~EOM + dn: #{path_to_dn(key)} + changetype: modify + replace: simpkvJsonValue + simpkvJsonValue: #{value} + EOM + end + + # @return LDIF to add a folder (organizationalUnit) + def folder_add_ldif(folder) + <<~EOM + dn: #{path_to_dn(folder, false)} + ou: #{File.basename(folder)} + objectClass: top + objectClass: organizationalUnit + EOM + end + + # Execute ldapadd with the specified LDIF content + # + # - Used to add a folder (organizationalUnit) or a key/value pair (simpkvEntry). + # - When ignore_already_exists is true, the attributes of the existing + # element will NOT be changed. So, ignore_already_exists is most useful + # when you want to add a folder and don't care if it already exists. + # + # @param ldif LDIF with key/value pair or folder to add + # @param ignore_already_exists Whether to ignore 'Already exists' failure + # @return results Hash + # * :success - Whether the ldapadd succeeded; Will be true when + # the ldapadd failed with 'Already exists' return code, but + # ignore_already_exists is true. + # * :exitstatus - The exitstatus of the ldapadd operation + # * :err_msg - nil when :success is true or the error message of the + # ldapadd operation + # + def ldap_add(ldif, ignore_already_exists = false) + # Maintainers: Comment out this line to see actual LDIF content when + # debugging. Since may contain sensitive info, we don't want to allow this + # output normally. + #Puppet.debug( "#{@name} add ldif:\n#{ldif}" ) + ldif_file = Tempfile.new('ldap_add') + ldif_file.puts(ldif) + ldif_file.close + + cmd = "#{@ldapadd} -f #{ldif_file.path}" + added = false + exitstatus = nil + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + added = true + exitstatus = 0 + done = true + when ldap_code_already_exists + if ignore_already_exists + added = true + exitstatus = 0 + else + err_msg = result[:stderr] + exitstatus = result[:exitstatus] + end + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + exitstatus = result[:exitstatus] + done = true + end + else + err_msg = result[:stderr] + exitstatus = result[:exitstatus] + done = true + end + retries -= 1 + end + + { :success => added, :exitstatus => exitstatus, :err_msg => err_msg } + ensure + ldif_file.close if ldif_file + ldif_file.unlink if ldif_file + end + + # LDAP return code for 'Already exists' + def ldap_code_already_exists + 68 + end + + # LDAP return code for 'No such object' + def ldap_code_no_such_object + 32 + end + + # LDAP return code for 'Server is busy' + def ldap_code_server_is_busy + 51 + end + + # Execute ldapmodify with the specified LDIF content + # + # - Used to modify the value (simpkvJsonValue) of an existing key/value pair + # (simpkvEntry) + # + # @param ldif LDIF with modification to affect + # @return results Hash + # * :success - Whether the ldapmodify succeeded + # * :exitstatus - The exitstatus of the ldapmodify operation + # * :err_msg - nil when :success is true or the error message of the + # ldapmodify operation + # + def ldap_modify(ldif) + # Maintainers: Comment out this line to see actual LDIF content when + # debugging. Since may contain sensitive info, we don't want to allow this + # output normally. + #Puppet.debug( "#{@name} modify ldif:\n#{ldif}" ) + ldif_file = Tempfile.new('ldap_modify') + ldif_file.puts(ldif) + ldif_file.close + + cmd = "#{@ldapmodify} -f #{ldif_file.path}" + modified = false + exitstatus = nil + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + modified = true + done = true + when ldap_code_no_such_object + # DN got removed out from underneath us. Going to just accept this + # failure for now, as unclear the complication in the logic to turn + # around and add the entry is worth it. + err_msg = result[:stderr] + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + done = true + end + else + err_msg = result[:stderr] + done = true + end + exitstatus = result[:exitstatus] + retries -= 1 + end + + { :success => modified, :exitstatus => exitstatus, :err_msg => err_msg } + ensure + ldif_file.close if ldif_file + ldif_file.unlink if ldif_file + + end + + # @return DN corresponding to a path + # + # @param path Folder or key path in a keystore + # @param leaf_is_key Whether the final node in this path is a key + # + def path_to_dn(path, leaf_is_key = true) + parts = path.split('/') + dn = nil + if parts.empty? + dn = @base_dn + else + attribute = leaf_is_key ? 'simpkvKey' : 'ou' + dn = "#{attribute}=#{parts.pop}" + parts.reverse.each do |folder| + dn += ",ou=#{folder}" + end + dn += ",#{@base_dn}" + end + + dn + end + + # Extract and validate configuration for use with ldapsearch, ldapadd, + # ldapmodify, and ldapdelete commands + # + # Parses the following configuration + # * `ldap_uri`: Required. The LDAP server URI. + # - This can be a LDAPI socket path or an ldap/ldaps URI + # specifying host and, optionally, port. + # - When using an 'ldap://' URI with StartTLS, `enable_tls` + # must be true and `tls_cert`, `tls_key`, and `tls_cacert` + # must be configured. + # - When using an 'ldaps://' URI, `tls_cert`, `tls_key`, and + # `tls_cacert` must be configured. + # + # * `base_dn`: Optional. The root DN for the 'simpkv' tree in LDAP. + # - Defaults to 'ou=simpkv,o=puppet,dc=simp' + # - Must already exist + # + # * `admin_dn`: Optional. The bind DN for simpkv administration. + # - Defaults to 'cn=Directory_Manager' + # - This identity must have permission to modify the LDAP tree + # below `base_dn`. + # + # * `admin_pw_file`: Required for all but LDAPI. A file containing the simpkv + # adminstration password. + # - Will be used for authentication when set, even with + # LDAPI. + # - When unset for LDAPI, the admin_dn is assumed to + # be properly configured for external EXTERNAL SASL + # authentication for the user compiling the manifest + # (e.g., 'puppet' for 'puppet agent', 'root' for + # 'puppet apply' and the Bolt user for Bolt plans). + # + # * `enable_tls`: Optional. Whether to enable TLS. + # - Defaults to true when `ldap_uri` is an 'ldaps://' URI, + # otherwise defaults to false. + # - Must be set to true to enable StartTLS when using an + # 'ldap://' URI. + # - When true `tls_cert`, `tls_key` and `tls_cacert` must + # be set. + # + # * `tls_cert`: Required for StartTLS or TLS. The certificate file. + # * `tls_key`: Required for StartTLS or TLS. The key file. + # * `tls_cacert`: Required for StartTLS or TLS. The cacert file. + # * `retries`: Optional. Number of times to retry an LDAP operation if the + # server reports it is busy. + # - Defaults to 1. + # + # @param config Hash backend-specific options + # + # @return parsed config Hash + # * :base_dn - Base DN of the simpkv tree + # * :cmd_env - Any environment variables required for the ldap* commands + # * :base_opts - Base options for the ldap* commands which include the LDAP + # server URL and authentication options + # * :retries - Number of times a ldap* command should be retried + # + # @raise RuntimeError upon any of the following validation failures: + # * 'ldap_uri' option is missing + # * 'ldap_uri' does not begin with 'ldapi:', 'ldap:', or 'ldaps:' + # * 'admin_pw_file' is not configured + # * 'admin_pw_file' file does not exist + # * TLS configuration is not complete when 'ldap_uri' begins with 'ldaps:' + # or 'enable_tls' present and set to true + # + def parse_config(config) + opts = {} + + ldap_uri = config['ldap_uri'] + raise("Plugin missing 'ldap_uri' configuration") if ldap_uri.nil? + + # TODO this regex for URI or socket can be better! + unless ldap_uri.match(%r{^(ldapi|ldap|ldaps)://\S.}) + raise("Invalid 'ldap_uri' configuration: #{ldap_uri}") + end + + if config.key?('base_dn') + # TODO Detect when non-escaped characters exist and fail? + opts[:base_dn] = config['base_dn'] + else + opts[:base_dn] = 'ou=simpkv,o=puppet,dc=simp' + Puppet.debug("simpkv plugin #{name}: Using default base DN #{opts[:base_dn]}") + end + + admin_dn = nil + if config.key?('admin_dn') + admin_dn = config['admin_dn'] + else + #FIXME Should not use admin for whole tree + admin_dn = 'cn=Directory_Manager' + Puppet.debug("simpkv plugin #{name}: Using default simpkv admin DN #{admin_dn}") + end + + admin_pw_file = config.fetch('admin_pw_file', nil) + unless ldap_uri.start_with?('ldapi') + raise("Plugin missing 'admin_pw_file' configuration") if admin_pw_file.nil? + end + + if admin_pw_file + raise("Configured 'admin_pw_file' #{admin_pw_file} does not exist") unless File.exist?(admin_pw_file) + end + + if tls_enabled?(config) + opts[:cmd_env], extra_opts = parse_tls_config(config) + opts[:base_opts] = %Q{#{extra_opts} -x -D "#{admin_dn}" -y #{admin_pw_file} -H #{ldap_uri}} + else + opts[:cmd_env] = '' + if admin_pw_file + # unencrypted ldap or ldapi with simple authentication + opts[:base_opts] = %Q{-x -D "#{admin_dn}" -y #{admin_pw_file} -H #{ldap_uri}} + else + # ldapi with EXTERNAL SASL + opts[:base_opts] = "-Y EXTERNAL -H #{ldap_uri}" + end + end + + if config.key?('retries') + opts[:retries] = config['retries'] + else + opts[:retries] = 1 + Puppet.debug("simpkv plugin #{name}: Using retries = #{opts[:retries]}") + end + + opts + end + + # Parse the LDIF output for a ldapsearch that corresponds to a folder + # list operation + # + # @param ldif_out LDIF console output of the ldapsearch operation + # + # @return folder listing results Hash + # * :keys - Hash of the key/value pairs for keys in the folder + # * :folders - Array of sub-folder names + # + def parse_list_ldif(ldif_out) + folders = [] + keys = {} + ldif_out.split(/^dn: /).each do |ldif| + next if ldif.strip.empty? + if ldif.match(/objectClass: organizationalUnit/i) + rdn = ldif.split("\n").first.split(',').first + folder_match = rdn.match(/^ou=(\S+)$/) + if folder_match + folders << folder_match[1] + else + Puppet.debug("Unexpected organizationalUnit entry:\n#{ldif}") + end + elsif ldif.match(/objectClass: simpkvEntry/i) + key_match = ldif.match(/simpkvKey: (\S+)/i) + if key_match + key = key_match[1] + value_match = ldif.match(/simpkvJsonValue: (\{.+?\})\n/i) + if value_match + keys[key] = value_match[1] + else + Puppet.debug("simpkvEntry missing simpkvJsonValue:\n#{ldif}") + end + else + Puppet.debug("simpkvEntry missing simpkvKey:\n#{ldif}") + end + else + Puppet.debug("Found unexpected object in simpkv tree:\n#{ldif}") + end + end + { :keys => keys, :folders => folders } + end + + # @return Pair of string modifiers for StartTLS/TLS via ldap* commands: + # [ , ] + # + # @param config Hash backend-specific options + # + def parse_tls_config(config) + tls_cert = config.fetch('tls_cert', nil) + tls_key = config.fetch('tls_key', nil) + tls_cacert = config.fetch('tls_cacert', nil) + + if tls_cert.nil? || tls_key.nil? || tls_cacert.nil? + err_msg = "TLS configuration incomplete:" + err_msg += ' tls_cert, tls_key, and tls_cacert must all be set' + raise(err_msg) + end + + cmd_env = [ + "LDAPTLS_CERT=#{tls_cert}", + "LDAPTLS_KEY=#{tls_key}", + "LDAPTLS_CACERT=#{tls_cacert}" + ].join(' ') + + if config['ldap_uri'].match(/^ldap:/) + # StartTLS + extra_opts = '-ZZ' + else + # TLS + extra_opts = '' + end + + [ cmd_env, extra_opts ] + end + + # Execute a command + # + # - Pipes within the command can cause inconsistent results. + # - DON'T USE THEM. + # - TODO. We don't currently check for '|' and fail , because we use '|' + # as the OR operator within a LDAP search term. Need more sophisticated + # check than simply the existence of a '|' in the command string! + # - This method does not wrap the execution with a Timeout block, because + # the commands being executed by this plugin (ldapsearch, ldapadd, etc.) + # have built-in timeout mechanisms. + # + # @param command The command to execute + # + # @return results Hash + # * :success - Whether the exist status was 0 + # * :exitstatus - The exit status + # * :stdout - Messages sent to stdout + # * :stderr - Messages sent to stderr + # + def run_command(command) + Puppet.debug( "#{@name} executing: #{command}" ) + + out_pipe_r, out_pipe_w = IO.pipe + err_pipe_r, err_pipe_w = IO.pipe + pid = spawn(command, :out => out_pipe_w, :err => err_pipe_w) + out_pipe_w.close + err_pipe_w.close + + Process.wait(pid) + exitstatus = $?.nil? ? nil : $?.exitstatus + stdout = out_pipe_r.read + out_pipe_r.close + stderr = err_pipe_r.read + err_pipe_r.close + + stderr = "#{command} failed:\n#{stderr}" if exitstatus != 0 + + { + :success => (exitstatus == 0), + :exitstatus => exitstatus, + :stdout => stdout, + :stderr => stderr + } + end + + # Verifies ldap* commands exist and sets base commands used in LDAP + # operations + # + # @param cmd_env Any environment variables required for the ldap* commands + # @param base_opts Base options for the ldap* commands which include the LDAP + # server URL and authentication options + # + # @raise RuntimeError if ldapadd, ldapdelete, ldapmodify, or ldapsearch + # commands cannot be found + # + def set_base_ldap_commands(cmd_env, base_opts) + # make sure all the openldap-utils commands we need are available + ldapadd = Facter::Core::Execution.which('ldapadd') + ldapdelete = Facter::Core::Execution.which('ldapdelete') + ldapmodify = Facter::Core::Execution.which('ldapmodify') + ldapsearch = Facter::Core::Execution.which('ldapsearch') + + { + 'ldapadd' => ldapadd, + 'ldapdelete' => ldapdelete, + 'ldapmodify' => ldapmodify, + 'ldapsearch' => ldapsearch + }.each do |base_cmd, cmd| + if cmd.nil? + raise("Missing required #{base_cmd} command. Ensure openldap-clients RPM is installed") + end + end + + @ldapsearch = [ + cmd_env, + ldapsearch, + base_opts, + + # TODO switch to ldif_wrap when we drop support for EL7 + # - EL7 only supports ldif-wrap + # - EL8 says it supports ldif_wrap (--help and man page), but actually + # accepts ldif-wrap or ldif_wrap + '-o "ldif-wrap=no" -LLL' + ].join(' ') + + @ldapadd = [ + cmd_env, + ldapadd, + base_opts, + ].join(' ') + + @ldapmodify = [ + cmd_env, + ldapmodify, + base_opts, + ].join(' ') + + @ldapdelete = [ + cmd_env, + ldapdelete, + base_opts, + ].join(' ') + end + + # @return Whether configuration enables TLS + # + # @param config Hash backend-specific options + def tls_enabled?(config) + tls_enabled = false + ldap_uri = config['ldap_uri'] + if ldap_uri.start_with?('ldapi') + tls_enabled = false + elsif ldap_uri.match(/^ldaps:/) + tls_enabled = true + elsif config.key?('enable_tls') + tls_enabled = config['enable_tls'] + else + tls_enabled = false + end + + tls_enabled + end + + # Updates the value of an existing key if the value has changed + # + # Do nothing if value is the same, as we don't want to change LDAP's + # modifyTimestamp + # + # @param key String key + # @param value String value + # + # @return results Hash + # * :result - Boolean indicating whether operation succeeded + # * :err_msg - String. Explanatory text upon failure; nil otherwise. + # + def update_value_if_changed(key, value) + results = nil + full_key_path = File.join(@instance_path, key) + current_result = get(key) + if current_result[:result] + if current_result[:result] != value + Puppet.debug("#{@name} Attempting modify for #{full_key_path}") + ldif = entry_modify_ldif(full_key_path, value) + ldap_results = ldap_modify(ldif) + if ldap_results[:success] + results = { :result => true, :err_msg => nil } + else + results = { :result => false, :err_msg => ldap_results[:err_msg] } + end + else + # no change needed + Puppet.debug("#{@name} #{full_key_path} value already correct") + results = { :result => true, :err_msg => nil } + end + else + err_msg = "Failed to retrieve current value for comparison: #{current_result[:err_msg]}" + results = { :result => false, :err_msg => err_msg } + end + + results + end + + # Verifies can access the LDAP server at the base DN + # + def verify_ldap_access + cmd = [ + @ldapsearch, + '-b', %Q{"#{@base_dn}"}, + '-s base', + '1.1' # only print out the dn, no attributes + ].join(' ') + + found = false + err_msg = nil + done = false + retries = @retries + until done + result = run_command(cmd) + case result[:exitstatus] + when 0 + found = true + done = true + when ldap_code_server_is_busy + if (retries == 0) + err_msg = result[:stderr] + done = true + end + else + err_msg = result[:stderr] + done = true + end + retries -= 1 + end + + unless found + raise("Plugin could not access #{@base_dn}: #{err_msg}") + end + end +end + diff --git a/lib/puppet_x/simpkv/plugin_template.rb b/lib/puppet_x/simpkv/plugin_template.rb index c027a0f..a1d3f06 100644 --- a/lib/puppet_x/simpkv/plugin_template.rb +++ b/lib/puppet_x/simpkv/plugin_template.rb @@ -12,15 +12,17 @@ # # - The plugin code must implement the API in this template. # -# - The plugin code must protect from cross-puppet-environment contamination. +# - The plugin code **must** protect from cross-puppet-environment contamination. # Different versions of the module containing this plugin may be loaded # into the puppetserver at the same time. So, unlike normal Ruby library # code for which only one version will be loaded at a time (e.g., gems # installed in the puppetserver), you have to explicitly design this plugin # code to prevent cross-environment-contamination. This is why the plugin # architecture requires this class to be anonymous and loads it appropriately. -# You must provide similar protections for any supporting Ruby code that you -# package in the module (e.g., a separate connector class). +# You **must** provide similar protections for any supporting Ruby code that you +# package in the module (e.g., a separate connector class). If you are not +# sure how to do this, just keep all of your plugin code within its anonymous +# class. # # - The plugin code must allow multiple instances to be instantiated and run # concurrently. @@ -33,6 +35,10 @@ # - When accessing the backend in the put(), get(), ... methods, the plugin code # should catch exceptions, convert them to meaningful error messages and then # return the failed status in its public API. +# +# - If your plugin uses Ruby Gems that do not come standard with Puppet Ruby, +# you must list them as requirements in your plugin's documentation and +# should provide instructions on how to install those Gems. ############################################################################### @@ -70,6 +76,10 @@ def initialize(name) # Insert the appropriate description of configuration your plugin # supports. # + # The simpkv adapter will call this method before any of the public API methods + # retrieve or change keystore state (i.e., delete(), deletetree(), exists(), + # that get(), list(), put()). + # # The plugin-specific configuration will be found in # `options['backends'][ options['backend'] ]` # @@ -267,8 +277,6 @@ def get(key) # * :keys - Hash of the key/value pairs for keys in the folder # * :folders - Array of sub-folder names # - # * :result - Hash of retrieved key/value pairs; nil if the - # retrieval operation failed # * :err_msg - String. Explanatory text upon failure; nil otherwise. # def list(keydir) diff --git a/lib/puppet_x/simpkv/simpkv.rb b/lib/puppet_x/simpkv/simpkv.rb index 1985eec..a38f89c 100644 --- a/lib/puppet_x/simpkv/simpkv.rb +++ b/lib/puppet_x/simpkv/simpkv.rb @@ -73,10 +73,16 @@ def initialize @plugin_info[plugin_type][:source] Puppet.warning(msg) else - @plugin_info[plugin_type] = { - :class => plugin_class, - :source => filename - } + if plugin_class.nil? + msg = "Skipping load of simpkv plugin from #{filename}: " + + 'Internal error: Plugin missing required plugin_class definition' + Puppet.warning(msg) + else + @plugin_info[plugin_type] = { + :class => plugin_class, + :source => filename + } + end end rescue SyntaxError => e Puppet.warning("simpkv plugin from #{filename} failed to load: #{e.message}") diff --git a/spec/acceptance/helpers/ldap_utils.rb b/spec/acceptance/helpers/ldap_utils.rb new file mode 100644 index 0000000..f793154 --- /dev/null +++ b/spec/acceptance/helpers/ldap_utils.rb @@ -0,0 +1,88 @@ +module Acceptance + module Helpers + module LdapUtils + + # @return DN for a folder path + # + # @param folder Folder path + # @param base_dn Base DN + # + def build_folder_dn(folder, base_dn) + parts = folder.split('/') + dn = '' + parts.reverse.each { |subfolder| dn += "ou=#{subfolder}," } + dn += base_dn + dn + end + + # @return DN for a key path + # + # @param key_path Key path + # @param base_dn Base DN + # + def build_key_dn(key_path, base_dn) + key_name = File.basename(key_path) + key_folder = File.dirname(key_path) + "simpkvKey=#{key_name},#{build_folder_dn(key_folder, base_dn)}" + end + + # @return Command with the LDAP server uri option and options and + # environment variables for authentication + # + # **ASSUMES** ldap_backend_config is valid! + # + # @param base_command Base command to be run (e.g., ldapsearch) + # @param ldap_backend_config ldap backend configuration + # + def build_ldap_command(base_command, ldap_backend_config) + ldap_uri = ldap_backend_config['ldap_uri'] + admin_dn = ldap_backend_config.fetch('admin_dn', nil) + admin_pw_file = ldap_backend_config.fetch('admin_pw_file', nil) + + opts = nil + enable_tls = nil + if ldap_uri.match(/^ldapi:/) + enable_tls = false + elsif ldap_uri.match(/^ldaps:/) + enable_tls = true + elsif ldap_backend_config.key?('enable_tls') + enable_tls = ldap_backend_config['enable_tls'] + else + enable_tls = false + end + + if enable_tls + tls_cert = ldap_backend_config['tls_cert'] + tls_key = ldap_backend_config['tls_key'] + tls_cacert = ldap_backend_config['tls_cacert'] + + cmd_env = [ + "LDAPTLS_CERT=#{tls_cert}", + "LDAPTLS_KEY=#{tls_key}", + "LDAPTLS_CACERT=#{tls_cacert}" + ].join(' ') + + if ldap_uri.match(/^ldap:/) + # StartTLS + opts = %Q{-ZZ -x -D "#{admin_dn}" -y #{admin_pw_file} -H #{ldap_uri}} + else + # TLS + opts = %Q{-x -D "#{admin_dn}" -y #{admin_pw_file} -H #{ldap_uri}} + end + + else + cmd_env = '' + if admin_pw_file + # unencrypted ldap or ldapi with simple authentication + opts = %Q{-x -D "#{admin_dn}" -y #{admin_pw_file} -H #{ldap_uri}} + else + # ldapi with EXTERNAL SASL + opts = "-Y EXTERNAL -H #{ldap_uri}" + end + end + + "#{cmd_env} #{base_command} #{opts}" + end + end + end +end diff --git a/spec/acceptance/helpers/test_data.rb b/spec/acceptance/helpers/test_data.rb index f0b19f0..acb3c83 100644 --- a/spec/acceptance/helpers/test_data.rb +++ b/spec/acceptance/helpers/test_data.rb @@ -55,7 +55,7 @@ def generate_backend_hiera(backend_configs) backends = {} backend_configs.each do |name, config| backend_tag = "simpkv::backend::#{name}" - hiera[backend_tag] = config + hiera[backend_tag] = Marshal.load(Marshal.dump(config)) hiera[backend_tag]['id'] = name backends[name]= "%{alias('#{backend_tag}')}" end diff --git a/spec/acceptance/helpers/utils.rb b/spec/acceptance/helpers/utils.rb index 08afe0d..ac304ac 100644 --- a/spec/acceptance/helpers/utils.rb +++ b/spec/acceptance/helpers/utils.rb @@ -6,12 +6,54 @@ module Acceptance::Helpers; end module Acceptance::Helpers::Utils + # @return Backend configuration Hash for the backend corresponding to app_id and + # plugin_type + # + # FIXME: Selects the backend based on an exact match with app_id. Should use + # the fuzzy matching logic built into the simpkv API. + # + # @param app_id The app_id for a key or '', if none specified + # + # @param plugin_type The plugin type to verify or nil if no verification + # is required + # + # @param backend_hiera Hash of backend configuration ('simpkv::options' Hash) + # + # @raise RuntimeError if no backend for the app_id exists in backend_hiera + # + def backend_config_for_app_id(app_id, plugin_type, backend_hiera) + config = {} + + if backend_hiera['simpkv::options']['backends'].key?(app_id) + if backend_hiera['simpkv::options']['backends'][app_id].is_a?(String) + # Assume this is an alias for simpkv::backend:: + config = backend_hiera["simpkv::backend::#{app_id}"] + else + backend_hiera['simpkv::options']['backends'][app_id] + end + elsif backend_hiera['simpkv::options']['backends'].key?('default') + if backend_hiera['simpkv::options']['backends']['default'].is_a?(String) + # Assume this is an alias for simpkv::backend::default + config = backend_hiera['simpkv::backend::default'] + else + config = backend_hiera['simpkv::options']['backends']['default'] + end + end + + if config.empty? || ( !plugin_type.nil? && (config['type'] != plugin_type) ) + fail("No '#{plugin_type}' backend found for #{app_id}") + end + + config + end + # @return key string persisted to the backend for the key_data # # FIXME: If the data is binary and specified by the 'file' attribute, - # this method ASSUMES the file is in the simpkv_test module + # this method *ASSUMES *the file is in the simpkv_test module + # + # @param key_data Hash with key data corresponding to Simpkv_test::KeyData # - # @param key_data def key_data_string(key_data) key_hash = {} if key_data.key?('value') diff --git a/spec/acceptance/nodesets/oel.yml b/spec/acceptance/nodesets/oel.yml index 101da67..5d06091 100644 --- a/spec/acceptance/nodesets/oel.yml +++ b/spec/acceptance/nodesets/oel.yml @@ -6,11 +6,11 @@ end -%> HOSTS: - oel7: + oel8: roles: - default - platform: el-7-x86_64 - box: generic/oracle7 + platform: el-8-x86_64 + box: generic/oracle8 hypervisor: <%= hypervisor %> CONFIG: diff --git a/spec/acceptance/nodesets/oel7.yml b/spec/acceptance/nodesets/oel7.yml index 5d06091..101da67 100644 --- a/spec/acceptance/nodesets/oel7.yml +++ b/spec/acceptance/nodesets/oel7.yml @@ -6,11 +6,11 @@ end -%> HOSTS: - oel8: + oel7: roles: - default - platform: el-8-x86_64 - box: generic/oracle8 + platform: el-7-x86_64 + box: generic/oracle7 hypervisor: <%= hypervisor %> CONFIG: diff --git a/spec/acceptance/suites/default/validate_file_entries.rb b/spec/acceptance/suites/default/validate_file_entries.rb index c9e373b..8814f58 100644 --- a/spec/acceptance/suites/default/validate_file_entries.rb +++ b/spec/acceptance/suites/default/validate_file_entries.rb @@ -3,9 +3,17 @@ # Validate file-plugin-managed keys on the local filesystem # -# - Conforms to the API specified in 'a simpkv plugin test' shared_examples -# - Uses local filesystem commands, since the file plugin has to be on the -# same host as the file keystore +# For each key specification, +# - Selects the backend whose name matches its 'app_id' or 'default', when +# no match is found +# - Checks for the existence of the key in the appropriate location in the +# backend +# - When the key is supposed to exist and does exist, verifies the stored data +# +# Conforms to the API specified in 'a simpkv plugin test' shared_examples +# +# NOTE: Uses local filesystem commands, since the file plugin has to be on the +# same host as the file keystore. # # @param key_info Hash of key information whose format corresponds to the # Simpkv_test::KeyInfo type alias @@ -21,52 +29,24 @@ # # @return Whether validation of keys succeeded # -def validate_file_entries(key_info, keys_should_exist, backend_hiera, host) - if keys_should_exist - validate_file_entries_present(key_info, backend_hiera, host) - else - validate_file_entries_absent(key_info, backend_hiera, host) - end -end - -# Validate that file-plugin-managed keys exist on the local filesystem -# -# For each key specification, -# - Selects the backend whose name matches its 'app_id' or 'default', when -# no match is found -# - Checks for the existence of the appropriate file for the backend -# - Verifies the file content, when the file exists -# -# @param key_info Hash of key information whose format corresponds to the -# Simpkv_test::KeyInfo type alias +# @raise RuntimeError if the appropriate backend for each app_id within key_info +# cannot be found in backend_hiera # -# @param backend_hiera Hash of backend configuration ('simpkv::options' Hash) -# -# @param host Host object on which the validator will execute commands -# - Must be same host as file file keystore -# -# @return Whether validation of keys succeeded -# -def validate_file_entries_present(key_info, backend_hiera, host) +def validate_file_entries(key_info, keys_should_exist, backend_hiera, host) errors = [] key_info.each do |app_id, key_struct| - root_path = file_root_path_for_app_id(app_id, backend_hiera) + config = backend_config_for_app_id(app_id, 'file', backend_hiera) key_struct.each do |key_type, keys| - key_root_path = (key_type == 'global') ? "#{root_path}/globals" : "#{root_path}/environments/production" keys.each do |key, key_data| - key_path = "#{key_root_path}/#{key}" - expected_key_string = key_data_string(key_data) - result = on(host, "cat #{key_path}", :accept_all_exit_codes => true) - if result.exit_code == 0 - if result.stdout.strip != expected_key_string - errors << [ - "Contents of #{key_path} did not match expected:", - " Expected: #{expected_key_string}", - " Actual: #{result.stdout}" - ].join("\n") - end + result = {} + if keys_should_exist + result = validate_file_key_entry_present(key, key_type, key_data, config, host) else - errors << "Validation of #{key_path} presence and data failed: #{result.stderr}" + result = validate_file_key_entry_absent(key, key_type, config, host) + end + + unless result[:success] + errors << result[:err_msg] end end end @@ -84,63 +64,89 @@ def validate_file_entries_present(key_info, backend_hiera, host) end end -# Validate that file-plugin-managed keys do not exist on the local filesystem +# Validate a that file-plugin-managed key exists on the local filesystem and +# has the correct stored data # -# For each key specification, -# - Selects the backend whose name matches its 'app_id' or 'default', when -# no match is found -# - Checks for the existence of the appropriate file for the backend +# @param key Key name # -# @param key_info Hash of key information whose format corresponds to the -# Simpkv_test::KeyInfo type alias +# @param key_type 'env' or 'global' for a key tied to the Puppet-environment +# or a global key, respectively # -# @param backend_hiera Hash of backend configuration ('simpkv::options' Hash) +# @param key_data Hash of key data whose format corresponds to the +# Simpkv_test::KeyData type alias +# +# @param config Backend configuration # # @param host Host object on which the validator will execute commands # - Must be same host as file file keystore # -# @return Whether validation of keys succeeded +# @return results Hash +# * :success - whether the validation succeeded +# * :err_msg - error message upon failure or nil otherwise # -def validate_file_entries_absent(key_info, backend_hiera, host) - errors = [] - key_info.each do |app_id, key_struct| - root_path = file_root_path_for_app_id(app_id, backend_hiera) - key_struct.each do |key_type, keys| - key_root_path = (key_type == 'global') ? "#{root_path}/globals" : "#{root_path}/environments/production" - keys.each do |key, key_data| - key_path = "#{key_root_path}/#{key}" - result = on(host, "ls -l #{key_path}", :accept_all_exit_codes => true) - if result.exit_code == 0 - errors << "Validation of #{key_path} absence failed: #{result.stdout}" - end - end - end - end +def validate_file_key_entry_present(key, key_type, key_data, config, host) + result = { :success => true } - if errors.size == 0 - true - else - warn('Validation Failures:') - errors.each do |error| - warn(" #{error}") + key_path = filesystem_key_path(key, key_type, config) + cmd_result = on(host, "cat #{key_path}", :accept_all_exit_codes => true) + if cmd_result.exit_code == 0 + expected_key_string = key_data_string(key_data) + if cmd_result.stdout.strip != expected_key_string + result = { + :success => false, + :err_msg => [ + "Data for #{key} did not match expected:", + " Expected: #{expected_key_string}", + " Actual: #{result.stdout}" + ].join("\n") + } end - - false + else + result = { + :success => false, + :err_msg => "Validation of #{key} presence failed: Could not find #{key_path}" + } end + + result end -# @return Root path for the file backend that corresponds to the app_id +# Validate a that file-plugin-managed key does not exists on the local filesystem # -# @param app_id The app_id for a key or '', if none specified -# @param backend_hiera Hash of backend configuration ('simpkv::options' Hash) +# @param key Key name +# +# @param key_type 'env' or 'global' for a key tied to the Puppet-environment +# or a global key, respectively +# +# @param config Backend configuration +# +# @param host Host object on which the validator will execute commands +# - Must be same host as file file keystore # -def file_root_path_for_app_id(app_id, backend_hiera) - root_path = '' - if backend_hiera['simpkv::options']['backends'].keys.include?(app_id) - root_path = backend_hiera["simpkv::backend::#{app_id}"]['root_path'] - elsif backend_hiera['simpkv::options']['backends'].keys.include?('default') - root_path = backend_hiera['simpkv::backend::default']['root_path'] +# @return results Hash +# * :success - whether the validation succeeded +# * :err_msg - error message upon failure or nil otherwise +# +def validate_file_key_entry_absent(key, key_type, config, host) + result = { :success => true } + + key_path = filesystem_key_path(key, key_type, config) + cmd_result = on(host, "ls -l #{key_path}", :accept_all_exit_codes => true) + if result.exit_code == 0 + result = { + :success => false, + :err_msg => "Validation of #{key} absence failed: Found #{key_path}" + } end - root_path + result +end + +def filesystem_key_path(key, key_type, config) + root_path = config.key?('root_path') ? config['root_path'] : '/var/simp/simpkv/file/default' + if key_type == 'global' + File.join(root_path, 'globals', key) + else + File.join(root_path, 'environments', 'production', key) + end end diff --git a/spec/acceptance/suites/ldap_plugin/00_ldap_server_spec.rb b/spec/acceptance/suites/ldap_plugin/00_ldap_server_spec.rb new file mode 100644 index 0000000..27d7a72 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/00_ldap_server_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper_acceptance' +require_relative 'ldap_test_configuration' + +test_name 'ldap server setup' + +describe 'ldap server setup' do + include_context('ldap test configuration') + let(:bootstrap_ldif) { File.read(File.join(__dir__, 'files', 'bootstrap.ldif')) } + + hosts.each do |host| + context "host set up on #{host}" do + it 'has a proper FQDN' do + on(host, "hostname #{fact_on(host, 'fqdn')}") + on(host, 'hostname -f > /etc/hostname') + end + end + end + + # FIXMEs + # - This test does not yet use a SIMP profile to set up the simp_data LDAP + # instance. + # - This test manually works around the lack of schema management in + # simp/ds389 (SIMP-9676). + # - This test does not set an ACI in the simp_data LDAP instance that would + # allow the puppet user to access the instance via without a password. + # + hosts_with_role(hosts, 'ldap_server').each do |host| + context "LDAP server set up on #{host}" do + let(:manifest) do + 'include ds389' + end + + let(:hieradata) do + { + 'ds389::instances' => { + 'simp_data_without_tls' => { + 'base_dn' => ldap_instances['simp_data_without_tls'][:base_dn], + 'root_dn' => ldap_instances['simp_data_without_tls'][:root_dn], + 'root_dn_password' => ldap_instances['simp_data_without_tls'][:root_pw], + 'listen_address' => '0.0.0.0', + 'port' => ldap_instances['simp_data_without_tls'][:port], + 'bootstrap_ldif_content' => bootstrap_ldif + }, + + 'simp_data_with_tls' => { + 'base_dn' => ldap_instances['simp_data_with_tls'][:base_dn], + 'root_dn' => ldap_instances['simp_data_with_tls'][:root_dn], + 'root_dn_password' => ldap_instances['simp_data_with_tls'][:root_pw], + 'listen_address' => '0.0.0.0', + 'port' => ldap_instances['simp_data_with_tls'][:port], + 'secure_port' => ldap_instances['simp_data_with_tls'][:secure_port], + 'bootstrap_ldif_content' => bootstrap_ldif, + 'enable_tls' => true, + 'tls_params' => { + 'source' => certdir + } + } + } + } + end + + it 'disables firewall for LDAP access via custom ports' do + # FIXME ds389 module does NOT manage firewall rules. So, for hosts that + # have firewalld running by default, we need to make sure it is stopped + # or add our own rules. This should be a non-issue when SIMP provides + # a 389-DS instance for simpkv in the simp_ds389 module. + on(host, 'puppet resource service firewalld ensure=stopped') + end + + it 'works with no errors' do + set_hieradata_on(host, hieradata) + apply_manifest_on(host, manifest, catch_failures: true) + end + + it 'is idempotent' do + apply_manifest_on(host, manifest, catch_changes: true) + end + + it "applies a simpkv custom schema to all 389-DS instances on #{host}" do + ldap_instances.each do |instance,config| + src = File.join(__dir__, 'files', '70simpkv.ldif') + dest = "/etc/dirsrv/slapd-#{instance}/schema/70simpkv.ldif" + scp_to(host, src, dest) + on(host, "chown dirsrv:dirsrv #{dest}") + #FIXME use dsconf schema reload instead + on(host, %Q{schema-reload.pl -Z #{instance} -D "#{config[:root_dn]}" -w "#{config[:root_pw]}" -P LDAPI}) + on(host, "egrep 'ERR\s*-\s*schemareload' /var/log/dirsrv/slapd-#{instance}/errors", + :acceptable_exit_codes => [1]) + end + end + end + end +end diff --git a/spec/acceptance/suites/ldap_plugin/05_ldap_client_setup_spec.rb b/spec/acceptance/suites/ldap_plugin/05_ldap_client_setup_spec.rb new file mode 100644 index 0000000..6a69e0d --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/05_ldap_client_setup_spec.rb @@ -0,0 +1,46 @@ + +require 'spec_helper_acceptance' +require_relative 'ldap_test_configuration' + +test_name 'simpkv client setup' + +describe 'simpkv client setup' do + include_context('ldap test configuration') + + # FIXME Can't compile manifests with simpkv functions unless the files containing + # the admin passwords already exist on each host + context 'Ensure LDAP password files for clients exists prior to using simpkv functions' do + let(:manifest) { <<-EOM + file { '/etc/simp': ensure => 'directory' } + + file { '#{ldap_instances['simp_data_without_tls'][:admin_pw_file]}': + ensure => present, + owner => 'root', + group => 'root', + mode => '0400', + content => Sensitive('#{ldap_instances['simp_data_without_tls'][:admin_pw]}') + } + + file { '#{ldap_instances['simp_data_with_tls'][:admin_pw_file]}': + ensure => present, + owner => 'root', + group => 'root', + mode => '0400', + content => Sensitive('#{ldap_instances['simp_data_with_tls'][:admin_pw]}') + } + EOM + } + + hosts.each do |host| + it "should create admin pw files needed by ldap plugin on #{host}" do + apply_manifest_on(host, manifest, :catch_failures => true) + end + end + end + + context 'Ensure openldap-clients package is installed on clients prior to using simpkv functions' do + it 'should install openlap-clients package' do + install_package_unless_present_on(hosts, 'openldap-clients') + end + end +end diff --git a/spec/acceptance/suites/ldap_plugin/10_ldapi_spec.rb b/spec/acceptance/suites/ldap_plugin/10_ldapi_spec.rb new file mode 100644 index 0000000..f6d064f --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/10_ldapi_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper_acceptance' +require_relative 'ldap_test_configuration' +require_relative 'validate_ldap_entries' + +test_name 'ldap_plugin using ldapi' + +describe 'ldap_plugin using ldapi' do + include_context('ldap test configuration') + + # Arbitrarily using the 389-DS instance configured with TLS. + let(:ldap_instance_name) { 'simp_data_with_tls' } + let(:ldap_instance) { ldap_instances[ldap_instance_name] } + let(:ldap_uri) { "ldapi://%2fvar%2frun%2fslapd-#{ldap_instance_name}.socket" } + let(:common_ldap_config) {{ + 'type' => 'ldap', + 'ldap_uri' => ldap_uri, + 'base_dn' => ldap_instance[:simpkv_base_dn], + }} + + # Command to run on the test host to clear out all stored key data. + # - All stored in same 389-DS instance, so single clear command + let(:clear_data_cmd) { + [ + build_ldap_command('ldapdelete', common_ldap_config), + '-r', + %Q{"ou=instances,#{ldap_instance[:simpkv_base_dn]}"} + ].join(' ') + } + + # simpkv::options hieradata for 3 distinct backends + # - 2 use LDAPI with EXTERNAL SASL authentication + # - 1 uses LDAPI with simple authentication + let(:backend_hiera) { + backend_configs = { + id1 => common_ldap_config, + id2 => common_ldap_config, + id3 => common_ldap_config.merge({ + 'admin_dn' => ldap_instance[:admin_dn], + 'admin_pw_file' => ldap_instance[:admin_pw_file] + }) + } + + # will set each 'id' to its corresponding backend name, which + # results in a unique tree for that backend name beneath the + # simpkv tree in the 389-DS instance + generate_backend_hiera(backend_configs) + } + + hosts_with_role(hosts, 'ldap_server').each do |host| + context "simpkv ldap plugin on #{host} using ldapi" do + it_behaves_like 'a simpkv plugin test', host + + context 'LDAP-specific features' do + let(:manifest) { %Q{simpkv::put('mykey', "Value for mykey", {})} } + let(:get_ldap_attributes_cmd) { + dn = "simpkvKey=mykey,ou=production,ou=environments,ou=default,ou=instances,#{ldap_instance[:simpkv_base_dn]}" + [ + build_ldap_command('ldapsearch', common_ldap_config), + '-o "ldif-wrap=no"', + '-LLL', + %Q{-b "#{dn}"}, + '+' + ].join(' ') + } + + it 'should not change LDAP modifyTimestamp when no changes are made' do + # store a key and retrieve its LDAP modifyTimestamp + set_hiera_and_apply_on(host, backend_hiera, manifest) + result1 = on(host, get_ldap_attributes_cmd) + timestamp1 = result1.stdout.split("\n").delete_if{ |line| !line.start_with?('modifyTimestamp:') }.first + + # store a key with the same content and retrieve its LDAP modifyTimestamp + set_hiera_and_apply_on(host, backend_hiera, manifest) + result2 = on(host, get_ldap_attributes_cmd) + timestamp2 = result2.stdout.split("\n").delete_if{ |line| !line.start_with?('modifyTimestamp:') }.first + + # key was not modified, so timestamp should be the same + expect( timestamp2 ).to eq(timestamp1) + end + end + end + end +end diff --git a/spec/acceptance/suites/ldap_plugin/20_ldap_protos_spec.rb b/spec/acceptance/suites/ldap_plugin/20_ldap_protos_spec.rb new file mode 100644 index 0000000..d9f7fe8 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/20_ldap_protos_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper_acceptance' +require_relative 'ldap_test_configuration' + +test_name 'ldap_plugin using unencrypted and encrypted LDAP' + +describe 'ldap_plugin using unencrypted and encrypted LDAP' do + include_context('ldap test configuration') + + # This test uses 2 389-DS instances (distinct LDAP servers) for 3 simpkv + # backends: + # * LDAP instance that requires encryption is used for 2 simpkv backends: + # - simpkv backend communicating via TLS + # - simpkv backend communicating via StartTLS + # * LDAP instance that does not allow encryption is used as 1 simpkv backend. + # + let(:ldap_with_tls) { ldap_instances['simp_data_with_tls'] } + let(:ldap_without_tls) { ldap_instances['simp_data_without_tls'] } + + hosts_with_role(hosts, 'ldap_server').each do |server| + context "with LDAP servers on #{server}" do + let(:server_fqdn) { fact_on(server, 'fqdn').strip } + let(:ldaps_uri) { "ldaps://#{server_fqdn}:#{ldap_with_tls[:secure_port]}" } + let(:ldap_starttls_uri) { "ldap://#{server_fqdn}:#{ldap_with_tls[:port]}" } + let(:ldap_uri) { "ldap://#{server_fqdn}:#{ldap_without_tls[:port]}" } + + hosts_with_role(hosts, 'client').each do |client| + context "with LDAP client #{client}" do + let(:client_fqdn) { fact_on(client, 'fqdn').strip } + let(:tls_cert) { "#{certdir}/public/#{client_fqdn}.pub" } + let(:tls_key) { "#{certdir}/private/#{client_fqdn}.pem" } + let(:tls_cacert) { "#{certdir}/cacerts/cacerts.pem" } + let(:ldaps_config) {{ + 'type' => 'ldap', + 'ldap_uri' => ldaps_uri, + 'base_dn' => ldap_with_tls[:simpkv_base_dn], + 'admin_dn' => ldap_with_tls[:admin_dn], + 'admin_pw_file' => ldap_with_tls[:admin_pw_file], + 'tls_cert' => tls_cert, + 'tls_key' => tls_key, + 'tls_cacert' => tls_cacert, + }} + + let(:ldap_starttls_config) {{ + 'type' => 'ldap', + 'ldap_uri' => ldap_starttls_uri, + 'base_dn' => ldap_with_tls[:simpkv_base_dn], + 'admin_dn' => ldap_with_tls[:admin_dn], + 'admin_pw_file' => ldap_with_tls[:admin_pw_file], + 'enable_tls' => true, + 'tls_cert' => tls_cert, + 'tls_key' => tls_key, + 'tls_cacert' => tls_cacert, + }} + + let(:ldap_config) {{ + 'type' => 'ldap', + 'ldap_uri' => ldap_uri, + 'base_dn' => ldap_without_tls[:simpkv_base_dn], + 'admin_dn' => ldap_without_tls[:admin_dn], + 'admin_pw_file' => ldap_without_tls[:admin_pw_file] + }} + + # Command to run on the test host to clear out all stored key data. + let(:clear_data_cmd) { + [ + build_ldap_command('ldapdelete', ldaps_config), + '-r', + %Q{"ou=instances,#{ldap_with_tls[:simpkv_base_dn]}"}, + ' ; ', + + build_ldap_command('ldapdelete', ldap_config), + '-r', + %Q{"ou=instances,#{ldap_without_tls[:simpkv_base_dn]}"}, + ].join(' ') + } + + # simpkv::options hieradata for 3 distinct backends + let(:backend_hiera) { + backend_configs = { + id1 => ldaps_config, + id2 => ldap_starttls_config, + id3 => ldap_config + } + + # will set each 'id' to its corresponding backend name, which + # results in unique trees for that backend name beneath the + # simpkv tree in the 389-DS instances + generate_backend_hiera(backend_configs) + } + + context "simpkv ldap_plugin on #{client} using ldap with & without TLS to #{server}" do + it_behaves_like 'a simpkv plugin test', client + end + end + end + end + end +end diff --git a/spec/acceptance/suites/ldap_plugin/30_errors_spec.rb b/spec/acceptance/suites/ldap_plugin/30_errors_spec.rb new file mode 100644 index 0000000..cedaede --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/30_errors_spec.rb @@ -0,0 +1,165 @@ +require 'spec_helper_acceptance' +require_relative 'ldap_test_configuration' + +test_name 'ldap_plugin errors' + +describe 'ldap_plugin errors' do + include_context('ldap test configuration') + + # The purpose of this test is twofold: + # - Make sure typical configuration error cases cause compilation errors. + # - Make sure the error message that pop out are useful! + # + # This test uses 2 389-DS instances (distinct LDAP servers) for testing + # ldap_plugin errors related to communication with the LDAP servers + # * LDAP instance that requires encryption (TLS or StartTLS) + # * LDAP instance that does not allow encryption + # + let(:ldap_with_tls) { ldap_instances['simp_data_with_tls'] } + let(:ldap_without_tls) { ldap_instances['simp_data_without_tls'] } + + # key will go to the default backend + let(:manifest) { %Q{simpkv::put('mykey', "Value for mykey", {})} } + let(:new_pki_certs_dir) { '/etc/pki/simp-testing-new' } + + context 'TLS error test prep' do + it 'generates a second set of host PKI certificates' do + # Generate and install new PKI certificates to a different directory + # on each SUT + # - Uses PKI-generation-infrastructure already available on the node + # with the default role + server = only_host_with_role(hosts, 'default') + host_dir = '/root/pki' + on(server, "cd #{host_dir}; cat #{host_dir}/pki.hosts | xargs bash make.sh") + + Dir.mktmpdir do |cert_dir| + scp_from(server, host_dir, cert_dir) + hosts.each { |sut| copy_pki_to(sut, cert_dir, new_pki_certs_dir) } + end + end + end + + hosts_with_role(hosts, 'ldap_server').each do |server| + context "with LDAP servers on #{server}" do + let(:server_fqdn) { fact_on(server, 'fqdn').strip } + let(:valid_ldaps_uri) { "ldaps://#{server_fqdn}:#{ldap_with_tls[:secure_port]}" } + let(:valid_ldap_uri) { "ldap://#{server_fqdn}:#{ldap_without_tls[:port]}" } + let(:failed_regex) { + # The full failure message tells the user the ldapsearch command that failed + # and its error messages, so that the user doesn't have to apply the manifest + # with --debug to figure out what is going on! + %r{Unable to construct 'ldap/default': Plugin could not access ou=simpkv,o=puppet,dc=simp.*ldapsearch} + } + + hosts_with_role(hosts, 'client').each do |client| + context "with LDAP client #{client}" do + + # valid backend config for ldap + let(:valid_ldap_config) {{ + 'type' => 'ldap', + 'ldap_uri' => valid_ldap_uri, + 'base_dn' => ldap_without_tls[:simpkv_base_dn], + 'admin_dn' => ldap_without_tls[:admin_dn], + 'admin_pw_file' => ldap_without_tls[:admin_pw_file] + }} + + + context 'with LDAP configuration errors' do + it 'fails to compile when LDAP URI has invalid host' do + bad_uri = 'ldap://oops.test.local' + invalid_config = valid_ldap_config.merge({ 'ldap_uri' => bad_uri }) + backend_configs = { 'default' => invalid_config } + backend_hiera = generate_backend_hiera(backend_configs) + + result = set_hiera_and_apply_on(client, backend_hiera, manifest, + { :expect_failures => true } ) + + expect( result.stderr ).to match(failed_regex) + end + + it 'fails to compile when LDAP URI has invalid port' do + # Our simpkv LDAP servers are intentionally not on the standard + # port (389) for LDAP, because they do not contain accounts data. + bad_uri = "ldap://#{server_fqdn}:389" + invalid_config = valid_ldap_config.merge({ 'ldap_uri' => bad_uri }) + backend_configs = { 'default' => invalid_config } + backend_hiera = generate_backend_hiera(backend_configs) + + result =set_hiera_and_apply_on(client, backend_hiera, manifest, + { :expect_failures => true } ) + + expect( result.stderr ).to match(failed_regex) + end + + it 'fails to compile when base DN is invalid' do + bad_base_dn = valid_ldap_config['base_dn'] + ',dc=oops' + invalid_config = valid_ldap_config.merge({ 'base_dn' => bad_base_dn }) + backend_configs = { 'default' => invalid_config } + backend_hiera = generate_backend_hiera(backend_configs) + + result = set_hiera_and_apply_on(client, backend_hiera, manifest, + { :expect_failures => true } ) + + expect( result.stderr ).to match(failed_regex) + end + + it 'fails to compile when admin DN is invalid' do + bad_admin_dn = valid_ldap_config['admin_dn'] + ',dc=oops' + invalid_config = valid_ldap_config.merge({ 'admin_dn' => bad_admin_dn }) + backend_configs = { 'default' => invalid_config } + backend_hiera = generate_backend_hiera(backend_configs) + + result = set_hiera_and_apply_on(client, backend_hiera, manifest, + { :expect_failures => true } ) + + expect( result.stderr ).to match(failed_regex) + end + + it 'fails to compile when admin password is invalid' do + # create a password file that has the right permissions but wrong content + bad_admin_pw_file = '/root/wrong_admin_password.txt' + on(client, "echo wrong_admin_password > #{bad_admin_pw_file}") + on(client, "chmod 600 #{bad_admin_pw_file}") + + invalid_config = valid_ldap_config.merge({ 'admin_pw_file' => bad_admin_pw_file }) + backend_configs = { 'default' => invalid_config } + backend_hiera = generate_backend_hiera(backend_configs) + + result = set_hiera_and_apply_on(client, backend_hiera, manifest, + { :expect_failures => true } ) + + expect( result.stderr ).to match(failed_regex) + end + + it 'fails to compile when TLS certs are invalid' do + # client is using different certs than the LDAP server knows about! + client_fqdn = fact_on(client, 'fqdn').strip + tls_cert = "#{new_pki_certs_dir}/public/#{client_fqdn}.pub" + tls_key = "#{new_pki_certs_dir}/private/#{client_fqdn}.pem" + tls_cacert = "#{new_pki_certs_dir}/cacerts/cacerts.pem" + + invalid_config = { + 'type' => 'ldap', + 'ldap_uri' => valid_ldaps_uri, + 'base_dn' => ldap_with_tls[:simpkv_base_dn], + 'admin_dn' => ldap_with_tls[:admin_dn], + 'admin_pw_file' => ldap_with_tls[:admin_pw_file], + 'tls_cert' => tls_cert, + 'tls_key' => tls_key, + 'tls_cacert' => tls_cacert, + } + + backend_configs = { 'default' => invalid_config } + backend_hiera = generate_backend_hiera(backend_configs) + + result = set_hiera_and_apply_on(client, backend_hiera, manifest, + { :expect_failures => true } ) + + expect( result.stderr ).to match(failed_regex) + end + end + end + end + end + end +end diff --git a/spec/acceptance/suites/ldap_plugin/files/70simpkv.ldif b/spec/acceptance/suites/ldap_plugin/files/70simpkv.ldif new file mode 100644 index 0000000..d58b8c2 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/files/70simpkv.ldif @@ -0,0 +1,44 @@ +# 70simpkv.ldif - Used for simpkv entries +################################################################################ +# +dn: cn=schema +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.1 + NAME 'simpkvKey' + DESC 'key' + SUP name + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +attributeTypes: ( + 1.3.6.1.4.1.47012.1.1.1.1.1.2 + NAME 'simpkvJsonValue' + DESC 'JSON-formatted value' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# +objectClasses: ( + 1.3.6.1.4.1.47012.1.1.1.1.2.1 + NAME 'simpkvEntry' + DESC 'simpkv entry' + SUP top + STRUCTURAL + MUST ( simpkvKey $ simpkvJsonValue ) + X-ORIGIN 'SIMP simpkv' + ) +# +################################################################################ +# + diff --git a/spec/acceptance/suites/ldap_plugin/files/bootstrap.ldif b/spec/acceptance/suites/ldap_plugin/files/bootstrap.ldif new file mode 100644 index 0000000..24ac9a1 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/files/bootstrap.ldif @@ -0,0 +1,30 @@ +# Create the top-level database +dn: cn="dc=simp",cn=mapping tree,cn=config +cn: dc=simp +objectClass: top +objectClass: extensibleObject +objectClass: nsMappingTree +nsslapd-state: backend +nsslapd-backend: UserData + +dn: dc=simp +dc: simp +objectClass: top +objectClass: domain + +dn: ou=Aliases,dc=simp +ou: Aliases +objectClass: top +objectClass: organizationalUnit + +dn: o=puppet,dc=simp +o: puppet +objectClass: top +objectClass: organization + +dn: ou=simpkv,o=puppet,dc=simp +ou: simpkv +objectClass: top +objectClass: organizationalUnit +description: Root directory of all simpkv LDAP trees + diff --git a/spec/acceptance/suites/ldap_plugin/ldap_test_configuration.rb b/spec/acceptance/suites/ldap_plugin/ldap_test_configuration.rb new file mode 100644 index 0000000..495bec0 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/ldap_test_configuration.rb @@ -0,0 +1,100 @@ +# Common configuration for the ldap plugin test +# +# * LDAP configuration needed to set up and access the LDAP +# instances containing simpkv data. +# - One instance will be TLS enabled and the other will not. +# +# * Context for 'a simpkv plugin test' shared_examples +# * Methods from Acceptance::Helpers::LdapUtils that the LDAP tests use +# to build their respective keystore clear data commands +# +require 'acceptance/helpers/ldap_utils' +require_relative 'validate_ldap_entries' + +shared_context 'ldap test configuration' do + include Acceptance::Helpers::LdapUtils + + # TODO Create a separate administrator bind DN and configure it appropriately + # for LDAPI via an ACI + # - This test configures the ldap_plugin to use the appropriate instance root + # dn and password as its admin user, instead of a specific simpkv admin + # user and password for the simpkv subtree within the instance. + # - The 'simp' 389-DS instances do not have an ACI set up for account-to-DN + # mapping for the 'root' user. + # + let(:base_dn) { 'dc=simp' } + let(:root_dn) { 'cn=Directory_Manager' } + let(:simpkv_base_dn) { "ou=simpkv,o=puppet,#{base_dn}"} + let(:admin_dn) { root_dn } + + let(:ldap_instances) { { + 'simp_data_without_tls' => { + # ds389::instance config + :base_dn => base_dn, + :root_dn => root_dn, + :root_pw => 'P@ssw0rdP@ssw0rd!N0TLS', + :port => 387, + + # simpkv ldap_plugin config + :simpkv_base_dn => simpkv_base_dn, + :admin_dn => admin_dn, + :admin_pw => 'P@ssw0rdP@ssw0rd!N0TLS', + :admin_pw_file => '/etc/simp/simp_data_without_tls_pw.txt', + }, + + 'simp_data_with_tls' => { + # ds389::instance config + :base_dn => base_dn, + :root_dn => root_dn, + :root_pw => 'P@ssw0rdP@ssw0rd!TLS', + :port => 388, # for StartTLS + :secure_port => 637, + + # simpkv ldap_plugin config + :simpkv_base_dn => simpkv_base_dn, + :admin_dn => admin_dn, + :admin_pw => 'P@ssw0rdP@ssw0rd!TLS', + :admin_pw_file => '/etc/simp/simp_data_with_tls_pw.txt', + } + + } } + + # PKI general + let(:certdir) { '/etc/pki/simp-testing/pki' } + + + # Context for 'a simpkv plug test' shared_examples + + # Method object to validate key/folder entries in an LDAP instance + # - Conforms to the API specified in 'a simpkv plugin test' shared_examples + let(:validator) { method(:validate_ldap_entries) } + + # The ids below are the backend/app_id names used in the test: + # - One must be 'default' or simpkv::options validation will fail. + # - 'a simpkv plugin test' shared_examples assumes there is a one-to-one + # mapping of the app_ids in the input key data to the backend names. + # Although simpkv supports fuzzy logic for that mapping, we set the + # backend names/app_ids to the same values, here, for simplicity. The + # fuzzy mapping logic is tested in the unit test. + # - The input-data-generator currently supports exactly 3 app_ids. + # - 'default' app_id is mapped to '' in the generated input key data, which, in + # turn causes simpkv functions to be called in the test manifests without + # an app_id set. In other words, 'default' maps to the expected, normal + # usage of simpkv functions. + # + let(:id1) { 'default' } + let(:id2) { 'custom' } + let(:id3) { 'custom_snowflake' } + + + # Hash of initial key information for the 3 test backends/app_ids. + # + # 'a simpkv plugin test' uses this data to test key storage operations + # and then transform the data into subsets that it uses to test key/folder + # existence, folder lists, and key and folder delete operations. + let(:initial_key_info) { + generate_initial_key_info(id1, id2, id3) + } + +end + diff --git a/spec/acceptance/suites/ldap_plugin/nodesets/default.yml b/spec/acceptance/suites/ldap_plugin/nodesets/default.yml new file mode 100644 index 0000000..da20de0 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/nodesets/default.yml @@ -0,0 +1,32 @@ +<% + if ENV['BEAKER_HYPERVISOR'] + hypervisor = ENV['BEAKER_HYPERVISOR'] + else + hypervisor = 'vagrant' + end +-%> +HOSTS: + el8: + roles: + - default + - ldap_server + - client + platform: el-8-x86_64 + box: generic/centos8 + hypervisor: <%= hypervisor %> + + el7: + roles: + - client + platform: el-7-x86_64 + box: centos/7 + hypervisor: <%= hypervisor %> + +CONFIG: + log_level: verbose + type: aio + vagrant_memsize: 1024 + vagrant_cpus: 2 +<% if ENV['BEAKER_PUPPET_COLLECTION'] -%> + puppet_collection: <%= ENV['BEAKER_PUPPET_COLLECTION'] %> +<% end -%> diff --git a/spec/acceptance/suites/ldap_plugin/nodesets/oel.yml b/spec/acceptance/suites/ldap_plugin/nodesets/oel.yml new file mode 100644 index 0000000..7e61c8c --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/nodesets/oel.yml @@ -0,0 +1,32 @@ +<% + if ENV['BEAKER_HYPERVISOR'] + hypervisor = ENV['BEAKER_HYPERVISOR'] + else + hypervisor = 'vagrant' + end +-%> +HOSTS: + oel8: + roles: + - default + - ldap_server + - client + platform: el-8-x86_64 + box: generic/oracle8 + hypervisor: <%= hypervisor %> + + oel7: + roles: + - client + platform: el-7-x86_64 + box: generic/oracle7 + hypervisor: <%= hypervisor %> + +CONFIG: + log_level: verbose + type: aio + vagrant_memsize: 1024 + vagrant_cpus: 2 +<% if ENV['BEAKER_PUPPET_COLLECTION'] -%> + puppet_collection: <%= ENV['BEAKER_PUPPET_COLLECTION'] %> +<% end -%> diff --git a/spec/acceptance/suites/ldap_plugin/validate_ldap_entries.rb b/spec/acceptance/suites/ldap_plugin/validate_ldap_entries.rb new file mode 100644 index 0000000..7722853 --- /dev/null +++ b/spec/acceptance/suites/ldap_plugin/validate_ldap_entries.rb @@ -0,0 +1,181 @@ +require 'acceptance/helpers/ldap_utils' +require 'acceptance/helpers/utils' +include Acceptance::Helpers::LdapUtils +include Acceptance::Helpers::Utils + +# Validate ldap-plugin-managed keys on the LDAP server +# +# For each key specification, +# - Selects the backend whose name matches its 'app_id' or 'default', when +# no match is found +# - Checks for the existence of the key in the appropriate location in the +# backend +# - When the key is supposed to exist and does exist, verifies the stored data +# +# Conforms to the API specified in 'a simpkv plugin test' shared_examples +# +# @param key_info Hash of key information whose format corresponds to the +# Simpkv_test::KeyInfo type alias +# +# @param keys_should_exist Whether keys should exist +# - true = verify keys are present with correct stored data +# - false = verify keys are absent +# +# @param backend_hiera Hash of backend configuration ('simpkv::options' Hash) +# +# @param host Host object on which the validator will execute commands +# - LDAP keystore not assumed to be on +# +# @return Whether validation of keys succeeded +# +# @raise RuntimeError if the appropriate backend for each app_id within key_info +# cannot be found in backend_hiera +# +def validate_ldap_entries(key_info, keys_should_exist, backend_hiera, host) + #TODO: Make the iteration through keys and backend config selection part + # of the test infrastructure instead of having this code replicated + # in each plugin-provided validator + # + errors = [] + key_info.each do |app_id, key_struct| + config = backend_config_for_app_id(app_id, 'ldap', backend_hiera) + key_struct.each do |key_type, keys| + keys.each do |key, key_data| + result = {} + if keys_should_exist + result = validate_ldap_key_entry_present(key, key_type, key_data, config, host) + else + result = validate_ldap_key_entry_absent(key, key_type, config, host) + end + + unless result[:success] + errors << result[:err_msg] + end + end + end + end + + if errors.size == 0 + true + else + warn('Validation Failures:') + errors.each do |error| + warn(" #{error}") + end + + false + end +end + +# Validate that a ldap-plugin-managed key exists on the LDAP server and has +# the correct stored data +# +# @param key Key name +# +# @param key_type 'env' or 'global' for a key tied to the Puppet-environment +# or a global key, respectively +# +# @param key_data Hash of key data whose format corresponds to the +# Simpkv_test::KeyData type alias +# +# @param config Backend configuration +# +# @param host Host object on which the validator will execute commands +# +# @return results Hash +# * :success - whether the validation succeeded +# * :err_msg - error message upon failure or nil otherwise +# +def validate_ldap_key_entry_present(key, key_type, key_data, config, host) + result = { :success => true } + + full_path = ldap_key_path(key, key_type, config) + dn = build_key_dn(full_path, config['base_dn']) + cmd = build_ldapsearch_cmd(dn, config, false) + cmd_result = on(host, cmd, :accept_all_exit_codes => true) + if cmd_result.stdout.match(%r{^dn: .*#{dn}}).nil? + result = { + :success => false, + :err_msg => "Validation of #{key} presence failed: Could not find #{dn}" + } + else + expected_key_string = key_data_string(key_data) + if cmd_result.stdout.match(/simpkvJsonValue: #{Regexp.escape(expected_key_string)}/).nil? + result = { + :success => false, + :err_msg => [ + "Data for #{key} did not match expected:", + " Expected: simpkvJsonValue: #{expected_key_string}", + " Actual: #{result.stdout}" + ].join("\n") + } + end + end + + result +end + +# Validate that a ldap-plugin-managed key does not exist on the LDAP server +# +# @param key Key name +# +# @param key_type 'env' or 'global' for a key tied to the Puppet-environment +# or a global key, respectively +# +# @param config Backend configuration +# +# @param host Host object on which the validator will execute commands +# +# @return results Hash +# * :success - whether the validation succeeded +# * :err_msg - error message upon failure or nil otherwise +# +def validate_ldap_key_entry_absent(key, key_type, config, host) + result = { :success => true } + + full_path = ldap_key_path(key, key_type, config) + dn = build_key_dn(full_path, config['base_dn']) + cmd = build_ldapsearch_cmd(dn, config, true) + cmd_result = on(host, cmd, :accept_all_exit_codes => true) + unless cmd_result.exit_code == 32 # No such object + result = { + :success => false, + :err_msg => "Validation of #{key} absence failed: Found #{dn}:\n#{result.stdout}" + } + end + + result +end + +# @return keypath for the key +def ldap_key_path(key, key_type, config) + plugin_instance_path = File.join('instances', config['id']) + if key_type == 'global' + File.join(plugin_instance_path, 'globals', key) + else + File.join(plugin_instance_path, 'environments', 'production', key) + end +end + +# @return ldapsearch command to list or check the existence of a DN +# +# @param dn DN to search for +# @param config LDAP backend config +# @param existence_only whether to just check for existence of the DN or +# query for a listing of the DN +# +def build_ldapsearch_cmd(dn, config, existence_only) + [ + build_ldap_command('ldapsearch', config), + '-s base', + "-b #{dn}", + + # TODO switch to ldif_wrap when we drop support for EL7 + # - EL7 only supports ldif-wrap + # - EL8 says it supports ldif_wrap (--help and man page), but actually + # accepts ldif-wrap or ldif_wrap + '-o "ldif-wrap=no"', + '-LLL', + existence_only ? '1.1' : '' + ].join(' ') +end diff --git a/spec/acceptance/suites/multiple_plugins/00_file_plugin_setup_spec.rb b/spec/acceptance/suites/multiple_plugins/00_file_plugin_setup_spec.rb new file mode 120000 index 0000000..89de272 --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/00_file_plugin_setup_spec.rb @@ -0,0 +1 @@ +../default/00_file_plugin_setup_spec.rb \ No newline at end of file diff --git a/spec/acceptance/suites/multiple_plugins/00_ldap_server_spec.rb b/spec/acceptance/suites/multiple_plugins/00_ldap_server_spec.rb new file mode 120000 index 0000000..025e1d1 --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/00_ldap_server_spec.rb @@ -0,0 +1 @@ +../ldap_plugin/00_ldap_server_spec.rb \ No newline at end of file diff --git a/spec/acceptance/suites/multiple_plugins/05_ldap_client_setup_spec.rb b/spec/acceptance/suites/multiple_plugins/05_ldap_client_setup_spec.rb new file mode 120000 index 0000000..4232cca --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/05_ldap_client_setup_spec.rb @@ -0,0 +1 @@ +../ldap_plugin/05_ldap_client_setup_spec.rb \ No newline at end of file diff --git a/spec/acceptance/suites/multiple_plugins/10_multiple_plugins_spec.rb b/spec/acceptance/suites/multiple_plugins/10_multiple_plugins_spec.rb new file mode 100644 index 0000000..e75e247 --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/10_multiple_plugins_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper_acceptance' +require_relative '../ldap_plugin/ldap_test_configuration' +require_relative 'validate_multiple_plugins_entries' + +test_name 'multiple plugins' + +# This test verifies that the simpkv API supports the use of different plugins +# simultaneously. It uses 1 file plugin instance and 2 ldap plugin instances. +# The ldap plugins are configured to use different 389-DS instances (LDAP +# servers). +# +# Without a running puppetserver, the file plugin only works when the +# file keystore is on the test host on which manifests are being compiled. So, +# for test simplicity, we will have also have the LDAP keystores on the test +# host and we will use LDAPI to communicate with the LDAP servers. + +describe 'multiple plugins' do + # We will use the LDAP instances configuration, id1, id2, id3, and + # initial_key_info defined in this context, but override the validator + # with one that handles both file and LDAP backends + include_context('ldap test configuration') + + let(:file_backend_config) {{ + 'type' => 'file', + 'root_path' => "/var/simp/simpkv/file/#{id1}" + }} + + let(:file_clear_data_cmd) { 'rm -rf /var/simp/simpkv/file' } + + let(:ldap1_name) { 'simp_data_with_tls' } + let(:ldap1) { ldap_instances[ldap1_name] } + let(:ldap1_uri) { "ldapi://%2fvar%2frun%2fslapd-#{ldap1_name}.socket" } + let(:ldap1_backend_config) {{ + 'type' => 'ldap', + 'ldap_uri' => ldap1_uri, + 'base_dn' => ldap1[:simpkv_base_dn], + 'admin_dn' => ldap1[:admin_dn], + 'admin_pw_file' => ldap1[:admin_pw_file] + }} + + let(:ldap1_clear_data_cmd) { + [ + build_ldap_command('ldapdelete', ldap1_backend_config), + '-r', + %Q{"ou=instances,#{ldap1[:simpkv_base_dn]}"} + ].join(' ') + } + + let(:ldap2_name) { 'simp_data_without_tls' } + let(:ldap2) { ldap_instances[ldap2_name] } + let(:ldap2_uri) { "ldapi://%2fvar%2frun%2fslapd-#{ldap2_name}.socket" } + let(:ldap2_backend_config) {{ + 'type' => 'ldap', + 'ldap_uri' => ldap2_uri, + 'base_dn' => ldap2[:simpkv_base_dn], + 'admin_dn' => ldap2[:admin_dn], + 'admin_pw_file' => ldap2[:admin_pw_file] + }} + + let(:ldap2_clear_data_cmd) { + [ + build_ldap_command('ldapdelete', ldap2_backend_config), + '-r', + %Q{"ou=instances,#{ldap2[:simpkv_base_dn]}"} + ].join(' ') + } + + # Command to run on the test host to clear out all stored key data. + let(:clear_data_cmd) { + [ + file_clear_data_cmd, + ldap1_clear_data_cmd, + ldap2_clear_data_cmd + ].join(' ; ') + } + + let(:backend_hiera) { + backend_configs = { + id1 => file_backend_config, + id2 => ldap1_backend_config, + id3 => ldap2_backend_config + } + + generate_backend_hiera(backend_configs) + } + + hosts.each do |host| + context "with LDAP and file keystores on #{host}" do + # Method object to validate key/folder entries in a file or LDAP instance + # - Conforms to the API specified in 'a simpkv plugin test' shared_examples + # - Defined here to override validator pulled in when we included the + # 'ldap test configuration' context + let(:validator) { method(:validate_multiple_plugin_entries) } + + it_behaves_like 'a simpkv plugin test', host + end + end +end diff --git a/spec/acceptance/suites/multiple_plugins/nodesets/default.yml b/spec/acceptance/suites/multiple_plugins/nodesets/default.yml new file mode 100644 index 0000000..e34fde7 --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/nodesets/default.yml @@ -0,0 +1,25 @@ +<% + if ENV['BEAKER_HYPERVISOR'] + hypervisor = ENV['BEAKER_HYPERVISOR'] + else + hypervisor = 'vagrant' + end +-%> +HOSTS: + el8: + roles: + - default + - ldap_server + - client + platform: el-8-x86_64 + box: generic/centos8 + hypervisor: <%= hypervisor %> + +CONFIG: + log_level: verbose + synced_folder : disabled + type: aio + vagrant_memsize: 1024 +<% if ENV['BEAKER_PUPPET_COLLECTION'] -%> + puppet_collection: <%= ENV['BEAKER_PUPPET_COLLECTION'] %> +<% end -%> diff --git a/spec/acceptance/suites/multiple_plugins/nodesets/oel.yml b/spec/acceptance/suites/multiple_plugins/nodesets/oel.yml new file mode 100644 index 0000000..b810ffd --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/nodesets/oel.yml @@ -0,0 +1,25 @@ +<% + if ENV['BEAKER_HYPERVISOR'] + hypervisor = ENV['BEAKER_HYPERVISOR'] + else + hypervisor = 'vagrant' + end +-%> +HOSTS: + oel8: + roles: + - default + - ldap_server + - client + platform: el-8-x86_64 + box: generic/oracle8 + hypervisor: <%= hypervisor %> + +CONFIG: + log_level: verbose + synced_folder : disabled + type: aio + vagrant_memsize: 1024 +<% if ENV['BEAKER_PUPPET_COLLECTION'] -%> + puppet_collection: <%= ENV['BEAKER_PUPPET_COLLECTION'] %> +<% end -%> diff --git a/spec/acceptance/suites/multiple_plugins/validate_multiple_plugins_entries.rb b/spec/acceptance/suites/multiple_plugins/validate_multiple_plugins_entries.rb new file mode 100644 index 0000000..1ca40d0 --- /dev/null +++ b/spec/acceptance/suites/multiple_plugins/validate_multiple_plugins_entries.rb @@ -0,0 +1,73 @@ +require 'acceptance/helpers/utils' +require_relative '../default/validate_file_entries' +require_relative '../ldap_plugin/validate_ldap_entries' + +include Acceptance::Helpers::Utils + +# Validate keys managed by file and ldap plugins on filesystems and LDAP servers, +# respectively +# +# For each key specification, +# - Selects the backend whose name matches its 'app_id' or 'default', when +# no match is found +# - Checks for the existence of the key in the appropriate location in the +# backend +# - When the key is supposed to exist and does exist, verifies the stored +# content +# +# Conforms to the API specified in 'a simpkv plugin test' shared_examples +# +# NOTE: Uses local filesystem commands for keys managed by the file plugin, +# since the file plugin has to be on the same host as the file keystore. +# +# @param key_info Hash of key information whose format corresponds to the +# Simpkv_test::KeyInfo type alias +# +# @param keys_should_exist Whether keys should exist +# - true = verify keys are present with correct stored data +# - false = verify keys are absent +# +# @param backend_hiera Hash of backend configuration ('simpkv::options' Hash) +# +# @param host Host object on which the validator will execute commands +# - LDAP keystore not assumed to be on +# +# @return Whether validation of keys succeeded +# +def validate_multiple_plugin_entries(key_info, keys_should_exist, backend_hiera, host) + errors = [] + key_info.each do |app_id, key_struct| + config = backend_config_for_app_id(app_id, nil, backend_hiera) + unless (config['type'] == 'file') || (config['type'] == 'ldap') + fail("Unsupported backend type '#{config['type']}' found in backend hiera:\n#{backend_hiera}") + end + + key_struct.each do |key_type, keys| + keys.each do |key, key_data| + result = {} + if keys_should_exist + exp = "validate_#{config['type']}_key_entry_present(key, key_type, key_data, config, host)" + result = eval(exp) + else + exp = "validate_#{config['type']}_key_entry_absent(key, key_type, config, host)" + result = eval(exp) + end + + unless result[:success] + errors << result[:err_msg] + end + end + end + end + + if errors.size == 0 + true + else + warn('Validation Failures:') + errors.each do |error| + warn(" #{error}") + end + + false + end +end diff --git a/spec/consul_support/README b/spec/consul_support/README deleted file mode 100644 index 5345dd7..0000000 --- a/spec/consul_support/README +++ /dev/null @@ -1,10 +0,0 @@ -Files in this directory are for the consul simpkv provider tests -(spec tests and what appears to be manual tests). The files have -been moved from the top directory to here, but - - **HAVE NOT BEEN FIXED** - -Also, it is unclear if the Vagrantfile needs to be moved back into -top-level directory in order for TraviCI to be able to use the -'vagrant' service that is configured in the .travis.yml....Couldn't -find any documentation on the vagrant service. diff --git a/spec/consul_support/Vagrantfile b/spec/consul_support/Vagrantfile deleted file mode 100644 index 7749aa7..0000000 --- a/spec/consul_support/Vagrantfile +++ /dev/null @@ -1,77 +0,0 @@ -# vim: set ft=ruby: -Vagrant.configure(2) do |config| - ENV['VAGRANT_DEFAULT_PROVIDER'] = 'docker' - ENV['VAGRANT_NO_PARALLEL'] = 'yes' - config.vm.define "consul-ssl" do |config| - config.vm.synced_folder ".", "/vagrant" - config.vm.provider "docker" do |d| - d.image = "consul:0.9.2" - d.has_ssh = false - d.env = { - "CONSUL_LOCAL_CONFIG" => '{ - "addresses": { - "https":"0.0.0.0" - }, - "ports" : { - "https" : 8501 - }, - "key_file" : "/vagrant/test/server.key", - "cert_file" : "/vagrant/test/server.crt", - "ca_file" : "/vagrant/test/ca.crt" - }' - } - d.ports = [ - "10500:8500", - "10501:8501", - ] - end - end - config.vm.define "consul-ssl-auth" do |config| - config.vm.synced_folder ".", "/vagrant" - config.vm.provider "docker" do |d| - d.image = "consul:0.9.2" - d.has_ssh = false - d.env = { - "CONSUL_LOCAL_CONFIG" => '{ - "addresses": { - "https":"0.0.0.0" - }, - "ports" : { - "https" : 8501 - }, - "key_file" : "/vagrant/test/server.key", - "cert_file" : "/vagrant/test/server.crt", - "ca_file" : "/vagrant/test/ca.crt", - "verify_incoming" : true - }' - } - d.ports = [ - "10504:8500", - "10503:8501", - ] - end - end - config.vm.define "etcd" do |config| - config.vm.synced_folder ".", "/vagrant", disabled: true - config.vm.provider "docker" do |d| - d.image = "elcolio/etcd" - d.has_ssh = false - d.ports = [ - "10379:2379", - ] - end - end - # vagrant_root = File.dirname(__FILE__); - # config.vm.define "puppet" do |config| - # config.vm.synced_folder ".", "/vagrant", disabled: true - # config.vm.provider "docker" do |d| - # d.image = "simpproject/centos:7-ruby21" - # d.has_ssh = true - # d.volumes = [ - # "#{vagrant_root}:/vagrant:z", - # ] - # d.cmd = [ "bash", "-c", "sudo yum install -y openssh-server && sudo /usr/sbin/sshd"] - # end - - # end -end diff --git a/spec/consul_support/manifests/test.pp b/spec/consul_support/manifests/test.pp deleted file mode 100644 index 3e95319..0000000 --- a/spec/consul_support/manifests/test.pp +++ /dev/null @@ -1,33 +0,0 @@ -class simpkv::test( -$url = 'mock://' -) { - $supports = simpkv::supports({'url' => $url}) - notify { "supports = ${supports}": } - $provider = simpkv::provider({'url' => $url}) - notify { "provider = ${provider}": } - $loopvar = { - '/meats/pork' => 'test1', - '/meats/chicken' => 'test4', - '/meats/turkey' => 'test5', - '/meats/beef' => 'test2', - '/fruits/apple' => 'test3', - '/fruits/banana' => 'test4', - }.each |$key, $value| { - simpkv::put({ 'url' => $url, 'key' => $key, 'value' => $value}); - $get = simpkv::get({'url' => $url, 'key' => $key}); - notify { "${key} get = ${get}": } - $atomic_get = simpkv::atomic_get({'url' => $url, 'key' => $key}); - notify { "${key} atomic_get = ${atomic_get}": } - simpkv::atomic_put({'url' => $url, 'key' => $key, 'value' => 'testzor', 'previous' => $atomic_get}); - $atomic_put = simpkv::atomic_get({'url' => $url, 'key' => $key}); - notify { "${key} atomic_put = ${atomic_put}": } - $info = simpkv::info({'url' => $url}) - notify { "${key} info = ${info}": } - } - $list = simpkv::list({ 'url' => $url, 'key' => '/meats' }) - notify { "first list = ${list}": } - simpkv::delete({'url' => $url, 'key' => '/meats/pork'}) - $listm = simpkv::list({'url' => $url, 'key' => '/meats' }) - notify { "second list = ${listm}": } - simpkv::put({ 'url' => $url, 'key' => "/hosts/${trusted[certname]}/ipaddress", 'value' => $::ipaddress}) -} diff --git a/spec/consul_support/prep_ci.sh b/spec/consul_support/prep_ci.sh deleted file mode 100755 index 0fd859d..0000000 --- a/spec/consul_support/prep_ci.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -dir=$(readlink -f $(dirname $0)) -docker pull consul -docker run -d -p "10500:8500" -p "10501:8501" -v "$dir:/vagrant" -e CONSUL_LOCAL_CONFIG='{ "addresses": { "https":"0.0.0.0" }, "ports" : { "https" : 8501 }, "key_file" : "/vagrant/test/server.key", "cert_file" : "/vagrant/test/server.crt", "ca_file" : "/vagrant/test/ca.crt"}' consul:0.8.5 -docker run -d -p "10504:8500" -p "10503:8501" -v "$dir:/vagrant" -e CONSUL_LOCAL_CONFIG='{ "addresses": { "https":"0.0.0.0" }, "ports" : { "https" : 8501 }, "key_file" : "/vagrant/test/server.key", "cert_file" : "/vagrant/test/server.crt", "ca_file" : "/vagrant/test/ca.crt", "verify_incoming": true}' consul:0.8.5 -sleep 5 -for i in $(docker ps -aq) -do - docker inspect "${i}" - docker logs "${i}" -done -curl -kvvvv https://172.17.0.1:10501 -curl -kvvvv --cacert $dir/test/ca.pem --cert $dir/test/server.crt --key $dir/test/server.key https://172.17.0.1:10503 diff --git a/spec/consul_support/test/ca.crt b/spec/consul_support/test/ca.crt deleted file mode 100644 index a5b3041..0000000 --- a/spec/consul_support/test/ca.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIJAMkaYZYjQlkWMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJNRDESMBAGA1UEBwwJQmFsdGltb3JlMRgwFgYDVQQK -DA9Pbnl4cG9pbnQsIEluYy4xEzARBgNVBAsMClJTUEVDIFRFU1QxEzARBgNVBAMM -CjE3Mi4xNy4wLjEwHhcNMTcwNDI3MTYxMTMxWhcNMTgwNDI3MTYxMTMxWjByMQsw -CQYDVQQGEwJVUzELMAkGA1UECAwCTUQxEjAQBgNVBAcMCUJhbHRpbW9yZTEYMBYG -A1UECgwPT255eHBvaW50LCBJbmMuMRMwEQYDVQQLDApSU1BFQyBURVNUMRMwEQYD -VQQDDAoxNzIuMTcuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -0bu9VvNj7upczDMQGI7JLa4QJ3AnnyVNy3nWOWEUjSBdsG9ahHVwG9Tv6uyJgorU -jzeNgwAt10jiM7YB6UA//xgLuAD3VpjpySfrPlYfm4FsnehgCPMglsX7LEmdu2OW -ynuz5WDqqHD1MoVUQG3PlEjx7zxXZWhYKIk93fdYnOiYatySmMXY/kt7123mx8ZI -4DSFr5fGvs3is14HjYdEjRl55QZNJcUrsyU+g/yaqjBc2pAyOhHPiWLO5sNSqTFw -C0/FQ4TJFqrD5OwyguE3bedxGsQiq3SpkYlB9Th55NO35eIemMJCd0FkThVPuaPX -+OH1mPDB27PcSG4fUy5ewQIDAQABo1AwTjAdBgNVHQ4EFgQU/01DyE+GpbN+I5VY -pSrIPXIH3bswHwYDVR0jBBgwFoAU/01DyE+GpbN+I5VYpSrIPXIH3bswDAYDVR0T -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcBCNtUFZvo1LoxdmioWupqP0P7xO -nkGupqyfh3EoUqRhI7P7hSPUJnmdOqPcmOGjv9wKcOFVKDBmvDaM8Z+09Xqg734O -J1ejVhfuOsurx91/3CLlF7IgnL/HpFxq3xRX5sZyf0jkRL5NY6EdMwXLzfhU5ZVC -0R1Ruwd61u1AfPgiNlSQExBQba1E8AOAR7Z0wBSgomgZ92TrcwjbnSmbG1YCQ2mP -5oXf512wgMq9I+ypJlPSxrm6xd9Sgi1QT4f5fnsAvKny7zONwe/ItmYwGH5geAPW -vwcgzSyLkETlYkXAa6IE968GsJkJSnE28zmywNjiZA07KhQMoLwnlMbF/Q== ------END CERTIFICATE----- diff --git a/spec/consul_support/test/run.sh b/spec/consul_support/test/run.sh deleted file mode 100755 index 7fa2e7c..0000000 --- a/spec/consul_support/test/run.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -bundle install --path=vendor -rpath="$(dirname $(dirname $0))/spec/fixtures/modules" -mkdir spec/fixtures/modules -ln -s . spec/fixtures/modules/simpkv -bundle exec puppet apply --modulepath=${rpath} -e 'class {"simpkv::test": url => "consul://172.17.0.1:8500/puppet" }' diff --git a/spec/consul_support/test/server.crt b/spec/consul_support/test/server.crt deleted file mode 100644 index a5b3041..0000000 --- a/spec/consul_support/test/server.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIJAMkaYZYjQlkWMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJNRDESMBAGA1UEBwwJQmFsdGltb3JlMRgwFgYDVQQK -DA9Pbnl4cG9pbnQsIEluYy4xEzARBgNVBAsMClJTUEVDIFRFU1QxEzARBgNVBAMM -CjE3Mi4xNy4wLjEwHhcNMTcwNDI3MTYxMTMxWhcNMTgwNDI3MTYxMTMxWjByMQsw -CQYDVQQGEwJVUzELMAkGA1UECAwCTUQxEjAQBgNVBAcMCUJhbHRpbW9yZTEYMBYG -A1UECgwPT255eHBvaW50LCBJbmMuMRMwEQYDVQQLDApSU1BFQyBURVNUMRMwEQYD -VQQDDAoxNzIuMTcuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -0bu9VvNj7upczDMQGI7JLa4QJ3AnnyVNy3nWOWEUjSBdsG9ahHVwG9Tv6uyJgorU -jzeNgwAt10jiM7YB6UA//xgLuAD3VpjpySfrPlYfm4FsnehgCPMglsX7LEmdu2OW -ynuz5WDqqHD1MoVUQG3PlEjx7zxXZWhYKIk93fdYnOiYatySmMXY/kt7123mx8ZI -4DSFr5fGvs3is14HjYdEjRl55QZNJcUrsyU+g/yaqjBc2pAyOhHPiWLO5sNSqTFw -C0/FQ4TJFqrD5OwyguE3bedxGsQiq3SpkYlB9Th55NO35eIemMJCd0FkThVPuaPX -+OH1mPDB27PcSG4fUy5ewQIDAQABo1AwTjAdBgNVHQ4EFgQU/01DyE+GpbN+I5VY -pSrIPXIH3bswHwYDVR0jBBgwFoAU/01DyE+GpbN+I5VYpSrIPXIH3bswDAYDVR0T -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcBCNtUFZvo1LoxdmioWupqP0P7xO -nkGupqyfh3EoUqRhI7P7hSPUJnmdOqPcmOGjv9wKcOFVKDBmvDaM8Z+09Xqg734O -J1ejVhfuOsurx91/3CLlF7IgnL/HpFxq3xRX5sZyf0jkRL5NY6EdMwXLzfhU5ZVC -0R1Ruwd61u1AfPgiNlSQExBQba1E8AOAR7Z0wBSgomgZ92TrcwjbnSmbG1YCQ2mP -5oXf512wgMq9I+ypJlPSxrm6xd9Sgi1QT4f5fnsAvKny7zONwe/ItmYwGH5geAPW -vwcgzSyLkETlYkXAa6IE968GsJkJSnE28zmywNjiZA07KhQMoLwnlMbF/Q== ------END CERTIFICATE----- diff --git a/spec/consul_support/test/server.key b/spec/consul_support/test/server.key deleted file mode 100644 index c5e984c..0000000 --- a/spec/consul_support/test/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDRu71W82Pu6lzM -MxAYjsktrhAncCefJU3LedY5YRSNIF2wb1qEdXAb1O/q7ImCitSPN42DAC3XSOIz -tgHpQD//GAu4APdWmOnJJ+s+Vh+bgWyd6GAI8yCWxfssSZ27Y5bKe7PlYOqocPUy -hVRAbc+USPHvPFdlaFgoiT3d91ic6Jhq3JKYxdj+S3vXbebHxkjgNIWvl8a+zeKz -XgeNh0SNGXnlBk0lxSuzJT6D/JqqMFzakDI6Ec+JYs7mw1KpMXALT8VDhMkWqsPk -7DKC4Tdt53EaxCKrdKmRiUH1OHnk07fl4h6YwkJ3QWROFU+5o9f44fWY8MHbs9xI -bh9TLl7BAgMBAAECggEBAKDUps0Wt3tunNq6DY3HcN5mrFyR1NBletKeC3jUyT2v -pCi4O2F37RBqqdAsswY4D+gDYbEjYgPFEDE3UR/c9TZY6iOgGgt+F0j6I5sZ0AMf -rHsqxvoV9ten2oSLrzkTlX6QfdNYDpo8hMrJE2KU0qmThnM+goZxDamIFLkyA/HN -yAEe5v5UAikG7F6nZcyaBLkOBpaIXjNjaxze3x9jPLDvcNt6ttnUezxvE8iPeyVh -SlBJi5GfyTJW45wgRqGu+ElsOW8xl3patdXWNpAFokGQ+M89dO/aVxFR727ElJJP -sm32r2XgJrZATbagS4IQQwifmJPZyD2XqrjuqiApVwECgYEA73znpNrLxE1GbT8U -8mbKIcBEX9BxfeS23Tucmi9U4j7KXgDdYcQI5lK/bCMzFkwgTPxJeC8WP0iXPoww -eeglnXFY9RHIhwpaMTT/48r5hLMw7yI0DN02CF09v9D6Mx+rFX3+SdinpUjry9rH -sk2HNBHYJftC5UxLS+M0d2srR/UCgYEA4DGme1OPT06ZhwcXKMPEZfKby2FlbND0 -EHgcuwyE4n0HnzfN3nswJF0A65rhyTZjmpPtGEjd7EJDBDw8HR/34p1dP2Mmv4Bd -Crzm8lgTx46vXTZPWcroHr2UxoTNtEZRmyn7pK5rkEVdJ7Jle/6DJQn9VcB4Ahhq -O7YHMKx/WB0CgYAYirhCUJmpGDIrZ9eBr77vDoDzQK0gtZt3uHAn7MnKFZ0vXO9S -4X/3+mrbbhACLPLycLgmtMyW152IL12YYI3aQI9ZLeVa0VjEyiWe9mHzk1lkaCDl -YJX07Xkyevvo2Uny/eJdNvKXIY3oahck0oUUdO+tlL4aOWNN946c3kKlMQKBgG4A -bos8aKilDDdwhzB9PbQ34bFczIMj01zxUkeE0P7AEilRHDX5g5mT+Iuhpv8vLJf/ -1OmBd0IhEPjXBTfVI6+RPtuHLs/vj7dhEIAAL1RO8kRuDWklYdcTdhghuTym9AuK -Aeq/mg9juV1s7tZz/q0Bxcd5dGiyyg0aN1Tujl0RAoGAUaHPSARAgZRhQHogvmMq -TbbdDmMJU5XqEwuYpFGv2mvnpFjvtPAhBdkZBZhEllWNyL92XVq13NHFRiZxdtJq -CqHZeRNGEKUU5YsyQddxNHZQxVEB5I82Bhjlydta44Fxx036/FV2JlZ+S4CAtNQX -+cJ7ih5HgUyt365M8s0pRiI= ------END PRIVATE KEY----- diff --git a/spec/unit/puppet_x/simpkv/ldap_plugin_spec.rb b/spec/unit/puppet_x/simpkv/ldap_plugin_spec.rb new file mode 100644 index 0000000..f6b3772 --- /dev/null +++ b/spec/unit/puppet_x/simpkv/ldap_plugin_spec.rb @@ -0,0 +1,1127 @@ +require 'spec_helper' + +require 'fileutils' +require 'tmpdir' + +# mimic loading that is done in simpkv.rb +project_dir = File.join(File.dirname(__FILE__), '..', '..', '..', '..') +plugin_file = File.join(project_dir, 'lib', 'puppet_x', 'simpkv', 'ldap_plugin.rb') +plugin_class = nil +obj = Object.new +obj.instance_eval(File.read(plugin_file), plugin_file) + +############################################################################### +# The ldap plugin is significantly tested in its acceptance test So, the +# testing here is largely for code paths not otherwise tested. +############################################################################### + +describe 'simpkv ldap plugin anonymous class' do + before(:each) do + @tmpdir = Dir.mktmpdir + @admin_pw_file = File.join(@tmpdir, 'admin_pw.txt') + File.open(@admin_pw_file, 'w') { |file| file.puts('P@ssw0rdP@ssw0rd!') } + @options = { + 'backends' => { + 'default' => { + 'id' => 'default', + 'type' => 'ldap', + 'ldap_uri' => 'ldapi://simpkv.ldap.example.com', + }, + 'unencrypted' => { + 'id' => 'unencrypted', + 'type' => 'ldap', + 'ldap_uri' => 'ldap://simpkv.ldap.example.com', + 'admin_pw_file' => @admin_pw_file, + }, + 'starttls' => { + 'id' => 'starttls', + 'type' => 'ldap', + 'ldap_uri' => 'ldap://simpkv.ldap.example.com', + 'admin_pw_file' => @admin_pw_file, + 'enable_tls' => true, + 'tls_cert' => '/certdir/public/client.example.com.pub', + 'tls_key' => '/certdir/private/client.example.com.pem', + 'tls_cacert' => '/certdir/cacerts/cacerts.pem' + }, + 'tls' => { + 'id' => 'tls', + 'type' => 'ldap', + 'ldap_uri' => 'ldaps://simpkv.ldap.example.com', + 'admin_pw_file' => @admin_pw_file, + 'tls_cert' => '/certdir/public/client.example.com.pub', + 'tls_key' => '/certdir/private/client.example.com.pem', + 'tls_cacert' => '/certdir/cacerts/cacerts.pem' + } + } + } + end + + after(:each) do + FileUtils.remove_entry_secure(@tmpdir) + end + + # DNs here assume production environment keys and default backend + let(:key) { 'environments/production/mykey' } + let(:base_key) { File.basename(key) } + let(:full_key_path) { "instances/default/#{key}" } + let(:production_dn) { 'ou=production,ou=environments,ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp'} + let(:key_dn) { "simpkvKey=#{base_key},#{production_dn}" } + let(:value) { 'myvalue' } + let(:stored_value) { %Q{{"value":"#{value}","metadata":{}}} } + + let(:folder) { 'environments/production/myfolder' } + let(:base_folder) { File.basename(folder) } +# let(:full_folder_path) { "instances/default/#{folder}" } + let(:folder_dn) { "ou=#{base_folder},#{production_dn}" } + + let(:ldap_busy_response) {{ + :success => false, + :exitstatus => 51, + :stdout => '', + :stderr => 'ldapxxx failed:\nServer busy' + }} + + let(:ldap_no_such_object_response) {{ + :success => false, + :exitstatus => 32, + :stdout => '', + :stderr => 'No such object' + }} + + let(:ldap_other_error_response) {{ + :success => false, + :exitstatus => 1, + :stdout => '', + :stderr => 'ldapxxx failed:\nOther error' + }} + + # success response from run_command for which we only care about + # :success or :exitstatus + let(:success_response_simple) {{ :success => true, :exitstatus => 0 }} + + context '#initialize' do + it 'is expected to set name' do + plugin_name = 'ldap/test' + plugin = plugin_class.new(plugin_name) + expect(plugin.name).to eq plugin_name + end + end + + # See parse_config tests for other permutations of valid and invalid config + context '#configure' do + before(:each) do + @plugin = plugin_class.new('ldap/default') + end + + it 'should succeed using valid config' do + options = @options.merge( {'backend' => 'default' } ) + + expect(@plugin).to receive(:set_base_ldap_commands) + expect(@plugin).to receive(:verify_ldap_access) + expect(@plugin).to receive(:ensure_instance_tree) + expect{ @plugin.configure(options) }.to_not raise_error + end + + context 'error cases' do + it 'should fail when options is not a Hash' do + expect { @plugin.configure('oops') } + .to raise_error(/Plugin misconfigured/) + end + + it "should fail when options missing 'backend' key" do + expect { @plugin.configure({}) } + .to raise_error(/Plugin misconfigured/) + end + + it "should fail when options missing 'backends' key" do + options = { 'backend' => 'test' } + expect { @plugin.configure(options) } + .to raise_error(/Plugin misconfigured: {.*backend.*}/) + end + + it "should fail when options 'backends' key is not a Hash" do + options = { + 'backend' => 'test', + 'backends' => 'oops' + } + expect { @plugin.configure(options) } + .to raise_error(/Plugin misconfigured/) + end + + it "should fail when options 'backends' does not have the specified backend" do + options = { + 'backend' => 'test', + 'backends' => { + 'test1' => { 'id' => 'test', 'type' => 'file'} + } + } + expect { @plugin.configure(options) } + .to raise_error(/Plugin misconfigured/) + end + + it "should fail when the correct 'backends' element has no 'id' key" do + options = { + 'backend' => 'test', + 'backends' => { + 'test1' => { 'id' => 'test', 'type' => 'file'}, + 'test' => {} + } + } + + expect { @plugin.configure(options) } + .to raise_error(/Plugin misconfigured/) + end + + it "should fail when the correct 'backends' element has no 'type' key" do + options = { + 'backend' => 'test', + 'backends' => { + 'test1' => { 'id' => 'test', 'type' => 'file'}, + 'test' => { 'id' => 'test' } + } + } + expect { @plugin.configure(options) } + .to raise_error(/Plugin misconfigured/) + end + + it "should fail when the correct 'backends' element has wrong 'type' value" do + options = { + 'backend' => 'test', + 'backends' => { + 'test1' => { 'id' => 'test', 'type' => 'file'}, + 'test' => { 'id' => 'test', 'type' => 'file' } + } + } + expect { @plugin.configure(options) } + .to raise_error(/Plugin misconfigured/) + end + end + end + + context 'public API' do + before(:each) do + @plugin = plugin_class.new('ldap/default') + end + + context 'when public API called before configure' do + it '#delete should return failure' do + result = @plugin.delete(key) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq 'Internal error: delete called before configure' + end + + it '#deletetree should return failure' do + result = @plugin.deletetree(folder) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq 'Internal error: deletetree called before configure' + end + + it '#exists should return failure' do + result = @plugin.exists(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq 'Internal error: exists called before configure' + end + + it '#get should return failure' do + result = @plugin.get(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq 'Internal error: get called before configure' + end + + it '#list should return failure' do + result = @plugin.list(folder) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq 'Internal error: list called before configure' + end + + it '#put should return failure' do + result = @plugin.put(key, value) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq 'Internal error: put called before configure' + end + end + + context 'after configure' do + before(:each) do + options = @options.merge( {'backend' => 'default' } ) + expect(Facter::Core::Execution).to receive(:which).with('ldapadd').and_return('/usr/bin/ldapadd') + expect(Facter::Core::Execution).to receive(:which).with('ldapdelete').and_return('/usr/bin/ldapdelete') + expect(Facter::Core::Execution).to receive(:which).with('ldapmodify').and_return('/usr/bin/ldapmodify') + expect(Facter::Core::Execution).to receive(:which).with('ldapsearch').and_return('/usr/bin/ldapsearch') + expect(@plugin).to receive(:verify_ldap_access) + expect(@plugin).to receive(:ensure_instance_tree) + @plugin.configure(options) + end + + + describe '#delete' do + it 'should return success when retries succeed' do + # ldapdelete will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(ldap_busy_response, success_response_simple) + + result = @plugin.delete(key) + expect(result[:result]).to be true + expect(result[:err_msg]).to be_nil + end + + it 'should return failure when retries fail' do + # ldapdelete will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.delete(key) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when other ldapdelete failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(ldap_other_error_response) + + result = @plugin.delete(key) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + describe '#deletetree' do + it 'should remove the folder tree from the intenral set of existing folders upon success' do + @plugin.existing_folders.add('instances/default/globals/app1') + @plugin.existing_folders.add('instances/default/globals/app1/group1') + @plugin.existing_folders.add('instances/default/globals/app1/group1/user1') + @plugin.existing_folders.add('instances/default/globals/app1/group2/user1') + @plugin.existing_folders.add('instances/default/globals/app2') + + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(success_response_simple) + + result = @plugin.deletetree('globals/app1') + expect(result[:result]).to be true + expect(result[:err_msg]).to be_nil + + expected_folders = Set.new + expected_folders.add('instances/default/globals/app2') + expect(@plugin.existing_folders).to eq(expected_folders) + end + + it 'should return success when retries succeed' do + # ldapdelete will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(ldap_busy_response, success_response_simple) + + result = @plugin.deletetree(folder) + expect(result[:result]).to be true + expect(result[:err_msg]).to be_nil + end + + it 'should return failure when retries fail' do + # ldapdelete will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.deletetree(folder) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when other ldapdelete failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapdelete/) + .and_return(ldap_other_error_response) + + result = @plugin.deletetree(folder) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + describe '#exists' do + let(:success_response_dn_match) {{ + :success => true, + :exitstatus => 0, + :stdout => "dn: #{key_dn}" + }} + + it 'should return success when retries succeed' do + # ldapsearch will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, success_response_dn_match) + + result = @plugin.exists(key) + expect(result[:result]).to be true + expect(result[:err_msg]).to be_nil + end + + it 'should return failure when retries fail' do + # ldapsearch will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.exists(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when other ldapsearch failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_other_error_response) + + result = @plugin.exists(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + describe '#get' do + let(:success_response_simpkvKey) {{ + :success => true, + :exitstatus => 0, + :stdout => <<~EOM + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{base_key} + simpkvJsonValue: #{stored_value} + EOM + }} + + it 'should return success when retries succeed' do + # ldapsearch will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, success_response_simpkvKey) + + result = @plugin.get(key) + expect(result[:result]).to eq(stored_value) + expect(result[:err_msg]).to be_nil + end + + it 'should fail when simpkvKey object missing simpkvJsonValue attribute' do + # successful query result, but instead of simpkvJsonValue attribute + # has simpkvValue attribute + success_response_malformed_simpkvKey = { + :success => true, + :exitstatus => 0, + :stdout => <<~EOM + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{base_key} + simpkvValue: #{stored_value} + EOM + } + + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(success_response_malformed_simpkvKey) + + result = @plugin.get(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to match(%r{Key retrieval did not return key/value entry}) + end + + it 'should return failure when retries fail' do + # ldapsearch will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.get(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when other ldapsearch failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_other_error_response) + + result = @plugin.get(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + describe '#list' do + let(:success_response_not_empty) {{ + :success => true, + :exitstatus => 0, + :stdout => <<~EOM + dn: #{folder_dn} + ou: #{base_folder} + objectClass: top + objectClass: organizationalUnit + + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{base_key} + simpkvJsonValue: #{stored_value} + EOM + }} + + it 'should return success when retries succeed' do + # ldapsearch will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, success_response_not_empty) + + result = @plugin.list(File.dirname(folder)) + expected_list = { + :keys => { base_key => stored_value }, + :folders => [ base_folder ] + } + + expect(result[:result]).to eq(expected_list) + expect(result[:err_msg]).to be_nil + end + + + it 'should return failure when retries fail' do + # ldapsearch will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.list(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when other ldapsearch failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_other_error_response) + + result = @plugin.list(key) + expect(result[:result]).to be_nil + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + describe '#put' do + let(:failed_ldap_result) {{ + :success => false, + :exitstatus => 1, + :err_msg => 'Some interim ldap operation failed' + }} + + let(:successful_ldap_result) {{ + :success => true, + :exitstatus => 0 + }} + + let(:failed_update_result) {{ + :result => false, + :err_msg => 'Update failed' + }} + + it 'should return failure when ensure_folder_path fails' do + expect(@plugin).to receive(:ensure_folder_path) + .with('instances/default/environments/production') + .and_return(failed_ldap_result) + + result = @plugin.put(key, value) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(failed_ldap_result[:err_msg]) + end + + + it "should return failure when ldap_add with 'already exists' error and update_value_if_changed fails" do + expect(@plugin).to receive(:ensure_folder_path) + .with('instances/default/environments/production') + .and_return(successful_ldap_result) + + already_exists_result = { + :success => false, + :exitstatus => 68, + :err_msg => 'Already exists' + } + expect(@plugin).to receive(:ldap_add).and_return(already_exists_result) + expect(@plugin).to receive(:update_value_if_changed).with(key,value) + .and_return(failed_update_result) + + result = @plugin.put(key, value) + expect(result).to eq(failed_update_result) + end + + it 'should return failure when ldap_add fails with other error' do + expect(@plugin).to receive(:ensure_folder_path) + .with('instances/default/environments/production') + .and_return(successful_ldap_result) + + expect(@plugin).to receive(:ldap_add).and_return(failed_ldap_result) + result = @plugin.put(key, value) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(failed_ldap_result[:err_msg]) + end + end + end + end + + context 'internal methods' do + before(:each) do + @plugin = plugin_class.new('ldap/ldapi') + options = @options.merge( {'backend' => 'default' } ) + + # allow instead of expect because of set_base_ldap_commands test + allow(Facter::Core::Execution).to receive(:which).with('ldapadd').and_return('/usr/bin/ldapadd') + allow(Facter::Core::Execution).to receive(:which).with('ldapdelete').and_return('/usr/bin/ldapdelete') + allow(Facter::Core::Execution).to receive(:which).with('ldapmodify').and_return('/usr/bin/ldapmodify') + allow(Facter::Core::Execution).to receive(:which).with('ldapsearch').and_return('/usr/bin/ldapsearch') + + expect(@plugin).to receive(:verify_ldap_access) + expect(@plugin).to receive(:ensure_instance_tree) + @plugin.configure(options) + end + + describe '#ensure_folder_path' do + it 'should return success when no folders in existing_folders & all ldap_add ' do + @plugin.existing_folders.clear + + [ + 'dn: ou=instances,ou=simpkv,o=puppet,dc=simp', + 'dn: ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp', + 'dn: ou=environments,ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp', + 'dn: ou=production,ou=environments,ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp' + ].each do |dn| + expect(@plugin).to receive(:ldap_add).with(/#{dn}/, true) + .and_return(success_response_simple) + end + + expect(@plugin.ensure_folder_path(File.dirname(full_key_path))) + .to eq(success_response_simple.merge({ :err_msg => nil })) + + expected_folders = Set.new + expected_folders.add('instances') + expected_folders.add('instances/default') + expected_folders.add('instances/default/environments') + expected_folders.add('instances/default/environments/production') + expect(@plugin.existing_folders).to eq(expected_folders) + end + + it 'should return success when some folders in existing_folders & ldap_add succeeds for new folders' do + @plugin.existing_folders.clear + @plugin.existing_folders.add('instances') + @plugin.existing_folders.add('instances/default') + @plugin.existing_folders.add('instances/default/environments') + dn = 'dn: ou=production,ou=environments,ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp' + + expect(@plugin).to receive(:ldap_add).with(/#{dn}/, true) + .and_return(success_response_simple) + + expect(@plugin.ensure_folder_path(File.dirname(full_key_path))) + .to eq(success_response_simple.merge({ :err_msg => nil })) + end + + it 'should return failure if any ldap_add fails' do + @plugin.existing_folders.clear + @plugin.existing_folders.add('instances') + @plugin.existing_folders.add('instances/default') + + dn1 = 'dn: ou=environments,ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp' + expect(@plugin).to receive(:ldap_add).with(/#{dn1}/, true) + .and_return(success_response_simple) + + dn2 = 'dn: ou=production,ou=environments,ou=default,ou=instances,ou=simpkv,o=puppet,dc=simp' + failed_add_response = { + :success => false, + :exitstatus => 1, + :err_msg => 'ldapadd failed' + + } + + expect(@plugin).to receive(:ldap_add).with(/#{dn2}/, true) + .and_return(failed_add_response) + + expect(@plugin.ensure_folder_path(File.dirname(full_key_path))) + .to eq(failed_add_response) + + failed_folder = 'instances/default/environments/production' + expect(@plugin.existing_folders.include?(failed_folder)).to be false + end + end + + describe '#ldap_add' do + let(:ldap_already_exists_response) {{ + :success => false, + :exitstatus => 68, + :stdout => '', + :stderr => 'ldapadd failed:\nDN already exists' + }} + + context 'ignore_already_exists=false (default)' do + it 'should return failure when ldapadd fails because DN already exists' do + expect(@plugin).to receive(:run_command).with(/ldapadd/) + .and_return(ldap_already_exists_response) + + result = @plugin.ldap_add('some ldif') + expect(result[:success]).to be false + expect(result[:exitstatus]).to eq(ldap_already_exists_response[:exitstatus]) + expect(result[:err_msg]).to eq(ldap_already_exists_response[:stderr]) + end + + it 'should return success when retries succeed' do + # ldapadd will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapadd/) + .and_return(ldap_busy_response, success_response_simple) + + result = @plugin.ldap_add('some ldif') + expect(result[:success]).to be true + expect(result[:exitstatus]).to eq 0 + expect(result[:err_msg]).to be_nil + end + + it 'should return failure when retries fail' do + # ldapadd will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapadd/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.ldap_add('some ldif') + expect(result[:success]).to be false + expect(result[:exitstatus]).to eq(ldap_busy_response[:exitstatus]) + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when other ldapadd failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapadd/) + .and_return(ldap_other_error_response) + + result = @plugin.ldap_add('some ldif') + expect(result[:success]).to be false + expect(result[:exitstatus]).to eq(ldap_other_error_response[:exitstatus]) + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + context 'ignore_already_exists=true' do + it 'should return success when ldapadd fails because DN already exists' do + expect(@plugin).to receive(:run_command).with(/ldapadd/) + .and_return(ldap_already_exists_response) + + result = @plugin.ldap_add('some ldif', true) + expect(result[:success]).to be true + expect(result[:exitstatus]).to eq 0 + expect(result[:err_msg]).to be_nil + end + end + end + + describe '#ldap_modify' do + it 'should return success when retries succeed' do + # ldapmodify will return busy code first time and then success + expect(@plugin).to receive(:run_command).with(/ldapmodify/) + .and_return(ldap_busy_response, success_response_simple) + + result = @plugin.ldap_modify('some LDIF content') + expect(result[:success]).to be true + expect(result[:exitstatus]).to eq 0 + expect(result[:err_msg]).to be_nil + end + + it 'should return failure when retries fail' do + # ldapmodify will return busy code both times + expect(@plugin).to receive(:run_command).with(/ldapmodify/) + .and_return(ldap_busy_response, ldap_busy_response) + + result = @plugin.ldap_modify('some LDIF content') + expect(result[:success]).to be false + expect(result[:exitstatus]).to eq(ldap_busy_response[:exitstatus]) + expect(result[:err_msg]).to eq(ldap_busy_response[:stderr]) + end + + it 'should return failure when DN no longer exists' do + expect(@plugin).to receive(:run_command).with(/ldapmodify/) + .and_return(ldap_no_such_object_response) + + result = @plugin.ldap_modify('some LDIF content') + expect(result[:success]).to be false + expect(result[:exitstatus]).to eq(ldap_no_such_object_response[:exitstatus]) + expect(result[:err_msg]).to eq(ldap_no_such_object_response[:stderr]) + end + + it 'should return failure when other ldapmodify failure occurs' do + expect(@plugin).to receive(:run_command).with(/ldapmodify/) + .and_return(ldap_other_error_response) + + result = @plugin.ldap_modify('some LDIF content') + expect(result[:success]).to be false + expect(result[:exitstatus]).to eq(ldap_other_error_response[:exitstatus]) + expect(result[:err_msg]).to eq(ldap_other_error_response[:stderr]) + end + end + + describe '#path_to_dn' do + context 'leaf_is_key=true (default)' do + it 'should return DN for simpkvKey node when path has no folders' do + actual = @plugin.path_to_dn('key') + expect(actual).to eq('simpkvKey=key,ou=simpkv,o=puppet,dc=simp') + end + + it 'should return DN for simpkvKey node when path has folders' do + actual = @plugin.path_to_dn('environments/dev/key') + expect(actual).to eq('simpkvKey=key,ou=dev,ou=environments,ou=simpkv,o=puppet,dc=simp') + end + end + + context 'leaf_is_folder=false' do + it 'should return DN for ou node when path has no folders' do + actual = @plugin.path_to_dn('folder', false) + expect(actual).to eq('ou=folder,ou=simpkv,o=puppet,dc=simp') + end + + it 'should return DN for ou node when path has folders' do + actual = @plugin.path_to_dn('environments/dev/folder', false) + expect(actual).to eq('ou=folder,ou=dev,ou=environments,ou=simpkv,o=puppet,dc=simp') + end + end + end + + describe '#parse_config' do + context 'valid configuration' do + it 'should default base_dn to ou=simpkv,o=puppet,dc=simp' do + config = @options['backends']['default'] + opts = @plugin.parse_config(config) + expect(opts[:base_dn]).to eq('ou=simpkv,o=puppet,dc=simp') + end + + it 'should default admin_dn to cn=Directory_Manager' do + config = @options['backends']['unencrypted'] + opts = @plugin.parse_config(config) + expect(opts[:base_opts]).to match(/-D "cn=Directory_Manager"/) + end + + it 'should default retries to 1' do + config = @options['backends']['default'] + opts = @plugin.parse_config(config) + expect(opts[:retries]).to eq(1) + end + + it 'should transform valid ldapi config without admin_dn and admin_pw_file' do + config = @options['backends']['default'] + opts = @plugin.parse_config(config) + expect(opts[:cmd_env]).to eq('') + expect(opts[:base_opts]).to eq("-Y EXTERNAL -H #{config['ldap_uri']}") + end + + it 'should transform valid ldapi config with admin_dn and admin_pw_file' do + config = @options['backends']['default'].merge( { + 'admin_dn' => 'cn=My_Directory_Manager', + 'admin_pw_file' => @admin_pw_file + } ) + opts = @plugin.parse_config(config) + expect(opts[:cmd_env]).to eq('') + exp_base = [ + '-x', + %Q{-D "#{config['admin_dn']}"}, + "-y #{config['admin_pw_file']}", + "-H #{config['ldap_uri']}" + ].join(' ') + expect(opts[:base_opts]).to eq(exp_base) + end + + it 'should transform valid unencrypted ldap config' do + config = @options['backends']['unencrypted'] + opts = @plugin.parse_config(config) + expect(opts[:cmd_env]).to eq('') + exp_base = [ + '-x', + '-D "cn=Directory_Manager"', + "-y #{config['admin_pw_file']}", + "-H #{config['ldap_uri']}" + ].join(' ') + expect(opts[:base_opts]).to eq(exp_base) + end + + it 'should transform valid unencrypted ldap config with enable_tls=false' do + config = @options['backends']['unencrypted'].merge( {'enable_tls' => false} ) + opts = @plugin.parse_config(config) + expect(opts[:cmd_env]).to eq('') + + exp_base = [ + '-x', + '-D "cn=Directory_Manager"', + "-y #{config['admin_pw_file']}", + "-H #{config['ldap_uri']}" + ].join(' ') + expect(opts[:base_opts]).to eq(exp_base) + end + + it 'should transform valid encrypted ldap (StartTLS) config' do + config = @options['backends']['starttls'] + opts = @plugin.parse_config(config) + exp_env = [ + "LDAPTLS_CERT=#{config['tls_cert']}", + "LDAPTLS_KEY=#{config['tls_key']}", + "LDAPTLS_CACERT=#{config['tls_cacert']}" + ].join(' ') + expect(opts[:cmd_env]).to eq(exp_env) + + exp_base = [ + '-ZZ', + '-x', + '-D "cn=Directory_Manager"', + "-y #{config['admin_pw_file']}", + "-H #{config['ldap_uri']}" + ].join(' ') + expect(opts[:base_opts]).to eq(exp_base) + end + + it 'should transform valid encrypted ldaps config' do + config = @options['backends']['tls'] + opts = @plugin.parse_config(config) + exp_env = [ + "LDAPTLS_CERT=#{config['tls_cert']}", + "LDAPTLS_KEY=#{config['tls_key']}", + "LDAPTLS_CACERT=#{config['tls_cacert']}" + ].join(' ') + expect(opts[:cmd_env]).to eq(exp_env) + + exp_base = [ + '', + '-x', + '-D "cn=Directory_Manager"', + "-y #{config['admin_pw_file']}", + "-H #{config['ldap_uri']}" + ].join(' ') + expect(opts[:base_opts]).to eq(exp_base) + end + end + + context 'invalid configuration' do + it 'should fail when ldap_uri is missing' do + config = {} + expect{ @plugin.parse_config(config) } + .to raise_error(/Plugin missing 'ldap_uri' configuration/) + end + + it 'should fail if ldap_uri is malformed' do + config = { 'ldap_uri' => 'ldaps:/too.few.slashes.com' } + expect{ @plugin.parse_config(config) } + .to raise_error(/Invalid 'ldap_uri' configuration/) + end + + it 'should fail if admin_pw_file missing and not ldapi' do + config = Marshal.load(Marshal.dump(@options['backends']['unencrypted'])) + config.delete('admin_pw_file') + expect{ @plugin.parse_config(config) } + .to raise_error(/Plugin missing 'admin_pw_file' configuration/) + end + + it 'should fail if admin_pw_file does not exist' do + config = @options['backends']['default'].merge({ 'admin_pw_file' => '/does/not/exist' }) + expect{ @plugin.parse_config(config) } + .to raise_error(%r{Configured 'admin_pw_file' /does/not/exist does not exist}) + end + + it 'should fail if TLS configuration incomplete' do + config = Marshal.load(Marshal.dump(@options['backends']['tls'])) + config.delete('tls_cacert') + expect{ @plugin.parse_config(config) } + .to raise_error(/TLS configuration incomplete/) + end + end + end + + # all the weird error cases tested below could only happen if someone manually + # inserted entries into the LDIF tree + describe '#parse_list_ldif' do + it 'skips a malformed organizationalUnit' do + # don't know how this could ever happen...totally artifical example + ldif = <<~EOM + dn: #{folder_dn} + ou: #{base_folder} + objectClass: top + objectClass: organizationalUnit + + dn: custom=something,#{production_dn} + custom: something + objectClass: top + objectClass: organizationalUnit + objectClass: custom + + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{base_key} + simpkvJsonValue: #{stored_value} + EOM + + result = @plugin.parse_list_ldif(ldif) + expected = { + :keys => { base_key => stored_value }, + :folders => [ base_folder ] + } + expect(result).to eq(expected) + end + + it 'skips simpkvEntry missing the simpkvJsonValue attribute' do + ldif = <<~EOM + dn: #{folder_dn} + ou: #{base_folder} + objectClass: top + objectClass: organizationalUnit + + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{base_key} + EOM + + result = @plugin.parse_list_ldif(ldif) + expected = { :keys=> {}, :folders => [ base_folder ] } + expect(result).to eq(expected) + end + + it 'skips simpkvEntry missing the simpkvKey attribute' do + ldif = <<~EOM + dn: #{folder_dn} + ou: #{base_folder} + objectClass: top + objectClass: organizationalUnit + + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvJsonValue: #{stored_value} + EOM + + result = @plugin.parse_list_ldif(ldif) + expected = { :keys=> {}, :folders => [ base_folder ] } + expect(result).to eq(expected) + end + + it 'skips any object that is not a simpkvEntry or organizationalUnit' do + ldif = <<~EOM + dn: #{folder_dn} + ou: #{base_folder} + objectClass: top + objectClass: organizationalUnit + + dn: custom=something,#{production_dn} + custom: something + objectClass: top + objectClass: custom + + dn: #{key_dn} + objectClass: simpkvEntry + objectClass: top + simpkvKey: #{base_key} + simpkvJsonValue: #{stored_value} + EOM + + result = @plugin.parse_list_ldif(ldif) + expected = { + :keys => { base_key => stored_value }, + :folders => [ base_folder ] + } + expect(result).to eq(expected) + end + end + + describe '#run_command' do + it 'returns success results when command succeeds' do + command = "ls #{__FILE__}" + result = @plugin.run_command(command) + expect( result[:success] ).to eq true + expect( result[:exitstatus] ).to eq 0 + expect( result[:stdout] ).to match "#{__FILE__}" + expect( result[:stderr] ).to eq '' + end + + it 'returns failed results when command fails' do + command = 'ls /some/missing/path1' + result = @plugin.run_command(command) + expect( result[:success] ).to eq false + expect( result[:exitstatus] ).to eq 2 + expect( result[:stdout] ).to eq '' + expect( result[:stderr] ).to match(/No such file or directory/) + end + end + + describe '#set_base_ldap_commands' do + it 'fails when ldap* commands cannot be found' do + expect(Facter::Core::Execution).to receive(:which).with('ldapadd').and_return(nil) + expect{ @plugin.set_base_ldap_commands('','some base opts') } + .to raise_error(/Missing required ldapadd command/) + end + end + + describe '#tls_enabled?' do + it 'returns false when using ldapi' do + config = @options['backends']['default'] + expect(@plugin.tls_enabled?(config)).to be false + end + + it 'returns true when using ldaps' do + config = @options['backends']['tls'] + expect(@plugin.tls_enabled?(config)).to be true + end + + it 'returns ignores enable_tls when using ldaps' do + config = @options['backends']['tls'].merge({ 'enable_tls' => false }) + expect(@plugin.tls_enabled?(config)).to be true + end + + it 'returns true when using ldap and enable_tls=true' do + config = @options['backends']['starttls'] + expect(@plugin.tls_enabled?(config)).to be true + end + + it 'returns false when using ldap and enable_tls=false' do + config = Marshal.load(Marshal.dump(@options['backends']['starttls'])) + config['enable_tls'] = false + expect(@plugin.tls_enabled?(config)).to be false + end + + it 'returns false when using ldap and enable_tls is absent' do + config = @options['backends']['unencrypted'] + expect(@plugin.tls_enabled?(config)).to be false + end + end + + describe '#update_value_if_changed' do + let(:new_stored_value) { '{"value":"new value","metadata":{}}' } + it 'should report failure when get() for the current value fails' do + failed_get_result = { :result => nil, 'err_msg' => 'No such object' } + expect(@plugin).to receive(:get).with(key).and_return(failed_get_result) + + result = @plugin.update_value_if_changed(key, new_stored_value) + expect(result[:result]).to be false + expect(result[:err_msg]).to match(/Failed to retrieve current value for comparison/) + end + + it 'should report failure when ldap_modify() fails' do + success_get_result = { :result => stored_value, 'err_msg' => nil } + expect(@plugin).to receive(:get).with(key).and_return(success_get_result) + expect(@plugin).to receive(:ldap_modify).with(/#{new_stored_value}/) + .and_return(ldap_other_error_response) + + result = @plugin.update_value_if_changed(key, new_stored_value) + expect(result[:result]).to be false + expect(result[:err_msg]).to eq(ldap_other_error_response[:err_msg]) + end + end + + # easiest way to test verify_ldap_access is via configure + describe '#verify_ldap_access' do + before(:each) do + @plugin2 = plugin_class.new('ldap/ldapi') + @options2 = @options.merge( {'backend' => 'default' } ) + expect(Facter::Core::Execution).to receive(:which).with('ldapadd').and_return('/usr/bin/ldapadd') + expect(Facter::Core::Execution).to receive(:which).with('ldapdelete').and_return('/usr/bin/ldapdelete') + expect(Facter::Core::Execution).to receive(:which).with('ldapmodify').and_return('/usr/bin/ldapmodify') + expect(Facter::Core::Execution).to receive(:which).with('ldapsearch').and_return('/usr/bin/ldapsearch') + end + + it 'should succeed when retries succeed' do + expect(@plugin2).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, success_response_simple) + expect(@plugin2).to receive(:ensure_instance_tree) + + expect{ @plugin2.configure(@options2) }.to_not raise_error + end + + it 'should fail when retries fail' do + expect(@plugin2).to receive(:run_command).with(/ldapsearch/) + .and_return(ldap_busy_response, ldap_busy_response) + + expect{ @plugin2.configure(@options2) } + .to raise_error(/Plugin could not access ou=simpkv,o=puppet,dc=simp/) + end + end + end +end diff --git a/spec/unit/puppet_x/simpkv/simpkv_spec.rb b/spec/unit/puppet_x/simpkv/simpkv_spec.rb index c690d1c..ae49ece 100644 --- a/spec/unit/puppet_x/simpkv/simpkv_spec.rb +++ b/spec/unit/puppet_x/simpkv/simpkv_spec.rb @@ -67,6 +67,7 @@ expect( adapter.plugin_info ).to_not be_empty expect( adapter.plugin_info.keys.include?('file') ).to be true expect( adapter.plugin_info.keys.include?('failer') ).to be true + expect( adapter.plugin_info.keys.include?('ldap') ).to be true expect( adapter.plugin_info.keys.include?('malformed') ).to be false end @@ -324,8 +325,8 @@ context '#backends' do it 'should list available backend plugins' do - # currently only 2 plugins (one real and one for test only) - expect( @adapter.backends ).to eq([ 'failer', 'file' ]) + # currently only 3 plugins (2 real and 1 for test only) + expect( @adapter.backends.sort ).to eq([ 'failer', 'file', 'ldap' ]) end end