#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2016, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION=ENV["ONE_LOCATION"]

if !ONE_LOCATION
    RUBY_LIB_LOCATION="/usr/lib/one/ruby"
else
    RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby"
end

$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+"/cli"

require 'command_parser'
require 'one_helper/oneuser_helper'
require 'one_helper/onequota_helper'

require 'uri'

cmd=CommandParser::CmdParser.new(ARGV) do
    usage "`oneuser` <command> [<args>] [<options>]"
    version OpenNebulaHelper::ONE_VERSION

    helper = OneUserHelper.new

    before_proc do
        helper.set_client(options) if ![:key].include?(@comm_name)
    end

    ########################################################################
    # Global Options
    ########################################################################
    set :option, CommandParser::OPTIONS+OpenNebulaHelper::CLIENT_OPTIONS

    list_options = CLIHelper::OPTIONS
    list_options << OpenNebulaHelper::XML
    list_options << OpenNebulaHelper::NUMERIC
    list_options << OpenNebulaHelper::DESCRIBE

    READ_FILE={
        :name => "read_file",
        :short => "-r",
        :large => "--read-file",
        :description => "Read password from file"
    }

    SHA1={
        :name => "sha1",
        :large => "--sha1",
        :description => "The password will be hashed using the sha1\n"<<
                        " "*31<<"algorithm"
    }

    SSH={
        :name => "ssh",
        :large => "--ssh",
        :description => "SSH Auth system",
        :proc => lambda { |o, options|
            options[:driver] = OpenNebula::User::SSH_AUTH
        }
    }

    X509={
        :name => "x509",
        :large => "--x509",
        :description => "x509 Auth system for x509 certificates",
        :proc => lambda { |o, options|
            options[:driver] = OpenNebula::User::X509_AUTH
        }
    }

    X509_PROXY={
        :name => "x509_proxy",
        :large => "--x509_proxy",
        :description => "x509 Auth system based on x509 proxy certificates",
        :proc => lambda { |o, options|
            options[:driver] = OpenNebula::User::X509_PROXY_AUTH
        }
    }

    KEY={
        :name => "key",
        :short => "-k path_to_private_key_pem",
        :large => "--key path_to_private_key_pem",
        :format => String,
        :description => "Path to the Private Key of the User"
    }

    CERT={
        :name => "cert",
        :short => "-c path_to_user_cert_pem",
        :large => "--cert path_to_user_cert_pem",
        :format => String,
        :description => "Path to the Certificate of the User"
    }

    PROXY={
        :name => "proxy",
        :large => "--proxy path_to_user_proxy_pem",
        :format => String,
        :description => "Path to the user proxy certificate"
    }

    TIME={
        :name  => "time",
        :large => "--time x",
        :format => Integer,
        :description => "Token duration in seconds, defaults to 36000 (10 h). "\
                        "To reset the token set time to 0." \
                        "To generate a non-expiring token use -1"\
                        " (not valid for ssh and x509 tokens). "\
    }

    DRIVER={
        :name => "driver",
        :large => "--driver driver",
        :format => String,
        :description => "Driver to authenticate this user"
    }

    FORCE = {
        :name   => "force",
        :large  => "--force" ,
        :description => "Force one_auth file rewrite"
    }

    TOKEN = {
        :name => "token",
        :large => "--token token_hint",
        :format => String,
        :description => "The Token to be loaded."
    }

    GROUP = {
        :name   => "group",
        :large  => "--group id|name" ,
        :description => "Effective GID to use with this token.",
        :format => String,
        :proc   => lambda { |o, options|
            OpenNebulaHelper.rname_to_id(o, "GROUP")
        }
    }

    GROUP_CREATE = {
        :name   => "group",
        :large  => "--group id|name" ,
        :description => "Comma-separated list of Groups for the new User. "\
                        "The first Group will be the main one.",
        :format => String,
        :proc   => lambda { |o, options|
            gids = o.split(",").map { |g|
                id = OpenNebulaHelper.rname_to_id(g, "GROUP")

                if (id[0] == -1)
                    puts id[1]
                    exit -1
                end

                id[1]
            }

            [0, gids]
        }
    }

    GLOBAL = {
        :name   => "global",
        :large  => "--global" ,
        :description => "Find a global Token."
    }

    STDIN_PASSWORD = {
        :name => "stdin_password",
        :large => "--stdin_password",
        :description => "enable stdin password"
    }

    auth_options = [READ_FILE, SHA1, SSH, X509, KEY, CERT, DRIVER]

    create_options = auth_options.clone.unshift(GROUP_CREATE)

    login_options  = [SSH,
                      X509,
                      X509_PROXY,
                      KEY,
                      CERT,
                      PROXY,
                      TIME,
                      FORCE,
                      GROUP,
                      STDIN_PASSWORD]

    set_options = [TOKEN, GLOBAL]

    ########################################################################
    # Formatters for arguments
    ########################################################################
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc("GROUP") do |arg|
        OpenNebulaHelper.rname_to_id(arg, "GROUP")
    end

    set :format, :userid, OneUserHelper.to_id_desc do |arg|
        helper.to_id(arg)
    end

    set :format, :userid_list, OneUserHelper.list_to_id_desc do |arg|
        helper.list_to_id(arg)
    end

    set :format, :password, OneUserHelper.password_to_str_desc do |arg|
        OneUserHelper.password_to_str(arg, options)
    end

    ########################################################################
    # Commands
    ########################################################################

    create_desc = <<-EOT.unindent
        Creates a new User
        Examples:
          oneuser create my_user my_password
          oneuser create my_user -r /tmp/mypass
          oneuser create my_user my_password --group users,102,testers
          oneuser create my_user --ssh --key /tmp/id_rsa
          oneuser create my_user --ssh -r /tmp/public_key
          oneuser create my_user --x509 --cert /tmp/my_cert.pem
    EOT

    command :create, create_desc, :username, [:password, nil],
            :options=>create_options do
        if args[1]
            pass = args[1]
        else
            rc = helper.password(options)
            if rc.first == 0
                pass = rc[1]
            else
                exit_with_code *rc
            end
        end

        driver = options[:driver] || OpenNebula::User::CORE_AUTH

        gids = options[:group] || []

        helper.create_resource(options) do |user|
            user.allocate(args[0], pass, driver, gids)
        end
    end

    update_desc = <<-EOT.unindent
        Update the template contents. If a path is not provided the editor will
        be launched to modify the current content.
    EOT

    command :update, update_desc, :userid, [:file, nil],
    :options=>OpenNebulaHelper::APPEND do
        helper.perform_action(args[0],options,"modified") do |obj|
            if options[:append]
                str = OpenNebulaHelper.append_template(args[0], obj, args[1])
            else
                str = OpenNebulaHelper.update_template(args[0], obj, args[1])
            end

            helper.set_client(options)
            obj = helper.retrieve_resource(obj.id)

            obj.update(str, options[:append])
        end
    end

    quota_desc = <<-EOT.unindent
        Set the quota limits for the user. If a path is not provided the editor
        will be launched to modify the current quotas.
    EOT

    command :quota, quota_desc, :userid, [:file, nil] do
        helper.perform_action(args[0], options, "modified") do |user|
            rc = user.info

            if OpenNebula.is_error?(rc)
                puts rc.message
                exit -1
            end

            str = OneQuotaHelper.set_quota(user, args[1])

            helper.set_client(options)
            user = helper.retrieve_resource(user.id)

            rc  = user.set_quota(str)

            if OpenNebula.is_error?(rc)
                puts rc.message
                exit -1
            end
        end
    end

    batchquota_desc = <<-EOT.unindent
        Sets the quota limits in batch for various users. If a path is not
        provided the editor will be launched to create new quotas.
    EOT

    command :batchquota, batchquota_desc, [:range, :userid_list],
            [:file, nil] do
        batch_str = OneQuotaHelper.get_batch_quota(args[1])

	helper.set_client(options)
        helper.perform_actions(args[0], options, "modified") do |user|
            str = OneQuotaHelper.merge_quota(user, batch_str)

            if OpenNebula.is_error?(str)
                str
            else
                user.set_quota(str)
            end
        end
    end

    defaultquota_desc = <<-EOT.unindent
        Sets the default quota limits for the users. If a path is not provided
        the editor will be launched to modify the current default quotas.
    EOT

    command :defaultquota, defaultquota_desc, [:file, nil] do
        system = System.new(OneUserHelper.get_client(options))

        default_quotas = system.get_user_quotas()

        if OpenNebula.is_error?(default_quotas)
            puts default_quotas.message
            exit(-1)
        end

        str = OneQuotaHelper.set_quota(default_quotas, args[0], true)

        system = System.new(OneUserHelper.get_client(options, true))
        rc  = system.set_user_quotas(str)

        if OpenNebula.is_error?(rc)
            puts rc.message
            exit(-1)
        end

        exit 0
    end

    umask_desc = <<-EOT.unindent
        Changes the umask used to create the default permissions. In a similar
        way to the Unix umask command, the expected value is a three-digit
        base-8 number. Each digit is a mask that disables permissions for the
        owner, group and other, respectively.

        If mask is not given, or if it is an empty string, the umask will
        be unset
    EOT

    command :umask, umask_desc, [:range, :userid_list], [:mask, nil] do
        helper.perform_actions(args[0],options,
                "umask changed") do |user|

            rc = user.info

            if OpenNebula.is_error?(rc)
                puts rc.message
                exit -1
            end

            user.delete_element('/USER/TEMPLATE/UMASK')

            tmp_str = user.template_str

            if !args[1].nil? && args[1] != ""
                tmp_str << "\nUMASK = #{args[1]}"
            end

            user.update(tmp_str)
        end
    end

    login_desc = <<-EOT.unindent
        Alias of token-create.
    EOT

    command :login, login_desc, [:username, nil], :options=>login_options do
        helper.token_create(args, options)
    end

    key_desc = <<-EOT.unindent
        Shows a public key from a private SSH key. Use it as password
        for the SSH authentication mechanism.
    EOT

    command :key, key_desc, :options=>[KEY] do
        require 'opennebula/ssh_auth'

        options[:key] ||= ENV['HOME']+'/.ssh/id_rsa'

        begin
            sshauth = SshAuth.new(:private_key=>options[:key])
        rescue Exception => e
            exit_with_code -1, e.message
        end

        puts sshauth.password
        exit_with_code 0
    end

    delete_desc = <<-EOT.unindent
        Deletes the given User
    EOT

    command :delete, delete_desc, [:range, :userid_list] do
        helper.perform_actions(args[0], options, "deleted") do |user|
            user.delete
        end
    end

    passwd_desc = <<-EOT.unindent
        Changes the given User's password
    EOT

    command :passwd, passwd_desc, :userid, [:password, nil],
            :options=>auth_options do
        if args[1]
            pass = args[1]
        else
            rc = helper.password(options)
            if rc.first == 0
                pass = rc[1]
            else
                exit_with_code *rc
            end
        end

        helper.perform_action(args[0],options,"Password changed") do |user|
            user.passwd(pass)
        end
    end

    chgrp_desc = <<-EOT.unindent
        Changes the User's primary group
    EOT

    command :chgrp, chgrp_desc, [:range, :userid_list], :groupid do
        helper.perform_actions(args[0],options,"Group changed") do |user|
            user.chgrp(args[1].to_i)
        end
    end

    addgroup_desc = <<-EOT.unindent
        Adds the User to a secondary group
    EOT

    command :addgroup, addgroup_desc, [:range, :userid_list], :groupid do
        gid = args[1]

        helper.perform_actions(args[0],options,"group added") do |user|
            user.addgroup( gid )
        end
    end

    delgroup_desc = <<-EOT.unindent
        Removes the User from a secondary group
    EOT

    command :delgroup, delgroup_desc, [:range, :userid_list], :groupid do
        gid = args[1]

        helper.perform_actions(args[0],options,"group deleted") do |user|
            user.delgroup( gid )
        end
    end

    chauth_desc = <<-EOT.unindent
        Changes the User's auth driver and its password (optional)
        Examples:
          oneuser chauth my_user core
          oneuser chauth my_user core new_password
          oneuser chauth my_user core -r /tmp/mypass
          oneuser chauth my_user --ssh --key /home/oneadmin/.ssh/id_rsa
          oneuser chauth my_user --ssh -r /tmp/public_key
          oneuser chauth my_user --x509 --cert /tmp/my_cert.pem
    EOT

    command :chauth, chauth_desc, :userid, [:auth, nil], [:password, nil],
            :options=>auth_options do
        if options[:driver]
            driver = options[:driver]
        elsif args[1]
            driver = args[1]
        else
            exit_with_code 0, "An Auth driver should be specified"
        end

        if args[2]
            pass = args[2]
        else
            rc = helper.password(options)
            if rc.first == 0
                pass = rc[1]
            else
                pass = ""
            end
        end

        helper.perform_action(args[0],
                            options,
                            "Auth driver and password changed") do |user|
            user.chauth(driver, pass)
        end
    end

    list_desc = <<-EOT.unindent
        Lists Users in the pool
    EOT

    command :list, list_desc, :options=>list_options do
        helper.list_pool(options)
    end

    show_desc = <<-EOT.unindent
        Shows information for the given User
    EOT

    command :show, show_desc, [:userid, nil],
            :options=>OpenNebulaHelper::XML do
        user=args[0] || OpenNebula::User::SELF
        helper.show_resource(user,options)
    end

    show_desc = <<-EOT.unindent
        Encodes user and password to use it with ldap
    EOT

    command :encode, show_desc, :username, [:password, nil] do
        ar=args.compact

        if defined?(URI::Parser)
            parser=URI::Parser.new
        else
            parser=URI
        end

        puts ar.map{|a| parser.escape(a) }.join(':')

        0
    end

    passwdsearch_desc = <<-EOT.unindent
        Searches for users with a specific auth driver that has the given
        string in their password field
    EOT

    command :passwdsearch, passwdsearch_desc, :driver, :password,
            :options=>[CLIHelper::CSV_OPT, OpenNebulaHelper::XML] do

        options[:list] = ["ID", "NAME", "AUTH", "PASSWORD"]
        options[:filter] = ["AUTH=#{args[0]}", "PASSWORD=#{args[1]}"]

        helper.list_pool(options)
    end

    token_add_desc = <<-EOT.unindent
        Creates the login token for authentication. The token can be used
        together with any authentication driver. The token will be stored in
        $HOME/.one/one_auth, and can be used subsequently to authenticate with
        oned through API, CLI or Sunstone.

        If <username> is ommited, it will infer it from the ONE_AUTH file.

        Example, request a valid token for a generic driver (e.g. core auth, LDAP...):
          oneuser token-create my_user --time 3600

        Example, request a group spefici token (new resources will be created in that
        group and only resources that belong to that group will be listed):
          oneuser token-create my_user --group <id|group>

        Example, generate and set a token for SSH based authentication:
          oneuser token-create my_user --ssh --key /tmp/id_rsa --time 72000

        Example, same using X509 certificates:
          oneuser token-create my_user --x509 --cert /tmp/my_cert.pem
                                --key /tmp/my_key.pk --time 72000

        Example, now with a X509 proxy certificate
          oneuser token-create my_user --x509_proxy --proxy /tmp/my_cert.pem
                                --time 72000
    EOT

    command :"token-create", token_add_desc, [:username, nil],
            :options=>login_options do

        helper.token_create(args, options)
    end

    token_set_desc = <<-EOT.unindent
        Generates a ONE_AUTH file that contains the token.

        You must provide one (and only one) of the following options:

        --token <token>    searches for a token that starts with that string. It must be
                           unique

        --group <id|group> returns the most durable token that provides access to that
                           specific group.

        --global           returns the most durable global token (non group specific).

        The argument 'username' is optional, if omitted it is inferred from the ONE_AUTH
        file.

        Example, set a token:
          $ oneuser token-set my_user --token 1d47
          export ONE_AUTH=/var/lib/one/.one/<file>.token; export ONE_EGID=-1

        You can copy & paste the output of the command and will load the proper
        environment variables.
    EOT

    command :"token-set", token_set_desc, [:username, nil],
            :options=>login_options+set_options do

        username = args[0]

        if username
            if username =~ /^\d+$/
                exit_with_code 1, "The argument should be the username, not the ID."
            end

            helper.client = helper.get_login_client(username, options)
        end

        user = helper.retrieve_resource(OpenNebula::User::SELF)

        rc = user.info
        if OpenNebula.is_error?(rc)
            puts rc.message
            exit_with_code 1, rc.message
        end

        # Process the options
        if options[:token]
            token_hint = options[:token]
            group = nil
        elsif options[:group]
            token_hint = nil
            group = options[:group]
        elsif options[:global]
            token_hint = nil
            group = -1
        else
            exit_with_code 1, "One of these options must be supplied:\n" <<
            "[--token <token>] [--group <id|group>] [--global]"
        end

        tokens = helper.find_token(user, token_hint, group, false)

        if tokens.length == 0
            exit_with_code 1, "No valid tokens found."
        end

        if token_hint && tokens.length > 1
            msg = "More than one token starting with '#{token_hint}' found."
            exit_with_code 1, msg
        end

        token = tokens[0]["TOKEN"]

        egid  = user["LOGIN_TOKEN[TOKEN='#{token}']/EGID"]

        auth_string = "#{user['NAME']}:#{token}"
        auth_file   = helper.auth_file(auth_string)

        begin
            FileUtils.mkdir_p(File.dirname(auth_file))
        rescue Errno::EEXIST
        end

        file = File.open(auth_file, "w")
        file.write(auth_string)
        file.close

        File.chmod(0600, auth_file)

        msg ="export ONE_AUTH=" + auth_file
        msg << "; export ONE_EGID=#{egid}" if egid

        exit_with_code 0, msg
    end

    token_delete_desc = <<-EOT.unindent
        Expires a token and removes the associated ONE_AUTH file if present.
    EOT

    command :"token-delete", token_delete_desc, [:username, nil], :token,
            :options=>login_options do

        if args.length == 1
            token_hint = args[0]
        else
            username, token_hint = args
        end

        if username
            helper.client = helper.get_login_client(username, options)
        end

        user = helper.retrieve_resource(OpenNebula::User::SELF)

        rc = user.info
        if OpenNebula.is_error?(rc)
            puts rc.message
            exit_with_code 1, rc.message
        end

        token = helper.find_token(user, token_hint, nil, true)

        if token.count > 1
            exit_with_code 1, "More than one token starting with '#{token_hint}' found."
        elsif token.count == 0
            exit_with_code 1, "No tokens found."
        end

        token = token[0]["TOKEN"]

        rc = user.login(user['NAME'], token, 0)

        if OpenNebula.is_error?(rc)
            puts rc.message
            exit_with_code 1, rc.message
        else
            puts "Token removed."
        end

        auth_string = "#{user['NAME']}:#{token}"
        auth_file   = helper.auth_file(auth_string)

        begin
            File.unlink(auth_file)
            puts "Removing #{auth_file}"
        rescue Errno::ENOENT
        end

        0
    end

    token_delete_all = <<-EOT.unindent
        Delete all the tokens of a user. This command is intented to be executed by a
        user that has MANAGE permissions of the target user.
    EOT

    command :"token-delete-all", token_delete_all, :username,
            :options=>login_options do

        username = args[0]

        if username =~ /^\d+$/
            exit_with_code 1, "The argument should be the username, not the ID."
        end

        helper.perform_action(username, options, "Tokens expired") do |user|
            user.login(username, "", 0)
        end
    end
end
