#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3 -p python3.pkgs.passlib
# vim: set filetype=python :
#
# available environment variables:
# - SFTPGO_AUTHD_USERNAME
# - SFTPGO_AUTHD_USER
# - SFTPGO_AUTHD_IP
# - SFTPGO_AUTHD_PROTOCOL  = { "DAV", "FTP", "HTTP", "SSH" }
# - SFTPGO_AUTHD_PASSWORD
# - SFTPGO_AUTHD_PUBLIC_KEY
# - SFTPGO_AUTHD_KEYBOARD_INTERACTIVE
# - SFTPGO_AUTHD_TLS_CERT
#
# user permissions:
# - see <repo:drakkan/sftpgo:internal/dataprovider/user.go>
# - "*" = grant all permissions
# - read-only perms:
#   - "list" = list files and directories
#   - "download"
# - rw perms:
#   - "upload"
#   - "overwrite" = allow uploads to replace existing files
#   - "delete" = delete files and directories
#     - "delete_files"
#     - "delete_dirs"
#   - "rename" = rename files and directories
#     - "rename_files"
#     - "rename_dirs"
#   - "create_dirs"
#   - "create_symlinks"
#   - "chmod"
#   - "chown"
#   - "chtimes" = change atime/mtime (access and modification times)
#
# home_dir:
# - it seems (empirically) that a user can't cd above their home directory.
#   though i don't have a reference for that in the docs.

import json
import os
import passlib.hosts

from hmac import compare_digest

authFail = dict(username="")

PERM_DENY = []
PERM_LIST = [ "list" ]
PERM_RO = [ "list", "download" ]
PERM_RW = [
  # read-only:
  "list",
  "download",
  # write:
  "upload",
  "overwrite",
  "delete",
  "rename",
  "create_dirs",
  "create_symlinks",
  # intentionally omitted:
  # "chmod",
  # "chown",
  # "chtimes",
]

TRUSTED_CREDS = [
  # /etc/shadow style creds.
  # mkpasswd -m sha-512
  # $<method>$<salt>$<hash>
  "$6$Zq3c2u4ghUH4S6EP$pOuRt13sEKfX31OqPbbd1LuhS21C9MICMc94iRdTAgdAcJ9h95gQH/6Jf6Ie4Obb0oxQtojRJ1Pd/9QHOlFMW.",  #< m. rocket boy
  "$6$B0NLGNdCL51PNse1$46G.aA1ATWIv5v.jUsKf4F3NS7emV2jB2gkZ3MytZtMvw2pjniHmRl0fywRjKW9TuXTeK9T50v.H0f2BaQ4PT1",  #< v. telephony
]
TRUSTED_VIEWING_OR_PLAYGROUND_CREDS = [
  # "$6$iikDajz5b.YH1.on$tfSzzBEtX8IeDiJJXCasOTxRTd7cFDKXU6dhlWYVhK6xDeJhV2fh6bmm1WIHItjIth9Eh9zNgUB8xibMIWCm/."  # fedi (2024-08-27); music appreciation
];

def mkAuthOk(username: str, permissions: dict[str, list[str]]) -> dict:
  return dict(
    status = 1,
    username = username,
    expiration_date = 0,
    home_dir = "/var/export",
    # uid/gid 0 means to inherit sftpgo uid.
    # - i.e. users can't read files which Linux user `sftpgo` can't read
    # - uploaded files belong to Linux user `sftpgo`
    # other uid/gid values aren't possible for localfs backend, unless i let sftpgo use `sudo`.
    uid = 0,
    gid = 0,
    # uid = 65534,
    # gid = 65534,
    max_sessions = 0,
    # quota_*: 0 means to not use SFTP's quota system
    quota_size = 0,
    quota_files = 0,
    permissions = permissions,
    upload_bandwidth = 0,
    download_bandwidth = 0,
    filters = dict(
      allowed_ip = [],
      denied_ip = [],
    ),
    public_keys = [],
    # other fields:
    # ? groups
    # ? virtual_folders
  )

def isLan(ip: str) -> bool:
  return ip.startswith("10.78.76.") \
    or ip.startswith("10.78.77.") \
    or ip.startswith("10.78.78.") \
    or ip.startswith("10.78.79.")

def isWireguard(ip: str) -> bool:
  return ip.startswith("10.0.10.")

def isTrustedCred(password: str, credlist: list[str] = TRUSTED_CREDS) -> bool:
  for cred in credlist:
    if passlib.hosts.linux_context.verify(password, cred):
      return True

  return False

def getAuthResponse(ip: str, username: str, password: str) -> dict:
  """
  return a sftpgo auth response either denying the user or approving them
  with a set of permissions.
  """
  if isTrustedCred(password) and username != "colin":
    # allow r/w access from those with a special token
    return mkAuthOk(username, permissions = {
      "/": PERM_RW,
      "/playground": PERM_RW,
      "/.public_for_test": PERM_RO,
      "/media/Music": PERM_RO,  #< i am too picky about Music organization
    })
  if isTrustedCred(password, TRUSTED_VIEWING_OR_PLAYGROUND_CREDS) and username != "colin":
    return mkAuthOk(username, permissions = {
      # error prone, but... not the worst if i miss something
      "/": PERM_LIST,
      "/media/archive": PERM_DENY,
      "/media/Books": PERM_RO,
      "/media/collections": PERM_DENY,
      "/media/games": PERM_RO,
      "/media/Music": PERM_RO,
      "/media/Pictures": PERM_RO,
      "/media/torrents": PERM_DENY,
      "/media/Videos": PERM_RO,
      "/playground": PERM_RW,
      "/.public_for_test": PERM_RO,
    })
  if isWireguard(ip):
    # allow any user from wireguard
    return mkAuthOk(username, permissions = {
      "/": PERM_RW,
      "/playground": PERM_RW,
      "/.public_for_test": PERM_RO,
    })
  if isLan(ip):
    if username == "anonymous":
      # allow anonymous users on the LAN
      return mkAuthOk("anonymous", permissions = {
        "/": PERM_RO,
        "/playground": PERM_RW,
        "/.public_for_test": PERM_RO,
      })
  if username == "anonymous":
    # anonymous users from the www can have even more limited access.
    # mostly because i need an easy way to test WAN connectivity :-)
    return mkAuthOk("anonymous", permissions = {
      # "/": PERM_DENY,
      "/": PERM_LIST,  #< REQUIRED, even for lftp to list a subdir
      "/media": PERM_DENY,
      "/playground": PERM_DENY,
      "/.public_for_test": PERM_RO,
      # "/README.md": PERM_RO,  #< does not work
    })

  return authFail

def main():
  ip = os.environ.get("SFTPGO_AUTHD_IP", "")
  username = os.environ.get("SFTPGO_AUTHD_USERNAME", "")
  password = os.environ.get("SFTPGO_AUTHD_PASSWORD", "")
  resp = getAuthResponse(ip, username, password)
  print(json.dumps(resp))

if __name__ == "__main__":
  main()
