-- gitano.usercommand
--
-- Gitano user-commands
--
-- Copyright 2012-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 log = require 'gitano.log'
local util = require 'gitano.util'
local repository = require 'gitano.repository'
local config = require 'gitano.config'
local pat = require 'gitano.patterns'

local sio = require 'luxio.simple'
local subprocess = require 'luxio.subprocess'

local builtin_whoami_short = "Find out how Gitano identifies you"

local builtin_whoami_helptext = [[
usage: whoami

Tells you who you are, what your email address is set to, what keys you have
registered etc.
]]

local function validate_single_argcmd(cmdline, msg)
   if #cmdline > 1 then
      log.error(msg)
      return false
   end

   return true
end

local function builtin_whoami_validate(_, _, cmdline)
   return validate_single_argcmd(cmdline, "usage: whoami")
end

local function builtin_whoami_prep(config, repo, cmdline, context)
   context.operation = "whoami"
   return config.repo:run_lace(context)
end

local function builtin_whoami_run(config, repo, cmdline, env)
   local username = env["GITANO_USER"]
   local userdata = config.users[username]

   if not userdata then
      return "I don't know who you are", 1
   end

   log.stdout("    User name:", username)
   log.stdout("    Real name:", userdata.real_name or "Unknown")
   log.stdout("Email address:", userdata.email_address or "unknown@example.com")
   local pfx = "      SSH key:"
   local longesttag = 0
   for tagname in pairs(userdata.keys) do
      if #tagname > longesttag then
         longesttag = #tagname
      end
   end
   for tagname, keydata in pairs(userdata.keys) do
      local suffix = (env.GITANO_KEYTAG == tagname) and " [*]" or ""
      local spaces = (" "):rep(longesttag - #tagname)
      log.stdout(pfx .. spaces, tagname, "=>", keydata.keytag .. suffix)
      pfx = "              "
   end
   local groups = {}
   local indirect = {}
   local maxname = 0
   for grname, gtab in pairs(config.groups) do
      log.ddebug(grname, "?")
      if gtab.filtered_members[username] then
         if #grname > maxname then
            maxname = #grname
         end
         groups[#groups+1] = grname
         log.ddebug(grname,"=", tostring(#groups))
         if not gtab.members[username] then
            indirect[grname] = gtab.filtered_members[username]
         end
      end
   end
   if #groups > 0 then
      pfx = "    In groups:"
      table.sort(groups)
      for _, group in ipairs(groups) do
         local spaces = (" "):rep(maxname - #group)
         log.ddebug(group, indirect[group] or "DIRECT")
         local via = indirect[group] and (" (via %s)"):format((indirect[group]:match("^[^!]+!([^!]+)")) or "???") or ""
         log.stdout(pfx, spaces .. group .. ":",
                    config.groups[group].settings.description .. via)
         pfx = "              "
      end
   end
   return "exit", 0
end

local builtin_sshkey_short = "SSH key management"
local builtin_sshkey_helptext = [[
usage: sshkey [<del|add> <tag>]

With no subcommand, sshkey will list the ssh keys you have, similarly to
the whoami command.

sshkey add <tag>

    Adds an ssh key with the given tag.  The content of the key must
    be supplied on a single line on stdin.

    cat id_rsa.pub | ssh gitano@somehost sshkey add personal-laptop

sshkey del <tag>

    Removes an ssh key with the given tag.  If the current access is
    via that ssh key then you cannot remove it.  Add a new key and
    switch to using that one first.
]]

local function builtin_sshkey_validate(config, _, cmdline)
   if #cmdline == 1 then
      cmdline[2] = "list"
   end
   if cmdline[2] == "list" then
      if #cmdline > 2 then
         log.error("sshkey list:  No arguments expected")
         return false
      end
   elseif cmdline[2] == "add" or cmdline[2] == "del" then
      if #cmdline ~= 3 then
         log.error("sshkey", cmdline[2] .. ":  Expected tag and no more")
         return false
      end
      if not cmdline[3]:match(pat.VALID_SSHKEYNAME) then
         log.error("sshkey:", cmdline[3], "is not a valid tag name.")
         log.state("Tag names start with a letter and may contain only letters")
         log.state("and numbers, underscores and dashes.  Tag names must be at")
         log.state("least two characters long.")
         return false
      end
   else
      log.error("sshkey: Unknown subcommand", cmdline[2])
      return false
   end

   return true
end

local function builtin_sshkey_prep(config, _, cmdline, context)
   context.operation = "sshkey"
   local action, reason = config.repo:run_lace(context)
   if action == "deny" then
      return reason
   end
   local utab = config.users[context.user]
   if cmdline[2] == "add" then
      -- Ensure that the given tag doesn't already exist
      if utab.keys[cmdline[3]] then
         return "deny", "Key tag " .. cmdline[3] .. " already exists"
      end
   elseif cmdline[2] == "del" then
      -- Ensure that the given tag does exist
      if not utab.keys[cmdline[3]] then
         return "deny", "Key tag " .. cmdline[3] .. " does not exist"
      end
      -- And that it's not the one we're currently using
      if cmdline[3] == context.keytag then
         return "deny", "Key tag " .. cmdline[3] .. " is in use"
      end
   end
   return action, reason
end

local function builtin_sshkey_run(conf, _, cmdline, env)
   local utab = conf.users[env.GITANO_USER]
   if cmdline[2] == "list" then
      if not next(utab.keys) then
         log.warn("There are no SSH keys registered for", env.GITANO_USER
                     .. ", sorry")
      else
         local pfx = "      SSH key:"
         for tagname, keydata in pairs(utab.keys) do
            local suffix = (env.GITANO_KEYTAG == tagname) and " [*]" or ""
            log.state(pfx, tagname, "=>", keydata.keytag .. suffix)
            pfx = "              "
         end
      end
   elseif cmdline[2] == "add" then
      local sshkey = sio.stdin:read("*l")
      local keytype, keydata, keytag = sshkey:match(pat.SSH_KEY_CONTENTS)
      if not (keytype and keydata and keytag) then
         log.error("Unable to parse key,", filename,
                   "did not smell like an OpenSSH v2 key")
         return "exit", 1
      end

      if util.ssh_type_is_invalid(keytype) then
         log.error("Unknown key type", keytype)
         return "exit", 1
      end

      local keytab = {
         data = sshkey,
         keyname = cmdline[3],
         username = env.GITANO_USER,
         keytag = keytag,
      }

      utab.keys[cmdline[3]] = keytab
   elseif cmdline[2] == "del" then
      utab.keys[cmdline[3]] = nil
   end

   if cmdline[2] ~= "list" then
      -- Store the config back.

      local action = (cmdline[2] == "add") and "Added" or "Deleted"
      action = action .. " " .. cmdline[3] .. " for " .. env.GITANO_USER
      local ok, msg = config.commit(conf, action, env.GITANO_USER, env.GITANO_ORIG_USER)

      if not ok then
         log.error(msg)
         return "exit", 1
      end
   end

   return "exit", 0
end

local builtin_passwd_short = "Set your password"

local builtin_passwd_helptext = [[
usage: passwd

Sets your password, the password is read from stdin.

If no password is provided your password is removed (if you have one).
]]

local function builtin_passwd_validate(_, _, cmdline)
   return validate_single_argcmd(cmdline, "usage: passwd")
end

local function builtin_passwd_prep(conf, repo, cmdline, context)
   context.operation = "passwd"

   local action, reason = conf.repo:run_lace(context)
   if action == "deny" then
      return reason
   end

   return action, reason
end

local function update_htpasswd(user, passwd)
   local htpasswd_path = os.getenv("HOME") .. "/htpasswd"
   local lock = util.lockfile(htpasswd_path .. ".lock")
   local flags = io.open(htpasswd_path, "r") and "-i" or "-ic"
   local exit_code

   if passwd ~= '' then
      local proc = subprocess.spawn_simple({
            "htpasswd", flags, htpasswd_path, user,
            stdin = passwd,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE
      })

      _, exit_code = proc:wait()
   else
      local proc = subprocess.spawn_simple({
            "htpasswd", "-D", htpasswd_path, user,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE
      })

      _, exit_code = proc:wait()
   end
   util.unlockfile(lock)
   return exit_code == 0
end

local function builtin_passwd_run(conf, _, cmdline, env)
   local user = env.GITANO_USER

   local password = sio.stdin:read("*l") or ""
   local method, hash = util.hash_password(password)

   if conf.users[user]["passwd.hash"] == nil and password == "" then
      log.chat(string.format("Password for %s is not set and no password was"
                                .. " provided, no action taken.", user))
      return "exit", 0
   end

   if password ~= "" then
      conf.users[user]["passwd.method"] = method
      conf.users[user]["passwd.hash"] = hash
   else
      -- user's password will be removed
      conf.users[user]["passwd.method"] = nil
      conf.users[user]["passwd.hash"] = nil
   end

   local ok, msg

   if conf.clod.settings["use_htpasswd"] == "yes" then
      ok = update_htpasswd(user, password)

      if not ok then
         log.error("Failed to update htpasswd file")
         return "exit", 1
      end
   end

   local action = string.format("%s password for %s",
                                password ~= '' and "Update" or "Remove", user)

   ok, msg = config.commit(conf, action, user)

   if not ok then
      log.error(msg)
      return "exit", 1
   end

   log.chat(string.format("%s password for %s",
                          password ~= '' and "Updated" or "Removed", user))

   return "exit", 0
end

local function register_commands(reg)
   assert(reg("whoami", builtin_whoami_short, builtin_whoami_helptext,
              builtin_whoami_validate,
              builtin_whoami_prep, builtin_whoami_run, false, false))
   assert(reg("sshkey", builtin_sshkey_short, builtin_sshkey_helptext,
              builtin_sshkey_validate, builtin_sshkey_prep,
              builtin_sshkey_run, false, false))
   assert(reg("passwd", builtin_passwd_short, builtin_passwd_helptext,
              builtin_passwd_validate, builtin_passwd_prep,
              builtin_passwd_run, false, false))
end

return {
   register = register_commands
}
