fastapi-gsap/ansible_collection/guildhouse/bastion/plugins/lookup/credential.py
Tyler J King 85afbd8d61 feat(ansible): add guildhouse.bastion Ansible Galaxy collection
Dynamic inventory plugin — queries Bastion for managed devices,
groups by OS and compliance state, bastion_* host vars, zero
credentials in inventory.

Credential lookup plugin — resolves short-lived credentials from
Bastion's CredentialResolver at execution time. Graceful
degradation when broker unavailable.

Chronicle callback plugin — reports playbook lifecycle events
(started, task completed, completed) to Chronicle. Optionally
triggers compliance re-evaluation after playbook completion.

Shared BastionClient for all plugins using stdlib urllib.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-14 11:23:03 -04:00

86 lines
2.9 KiB
Python

# Copyright 2026 Guildhouse Dev
# SPDX-License-Identifier: Apache-2.0
"""Bastion credential lookup plugin for Ansible.
Resolves short-lived credentials from Bastion's CredentialResolver
at playbook execution time. Replaces static credentials in
Ansible Vault with dynamic, scoped, time-limited credentials.
SECURITY: Credentials are resolved per-host at execution time.
They are NOT cached, NOT written to facts, NOT stored in any
persistent structure. Ansible holds them only for the duration
of the connection to that specific host.
"""
from __future__ import annotations
import logging
import os
logger = logging.getLogger(__name__)
DOCUMENTATION = """
name: guildhouse.bastion.credential
short_description: Resolve credentials from Bastion
description:
- Acquires short-lived credentials from Bastion CredentialResolver
- Zero credential storage -- resolved at execution time only
options:
host:
description: Target hostname to resolve credential for
required: true
type:
description: Credential type (kerberos, oauth, ssh_cert, auto)
required: false
default: auto
ac_id:
description: Authorization Context ID for governed resolution
required: false
env:
- name: BASTION_AC_ID
"""
# OS type to credential type mapping for auto-detection
_AUTO_TYPE_MAP = {
"windows": "kerberos",
"linux": "ssh_cert",
"macos": "ssh_cert",
}
def resolve_credential(host, credential_type="auto", ac_id=None, host_vars=None):
"""Resolve a credential for the specified host.
1. Determine credential type (auto-detect from bastion_os_type
host var, or use explicit type)
2. Call Bastion broker's credential resolution endpoint
3. Return the credential value
NOTE: Until the broker exposes a /credentials/resolve HTTP
endpoint, this returns None with a warning, allowing Ansible
to fall back to its own credential resolution.
"""
if credential_type == "auto" and host_vars:
os_type = host_vars.get("bastion_os_type", "").lower()
credential_type = _AUTO_TYPE_MAP.get(os_type, "oauth")
# Import here to avoid hard dependency on bastion_client at
# module load time (Ansible loads all lookup plugins eagerly).
try:
from ansible_collection.guildhouse.bastion.plugins.module_utils.bastion_client import BastionClient
client = BastionClient()
result = client.resolve_credential(credential_type, host, ac_id)
if result is not None:
return result
except ImportError:
pass
except Exception as e:
logger.warning("Bastion credential resolution failed for %s: %s", host, e)
logger.debug(
"Bastion credential resolution not available for %s "
"(type=%s). Falling back to Ansible credential resolution.",
host, credential_type,
)
return None