192 lines
6.1 KiB
Python
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", "")]
|