1"""
2:maintainer:    SaltStack
3:maturity:      new
4:platform:      all
5
6Runner functions supporting the Vault modules. Configuration instructions are
7documented in the execution module docs.
8"""
9
10import base64
11import json
12import logging
13import string
14import time
15
16import requests
17import salt.crypt
18import salt.exceptions
19
20log = logging.getLogger(__name__)
21
22
23def generate_token(
24    minion_id, signature, impersonated_by_master=False, ttl=None, uses=None
25):
26    """
27    Generate a Vault token for minion minion_id
28
29    minion_id
30        The id of the minion that requests a token
31
32    signature
33        Cryptographic signature which validates that the request is indeed sent
34        by the minion (or the master, see impersonated_by_master).
35
36    impersonated_by_master
37        If the master needs to create a token on behalf of the minion, this is
38        True. This happens when the master generates minion pillars.
39
40    ttl
41        Ticket time to live in seconds, 1m minutes, or 2h hrs
42
43    uses
44        Number of times a token can be used
45    """
46    log.debug(
47        "Token generation request for %s (impersonated by master: %s)",
48        minion_id,
49        impersonated_by_master,
50    )
51    _validate_signature(minion_id, signature, impersonated_by_master)
52    try:
53        config = __opts__.get("vault", {})
54        verify = config.get("verify", None)
55        # Vault Enterprise requires a namespace
56        namespace = config.get("namespace")
57        # Allow disabling of minion provided values via the master
58        allow_minion_override = config["auth"].get("allow_minion_override", False)
59        # This preserves the previous behavior of default TTL and 1 use
60        if not allow_minion_override or uses is None:
61            uses = config["auth"].get("uses", 1)
62        if not allow_minion_override or ttl is None:
63            ttl = config["auth"].get("ttl", None)
64        storage_type = config["auth"].get("token_backend", "session")
65
66        if config["auth"]["method"] == "approle":
67            if _selftoken_expired():
68                log.debug("Vault token expired. Recreating one")
69                # Requesting a short ttl token
70                url = "{}/v1/auth/approle/login".format(config["url"])
71                payload = {"role_id": config["auth"]["role_id"]}
72                if "secret_id" in config["auth"]:
73                    payload["secret_id"] = config["auth"]["secret_id"]
74                # Vault Enterprise call requires headers
75                headers = None
76                if namespace is not None:
77                    headers = {"X-Vault-Namespace": namespace}
78                response = requests.post(
79                    url, headers=headers, json=payload, verify=verify
80                )
81                if response.status_code != 200:
82                    return {"error": response.reason}
83                config["auth"]["token"] = response.json()["auth"]["client_token"]
84
85        url = _get_token_create_url(config)
86        headers = {"X-Vault-Token": config["auth"]["token"]}
87        if namespace is not None:
88            headers["X-Vault-Namespace"] = namespace
89        audit_data = {
90            "saltstack-jid": globals().get("__jid__", "<no jid set>"),
91            "saltstack-minion": minion_id,
92            "saltstack-user": globals().get("__user__", "<no user set>"),
93        }
94        payload = {
95            "policies": _get_policies(minion_id, config),
96            "num_uses": uses,
97            "meta": audit_data,
98        }
99
100        if ttl is not None:
101            payload["explicit_max_ttl"] = str(ttl)
102
103        if payload["policies"] == []:
104            return {"error": "No policies matched minion"}
105
106        log.trace("Sending token creation request to Vault")
107        response = requests.post(url, headers=headers, json=payload, verify=verify)
108
109        if response.status_code != 200:
110            return {"error": response.reason}
111
112        auth_data = response.json()["auth"]
113        ret = {
114            "token": auth_data["client_token"],
115            "lease_duration": auth_data["lease_duration"],
116            "renewable": auth_data["renewable"],
117            "issued": int(round(time.time())),
118            "url": config["url"],
119            "verify": verify,
120            "token_backend": storage_type,
121            "namespace": namespace,
122        }
123        if uses >= 0:
124            ret["uses"] = uses
125
126        return ret
127    except Exception as e:  # pylint: disable=broad-except
128        return {"error": str(e)}
129
130
131def unseal():
132    """
133    Unseal Vault server
134
135    This function uses the 'keys' from the 'vault' configuration to unseal vault server
136
137    vault:
138      keys:
139        - n63/TbrQuL3xaIW7ZZpuXj/tIfnK1/MbVxO4vT3wYD2A
140        - S9OwCvMRhErEA4NVVELYBs6w/Me6+urgUr24xGK44Uy3
141        - F1j4b7JKq850NS6Kboiy5laJ0xY8dWJvB3fcwA+SraYl
142        - 1cYtvjKJNDVam9c7HNqJUfINk4PYyAXIpjkpN/sIuzPv
143        - 3pPK5X6vGtwLhNOFv1U2elahECz3HpRUfNXJFYLw6lid
144
145    .. note: This function will send unsealed keys until the api returns back
146             that the vault has been unsealed
147
148    CLI Examples:
149
150    .. code-block:: bash
151
152        salt-run vault.unseal
153    """
154    for key in __opts__["vault"]["keys"]:
155        ret = __utils__["vault.make_request"](
156            "PUT", "v1/sys/unseal", data=json.dumps({"key": key})
157        ).json()
158        if ret["sealed"] is False:
159            return True
160    return False
161
162
163def show_policies(minion_id):
164    """
165    Show the Vault policies that are applied to tokens for the given minion
166
167    minion_id
168        The minions id
169
170    CLI Example:
171
172    .. code-block:: bash
173
174        salt-run vault.show_policies myminion
175    """
176    config = __opts__["vault"]
177    return _get_policies(minion_id, config)
178
179
180def _validate_signature(minion_id, signature, impersonated_by_master):
181    """
182    Validate that either minion with id minion_id, or the master, signed the
183    request
184    """
185    pki_dir = __opts__["pki_dir"]
186    if impersonated_by_master:
187        public_key = "{}/master.pub".format(pki_dir)
188    else:
189        public_key = "{}/minions/{}".format(pki_dir, minion_id)
190
191    log.trace("Validating signature for %s", minion_id)
192    signature = base64.b64decode(signature)
193    if not salt.crypt.verify_signature(public_key, minion_id, signature):
194        raise salt.exceptions.AuthenticationError(
195            "Could not validate token request from {}".format(minion_id)
196        )
197    log.trace("Signature ok")
198
199
200def _get_policies(minion_id, config):
201    """
202    Get the policies that should be applied to a token for minion_id
203    """
204    _, grains, _ = salt.utils.minions.get_minion_data(minion_id, __opts__)
205    policy_patterns = config.get(
206        "policies", ["saltstack/minion/{minion}", "saltstack/minions"]
207    )
208    mappings = {"minion": minion_id, "grains": grains or {}}
209
210    policies = []
211    for pattern in policy_patterns:
212        try:
213            for expanded_pattern in _expand_pattern_lists(pattern, **mappings):
214                policies.append(
215                    expanded_pattern.format(**mappings).lower()  # Vault requirement
216                )
217        except KeyError:
218            log.warning("Could not resolve policy pattern %s", pattern)
219
220    log.debug("%s policies: %s", minion_id, policies)
221    return policies
222
223
224def _expand_pattern_lists(pattern, **mappings):
225    """
226    Expands the pattern for any list-valued mappings, such that for any list of
227    length N in the mappings present in the pattern, N copies of the pattern are
228    returned, each with an element of the list substituted.
229
230    pattern:
231        A pattern to expand, for example ``by-role/{grains[roles]}``
232
233    mappings:
234        A dictionary of variables that can be expanded into the pattern.
235
236    Example: Given the pattern `` by-role/{grains[roles]}`` and the below grains
237
238    .. code-block:: yaml
239
240        grains:
241            roles:
242                - web
243                - database
244
245    This function will expand into two patterns,
246    ``[by-role/web, by-role/database]``.
247
248    Note that this method does not expand any non-list patterns.
249    """
250    expanded_patterns = []
251    f = string.Formatter()
252
253    # This function uses a string.Formatter to get all the formatting tokens from
254    # the pattern, then recursively replaces tokens whose expanded value is a
255    # list. For a list with N items, it will create N new pattern strings and
256    # then continue with the next token. In practice this is expected to not be
257    # very expensive, since patterns will typically involve a handful of lists at
258    # most.
259
260    for (_, field_name, _, _) in f.parse(pattern):
261        if field_name is None:
262            continue
263        (value, _) = f.get_field(field_name, None, mappings)
264        if isinstance(value, list):
265            token = "{{{0}}}".format(field_name)
266            expanded = [pattern.replace(token, str(elem)) for elem in value]
267            for expanded_item in expanded:
268                result = _expand_pattern_lists(expanded_item, **mappings)
269                expanded_patterns += result
270            return expanded_patterns
271    return [pattern]
272
273
274def _selftoken_expired():
275    """
276    Validate the current token exists and is still valid
277    """
278    try:
279        verify = __opts__["vault"].get("verify", None)
280        # Vault Enterprise requires a namespace
281        namespace = __opts__["vault"].get("namespace")
282        url = "{}/v1/auth/token/lookup-self".format(__opts__["vault"]["url"])
283        if "token" not in __opts__["vault"]["auth"]:
284            return True
285        headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]}
286        # Add Vault namespace to headers if Vault Enterprise enabled
287        if namespace is not None:
288            headers["X-Vault-Namespace"] = namespace
289        response = requests.get(url, headers=headers, verify=verify)
290        if response.status_code != 200:
291            return True
292        return False
293    except Exception as e:  # pylint: disable=broad-except
294        raise salt.exceptions.CommandExecutionError(
295            "Error while looking up self token : {}".format(str(e))
296        )
297
298
299def _get_token_create_url(config):
300    """
301    Create Vault url for token creation
302    """
303    role_name = config.get("role_name", None)
304    auth_path = "/v1/auth/token/create"
305    base_url = config["url"]
306    return "/".join(x.strip("/") for x in (base_url, auth_path, role_name) if x)
307