#!/usr/bin/env bash

#
# docker-compose-vault-key
#
# Cache an Ansible (Docker) vault password in the Linux keyring.
#

set -euo pipefail

PATH="/usr/bin"

PRG=$( basename "$0" )

KEY_NAME="docker-compose-vault"

# Keyring selection:
#
# - @u: User keyring (shared by all sessions of the same user).
#       This is intentional: the vault password represents a user-level
#       activation state, not a terminal/session-specific state.
#
# - @s: Session keyring (only for the current shell/session).
#       Not used because multiple sessions of the same user must share
#       the same cached key and expiry state.
#
# Use user keyring (@u) because the vault password is a shared state between
# sessions of the same user.
KEYRING="@u"

TTL="${DOCKER_COMPOSE_VAULT_KEY_TTL:-900}"

# The expiry file is user-scoped because the key is stored in the user keyring (@u).
# Multiple sessions of the same user share the same key and expiry timestamp.
EXPIRY_FILE="${XDG_RUNTIME_DIR:-/tmp}/${PRG}-${USER:-$(id -un)}.expiry"

find_key() {
    keyctl search "$KEYRING" user "$KEY_NAME" 2>/dev/null
}

parse_ttl() {

   local VALUE="$1"
   local NUMBER

    if [[ ! "$VALUE" =~ ^[0-9]+([smh])?$ ]]; then
        echo "Invalid TTL: '$VALUE'" >&2
        echo "Expected: <seconds>, <seconds>s, <minutes>m or <hours>h." >&2
        exit 1
    fi

#    case "$VALUE" in
#        *m)
#            echo $(( ${VALUE%m} * 60 ))
#            ;;
#        *h)
#            echo $(( ${VALUE%h} * 3600 ))
#            ;;
#        *s)
#            echo "${VALUE%s}"
#            ;;
#        *)
#            echo "$VALUE"
#            ;;
#    esac

    NUMBER="${VALUE%[smh]}"

    case "$VALUE" in
        *h)
            [[ ${#NUMBER} -le 2 ]] || {
                echo "Maximum TTL is approximately 12 hours." >&2
                exit 1
            }
            echo $(( NUMBER * 3600 ))
            ;;

        *m)
            [[ ${#NUMBER} -le 3 ]] || {
                echo "Maximum TTL is approximately 12 hours." >&2
                exit 1
            }
            echo $(( NUMBER * 60 ))
            ;;

        *)
            [[ ${#NUMBER} -le 5 ]] || {
                echo "Maximum TTL is approximately 12 hours." >&2
                exit 1
            }
            echo "$NUMBER"
            ;;
    esac
}

activate() {

    local ID
    local PASS=""
    local NOW
    local REPLACE=false

    if [[ "${1:-}" == "true" ]]; then
        REPLACE=true
    fi

    if ID=$(find_key); then

        if [[ "$REPLACE" != true ]]; then
            echo "Vault password already cached."
            echo "Use '$PRG activate --replace' to replace it."
            return 0
        fi

        keyctl unlink "$ID" "$KEYRING"
        rm -f "$EXPIRY_FILE"

        echo "Existing vault password removed."
    fi

    read -rsp "Vault password: " PASS
    echo

    ID=$(printf "%s" "$PASS" | keyctl padd user "$KEY_NAME" "$KEYRING")

    unset PASS

    TTL=$(parse_ttl "$TTL")
    keyctl timeout "$ID" "$TTL"

    NOW=$(date +%s)
    echo "$(( NOW + TTL ))" > "$EXPIRY_FILE"

    echo "Password cached for $TTL seconds."
}

deactivate() {

    local ID

    if ID=$(find_key); then
        keyctl unlink "$ID" "$KEYRING"
        rm -f "$EXPIRY_FILE"
        echo "Password removed."
    else
        echo "No cached password."
    fi
}

status() {

    local ID
    local EXPIRY
    local REMAINING

    if ID=$(find_key); then

        echo "Docker compose vault status"
        echo
        echo "Status      : cached"
        echo "Key ID      : $ID"
        echo "Key name    : $KEY_NAME"
        echo "Keyring     : $KEYRING"
        echo "User        : $(id -un) ($(id -u))"

        if [[ -f "$EXPIRY_FILE" ]]; then
            EXPIRY=$(cat "$EXPIRY_FILE")
            REMAINING=$(( EXPIRY - $(date +%s) ))

            if (( REMAINING > 0 )); then
                echo "Remaining   : ${REMAINING}s"
                echo "Expires     : $(date -d "@$EXPIRY" +"%H:%M:%S")"
            fi
        fi

    else
        [[ -f "$EXPIRY_FILE" ]] && rm "$EXPIRY_FILE"
        echo "Status      : not cached"
        exit 1
    fi
}

password() {

    local ID

    ID=$(find_key)

    if [[ -z "$ID" ]]; then
        echo "Vault password not cached." >&2
        exit 1
    fi

    keyctl pipe "$ID"
}

show() {
    echo "WARNING: Displaying the vault password on the terminal." >&2
    password
    echo
}

usage() {
cat <<EOF
Usage:

    $PRG activate [--ttl TIME] [--replace]
    $PRG deactivate
    $PRG status
    $PRG password
    $PRG show

Options:

    --ttl TIME     Cache lifetime.
                   Supported suffixes:
                     s  seconds
                     m  minutes
                     h  hours
                   Without a suffix, the value is interpreted as seconds.

                   Maximum TTL TIME is approximately 12 hours.

    --replace      Replace an existing cached password.

Examples:

    $PRG activate
    $PRG activate --ttl 15m
    $PRG activate --ttl 2h
    $PRG activate --ttl 900
    $PRG activate --ttl 1h --replace

    # Display the cached password enclosed in angle brackets.
    # Useful to verify that the password command returns the expected value.
    printf '<%s>\n' "\$($PRG password)"

Environment:

    DOCKER_COMPOSE_VAULT_KEY_TTL
        Default: 900 seconds (15 minutes)
EOF
}

main() {

    local COMMAND="${1:-}"
    local REPLACE="${2:-false}"

    case "$COMMAND" in
        activate)
            shift

            while [[ $# -gt 0 ]]; do
                case "$1" in
                    --ttl)
                        if [[ -z "${2:-}" ]]; then
                            echo "Error: --ttl requires a value" >&2
                            exit 1
                        fi
                        TTL=$(parse_ttl "$2")
                        shift 2
                        ;;

                    --ttl=*)
                        TTL=$(parse_ttl "${1#*=}")
                        shift
                        ;;

                    --replace)
                        REPLACE=true
                        shift
                        ;;

                    -h|--help)
                        usage
                        return 0
                        ;;

                    *)
                        echo "Unknown activate option: $1" >&2
                        usage
                        exit 1
                        ;;
                esac
            done

            activate "$REPLACE"
            ;;

        deactivate)
            shift

            if [[ $# -gt 0 ]]; then
                echo "deactivate does not accept arguments" >&2
                exit 1
            fi

            deactivate
            ;;

        status)
            shift

            if [[ $# -gt 0 ]]; then
                echo "status does not accept arguments" >&2
                exit 1
            fi

            status
            ;;

        password)
            shift

            if [[ $# -gt 0 ]]; then
                echo "password does not accept arguments" >&2
                exit 1
            fi

            password
            ;;

        show)
            shift

            if [[ $# -gt 0 ]]; then
                echo "show does not accept arguments" >&2
                exit 1
            fi

            show
            ;;

        -h|--help|"")
            usage
            ;;

        *)
            echo "Unknown command: $COMMAND" >&2
            usage
            exit 1
            ;;
    esac
}

main "$@"
