1# -*- coding: utf-8 -*-
2# Copyright: (c) 2020, Adam Migus <adam@migus.org>
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4from __future__ import absolute_import, division, print_function
5
6__metaclass__ = type
7
8DOCUMENTATION = r"""
9name: tss
10author: Adam Migus (@amigus) <adam@migus.org>
11short_description: Get secrets from Thycotic Secret Server
12version_added: 1.0.0
13description:
14    - Uses the Thycotic Secret Server Python SDK to get Secrets from Secret
15      Server using token authentication with I(username) and I(password) on
16      the REST API at I(base_url).
17requirements:
18    - python-tss-sdk - https://pypi.org/project/python-tss-sdk/
19options:
20    _terms:
21        description: The integer ID of the secret.
22        required: true
23        type: int
24    base_url:
25        description: The base URL of the server, e.g. C(https://localhost/SecretServer).
26        env:
27            - name: TSS_BASE_URL
28        ini:
29            - section: tss_lookup
30              key: base_url
31        required: true
32    username:
33        description: The username with which to request the OAuth2 Access Grant.
34        env:
35            - name: TSS_USERNAME
36        ini:
37            - section: tss_lookup
38              key: username
39    password:
40        description:
41            - The password associated with the supplied username.
42            - Required when I(token) is not provided.
43        env:
44            - name: TSS_PASSWORD
45        ini:
46            - section: tss_lookup
47              key: password
48    domain:
49        default: ""
50        description:
51          - The domain with which to request the OAuth2 Access Grant.
52          - Optional when I(token) is not provided.
53          - Requires C(python-tss-sdk) version 1.0.0 or greater.
54        env:
55            - name: TSS_DOMAIN
56        ini:
57            - section: tss_lookup
58              key: domain
59        required: false
60        version_added: 3.6.0
61    token:
62        description:
63          - Existing token for Thycotic authorizer.
64          - If provided, I(username) and I(password) are not needed.
65          - Requires C(python-tss-sdk) version 1.0.0 or greater.
66        env:
67            - name: TSS_TOKEN
68        ini:
69            - section: tss_lookup
70              key: token
71        version_added: 3.7.0
72    api_path_uri:
73        default: /api/v1
74        description: The path to append to the base URL to form a valid REST
75            API request.
76        env:
77            - name: TSS_API_PATH_URI
78        required: false
79    token_path_uri:
80        default: /oauth2/token
81        description: The path to append to the base URL to form a valid OAuth2
82            Access Grant request.
83        env:
84            - name: TSS_TOKEN_PATH_URI
85        required: false
86"""
87
88RETURN = r"""
89_list:
90    description:
91        - The JSON responses to C(GET /secrets/{id}).
92        - See U(https://updates.thycotic.net/secretserver/restapiguide/TokenAuth/#operation--secrets--id--get).
93    type: list
94    elements: dict
95"""
96
97EXAMPLES = r"""
98- hosts: localhost
99  vars:
100      secret: >-
101        {{
102            lookup(
103                'community.general.tss',
104                102,
105                base_url='https://secretserver.domain.com/SecretServer/',
106                username='user.name',
107                password='password'
108            )
109        }}
110  tasks:
111      - ansible.builtin.debug:
112          msg: >
113            the password is {{
114              (secret['items']
115                | items2dict(key_name='slug',
116                             value_name='itemValue'))['password']
117            }}
118
119- hosts: localhost
120  vars:
121      secret: >-
122        {{
123            lookup(
124                'community.general.tss',
125                102,
126                base_url='https://secretserver.domain.com/SecretServer/',
127                username='user.name',
128                password='password',
129                domain='domain'
130            )
131        }}
132  tasks:
133      - ansible.builtin.debug:
134          msg: >
135            the password is {{
136              (secret['items']
137                | items2dict(key_name='slug',
138                             value_name='itemValue'))['password']
139            }}
140
141- hosts: localhost
142  vars:
143      secret_password: >-
144        {{
145            ((lookup(
146                'community.general.tss',
147                102,
148                base_url='https://secretserver.domain.com/SecretServer/',
149                token='thycotic_access_token',
150            )  | from_json).get('items') | items2dict(key_name='slug', value_name='itemValue'))['password']
151        }}
152  tasks:
153      - ansible.builtin.debug:
154          msg: the password is {{ secret_password }}
155"""
156
157import abc
158
159from ansible.errors import AnsibleError, AnsibleOptionsError
160from ansible.module_utils import six
161from ansible.plugins.lookup import LookupBase
162from ansible.utils.display import Display
163
164try:
165    from thycotic.secrets.server import SecretServer, SecretServerError
166
167    HAS_TSS_SDK = True
168except ImportError:
169    SecretServer = None
170    SecretServerError = None
171    HAS_TSS_SDK = False
172
173try:
174    from thycotic.secrets.server import PasswordGrantAuthorizer, DomainPasswordGrantAuthorizer, AccessTokenAuthorizer
175
176    HAS_TSS_AUTHORIZER = True
177except ImportError:
178    PasswordGrantAuthorizer = None
179    DomainPasswordGrantAuthorizer = None
180    AccessTokenAuthorizer = None
181    HAS_TSS_AUTHORIZER = False
182
183
184display = Display()
185
186
187@six.add_metaclass(abc.ABCMeta)
188class TSSClient(object):
189    def __init__(self):
190        self._client = None
191
192    @staticmethod
193    def from_params(**server_parameters):
194        if HAS_TSS_AUTHORIZER:
195            return TSSClientV1(**server_parameters)
196        else:
197            return TSSClientV0(**server_parameters)
198
199    def get_secret(self, term):
200        display.debug("tss_lookup term: %s" % term)
201
202        secret_id = self._term_to_secret_id(term)
203        display.vvv(u"Secret Server lookup of Secret with ID %d" % secret_id)
204
205        return self._client.get_secret_json(secret_id)
206
207    @staticmethod
208    def _term_to_secret_id(term):
209        try:
210            return int(term)
211        except ValueError:
212            raise AnsibleOptionsError("Secret ID must be an integer")
213
214
215class TSSClientV0(TSSClient):
216    def __init__(self, **server_parameters):
217        super(TSSClientV0, self).__init__()
218
219        if server_parameters.get("domain"):
220            raise AnsibleError("The 'domain' option requires 'python-tss-sdk' version 1.0.0 or greater")
221
222        self._client = SecretServer(
223            server_parameters["base_url"],
224            server_parameters["username"],
225            server_parameters["password"],
226            server_parameters["api_path_uri"],
227            server_parameters["token_path_uri"],
228        )
229
230
231class TSSClientV1(TSSClient):
232    def __init__(self, **server_parameters):
233        super(TSSClientV1, self).__init__()
234
235        authorizer = self._get_authorizer(**server_parameters)
236        self._client = SecretServer(
237            server_parameters["base_url"], authorizer, server_parameters["api_path_uri"]
238        )
239
240    @staticmethod
241    def _get_authorizer(**server_parameters):
242        if server_parameters.get("token"):
243            return AccessTokenAuthorizer(
244                server_parameters["token"],
245            )
246
247        if server_parameters.get("domain"):
248            return DomainPasswordGrantAuthorizer(
249                server_parameters["base_url"],
250                server_parameters["username"],
251                server_parameters["domain"],
252                server_parameters["password"],
253                server_parameters["token_path_uri"],
254            )
255
256        return PasswordGrantAuthorizer(
257            server_parameters["base_url"],
258            server_parameters["username"],
259            server_parameters["password"],
260            server_parameters["token_path_uri"],
261        )
262
263
264class LookupModule(LookupBase):
265    def run(self, terms, variables, **kwargs):
266        if not HAS_TSS_SDK:
267            raise AnsibleError("python-tss-sdk must be installed to use this plugin")
268
269        self.set_options(var_options=variables, direct=kwargs)
270
271        tss = TSSClient.from_params(
272            base_url=self.get_option("base_url"),
273            username=self.get_option("username"),
274            password=self.get_option("password"),
275            domain=self.get_option("domain"),
276            token=self.get_option("token"),
277            api_path_uri=self.get_option("api_path_uri"),
278            token_path_uri=self.get_option("token_path_uri"),
279        )
280
281        try:
282            return [tss.get_secret(term) for term in terms]
283        except SecretServerError as error:
284            raise AnsibleError("Secret Server lookup failure: %s" % error.message)
285