#!/usr/bin/env bash

# This script is a wrapper around git-shell that first checks if a user has permission
# to access a given git repository.
#
# This script should be called only by the ssh daemon's ForceCommand.
# When a user tries to access a git repository over ssh, this script checks that the username
# associated with the public key is authorized to access the requested repository. If so, it
# modifies the original git command to point to the correct location of the requested repository
# on disk and runs the original command through the git-shell. If the username is not authorized,
# it rejects the request.


# Write to a log file if the MARKUS_LOG_FILE environment variable is set.
# This function always returns 0 so that subsequent commands after
# && will always be executed.
write_log() {
  if [[ -n "${MARKUS_LOG_FILE}" ]]; then
    echo "$(date): ${1}" >> "${MARKUS_LOG_FILE}"
  fi
  return 0
}

# Convert the MARKUS_REPO_LOC_PATTERN to an actual path that points to a repository on disk.
# This function replaces '(instance)' with the REQUESTED_INSTANCE variable and appends the
# REQUESTED_REPO_DIR variable to the end. In general REQUESTED_INSTANCE should be the same as
# the relative url root for a given MarkUs instance. REQUESTED_REPO_DIR is a specific
# repository in that instance.
#
# For example:
#  MARKUS_REPO_LOC_PATTERN='/some/path/(instance)/repos'
#  REQUESTED_INSTANCE=2021-02
#  REQUESTED_COURSE=csc108
#  REQUESTED_REPO_DIR=csc108/group_123.git
#  echo $(updated_repo_path)  # /some/path/2021-02/repos/csc108/group_123.git
updated_repo_path() {
  echo "${MARKUS_REPO_LOC_PATTERN//(instance)/${REQUESTED_INSTANCE}}/${REQUESTED_REPO_DIR}"
}

# shellcheck disable=SC1090
source "${HOME}/.ssh/rc"

# The following environment variables must be set:
#
# LOGIN_USER: set in the authorized keys file generated by MarkUs (tells us which username is associated
#             with the public key trying to connect to git over ssh)
# GIT_SHELL: the location of the git-shell executable on disk, usually this is /usr/bin/git-shell
# MARKUS_REPO_LOC_PATTERN: a string containing '(instance)' and '(repository)'. See the docstring for the
#                          updated_repo_path function for details
#
# It is highly recommended that the GIT_SHELL, MARKUS_REPO_LOC_PATTERN, and MARKUS_LOG_FILE be variables
# sourced from the "${HOME}/.ssh/rc" file (see above). You may also hard-code them in this file directly
# if you wish or source them from another file. If you do either, you must modify this file accordingly.
[[ -z ${LOGIN_USER} ]] && write_log 'ERROR: LOGIN_USER not set' && exit 1
[[ -z ${GIT_SHELL} ]] && write_log 'ERROR: GIT_SHELL not set' && exit 1
[[ -z ${MARKUS_REPO_LOC_PATTERN} ]] && write_log 'ERROR: MARKUS_REPO_LOC_PATTERN not set' && exit 1

REQUESTED_REPO_PATH=$(echo "${SSH_ORIGINAL_COMMAND}" | rev | cut -f1 -d' ' | rev) # ex: /2021-02/csc108/group_1.git'
_REQUESTED_DIR_PATH=$(dirname "${REQUESTED_REPO_PATH}") # ex: /2021-02/csc108
REQUESTED_COURSE=$(basename "${_REQUESTED_DIR_PATH}") # ex: csc108
REQUESTED_INSTANCE=$(basename "$(dirname "${_REQUESTED_DIR_PATH}")") # ex: 2021-02

REQUESTED_REPO_BASE=$(basename "${REQUESTED_REPO_PATH}") # ex: group_1.git
REQUESTED_REPO_DIR="${REQUESTED_COURSE}/${REQUESTED_REPO_BASE}" # ex: csc108/group_1.git'
UPDATED_REPO_PATH="$(updated_repo_path)" # ex: /some/path/csc108/repos/group_123.git
SSH_UPDATED_COMMAND="$(echo "${SSH_ORIGINAL_COMMAND}" | rev | cut -f2- -d' ' | rev) '${UPDATED_REPO_PATH}" # ex: upload-pack '/some/path/csc108/repos/group_123.git'

if [[ -z ${MARKUS_USE_ACCESS_FILE} ]]; then
  PERMITTED_QUERY="SELECT check_repo_permissions(:'user_name', :'course_name', :'repo_name')"
  SERVICE=$(if [ "${REQUESTED_INSTANCE}" == '/' ]; then echo "${DEFAULT_SERVICE:-markus}"; else echo "${REQUESTED_INSTANCE}"; fi)
  if [ -n "${INSTANCE}" ] && [ "$(echo 'SELECT database_identifier()' | psql service="${SERVICE}" -qtA)" = "${INSTANCE}" ]; then
    USER_PERMITTED=$(echo "${PERMITTED_QUERY}" | psql service="${SERVICE}" -qtA -v user_name="${LOGIN_USER}" -v course_name="${REQUESTED_COURSE}" -v repo_name="${REQUESTED_REPO_BASE%.*}")
  else
    # Deny if the INSTANCE environment variable does not match the database identifier
    # This prevents users with the same username in a different instance from accessing each other's repositories
    USER_PERMITTED=f
  fi
else
  GIT_ACCESS_FILE="$(dirname "$(dirname "${UPDATED_REPO_PATH}")")/.access" # ex: /some/path/csc108/repos/.access
  # A string containing all repository names (without .git) that the user with username == LOGIN_USER is
  # allowed to access according to the GIT_ACCESS_FILE.
  AVAILABLE_REPOS=$(grep -P ",${LOGIN_USER}(?:,|\s*$)" "${GIT_ACCESS_FILE}" | cut -f1 -d,)
  REQUESTED_REPO="${REQUESTED_REPO_DIR%.*}" # ex: csc108/group_1
  USER_PERMITTED=$(grep -qP "^${REQUESTED_REPO}|${REQUESTED_COURSE}/\*$" <(echo "${AVAILABLE_REPOS}") && echo 't' || echo 'f')
fi

if [ "${USER_PERMITTED}" == 't' ]; then
  # If the LOGIN_USER is permitted to access the requested repo, run the modified GIT_SHELL command and
  # replace any mention of UPDATED_REPO_PATH with REQUESTED_REPO_PATH from stderr. The result of stderr
  # here will be shown to the user so this is to ensure that the location of files on disk is not revealed.
  { GIT_OUTPUT=$(${GIT_SHELL} -c "${SSH_UPDATED_COMMAND}" 2> >(sed "s:${UPDATED_REPO_PATH}:${REQUESTED_REPO_PATH}:g" >&2) 2>&1 >&3 3>&-); } 3>&1
  GIT_STATUS=$?
  if [ "${GIT_STATUS}" -ne 0 ]; then
    write_log "ERROR: LOGIN_USER=${LOGIN_USER} cmd=${SSH_ORIGINAL_COMMAND} GIT_OUTPUT=${GIT_OUTPUT}"
  else
    write_log "SUCCESS: LOGIN_USER=${LOGIN_USER} cmd=${SSH_ORIGINAL_COMMAND}"
  fi
  exit "${GIT_STATUS}"
else
  write_log "PERMISSION DENIED: LOGIN_USER=${LOGIN_USER} cmd=${SSH_ORIGINAL_COMMAND}"
  exit 1
fi
