Source code for saltext.vault.beacons.vault_lease

"""
Beacon for the Vault integration. Sends events when a
lease's TTL undercuts a specified value. By default, also
tries to renew leases before sending an event.

.. versionadded:: 1.1.0

Event description
-----------------

When a lease undercuts its minimum TTL, an event is sent.

The event tag's format is: ``salt/beacon/<minion ID>/vault_lease_<lease cache key>/expire``

The event data contains (non-exhaustive):

* ``expires_in`` - number of seconds left until the lease is revoked by Vault (can be ``-1`` if already revoked)
* ``lease_id`` - the lease ID of the expiring lease
* ``ckey`` - the cache key of the expiring lease
* ``meta`` - custom metadata, e.g. for use in a reactor
* ``expired`` - if the lease is already expired

Example configuration
---------------------
.. code-block:: yaml

    beacons:
      vault_basic:
        - beacon_module: vault_lease
        - leases:
            - db.database.dynamic.basic_lease.default

      vault_advanced:
        - beacon_module: vault_lease
        - leases:
            db.database.dynamic.write_stuff.default: {}
            db.database.dynamic.monitoring.default:
              renew: false
            db.database.dynamic.read_stuff.default:
              min_ttl: 6h
              meta:
                sls: read.stuff
        - min_ttl: 1h
        - meta:
            sls: write.stuff
        - check_server: true

.. _beacon-state-example:

Example for enabling beacon via state
-------------------------------------
This beacon can be added dynamically when explicitly caching
database leases.

.. code-block:: yaml

    Important Vault lease is cached:
      vault_db.creds_cached:
        - name: my_important_role
        - valid_for: 6h  # minimum TTL for the lease to be returned by get_creds
        - revoke_delay: 30m
        - beacon: true   # also add a beacon for monitoring
        - beacon_interval: 300  # interval between beacon runs
        - min_ttl: 12h   # minimum TTL for the beacon to accept the lease as valid
        - meta: my.important.state  # can be used with a reactor
        - order: first   # leases should be cached early

Configuration reference
-----------------------
.. vconf:: lease_beacon.leases

``leases``
    Leases to monitor, referenced by their cache keys.
    This can be a string (single lease), list (multiple leases)
    or mapping (multiple leases with parameter overrides).

.. vconf:: lease_beacon.min_ttl

``min_ttl``
    Minimum TTL a monitored lease should have.
    Can be overridden per configured lease in :vconf:`lease_beacon.leases`.
    If a ``min_ttl`` was set on the lease during its creation,
    this value must be equal or greater to have any effect.
    Defaults to ``300``.

.. vconf:: lease_beacon.check_server

``check_server``
    Whether cached leases should be validated with the Vault server
    before declaring them as valid.
    Can be overridden per configured lease in :vconf:`lease_beacon.leases`.
    There is no equivalent parameter that can be set on the lease during
    its creation currently.
    Defaults to false.

.. vconf:: lease_beacon.meta

``meta``
    Arbitrary metadata to include in expiry events.
    Can be overridden per configured lease in :vconf:`lease_beacon.leases`.
    If ``meta`` was set on the lease during creation, the corresponding
    value takes precedence. If both values are either mappings or lists,
    they are merged together.

.. vconf:: lease_beacon.renew

``renew``
    Before sending an event, try to renew the lease as needed.
    Defaults to true.
"""

import logging
from typing import TYPE_CHECKING

import salt.utils.beacons
import salt.utils.dictupdate as dup

from saltext.vault.utils import vault
from saltext.vault.utils.vault.helpers import timestring_map

if TYPE_CHECKING:
    from saltext.vault.utils._types import SaltContext
    from saltext.vault.utils._types import SaltLogger
    from saltext.vault.utils._types import SaltOpts

    __opts__: SaltOpts
    __context__: SaltContext


log: "SaltLogger" = logging.getLogger(__name__)  # type: ignore

__virtualname__ = "vault_lease"


def __virtual__():
    return __virtualname__


[docs] def validate(config): """ Validate the beacon configuration """ if not isinstance(config, list): return False, "Configuration for vault_lease must be a list" config = salt.utils.beacons.list_to_dict(config) if "leases" not in config: return False, "Requires monitored lease(s) cache key(s) in `leases`" if not isinstance(config["leases"], (dict, list, str)): return False, "`leases` must be a dict, list or str" if isinstance(config["leases"], str): if "*" in config["leases"]: return False, "`leases` does not support globs" else: if any("*" in lease for lease in config["leases"]): return False, "`leases` does not support globs" if isinstance(config["leases"], dict) and any( not isinstance(cfg, dict) for cfg in config["leases"].values() ): return False, "`leases` mapping values must be dicts" return True, "Valid beacon configuration."
[docs] def beacon(config): """ Watch the configured lease(s). """ config = _render_config(config) # background processes should not pass __context__ store = vault.get_lease_store(__opts__, {}) events = [] for lease, lease_config in config["leases"].items(): info = store.list_info(match=lease) if not info: events.append(_enrich_info(lease, lease_config, {"expires_in": -1, "expired": True})) continue lease_info = info[lease] effective_config = _merge_lease_config(lease_config, lease_info) if effective_config.get("check_server"): try: store.lookup(lease_info["lease_id"]) except vault.VaultNotFoundError: store.revoke(lease_info["lease_id"], delta=lease_info.get("revoke_delay")) lease_info["expires_in"] = -1 lease_info["expired"] = True events.append(_enrich_info(lease, effective_config, lease_info)) continue if lease_info["expired"]: events.append(_enrich_info(lease, effective_config, lease_info)) continue if timestring_map(effective_config["min_ttl"]) >= lease_info["expires_in"]: if not effective_config.get("renew", True): events.append(_enrich_info(lease, effective_config, lease_info)) continue # attempt renewal res = store.get( lease, valid_for=effective_config["min_ttl"], revoke=False, check_server=effective_config.get("check_server", False), ) if not res: events.append(_enrich_info(lease, effective_config, lease_info)) continue return events
def _enrich_info(lease, effective_config, info): info["ckey"] = lease info["meta"] = effective_config.get("meta") info["min_ttl"] = effective_config.get("min_ttl", 300) info["check_server"] = effective_config.get("check_server") info.pop("id", None) info["tag"] = "expire" return info def _render_config(cfg): config = salt.utils.beacons.list_to_dict(cfg) if isinstance(config["leases"], str): config["leases"] = {config["leases"]: {}} if not isinstance(config["leases"], dict): config["leases"] = {lease: {} for lease in config["leases"]} defaults = {} for param in ("min_ttl", "meta", "check_server", "renew"): if param in config: defaults[param] = config[param] return { "leases": { lease: {**defaults, **lease_config} for lease, lease_config in config["leases"].items() } } def _merge_lease_config(cfg, lease): if cfg.get("min_ttl") is not None and lease.get("min_ttl") is not None: cfg["min_ttl"] = ( lease["min_ttl"] if timestring_map(lease["min_ttl"]) >= timestring_map(cfg["min_ttl"]) else cfg["min_ttl"] ) elif lease.get("min_ttl") is not None: cfg["min_ttl"] = lease["min_ttl"] elif "min_ttl" not in cfg: cfg["min_ttl"] = 300 cfg["meta"] = _merge_meta(cfg.get("meta"), lease.get("meta")) return cfg def _merge_meta(default, ovrr): if ovrr is None: return default default = default if default is not None else {} for val in (default, ovrr): if not isinstance(val, (dict, list)): return ovrr if type(default) is not type(ovrr): return ovrr if isinstance(default, list): return default + ovrr return dup.merge(default, ovrr, merge_lists=True) or None