1# -*- coding: utf-8 -*-
2#
3#  Copyright 2018 Edoardo Tenani <e.tenani@arduino.cc> [@endorama]
4#
5# This file is part of Ansible.
6#
7# Ansible is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Ansible is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
19#
20
21from __future__ import (absolute_import, division, print_function)
22__metaclass__ = type
23
24DOCUMENTATION = """
25    name: sops
26    author: Edoardo Tenani (@endorama) <e.tenani@arduino.cc>
27    short_description: Read sops encrypted file contents
28    version_added: '0.1.0'
29    description:
30        - This lookup returns the contents from a file on the Ansible controller's file system.
31        - This lookup requires the C(sops) executable to be available in the controller PATH.
32    options:
33        _terms:
34            description: Path(s) of files to read.
35            required: true
36        rstrip:
37            description: Whether to remove trailing newlines and spaces.
38            type: bool
39            default: true
40        base64:
41            description:
42                - Base64-encodes the parsed result.
43                - Use this if you want to store binary data in Ansible variables.
44            type: bool
45            default: false
46        input_type:
47            description:
48                - Tell sops how to interpret the encrypted file.
49                - By default, sops will chose the input type from the file extension.
50                  If it detects the wrong type for a file, this could result in decryption
51                  failing.
52            type: str
53            choices:
54                - binary
55                - json
56                - yaml
57                - dotenv
58        output_type:
59            description:
60                - Tell sops how to interpret the decrypted file.
61                - By default, sops will chose the output type from the file extension.
62                  If it detects the wrong type for a file, this could result in decryption
63                  failing.
64            type: str
65            choices:
66                - binary
67                - json
68                - yaml
69                - dotenv
70        empty_on_not_exist:
71            description:
72                - When set to C(true), will not raise an error when a file cannot be found,
73                  but return an empty string instead.
74            type: bool
75            default: false
76    extends_documentation_fragment:
77        - community.sops.sops
78        - community.sops.sops.ansible_variables
79    notes:
80        - This lookup does not understand 'globbing' - use the fileglob lookup instead.
81"""
82
83EXAMPLES = """
84- name: Output secrets to screen (BAD IDEA!)
85  ansible.builtin.debug:
86    msg: "Content: {{ lookup('community.sops.sops', item) }}"
87  loop:
88    - sops-encrypted-file.enc.yaml
89
90- name: Add SSH private key
91  ansible.builtin.copy:
92    # Note that rstrip=false is necessary for some SSH versions to be able to use the key
93    content: "{{ lookup('community.sops.sops', user + '-id_rsa', rstrip=false) }}"
94    dest: /home/{{ user }}/.ssh/id_rsa
95    owner: "{{ user }}"
96    group: "{{ user }}"
97    mode: 0600
98  no_log: true  # avoid content to be written to log
99
100- name: The file file.json is a YAML file, which contains the encryption of binary data
101  ansible.builtin.debug:
102    msg: "Content: {{ lookup('community.sops.sops', 'file.json', input_type='yaml', output_type='binary') }}"
103
104"""
105
106RETURN = """
107    _raw:
108        description: Decrypted file content.
109        type: list
110        elements: str
111"""
112
113import base64
114
115from ansible.errors import AnsibleLookupError
116from ansible.plugins.lookup import LookupBase
117from ansible.module_utils.common.text.converters import to_native
118from ansible_collections.community.sops.plugins.module_utils.sops import Sops, SopsError
119
120from ansible.utils.display import Display
121display = Display()
122
123
124class LookupModule(LookupBase):
125
126    def run(self, terms, variables=None, **kwargs):
127        self.set_options(var_options=variables, direct=kwargs)
128        rstrip = self.get_option('rstrip')
129        use_base64 = self.get_option('base64')
130        input_type = self.get_option('input_type')
131        output_type = self.get_option('output_type')
132        empty_on_not_exist = self.get_option('empty_on_not_exist')
133
134        ret = []
135
136        def get_option_value(argument_name):
137            return self.get_option(argument_name)
138
139        for term in terms:
140            display.debug("Sops lookup term: %s" % term)
141            lookupfile = self.find_file_in_search_path(variables, 'files', term, ignore_missing=empty_on_not_exist)
142            display.vvvv(u"Sops lookup using %s as file" % lookupfile)
143
144            if not lookupfile:
145                if empty_on_not_exist:
146                    ret.append('')
147                    continue
148                raise AnsibleLookupError("could not locate file in lookup: %s" % to_native(term))
149
150            try:
151                output = Sops.decrypt(
152                    lookupfile, display=display, rstrip=rstrip, decode_output=not use_base64,
153                    input_type=input_type, output_type=output_type, get_option_value=get_option_value)
154            except SopsError as e:
155                raise AnsibleLookupError(to_native(e))
156
157            if use_base64:
158                output = to_native(base64.b64encode(output))
159
160            ret.append(output)
161
162        return ret
163