#!/usr/bin/perl -T
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

use 5.10.1;
use strict;
use warnings;

use lib qw(. lib local/lib/perl5);

use Bugzilla;
use Bugzilla::Mailer;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Token;
use Bugzilla::User;
use Bugzilla::Util;
use Date::Format;

my $template = Bugzilla->template;
my $cgi = Bugzilla->cgi;

my $action = $cgi->param('action') || '';

my $vars = {};
my $target;

if (!$action) {
    # redirect to index.cgi if no action is defined.
    print $cgi->redirect(correct_urlbase() . 'index.cgi');
    exit;
}
# prepare-sudo: Display the sudo information & login page
elsif ($action eq 'prepare-sudo') {
    # We must have a logged-in user to do this
    # That user must be in the 'bz_sudoers' group
    my $user = Bugzilla->login(LOGIN_REQUIRED);
    unless ($user->in_group('bz_sudoers')) {
        ThrowUserError('auth_failure', {  group => 'bz_sudoers',
                                         action => 'begin',
                                         object => 'sudo_session' }
        );
    }

    # Do not try to start a new session if one is already in progress!
    if (defined(Bugzilla->sudoer)) {
        ThrowUserError('sudo_in_progress', { target => $user->login });
    }

    # Keep a temporary record of the user visiting this page
    $vars->{'token'} = issue_session_token('sudo_prepared');

    if ($user->authorizer->can_login) {
        my $value = generate_random_password();
        my %args;
        $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};

        $cgi->send_cookie(-name => 'Bugzilla_login_request_cookie',
                          -value => $value,
                          -httponly => 1,
                          %args);

        # The user ID must not be set when generating the token, because
        # that information will not be available when validating it.
        local Bugzilla->user->{userid} = 0;
        $vars->{'login_request_token'} = issue_hash_token(['login_request', $value]);
    }

    # Show the sudo page
    $vars->{'target_login_default'} = $cgi->param('target_login');
    $vars->{'reason_default'} = $cgi->param('reason');
    $target = 'admin/sudo.html.tmpl';
}
# begin-sudo: Confirm login and start sudo session
elsif ($action eq 'begin-sudo') {
    # log in the user
    my $user = Bugzilla->login(LOGIN_REQUIRED);

    my $target_login     = $cgi->param('target_login');
    my $reason           = $cgi->param('reason') || '';
    my $token            = $cgi->param('token');
    my $current_password = $cgi->param('current_password');
    my $mfa_token        = $cgi->param('mfa_token');

    # validate entered password
    my $crypt_password = $user->cryptpassword;
    unless (($user->mfa && $mfa_token)
            || bz_crypt($current_password, $crypt_password) eq $crypt_password)
    {
        ThrowUserError('sudo_password_required',
                       { target_login => $target_login, reason => $reason });
    }

    # Check for MFA
    if ($user->mfa) {
        if (!$mfa_token) {
            # display 2fa verification
            $user->mfa_provider->verify_prompt({
                postback => {
                    action => 'relogin.cgi',
                    fields => {
                        token        => $token,
                        action       => $action,
                        reason       => $reason,
                        target_login => $target_login,
                    },
                },
                reason => 'Impersonating another user',
            });
        }
        else {
            # verify mfa token and override with values stored in token data
            my $event = $user->mfa_provider->verify_token($mfa_token);
            $target_login = $event->{postback}->{fields}->{target_login};
            $reason       = $event->{postback}->{fields}->{reason};
            $token        = $event->{postback}->{fields}->{token};
        }
    }

    # The user must be in the 'bz_sudoers' group
    unless ($user->in_group('bz_sudoers')) {
        ThrowUserError('auth_failure', {  group => 'bz_sudoers',
                                         action => 'begin',
                                         object => 'sudo_session' }
        );
    }

    # Do not try to start a new session if one is already in progress!
    if (defined(Bugzilla->sudoer)) {
        ThrowUserError('sudo_in_progress', { target => $user->login });
    }

    # Did the user actually go trough the 'sudo-prepare' action?  Do some 
    # checks on the token the action should have left.
    my ($token_user, $token_timestamp, $token_data) = Bugzilla::Token::GetTokenData($token);
    unless (defined($token_user)
            && defined($token_data)
            && ($token_user == $user->id)
            && ($token_data eq 'sudo_prepared'))
    {
        ThrowUserError('sudo_preparation_required', 
                       { target_login => $target_login, reason => $reason });
    }
    delete_token($cgi->param('token'));

    # Get & verify the target user (the user who we will be impersonating)
    my $target_user = new Bugzilla::User({ name => $target_login });
    unless (defined($target_user)
            && $target_user->id
            && $user->can_see_user($target_user))
    {
        ThrowUserError('user_match_failed', { name => $target_login });
    }
    if ($target_user->in_group('bz_sudo_protect')) {
        ThrowUserError('sudo_protected', { login => $target_user->login });
    }

    # Calculate the session expiry time (T + 6 hours)
    my $time_string = time2str('%a, %d-%b-%Y %T %Z', time + MAX_SUDO_TOKEN_AGE, 'GMT');

    # For future sessions, store the unique ID of the target user
    $token = Bugzilla::Token::_create_token($user->id, 'sudo', $target_user->id);

    my %args;
    if (Bugzilla->params->{ssl_redirect}) {
        $args{'-secure'} = 1;
    }

    $cgi->send_cookie('-name'    => 'sudo',
                      '-expires' => $time_string,
                      '-value'   => $token,
                      '-httponly' => 1,
                      %args);

    # For the present, change the values of Bugzilla::user & Bugzilla::sudoer
    Bugzilla->sudo_request($target_user, $user);

    # NOTE: If you want to log the start of an sudo session, do it here.

    # If we have a reason passed in, keep it under 200 characters
    $reason = substr($reason, 0, 200);

    # Go ahead and send out the message now
    my $message;
    my $mail_template = Bugzilla->template_inner($target_user->setting('lang'));
    $mail_template->process('email/sudo.txt.tmpl', { reason => $reason }, \$message);
    MessageToMTA($message);

    $vars->{'message'} = 'sudo_started';
    $vars->{'target'} = $target_user->login;
    $target = 'global/message.html.tmpl';
}
# end-sudo: End the current sudo session (if one is in progress)
elsif ($action eq 'end-sudo') {
    # Regardless of our state, delete the sudo cookie if it exists
    my $token = $cgi->cookie('sudo');
    $cgi->remove_cookie('sudo');

    # Are we in an sudo session?
    Bugzilla->login(LOGIN_OPTIONAL);
    my $sudoer = Bugzilla->sudoer;
    if (defined($sudoer)) {
        Bugzilla->sudo_request($sudoer, undef);
    }
    # Now that the session is over, remove the token from the DB.
    delete_token($token);

    # NOTE: If you want to log the end of an sudo session, so it here.
    
    $vars->{'message'} = 'sudo_ended';
    $target = 'global/message.html.tmpl';
}
# No valid action found
else {
    Bugzilla->login(LOGIN_OPTIONAL);
    ThrowUserError('unknown_action', {action => $action});
}

# Display the template
print $cgi->header();
$template->process($target, $vars)
      || ThrowTemplateError($template->error());