-- gitano.auth
--
-- Gitano entry point for unified authorisation/authentication
--
-- Copyright 2014 Richard Ipsum <richard.ipsum@codethink.co.uk>
-- Copyright 2016-2017 Daniel Silverstone <dsilvers@digital-scurf.org>
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions
-- are met:
-- 1. Redistributions of source code must retain the above copyright
--    notice, this list of conditions and the following disclaimer.
-- 2. Redistributions in binary form must reproduce the above copyright
--    notice, this list of conditions and the following disclaimer in the
--    documentation and/or other materials provided with the distribution.
-- 3. Neither the name of the author nor the names of their contributors
--    may be used to endorse or promote products derived from this software
--    without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-- ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-- SUCH DAMAGE.
--

local config = require 'gitano.config'
local command = require 'gitano.command'
local log = require 'gitano.log'
local repository = require 'gitano.repository'
local util = require 'gitano.util'
local i18n = require 'gitano.i18n'
local hooks = require 'gitano.hooks'
local gall = require 'gall'
local luxio = require 'luxio'

local function load_admin_conf(repo_root)
   local admin_repo = gall.repository.new((repo_root or "") ..
         "/gitano-admin.git")

   if not admin_repo then
      log.critical(i18n.expand("ERROR_CANNOT_FIND_ADMIN_REPO"))
      return nil
   end

   local admin_head = admin_repo:get(admin_repo.HEAD)

   if not admin_head then
      log.critical(i18n.expand("ERROR_BAD_ADMIN_REPO"))
      return nil
   end

   local admin_conf, msg = config.parse(admin_head)

   if not admin_conf then
      log.critical(i18n.expand("ERROR_CANNOT_PARSE_ADMIN"))
      log.critical("  * " .. (msg or "No error?"))
      return nil
   end

   return admin_conf
end

local function set_environment(repo_root, repo, context, transactionid)
   local env = {
      ["GITANO_ROOT"] = repo_root,
      ["GITANO_USER"] = context.user,
      ["GITANO_KEYTAG"] = context.keytag,
      ["GITANO_PROJECT"] = (repo or {}).name or "",
      ["GITANO_SOURCE"] = context.source,
      ["GITANO_TRANSACTION_ID"] = transactionid,
      ["GITANO_RUNNING"] = "yes",
   }

   for k, v in pairs(env) do
      luxio.setenv(k, v)
   end

   return env
end

local function is_authorized(user, source, cmdline, repo_root,
                             transactionid, start_log_level, keytag)

   local authorized = false

   config.repo_path(repo_root)

   if not user or not cmdline then
      return nil
   end

   local parsed_cmdline, warnings = util.parse_cmdline(cmdline)

   if (#warnings > 0) then
      log.error(i18n.expand("ERROR_PARSING_COMMAND"))
      return nil
   end

   local admin_conf = load_admin_conf(repo_root)
   if admin_conf == nil then
      log.fatal(i18n.expand("ERROR_COULD_NOT_LOAD_CONFIG"))
   end

   if admin_conf.groups["gitano-admin"].filtered_members[user] then
      log.set_level(start_log_level)
   end

   if not admin_conf.global.silent then
      log.bump_level(log.level.CHAT)
   end

   ip = os.getenv("REMOTE_ADDR") or "unknown ip"
   log.syslog.info(
      i18n.expand("CLIENT_CONNECTED",
                  { ip=ip, user=user, key=keytag, cmdline=cmdline}))

   local cancel
   cancel, ip, user, keytag, parsed_cmdline =
      (function(c,i,u,k,...)
	    return c, i, u, k, {...}
      end)(hooks.run(hooks.names.PREAUTH_CMDLINE, false,
		     ip, user, keytag, unpack(parsed_cmdline)))

   if cancel == nil then
      log.syslog.err(i18n.expand("PREAUTH_CMDLINE_HOOK_ABORTED", {reason=ip}))
      log.critical(i18n.expand("PREAUTH_CMDLINE_HOOK_DECLINED", {reason=ip}))
      return nil
   end
   if cancel then
      log.critical(i18n.expand("PREAUTH_CMDLINE_HOOK_DECLINED", {reason=ip}))
      return nil
   end

   local cmd = command.get(parsed_cmdline[1])

   if not cmd then
      log.critical(i18n.expand("ERROR_UNKNOWN_COMMAND", {cmd=parsed_cmdline[1]}))
      return nil
   end

   local repo
   if cmd.takes_repo and #parsed_cmdline >= 1 then
      repo, parsed_cmdline = cmd.detect_repo(admin_conf, parsed_cmdline)
      if not repo and not parsed_cmdline then
         return nil
      end
   end

   if user == "gitano-bypass" then
      log.state(i18n.expand("BYPASS_USER_BANNER_HEADER"))
      log.state(i18n.expand("BYPASS_USER_ALERT_MESSAGE"))
      log.state(i18n.expand("BYPASS_USER_BANNER_FOOTER"))
   end

   if not cmd.validate(admin_conf, repo, parsed_cmdline) then
      log.critical(i18n.expand("ERROR_VALIDATION_FAILED"))
      return nil
   end

   local context = {source = source, user = user, keytag = keytag}
   local action, reason = cmd.prep(admin_conf, repo, parsed_cmdline, context)

   if not action then
      log.critical(reason)
      log.critical(i18n.expand("ERROR_RULESET_UNCLEAN"))
      return nil
   end

   local env
   if action == "allow" then
      log.info(reason or i18n.expand("RULESET_ALLOWED"))
      authorized = true
      env = set_environment(repo_root, repo, context, transactionid)
   else
      log.critical(reason)
      log.critical(i18n.expand("RULESET_DENIED"))
   end

   return authorized, cmd, parsed_cmdline, admin_conf, env, repo
end

return {
   is_authorized = is_authorized
}
