#!/bin/env sh
#
# Set up everything BloodHound needs: the PostgreSQL database and the Neo4j
# credentials. It only changes what is missing or wrong, so it can be run at
# any time (bloodhound-start runs it every time).

set -e

BHAPI_CONFIG=/etc/bhapi/bhapi.json
NEO4J_URL=http://localhost:7474
NEO4J_LOG=/etc/neo4j/logs/neo4j.log

RESET='\033[0m'
RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'

[ "$(id -u)" -ne 0 ] && exec sudo "$0" "$@"

info() {
  echo " ${YELLOW}[*]${RESET} $*"
}

error() {
  echo " ${RED}[!]${RESET} $*" >&2
}

# Set a value in the config, which holds secrets: only root and bhapi
# (_bloodhound) can read it
config_set() { # <key> <value>
  tmp=$(mktemp "${BHAPI_CONFIG}.XXXXXX")
  if ! BHAPI_VALUE="$2" jq "$1 = env.BHAPI_VALUE" "${BHAPI_CONFIG}" >"${tmp}"; then
    rm -f "${tmp}"
    error "Failed to update ${BHAPI_CONFIG}"
    exit 1
  fi
  chown root:_bloodhound "${tmp}"
  chmod 640 "${tmp}"
  mv "${tmp}" "${BHAPI_CONFIG}"
  CONFIG_CHANGED=1
}

## PostgreSQL
if ! systemctl is-active --quiet postgresql; then
  info "Starting PostgreSQL"
  systemctl start postgresql
fi

# Create the user and the database if needed, with the password of the config.
# It is passed to create-database-bloodhound in the BLOODHOUND_DB_PASSWORD
# environment variable (runuser keeps it), as arguments are visible in ps
db_password=$(jq -r '.database.secret // empty' "${BHAPI_CONFIG}")
if ! output=$(BLOODHOUND_DB_PASSWORD="${db_password}" \
    runuser -u postgres -- /usr/share/bloodhound/create-database-bloodhound 2>&1); then
  error "Failed to set up the PostgreSQL database:"
  echo "${output}" >&2
  exit 1
fi
case ${output} in *Creating*)
  info "Created the PostgreSQL database"
  ;;
esac

## Neo4j
if ! neo4j status >/dev/null 2>&1; then
  info "Starting Neo4j"
  if ! output=$(neo4j start 2>&1); then
    error "Neo4j failed to start:"
    echo "${output}" >&2
    exit 1
  fi
fi

# It can take a while to answer, mainly the first time
if ! curl --silent --output /dev/null "${NEO4J_URL}/"; then
  printf '%b' " ${YELLOW}[*]${RESET} Waiting for Neo4j"
  i=0
  until curl --silent --output /dev/null "${NEO4J_URL}/"; do
    i=$((i + 1))
    if [ "${i}" -gt 180 ]; then
      echo
      error "Neo4j is not answering on ${NEO4J_URL}, see ${NEO4J_LOG}"
      exit 1
    fi
    printf .
    sleep 1
  done
  echo
fi

# Run a Cypher statement and print "OK" or the Neo4j error code. Credentials
# go through the environment and stdin, so they never show up in the process
# list
neo4j_request() { # <user> <password> <database> <statement> [<new password>]
  NEO4J_AUTH="$1:$2" NEO4J_OLD="$2" NEO4J_NEW="$5" NEO4J_QUERY="$4" \
    jq -nr --arg url "${NEO4J_URL}/db/$3/tx/commit" '
      "url = \($url | @json)",
      "user = \(env.NEO4J_AUTH | @json)",
      "header = \("Content-Type: application/json" | @json)",
      "data-binary = \({statements: [{statement: env.NEO4J_QUERY,
        parameters: {old: env.NEO4J_OLD, new: env.NEO4J_NEW}}]} | tojson | @json)"' \
    | curl --silent --max-time 30 --config - \
    | jq -r 'if (.errors | length) > 0 then .errors[0].code else "OK" end' 2>/dev/null || true
}

neo4j_login() { # <user> <password>
  status=$(neo4j_request "$1" "$2" neo4j "RETURN 1")
  # Neo4j blocks the logins for 5 seconds after 3 failures
  if [ "${status}" = Neo.ClientError.Security.AuthenticationRateLimit ]; then
    sleep 6
    status=$(neo4j_request "$1" "$2" neo4j "RETURN 1")
  fi
  echo "${status}"
}

# Use these credentials if they are valid, else return 1
neo4j_try() { # <user> <password>
  case $(neo4j_login "$1" "$2") in
    OK)
      password=$2
      ;;
    Neo.ClientError.Security.CredentialsExpired)
      # Default password, which has to be changed before using Neo4j
      password=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
      # shellcheck disable=SC2016 # $old and $new are Cypher parameters
      if [ "$(neo4j_request "$1" "$2" system \
          'ALTER CURRENT USER SET PASSWORD FROM $old TO $new' "${password}")" != OK ]; then
        error "Failed to change the default password of the Neo4j user $1"
        exit 1
      fi
      info "Set a random password for the Neo4j user $1, saved in ${BHAPI_CONFIG}"
      ;;
    Neo.ClientError.Security.Unauthorized)
      return 1
      ;;
    *)
      error "Can't connect to Neo4j on ${NEO4J_URL}, see ${NEO4J_LOG}"
      exit 1
      ;;
  esac

  if [ "$1" != "${NEO4J_USER}" ]; then
    config_set .neo4j.username "$1"
    info "Fixed the Neo4j user in ${BHAPI_CONFIG}: $1"
  fi
  if [ "${password}" != "${NEO4J_PASSWORD}" ]; then
    config_set .neo4j.secret "${password}"
  fi
}

neo4j_ask() {
  error "Neo4j rejected the credentials of ${BHAPI_CONFIG}"
  if [ ! -t 0 ]; then
    echo "     Please set the right username and secret in its neo4j section" >&2
    exit 1
  fi
  for attempt in 1 2 3; do
    read -r -p "     Neo4j user [neo4j]: " user
    printf "     Password of %s: " "${user:=neo4j}"
    trap 'stty echo' EXIT
    trap 'stty echo; exit 1' INT TERM
    stty -echo
    read -r pass || true
    stty echo
    trap - EXIT INT TERM
    echo
    if neo4j_try "${user}" "${pass}"; then
      info "Saved in ${BHAPI_CONFIG}"
      return
    fi
    [ "${attempt}" -lt 3 ] && error "Wrong user or password"
  done
  error "Wrong user or password. If you don't remember it, see how to reset it:"
  echo "     https://neo4j.com/docs/operations-manual/4.4/configuration/password-and-user-recovery/" >&2
  exit 1
}

NEO4J_USER=$(jq -r '.neo4j.username // "neo4j"' "${BHAPI_CONFIG}")
NEO4J_PASSWORD=$(jq -r '.neo4j.secret // ""' "${BHAPI_CONFIG}")

# Try the configured credentials, else the default user (in case the config
# has a wrong one) and the default password (in case it was not changed yet)
if ! neo4j_try "${NEO4J_USER}" "${NEO4J_PASSWORD}"; then
  if [ "${NEO4J_USER}" = neo4j ] || ! neo4j_try neo4j "${NEO4J_PASSWORD}"; then
    if [ "${NEO4J_PASSWORD}" = neo4j ] || ! neo4j_try neo4j neo4j; then
      neo4j_ask
    fi
  fi
fi

# BloodHound only reads its config when it starts
if [ -n "${CONFIG_CHANGED}" ]; then
  systemctl try-restart bloodhound
fi

echo " ${GREEN}[+]${RESET} BloodHound is set up"
