Files
ansible/lookup_plugins/keepass.py
2026-09-08 16:32:02 +02:00

192 lines
6.1 KiB
Python

# Copy this file to ~/.ansible/plugins/lookup/keepass.py
#
# Prerequisites:
# pip install keepassxc-proxy-client
#
# KeePassXC setup:
# Tools > Settings > Browser Integration > Enable browser integration
# The plugin will prompt you to approve the association in KeePassXC on first run.
# Association credentials are saved to ~/.keepassxc_ansible_assoc (override with
# env var KEEPASSXC_ASSOC_FILE).
#
# Usage:
# {{ lookup('keepass', 'https://example.com', 'password') }}
# {{ lookup('keepass', 'https://example.com', 'username') }}
# {{ lookup('keepass', 'https://example.com') }} # defaults to password
#
# Terms:
# terms[0] URL (or title) to search for — matched against KeePassXC entries
# terms[1] Attribute to return: username / user / password / pass / passwd
# Defaults to 'password' if omitted.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
import os
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
DOCUMENTATION = r"""
lookup: keepass
short_description: Fetch credentials from KeePassXC via the browser proxy protocol
description:
- Connects to a running KeePassXC instance over its browser-integration Unix socket
(or Windows named pipe) and retrieves credentials for a given URL.
- On first use the plugin associates itself with KeePassXC; you will see an
approval dialog in KeePassXC. The association is saved to disk so subsequent
runs are silent.
options:
_terms:
description:
- First term is the URL (or entry title URL) to look up.
- Second term (optional) is the attribute to return.
Accepted values are username, user, password, pass, passwd.
Defaults to password.
required: true
notes:
- KeePassXC must be running with Browser Integration enabled.
- Requires the Python package keepassxc-proxy-client.
"""
EXAMPLES = r"""
- name: Get password for an entry
debug:
msg: "{{ lookup('keepass', 'https://example.com') }}"
- name: Get username for an entry
debug:
msg: "{{ lookup('keepass', 'https://example.com', 'username') }}"
- name: Set pi account password from KeePassXC
user:
name: pi
password: "{{ lookup('keepass', 'default_rpi_password') | password_hash('sha512') }}"
"""
ASSOC_FILE_ENV = "KEEPASSXC_ASSOC_FILE"
ASSOC_FILE_DEFAULT = os.path.expanduser("~/.keepassxc_ansible_assoc")
ASSOC_NAME = "ansible-keepassxc"
USERNAME_ATTRS = {"username", "user"}
PASSWORD_ATTRS = {"password", "pass", "passwd"}
def _get_assoc_file():
return os.environ.get(ASSOC_FILE_ENV, ASSOC_FILE_DEFAULT)
def _load_assoc(connection):
"""Load a previously saved association from disk into *connection*. Returns True on success."""
path = _get_assoc_file()
if not os.path.exists(path):
return False
try:
with open(path, "r") as fh:
data = json.load(fh)
connection.load_associate(data["name"], bytes(data["public_key"]))
return True
except (KeyError, ValueError, OSError):
return False
def _save_assoc(connection):
"""Persist the current association to disk."""
path = _get_assoc_file()
assoc_id, public_key = connection.dump_associate()
data = {"name": assoc_id, "public_key": list(public_key)}
with open(path, "w") as fh:
json.dump(data, fh)
os.chmod(path, 0o600)
def _connect():
"""Return an authenticated, test-verified Connection to KeePassXC."""
try:
from keepassxc_proxy_client.protocol import Connection, ResponseUnsuccesfulException
except ImportError:
raise AnsibleError(
"keepassxc-proxy-client is not installed. "
"Run: pip install keepassxc-proxy-client"
)
conn = Connection()
try:
conn.connect()
except Exception as exc:
raise AnsibleError(
"Cannot connect to KeePassXC. Is it running with Browser Integration enabled? "
"Error: {}".format(exc)
)
conn.change_public_keys()
# Try to reuse an existing association
if _load_assoc(conn):
try:
conn.test_associate(trigger_unlock=True)
return conn # existing association is still valid
except Exception:
pass # fall through and re-associate
# First run (or stale association) — ask KeePassXC to authorise us
try:
conn.associate()
except Exception as exc:
raise AnsibleError(
"KeePassXC association failed. "
"Please approve the request in the KeePassXC dialog. "
"Error: {}".format(exc)
)
try:
conn.test_associate(trigger_unlock=True)
except Exception as exc:
raise AnsibleError("KeePassXC association test failed: {}".format(exc))
_save_assoc(conn)
return conn
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
if not terms or len(terms) > 2:
raise AnsibleError(
"keepass lookup expects 1 or 2 arguments: "
"lookup('keepass', '<url>', '<attribute>')"
)
url = terms[0]
attr = terms[1].lower() if len(terms) == 2 else "password"
if attr not in USERNAME_ATTRS | PASSWORD_ATTRS:
raise AnsibleError(
"keepass: unsupported attribute '{}'. "
"Use one of: username, user, password, pass, passwd".format(attr)
)
conn = _connect()
try:
entries = conn.get_logins(url)
except Exception as exc:
raise AnsibleError(
"keepass: failed to retrieve logins for '{}': {}".format(url, exc)
)
if not entries:
raise AnsibleError(
"keepass: no entries found in KeePassXC for URL '{}'".format(url)
)
# Return the first matching entry (KeePassXC already filters by URL)
entry = entries[0]
if attr in USERNAME_ATTRS:
return [entry.get("login", "")]
else:
return [entry.get("password", "")]