#!/bin/bash
# Copyright 2020 Security Onion Solutions. All rights reserved.
#
# This program is distributed under the terms of version 2 of the
# GNU General Public License.  See LICENSE for further details.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

. /usr/sbin/so-common

if [[ $# -lt 1 || $# -gt 2 ]]; then
  echo "Usage: $0 <list|add|update|enable|disable|validate|valemail|valpass> [email]"
  echo ""
  echo "     list: Lists all user email addresses currently defined in the identity system"
  echo "      add: Adds a new user to the identity system; requires 'email' parameter"
  echo "   update: Updates a user's password; requires 'email' parameter"
  echo "   enable: Enables a user; requires 'email' parameter"
  echo "  disable: Disables a user; requires 'email' parameter"
  echo " validate: Validates that the given email address and password are acceptable for defining a new user; requires 'email' parameter"
  echo " valemail: Validates that the given email address is acceptable for defining a new user; requires 'email' parameter"
  echo "  valpass: Validates that a password is acceptable for defining a new user"
  echo ""
  echo " Note that the password can be piped into STDIN to avoid prompting for it"
  exit 1
fi

operation=$1
email=$2

kratosUrl=${KRATOS_URL:-http://127.0.0.1:4434}
databasePath=${KRATOS_DB_PATH:-/opt/so/conf/kratos/db/db.sqlite}
argon2Iterations=${ARGON2_ITERATIONS:-3}
argon2Memory=${ARGON2_MEMORY:-14}
argon2Parallelism=${ARGON2_PARALLELISM:-2}
argon2HashSize=${ARGON2_HASH_SIZE:-32}

function fail() {
  msg=$1
  echo "$1"
  exit 1
}

function require() {
  cmd=$1
  which "$1" 2>&1 > /dev/null
  [[ $? != 0 ]] && fail "This script requires the following command be installed: ${cmd}"
}

# Verify this environment is capable of running this script
function verifyEnvironment() {
  require "argon2"
  require "jq"
  require "curl"
  require "openssl"
  require "sqlite3"
  [[ ! -f $databasePath ]] && fail "Unable to find database file; specify path via KRATOS_DB_PATH environment variable"
  response=$(curl -Ss ${kratosUrl}/)
  [[ "$response" != "404 page not found" ]] && fail "Unable to communicate with Kratos; specify URL via KRATOS_URL environment variable"
}

function findIdByEmail() {
  email=$1

  response=$(curl -Ss ${kratosUrl}/identities)
  identityId=$(echo "${response}" | jq ".[] | select(.verifiable_addresses[0].value == \"$email\") | .id")
  echo $identityId
}

function validatePassword() {
  password=$1

  len=$(expr length "$password")
  if [[ $len -lt 6 ]]; then
    echo "Password does not meet the minimum requirements"
    exit 2
  fi
}

function validateEmail() {
  email=$1
  # (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
  if [[ ! "$email" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}$ ]]; then
    echo "Email address is invalid"
    exit 3
  fi
}

function updatePassword() {
  identityId=$1
  
  if [ -z "$password" ]; then
    # Read password from stdin (show prompt only if no stdin was piped in)
    test -t 0
    if [[ $? == 0 ]]; then
      echo "Enter new password:"
    fi
    read -rs password

    validatePassword "$password"
  fi

  if [[ -n $identityId ]]; then
    # Generate password hash
    salt=$(openssl rand -hex 8)
    passwordHash=$(echo "${password}" | argon2 ${salt} -id -t $argon2Iterations -m $argon2Memory -p $argon2Parallelism -l $argon2HashSize -e)

    # Update DB with new hash
    echo "update identity_credentials set config=CAST('{\"hashed_password\":\"${passwordHash}\"}' as BLOB) where identity_id=${identityId};" | sqlite3 "$databasePath"
    [[ $? != 0 ]] && fail "Unable to update password"
  fi
}

function listUsers() {
  response=$(curl -Ss ${kratosUrl}/identities)
  [[ $? != 0 ]] && fail "Unable to communicate with Kratos"

  echo "${response}" | jq -r ".[] | .verifiable_addresses[0].value" | sort
}

function createUser() {
  email=$1

  now=$(date -u +%FT%TZ)
  addUserJson=$(cat <<EOF
{
  "traits": {"email":"${email}"},
  "schema_id": "default"
}
EOF
  )
  
  response=$(curl -Ss ${kratosUrl}/identities -d "$addUserJson")
  [[ $? != 0 ]] && fail "Unable to communicate with Kratos"

  identityId=$(echo "${response}" | jq ".id")
  if [[ ${identityId} == "null" ]]; then
    code=$(echo "${response}" | jq ".error.code")
    [[ "${code}" == "409" ]] && fail "User already exists"

    reason=$(echo "${response}" | jq ".error.message")
    [[ $? == 0 ]] && fail "Unable to add user: ${reason}"
  fi

  updatePassword $identityId
}

function updateStatus() {
  email=$1
  status=$2

  identityId=$(findIdByEmail "$email")
  [[ ${identityId} == "" ]] && fail "User not found"

  response=$(curl -Ss "${kratosUrl}/identities/$identityId")
  [[ $? != 0 ]] && fail "Unable to communicate with Kratos"

  oldConfig=$(echo "select config from identity_credentials where identity_id=${identityId};" | sqlite3 "$databasePath")
  if [[ "$status" == "locked" ]]; then
    config=$(echo $oldConfig | sed -e 's/hashed/locked/')
    echo "update identity_credentials set config=CAST('${config}' as BLOB) where identity_id=${identityId};" | sqlite3 "$databasePath"
    [[ $? != 0 ]] && fail "Unable to lock credential record"

    echo "delete from sessions where identity_id=${identityId};" | sqlite3 "$databasePath"
    [[ $? != 0 ]] && fail "Unable to invalidate sessions"    
  else
    config=$(echo $oldConfig | sed -e 's/locked/hashed/')
    echo "update identity_credentials set config=CAST('${config}' as BLOB) where identity_id=${identityId};" | sqlite3 "$databasePath"
    [[ $? != 0 ]] && fail "Unable to unlock credential record"
  fi  

  updatedJson=$(echo "$response" | jq ".traits.status = \"$status\" | del(.verifiable_addresses) | del(.id) | del(.schema_url)")
  response=$(curl -Ss -XPUT ${kratosUrl}/identities/$identityId -d "$updatedJson")
  [[ $? != 0 ]] && fail "Unable to mark user as locked"

}

function updateUser() {
  email=$1

  identityId=$(findIdByEmail "$email")
  [[ ${identityId} == "" ]] && fail "User not found"

  updatePassword $identityId 
}

function deleteUser() {
  email=$1

  identityId=$(findIdByEmail "$email")
  [[ ${identityId} == "" ]] && fail "User not found"

  response=$(curl -Ss -XDELETE "${kratosUrl}/identities/$identityId")
  [[ $? != 0 ]] && fail "Unable to communicate with Kratos"
}

case "${operation}" in
  "add")
    verifyEnvironment
    [[ "$email" == "" ]] && fail "Email address must be provided"

    validateEmail "$email"
    updatePassword
    createUser "$email"
    echo "Successfully added new user to SOC"
    check_container thehive && echo $password | so-thehive-user-add "$email"
    check_container fleet && echo $password | so-fleet-user-add "$email"
    ;;

  "list")
    verifyEnvironment
    listUsers
    ;;

  "update")
    verifyEnvironment
    [[ "$email" == "" ]] && fail "Email address must be provided"

    updateUser "$email"
    echo "Successfully updated user"
    ;;

  "enable")
    verifyEnvironment
    [[ "$email" == "" ]] && fail "Email address must be provided"

    updateStatus "$email" 'active'
    echo "Successfully enabled user"
    check_container thehive && so-thehive-user-enable "$email" true
    check_container fleet && so-fleet-user-enable "$email" true   
    ;;

  "disable")
    verifyEnvironment
    [[ "$email" == "" ]] && fail "Email address must be provided"

    updateStatus "$email" 'locked'
    echo "Successfully disabled user"
    check_container thehive && so-thehive-user-enable "$email" false
    check_container fleet && so-fleet-user-enable "$email" false   
    ;;    

  "delete")
    verifyEnvironment
    [[ "$email" == "" ]] && fail "Email address must be provided"

    deleteUser "$email"
    echo "Successfully deleted user"
    check_container thehive && so-thehive-user-enable "$email" false
    check_container fleet && so-fleet-user-enable "$email" false
    ;;

  "validate")
    validateEmail "$email"
    updatePassword
    echo "Email and password are acceptable"
    ;;

  "valemail")
    validateEmail "$email"
    echo "Email is acceptable"
    ;;

  "valpass")
    updatePassword
    echo "Password is acceptable"
    ;;

  *)
    fail "Unsupported operation: $operation"
    ;;
esac

exit 0