#! /opt/appfs/rkeene.org/crystal/platform/latest/bin/crystal

require "sqlite3"
require "ini"

# User-specified constants
## UID offset when mapping Flint user IDs to OS user IDs
UID_OFFSET = 1024 * 1024

## 
FLINT_ROOT = "/home/chiselapp/chisel"

# Import the C functions for setuid/setgid
lib C
	fun setuid(uid : Int32) : Int32
	fun setgid(gid : Int32) : Int32
end

# Usage information
def print_help
	print("Usage: suid-fossil <fossil-args...>\n")
end

# Convert a UserID or a Username to the other via the DB
def userid_from_db(userdb : String, username : String) : Int32
	userid = nil

	DB.open "sqlite3://#{userdb}" {|db|
		userid = db.query_one "SELECT id FROM users WHERE username = ? LIMIT 1", username, as: {Int32}
	}

	if userid.nil?
		raise "User Name could not be found"
	end

	userid
end

def username_from_db(userdb : String, userid : Int32) : String
	username = nil

	DB.open "sqlite3://#{userdb}" {|db|
		username = db.query_one "SELECT username FROM users WHERE id = ? LIMIT 1", userid, as: {String}
	}

	if username.nil?
		raise "User ID could not be found"
	end

	username
end

# Find the Flint DB given a path to the Flint root
def find_db(root : String) : String
	dbconfig_file = File.join(root, "config", "sqlite.cnf")

	dbconfig_text = File.read(dbconfig_file)

	dbconfig = INI.parse(dbconfig_text)

	dbfile = dbconfig["config"]["database"]
	dbfile = File.expand_path(dbfile, File.join(root, "db"))

	dbfile
end

# Find a Flint User ID from an OS File
def userid_from_file(file : String) : Int32
	info = File.info(file)

	Int32.new(info.owner - UID_OFFSET)
end

# Run Fossil, wrapped as a Flint UserName/UserID
def suid_fossil(username : String, userid : Int32, fossil_args : Array, fossil_command = "fossil")
	# Ensure that the user ID is sane
	if userid < 0
		raise "User ID out of bounds (too low)"
	end

	if userid > (UInt32.new(Int32::MAX) - UID_OFFSET - 1)
		raise "User ID out of bounds (too high)"
	end

	# Compute OS UID from Flint User ID
	uid = userid + UID_OFFSET

	# Create Fossil home directory
	home = "/tmp/suid-fossil/#{userid}"

	if !Dir.exists?(home)
		Dir.mkdir(home, 0o700)
		File.chown(home, uid, uid)
	end

	ENV["HOME"] = home

	# Set OS UID/GID
	## Opportunistic -- if it fails, we do not care
	C.setgid(uid)

	uidcheck = C.setuid(uid)
	if (uidcheck != 0)
		raise "Unable to switch to UID #{uid}"
	end

	# If possible, update environment with usernames
	ENV["USER"] = username
	ENV["USERNAME"] = username

	Process.exec(fossil_command, fossil_args)

	raise "Failed to run Fossil"
end

# -------------------------------
# MAIN
# -------------------------------
fossil_args = ARGV

flint_root = ENV.fetch("FLINT_ROOT", FLINT_ROOT)
userid     = ENV["FLINT_USERID"]?
username   = ENV["FLINT_USERNAME"]?

# Find DB if possible
userdb   = ENV["FLINT_USERDB"]?

if userdb.nil?
	if !flint_root.nil?
		userdb = find_db(flint_root)
	end
end

# Find User ID
## Check to see if this is a CGI call, if so
## we take the user ID from the filename
if userid.nil?
	if ENV.has_key?("GATEWAY_INTERFACE")
		userid = userid_from_file(ARGV[0])
	end
end

if userid.nil?
	if username.nil?
		raise "Unhandled -- must specify one of FLINT_USERNAME or FLINT_USERID"
	end

	if userdb.nil?
		raise "Unhandled -- must specify FLINT_USERDB or FLINT_ROOT"
	end

	userid = userid_from_db(userdb, username)
else
	userid = userid.to_i32()
end

# Find User Name
if username.nil?
	if userdb.nil?
		raise "Unhandled -- must specify FLINT_USERDB or FLINT_ROOT"
	end

	username = username_from_db(userdb, userid)
end

# Run Fossil
suid_fossil(username, userid, fossil_args)
