From 4c6e2f2a40cb93520502933c9f7f01976d3ea521 Mon Sep 17 00:00:00 2001 From: "Adam C. Migus" Date: Mon, 13 Jul 2020 01:37:20 -0400 Subject: [PATCH] Add the Thycotic Secret Server lookup plugin. (#91) * Add the Thycotic Secret Server lookup plugin. * Update plugins/lookup/tss.py Co-Authored-By: Felix Fontein * Fix import error check per code review. * Apply suggestions from code review Co-Authored-By: Felix Fontein * Trivial changes based on suggestions from code review. * Add a unittest for plugins/lookup/tss.py * Add copyrights. * Fixed formatting bug in test_tss.py * Fix formatting bugs in tss.py and test_tss.py * Apply suggestions from code review Co-authored-by: Felix Fontein Co-authored-by: Felix Fontein --- plugins/lookup/tss.py | 131 ++++++++++++++++++++++++++ tests/unit/plugins/lookup/test_tss.py | 43 +++++++++ 2 files changed, 174 insertions(+) create mode 100644 plugins/lookup/tss.py create mode 100644 tests/unit/plugins/lookup/test_tss.py diff --git a/plugins/lookup/tss.py b/plugins/lookup/tss.py new file mode 100644 index 0000000000..0691155c6a --- /dev/null +++ b/plugins/lookup/tss.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, Adam Migus +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" +lookup: tss +author: Adam Migus (adam@migus.org) +short_description: Get secrets from Thycotic Secret Server +version_added: 1.0.0 +description: + - Uses the Thycotic Secret Server Python SDK to get Secrets from Secret + Server using token authentication with I(username) and I(password) on + the REST API at I(base_url). +requirements: + - python-tss-sdk - https://pypi.org/project/python-tss-sdk/ +options: + _terms: + description: The integer ID of the secret. + required: true + type: int + base_url: + description: The base URL of the server, e.g. C(https://localhost/SecretServer). + env: + - name: TSS_BASE_URL + ini: + - section: tss_lookup + key: base_url + required: true + username: + description: The username with which to request the OAuth2 Access Grant. + env: + - name: TSS_USERNAME + ini: + - section: tss_lookup + key: username + required: true + password: + description: The password associated with the supplied username. + env: + - name: TSS_PASSWORD + ini: + - section: tss_lookup + key: password + required: true + api_path_uri: + default: /api/v1 + description: The path to append to the base URL to form a valid REST + API request. + env: + - name: TSS_API_PATH_URI + required: false + token_path_uri: + default: /oauth2/token + description: The path to append to the base URL to form a valid OAuth2 + Access Grant request. + env: + - name: TSS_TOKEN_PATH_URI + required: false +""" + +RETURN = r""" +_list: + description: + - The JSON responses to C(GET /secrets/{id}). + - See U(https://updates.thycotic.net/secretserver/restapiguide/TokenAuth/#operation--secrets--id--get). +""" + +EXAMPLES = r""" +- hosts: localhost + vars: + secret: "{{ lookup('community.general.tss', 1) }}" + tasks: + - debug: msg="the password is {{ (secret['items'] | items2dict(key_name='slug', value_name='itemValue'))['password'] }}" +""" + +from ansible.errors import AnsibleError, AnsibleOptionsError + +sdk_is_missing = False + +try: + from thycotic.secrets.server import ( + SecretServer, + SecretServerAccessError, + SecretServerError, + ) +except ImportError: + sdk_is_missing = True + +from ansible.utils.display import Display +from ansible.plugins.lookup import LookupBase + + +display = Display() + + +class LookupModule(LookupBase): + @staticmethod + def Client(server_parameters): + return SecretServer(**server_parameters) + + def run(self, terms, variables, **kwargs): + if sdk_is_missing: + raise AnsibleError("python-tss-sdk must be installed to use this plugin") + + self.set_options(var_options=variables, direct=kwargs) + + secret_server = LookupModule.Client( + { + "base_url": self.get_option("base_url"), + "username": self.get_option("username"), + "password": self.get_option("password"), + "api_path_uri": self.get_option("api_path_uri"), + "token_path_uri": self.get_option("token_path_uri"), + } + ) + result = [] + + for term in terms: + display.debug("tss_lookup term: %s" % term) + try: + id = int(term) + display.vvv(u"Secret Server lookup of Secret with ID %d" % id) + result.append(secret_server.get_secret_json(id)) + except ValueError: + raise AnsibleOptionsError("Secret ID must be an integer") + except SecretServerError as error: + raise AnsibleError("Secret Server lookup failure: %s" % error.message) + return result diff --git a/tests/unit/plugins/lookup/test_tss.py b/tests/unit/plugins/lookup/test_tss.py new file mode 100644 index 0000000000..cca2f6ff5f --- /dev/null +++ b/tests/unit/plugins/lookup/test_tss.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# (c) 2020, Adam Migus +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.community.general.tests.unit.compat.unittest import TestCase +from ansible_collections.community.general.tests.unit.compat.mock import ( + patch, + MagicMock, +) +from ansible_collections.community.general.plugins.lookup import tss +from ansible.plugins.loader import lookup_loader + + +class MockSecretServer(MagicMock): + RESPONSE = '{"foo": "bar"}' + + def get_secret_json(self, path): + return self.RESPONSE + + +class TestLookupModule(TestCase): + def setUp(self): + tss.sdk_is_missing = False + self.lookup = lookup_loader.get("community.general.tss") + + @patch( + "ansible_collections.community.general.plugins.lookup.tss.LookupModule.Client", + MockSecretServer(), + ) + def test_get_secret_json(self): + self.assertListEqual( + [MockSecretServer.RESPONSE], + self.lookup.run( + [1], + [], + **{"base_url": "dummy", "username": "dummy", "password": "dummy", } + ), + )