package Thruk::Authentication::User;

use warnings;
use strict;
use Carp qw/confess/;
use Cpanel::JSON::XS ();

use Thruk::Constants ':backend_handling';
use Thruk::Utils ();
use Thruk::Utils::Auth ();
use Thruk::Utils::Log qw/:all/;

=head1 NAME

Thruk::Authentication::User - Authenticate a remote user configured using a cgi.cfg

=head1 DESCRIPTION

This module allows you to authenticate the users.

=head1 METHODS

=head2 new

create a new C<Thruk::Authentication::User> object.

 Thruk::Authentication::User->new();

=cut

sub new {
    my($class, $c, $username, $sessiondata, $superuser, $internal) = @_;
    my $self = {};
    bless $self, $class;

    confess("no username") unless defined $username;

    $self->{'username'}           = $username;
    $self->{'roles'}              = [];
    $self->{'teams'}              = [];
    $self->{'groups'}             = [];
    $self->{'alias'}              = undef;
    $self->{'roles_from_groups'}  = {};
    $self->{'roles_from_profile'} = {};
    $self->{'roles_from_teams'}   = {};
    $self->{'superuser'}          = $superuser ? 1 : 0;
    $self->{'internal'}           = $internal  ? 1 : 0;
    $self->{'admin_role_from_system_and_conf'} = $c->config->{'admin_role_from_system_and_conf'} // 1;
    $self->{'permissions'}        = [];

    # add roles from cgi_conf
    for my $role (@{$Thruk::Constants::possible_roles}) {
        next unless defined $c->config->{$role};
        for my $member (@{$c->config->{$role}}) {
            if(Thruk::Base::wildcard_match($username, $member)) {
                push @{$self->{'roles'}}, $role;
                last;
            }
        }
    }
    $self->{'roles_from_cgi_cfg'} = Thruk::Base::array2hash($self->{'roles'});

    $self->{'roles_from_session'} = {};
    if($sessiondata && $sessiondata->{'roles'}) {
        push @{$self->{'roles'}}, @{$sessiondata->{'roles'}};
        $self->{'roles_from_session'} = Thruk::Base::array2hash($sessiondata->{'roles'});
    }

    # oauth groups are mapped to teams
    if($sessiondata && $sessiondata->{'oauth_groups'}) {
        push @{$self->{'teams'}}, @{$sessiondata->{'oauth_groups'}};
    }

    # ex.: user settings from var/users/<name>
    $self->{settings} = $self->{'internal'} ? {} : Thruk::Utils::get_user_data($c, $username);

    $self->{'roles_from_profile'} = {};
    if($self->{settings}->{'roles'}) {
        push @{$self->{'roles'}}, @{$self->{settings}->{'roles'}};
        $self->{'roles_from_profile'} = Thruk::Base::array2hash($self->{settings}->{'roles'});
    }

    if($self->{settings}->{'teams'}) {
        push @{$self->{'teams'}}, @{$self->{settings}->{'teams'}};
    }

    # expand teams recursively
    local $ENV{THRUK_LOGIN_FORM_USER} = $username; # user is not fully logged in, but make username available for scripts
    $self->_expand_teams($c, $self->{'teams'});

    $self->{'permissions'} = Thruk::Base::array_uniq_obj($self->{'permissions'});
    $self->{'roles'}       = Thruk::Base::array_uniq($self->{'roles'});

    # Is this user internal or an admin user?
    if($self->{'internal'} || $self->check_user_roles('admin')) {
        $self->grant('admin');
        $self->{'can_submit_commands'}     = 1;
        $self->{'can_submit_commands_src'} = "admin role";
    }

    if($self->{'internal'} && !$self->{'timestamp'}) {
        $self->{'timestamp'}        = time();
        $self->{'contact_src_peer'} = [];
    }

    $self->{'roles'} = [ sort @{Thruk::Base::array_uniq($self->{'roles'})} ];

    return $self;
}

########################################

=head2 set_dynamic_attributes

  set_dynamic_attributes($c, [$skip_db_access], [$roles], [$force_roles])

sets attributes based on livestatus data

=cut
sub set_dynamic_attributes {
    my($self, $c, $skip_db_access,$roles, $force_roles) = @_;
    return $self if $ENV{'THRUK_BASH_COMP'};
    $c->stats->profile(begin => "User::set_dynamic_attributes");

    my $username = $self->{'username'};
    confess("no username") unless defined $username;

    # internal technical users do not have any dynamic attributes
    if($self->{'internal'}) {
        $self->clean_roles($roles, $force_roles);
        $c->stats->profile(end => "User::set_dynamic_attributes");
        return $self;
    }

    my($data, $use_cached);
    if($skip_db_access) {
        $use_cached = 1;
    } else {
        my($prev_backends) = $c->db->select_backends('get_services');

        _debug("fetching user data from livestatus") if Thruk::Base->verbose;
        eval {
            local $c->stash->{backend_errors_handling} = DIE;
            my($alias, $email, $can_submit_commands, $groups,$src_peers) = $self->_fetch_user_data($c);
            $data->{'alias'}                   = $alias               if defined $alias;
            $data->{'email'}                   = $email               if defined $email;
            $data->{'can_submit_commands'}     = $can_submit_commands if defined $can_submit_commands;
            $data->{'can_submit_commands_src'} = "set as contact attribute" if defined $can_submit_commands;
            $data->{'contact_src_peer'}        = $src_peers;
            $data->{'contactgroups'}           = $groups              if defined $groups;
            $data->{'timestamp'}               = time();
        };
        my $err = $@;
        if($err) {
            _debug("failed to fetch user data from livestatus: %s", $err);
            $use_cached = 1;
        }

        # restore original selected backends
        Thruk::Action::AddDefaults::set_enabled_backends($c, $prev_backends);
    }

    if($use_cached) {
        _debug("using cached user data") if Thruk::Base->verbose;
        my $cache = $c->cache->get->{'users'};
        $data = {};
        if(ref $cache eq 'HASH') {
            $data = $cache->{$username} || {};
        }
        if($data->{'contactgroups'} && ref $data->{'contactgroups'} eq 'HASH') {
            $data->{'contactgroups'} = [sort keys %{$data->{'contactgroups'}}];
        }
    }
    $self->_apply_user_data($c, $data);

    $self->clean_roles($roles, $force_roles);
    $self->clean_roles($self->{'roles_restriction'}, $force_roles) if $self->{'roles_restriction'};

    if($self->check_user_roles('admin')) {
        $self->grant('admin');
    }

    $self->{'roles'} = [ sort @{Thruk::Base::array_uniq($self->{'roles'})} ];

    if(!$skip_db_access && !$roles) {
        $c->cache->set('users', $username, $data);
    }

    $self->{'roles_restriction'} = $roles if defined $roles;

    $c->stats->profile(end => "User::set_dynamic_attributes");
    return $self;
}

########################################

=head2 clean_roles

  clean_roles($roles)

limit roles to given list (using the intersection of user roles and list)

=cut
sub clean_roles {
    my($self, $roles, $force_roles) = @_;
    if($force_roles) {
        $self->{'roles'} = Thruk::Base::array_uniq($force_roles);
        return $force_roles;
    }
    return($self->{'roles'}) unless defined $roles;

    my $readonly;
    if($self->check_user_roles('authorized_for_read_only')) {
        $readonly = 1;
        push @{$roles}, 'authorized_for_read_only';
    }

    my $cleaned_roles = [];
    for my $r (@{$roles}) {
        if($self->check_role_permissions($r)) {
            push @{$cleaned_roles}, $r;
        }
    }
    $self->{'roles'} = Thruk::Base::array_uniq($cleaned_roles);

    # update read-only flag
    if($readonly || $self->check_user_roles('authorized_for_read_only')) {
        $self->{'can_submit_commands'}     = 0;
        $self->{'can_submit_commands_src'} = "read_only role";
    }

    # clean role origins
    my $roles_hash = Thruk::Base::array2hash($cleaned_roles);
    for my $key (qw/roles_from_cgi_cfg roles_from_session/) {
        next unless $self->{$key};
        for my $k2 (sort keys %{$self->{$key}}) {
            delete $self->{$key}->{$k2} unless $roles_hash->{$k2};
        }
    }

    return($self->{'roles'});
}

########################################
sub _fetch_user_data {
    my($self, $c) = @_;

    # is the contact allowed to send commands?
    my($can_submit_commands,$alias,$email);
    my $src_peers = [];
    confess("no db") unless $c->db();
    my $groups;
    my $data   = $c->db->get_can_submit_commands($self->{'username'});
    if(defined $data) {
        for my $dat (@{$data}) {
            $alias = $dat->{'alias'} if defined $dat->{'alias'};
            $email = $dat->{'email'} if defined $dat->{'email'};
            if(defined $dat->{'can_submit_commands'} && (!defined $can_submit_commands || $dat->{'can_submit_commands'} == 0)) {
                $can_submit_commands = $dat->{'can_submit_commands'};
                push @{$src_peers} , $dat->{'peer_key'};
            }
            if(defined $dat->{'groups'}) {
                $groups = [] unless defined $groups;
                push(@{$groups}, @{$dat->{'groups'}});
            }
        }
    }

    # add roles from groups in cgi.cfg
    $groups = [sort keys %{$c->db->get_contactgroups_by_contact($self->{'username'})}] unless defined $groups;
    $groups = Thruk::Base::array_uniq($groups);

    return($alias, $email, $can_submit_commands, $groups, $src_peers);
}

########################################
sub _apply_user_data {
    my($self, $c, $data) = @_;

    my $can_submit_commands     = $data->{'can_submit_commands'};
    my $can_submit_commands_src = $data->{'can_submit_commands_src'};
    if(!defined $can_submit_commands) {
        $can_submit_commands     = $c->config->{'can_submit_commands'} || 0;
        $can_submit_commands_src = "config default";
    }

    my $roles  = [];
    my $roles_by_group = {};
    for my $key (@{$Thruk::Constants::possible_roles}) {
        my $role = $key;
        $role =~ s/^authorized_for_/authorized_contactgroup_for_/gmx;

        next unless defined $c->config->{$role};

        # make group=* work, even if contact does not have any group
        for my $testgroup (@{$c->config->{$role}}) {
            if($testgroup eq '*') {
                push @{$roles_by_group->{$key}}, '*';
                push @{$roles}, $key;
            }
        }

        # check groups against roles
        for my $contactgroup (@{$data->{'contactgroups'}}) {
            for my $testgroup (@{$c->config->{$role}}) {
                if(Thruk::Base::wildcard_match($contactgroup, $testgroup)) {
                    $roles_by_group->{$key} = [] unless defined $roles_by_group->{$key};
                    push @{$roles_by_group->{$key}}, $contactgroup;
                    push @{$roles}, $key;
                    last;
                }
            }
        }
    }

    push @{$self->{'roles'}}, @{$roles};

    # override can_submit_commands from cgi.cfg
    if($self->check_user_roles('admin')) {
        $can_submit_commands     = 1;
        $can_submit_commands_src = "admin role";
    }
    elsif(grep /^authorized_for_all_host_commands$/mx, @{$self->{'roles'}}) {
        $can_submit_commands     = 1;
        $can_submit_commands_src = "authorized_for_all_host_commands role";
    }
    elsif(grep /^authorized_for_all_service_commands$/mx, @{$self->{'roles'}}) {
        $can_submit_commands     = 1;
        $can_submit_commands_src = "authorized_for_all_service_commands role";
    }
    elsif(grep /^authorized_for_system_commands$/mx, @{$self->{'roles'}}) {
        $can_submit_commands     = 1;
        $can_submit_commands_src = "authorized_for_system_commands role";
    }
    elsif(grep /^authorized_for_read_only$/mx, @{$self->{'roles'}}) {
        # read_only role already supplied via cgi.cfg, enforce
        $can_submit_commands = 0;
        $can_submit_commands_src = "authorized_for_read_only role";
    }

    _debug("can_submit_commands: $can_submit_commands");
    if($can_submit_commands != 1) {
        if(!grep /^authorized_for_read_only$/mx, @{$self->{'roles'}}) {
            push @{$roles}, 'authorized_for_read_only';
        }
    }

    $data->{'roles'}                   = Thruk::Base::array_uniq($roles);
    $data->{'can_submit_commands'}     = $can_submit_commands;
    $data->{'can_submit_commands_src'} = $can_submit_commands_src;
    $data->{'roles_by_group'}          = $roles_by_group;

    $self->{'alias'}                   = $data->{'alias'} // $self->{'alias'};
    $self->{'email'}                   = $data->{'email'} // $self->{'email'};
    $self->{'roles_from_groups'}       = $data->{'roles_by_group'};
    $self->{'groups'}                  = $data->{'contactgroups'} || [];
    $self->{'can_submit_commands'}     = $data->{'can_submit_commands'};
    $self->{'can_submit_commands_src'} = $data->{'can_submit_commands_src'};
    $self->{'contact_src_peer'}        = $data->{'contact_src_peer'} // [];
    $self->{'timestamp'}               = $data->{'timestamp'};
    push @{$self->{'roles'}}, @{$roles};

    return;
}

=head2 get

get user attribute

 get($attribute)

=cut

sub get {
    my($self, $attr) = @_;
    return($self->{$attr});
}

=head2 is_locked

  is_locked()

returns true if user is locked

=cut

sub is_locked {
    my($self) = @_;
    if($self->{'settings'}->{'login'} && $self->{'settings'}->{'login'}->{'locked'}) {
        return(1);
    }
    return(0);
}

########################################

=head2 can_choose_role

 can_choose_role(<$role>)

 returns 1 if user has given role.

 for example:
  $c->user->check_user_role('authorized_for_all_services')

=cut

sub can_choose_role {
    my($self, $role) = @_;

    return 1 if $role eq 'authorized_for_read_only';

    my @found = grep(/^\Q$role\E$/mx, @{$self->{'roles'}});
    return 1 if scalar @found >= 1;
    return(0);
}

########################################

=head2 check_user_roles

 check_user_roles(<$role>)

 returns 1 if user has permissions as in all given roles.

 for example:
  $c->user->check_user_roles('admin')
  $c->user->check_user_roles('authorized_for_all_services')
  $c->user->check_user_roles(['authorized_for_system_commands', 'authorized_for_configuration_information'])

=cut

sub check_user_roles {
    my($self, $role) = @_;
    if(ref $role eq 'ARRAY') {
        for my $r (@{$role}) {
            if(!$self->check_user_roles($r)) {
                return(0);
            }
        }
        return(1);
    }
    my @found = grep(/^\Q$role\E$/mx, @{$self->{'roles'}});
    return 1 if scalar @found >= 1;

    if($role eq 'admin') {
        if($self->check_user_roles('authorized_for_admin')) {
            return(1);
        }
        # for historical reasons (there was no explicit admin role in the past) any user with both, the
        # - authorized_for_system_commands and
        # - authorized_for_configuration_information
        # gains the full admin role as well.
        # change this behavior with the 'admin_role_from_system_and_conf' setting.
        if($self->{'admin_role_from_system_and_conf'}
            && $self->check_user_roles('authorized_for_system_commands')
            && $self->check_user_roles('authorized_for_configuration_information')
        ) {
            return(1);
        }
    }

    if($role eq 'authorized_for_all_hosts') {
        # this roles is implicitly granted if there are * permissions
        return(1) if _has_star_permissions($self->{'permissions'});
    }

    if($role eq 'authorized_for_all_host_commands') {
        # this roles is implicitly granted if there are * permissions with commands enabled
        return(1) if _has_star_permissions($self->{'permissions'}, 1);
    }

    if($role eq 'authorized_for_all_services') {
        # this roles is implicitly granted if there are * permissions
        return(1) if _has_star_permissions($self->{'permissions'}, 0, 1);
    }

    if($role eq 'authorized_for_all_service_commands') {
        # this roles is implicitly granted if there are * permissions with commands enabled
        return(1) if _has_star_permissions($self->{'permissions'}, 1, 1);
    }

    return(0);
}

########################################

=head2 check_permissions

 check_permissions('host', $hostname)
 check_permissions('service', $servicename, $hostname)
 check_permissions('hostgroup', $hostgroup_name)
 check_permissions('servicegroup', $servicegroup_name)
 check_permissions(contact', $contact_name)
 check_permissions(contactgroup', $contactgroup_name)

 for example:
 $c->check_permissions('service', $service, $host)

returns true if users has view permissions for the given type and value.

=cut

sub check_permissions {
    my($self, $c, $type, $value, $value2, $value3, $cmd_permissions) = @_;

    return 1 if $c->check_user_roles('authorized_for_admin');

    $type   = '' unless defined $type;
    $value  = '' unless defined $value;
    $value2 = '' unless defined $value2;

    my $count = 0;
    if($type eq 'host') {
        return 1 if $self->check_user_roles($cmd_permissions ? 'authorized_for_all_host_commands' : 'authorized_for_all_hosts');
        if($cmd_permissions && !$self->get('can_submit_commands')) {
            my $tst_filter = Thruk::Utils::Auth::permissions_filter($c, 'hosts', 1);
            if(!defined $tst_filter || scalar @{$tst_filter} == 0) {
                # no filter means no permissions, so simply return instead of a useless db query
                return 0;
            }
        }
        my $hosts = $c->db->get_host_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'hosts', $value2, $cmd_permissions), name => $value ]);
        $count = 1 if defined $hosts && scalar @{$hosts} > 0;
    }
    elsif($type eq 'service') {
        return 1 if $self->check_user_roles($cmd_permissions ? 'authorized_for_all_service_commands' : 'authorized_for_all_services');
        if($cmd_permissions && !$self->get('can_submit_commands')) {
            my $tst_filter = Thruk::Utils::Auth::permissions_filter($c, 'services', 1);
            if(!defined $tst_filter || scalar @{$tst_filter} == 0) {
                # no filter means no permissions, so simply return instead of a useless db query
                return 0;
            }
        }
        my $services = $c->db->get_service_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'services', $value3, $cmd_permissions), description => $value, host_name => $value2 ]);
        $count = 1 if defined $services && scalar @{$services} > 0;
    }
    elsif($type eq 'host_services') {
        return 1 if $self->check_user_roles($cmd_permissions ? 'authorized_for_all_service_commands' : 'authorized_for_all_services');
        # does the user have permissions for all services of the host?
        my $services1 = $c->db->get_service_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'services', $value2, $cmd_permissions), host_name => $value ]);
        my $services2 = $c->db->get_service_names(filter => [                                                                                host_name => $value ]);
        # authorization permitted when the amount of services is the same number as services with authorization
        $count = 1 if defined $services1 && defined $services2 && scalar @{$services1} == scalar @{$services2};
    }
    elsif($type eq 'hostgroup') {
        return 1 if $self->check_user_roles($cmd_permissions ? 'authorized_for_all_host_commands' : 'authorized_for_all_hosts');
        my $hosts1 = $c->db->get_host_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'hosts', $value2, $cmd_permissions), groups => { '>=' => $value } ]);
        my $hosts2 = $c->db->get_host_names(filter => [ groups => { '>=' => $value } ]);
        $count = 0;
        # authorization permitted when the amount of hosts is the same number as hosts with authorization
        if(defined $hosts1 && defined $hosts2 && scalar @{$hosts1} == scalar @{$hosts2} && scalar @{$hosts1} != 0) {
            $count = 1;
        }
    }
    elsif($type eq 'servicegroup') {
        return 1 if $self->check_user_roles($cmd_permissions ? 'authorized_for_all_service_commands' : 'authorized_for_all_services');
        my $services1 = $c->db->get_service_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'services', $value3, $cmd_permissions), groups => { '>=' => $value } ]);
        my $services2 = $c->db->get_service_names(filter => [ groups => { '>=' => $value } ]);
        $count = 0;
        # authorization permitted when the amount of services is the same number as services with authorization
        if(defined $services1 && defined $services2 && scalar @{$services1} == scalar @{$services2} && scalar @{$services1} != 0) {
            $count = 1;
        }
    }
    elsif($type eq 'contact') {
        if($value eq $c->user->{'username'}) {
            $count = 1;
        }
    }
    elsif($type eq 'contactgroup') {
        # admin privileges are checked already, so if we reach this point, the user is no admin
        # so simply deny access to contactgroups
        $count = 0;
    }
    else {
        $c->error("unknown auth role check: ".$type);
        return 0;
    }
    $count = 0 unless defined $count;
    _debug("count: ".$count);
    if($count > 0) {
        _debug("check_permissions('".$type."', '".$value."', '".$value2."') -> access granted");
        return 1;
    }
    _debug("check_permissions('".$type."', '".$value."', '".$value2."') -> access denied");
    return 0;
}

########################################

=head2 check_cmd_permissions

 check_cmd_permissions('system')
 check_cmd_permissions('host', $hostname)
 check_cmd_permissions('all_hosts')
 check_cmd_permissions('service', $servicename, $hostname)
 check_cmd_permissions('host_services', $hostname)
 check_cmd_permissions('all_services')
 check_cmd_permissions('hostgroup', $hostgroup_name)
 check_cmd_permissions('servicegroup', $servicegroup_name)
 check_cmd_permissions('contact', $contact_name)
 check_cmd_permissions('contactgroup', $contactgroup_name)

 for example:
 $c->check_cmd_permissions('service', $service, $host)

returns true if users has command permissions for the given type and value.

=cut

sub check_cmd_permissions {
    my($self, $c, $type, $value, $value2) = @_;

    $type   = '' unless defined $type;
    $value  = '' unless defined $value;
    $value2 = '' unless defined $value2;

    # cache permissions for this request
    my $cache_key = join(';', $type, $value, $value2);
    return($c->stash->{'_cmd_perm_cache'}->{$cache_key} //= $self->_check_cmd_permissions($c, $type, $value, $value2));
}

sub _check_cmd_permissions {
    my($self, $c, $type, $value, $value2) = @_;

    return 0 if $self->check_user_roles('authorized_for_read_only');
    return 0 if !$self->{'can_submit_commands'};
    return 1 if $self->check_user_roles('authorized_for_admin');

    if($type eq 'system') {
        return 1 if $self->check_user_roles('authorized_for_system_commands');
    }
    elsif($type eq 'host') {
        return 1 if $self->check_user_roles('authorized_for_all_host_commands');
        return 1 if $self->check_permissions($c, 'host', $value, 1, undef, 1);
    }
    elsif($type eq 'hostgroup') {
        return 1 if $self->check_user_roles('authorized_for_all_host_commands');
        return 1 if $self->check_permissions($c, 'hostgroup', $value, 1, undef, 1);
    }
    elsif($type eq 'all_hosts') {
        return 1 if $self->check_user_roles('authorized_for_all_host_commands');
    }
    elsif($type eq 'service') {
        return 1 if $self->check_user_roles('authorized_for_all_service_commands');
        return 1 if $self->check_permissions($c, 'service', $value, $value2, 1, 1);
    }
    elsif($type eq 'host_services') {
        return 1 if $self->check_user_roles('authorized_for_all_service_commands');
        return 1 if $self->check_permissions($c, 'host_services', $value, 1, undef, 1);
    }
    elsif($type eq 'all_services') {
        return 1 if $self->check_user_roles('authorized_for_all_service_commands');
    }
    elsif($type eq 'servicegroup') {
        return 1 if $self->check_user_roles('authorized_for_all_service_commands');
        return 1 if $self->check_permissions($c, 'servicegroup', $value, 1, undef, 1);
    }
    elsif($type eq 'contact') {
        return 1 if $self->check_permissions($c, 'contact', $value, 1);
    }
    elsif($type eq 'contactgroup') {
        return 1 if $self->check_permissions($c, 'contactgroup', $value, 1);
    }
    else {
        $c->error("unknown cmd auth role check: ".$type);
        return 0;
    }
    return 0;
}

########################################

=head2 check_show_command_line_permissions

 check_show_command_line_permissions('host', $hostname)
 check_show_command_line_permissions('service', $servicename, $hostname)

 for example:
 $c->check_show_command_line_permissions('service', $service, $host)

returns true if users has permissions to see the expanded command.

=cut

sub check_show_command_line_permissions {
    my($self, $c, $type, $value, $value2) = @_;

    $type   = '' unless defined $type;
    $value  = '' unless defined $value;
    $value2 = '' unless defined $value2;

    if($c->config->{'show_full_commandline'} == 2) {
        return 1;
    }

    if($c->config->{'show_full_commandline'} == 1 && $self->check_user_roles('authorized_for_configuration_information')) {
        return 1;
    }

    # cache permissions for this request
    my $cache_key = join(';', $type, $value, $value2);
    return($c->stash->{'_cmdline_perm_cache'}->{$cache_key} //= $self->_check_cmd_line_permissions($c, $type, $value, $value2));
}

# command line permissions from teams
sub _check_cmd_line_permissions {
    my($self, $c, $type, $value, $value2) = @_;

    my $count = 0;
    if($type eq 'host') {
        my $tst_filter = Thruk::Utils::Auth::permissions_filter($c, 'hosts', undef, 1);
        if(!defined $tst_filter || scalar @{$tst_filter} == 0) {
            # no filter means no permissions, so simply return instead of a useless db query
            return 0;
        }
        my $hosts = $c->db->get_host_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'hosts', 1, undef, 1), name => $value ]);
        $count = 1 if defined $hosts && scalar @{$hosts} > 0;
    }
    elsif($type eq 'service') {
        my $tst_filter = Thruk::Utils::Auth::permissions_filter($c, 'services', undef, 1);
        if(!defined $tst_filter || scalar @{$tst_filter} == 0) {
            # no filter means no permissions, so simply return instead of a useless db query
            return 0;
        }
        my $services = $c->db->get_service_names(filter => [ Thruk::Utils::Auth::get_auth_filter($c, 'services', 1, undef, 1), description => $value, host_name => $value2 ]);
        $count = 1 if defined $services && scalar @{$services} > 0;
    }
    else {
        $c->error("unknown auth role check: ".$type);
        return 0;
    }
    $count = 0 unless defined $count;
    _debug("count: ".$count);
    if($count > 0) {
        _debug("_check_cmd_line_permissions('".$type."', '".$value."', '".$value2."') -> access granted");
        return 1;
    }
    _debug("_check_cmd_line_permissions('".$type."', '".$value."', '".$value2."') -> access denied");
    return 0;
}

########################################

=head2 check_role_permissions

 check_role_permissions($role)

 returns 1 if user is allowed to use given role. Don't mix up with check_user_roles()

=cut

sub check_role_permissions {
    my($self, $role) = @_;
    return 1 if $role eq 'authorized_for_read_only';
    return 1 if $self->check_user_roles('admin');
    return 1 if $self->can_choose_role($role);
    return 0;
}

########################################

=head2 transform_username

run transformation rules for username

 transform_username($config, $username, [$c])

=cut

sub transform_username {
    my($config, $username, $c) = @_;

    return $username if(!defined $username || $username eq '');

    # change case?
    $username = lc($username) if $config->{'make_auth_user_lowercase'};
    $username = uc($username) if $config->{'make_auth_user_uppercase'};

    # regex replace?
    if($config->{'make_auth_replace_regex'}) {
        _debug("authentication regex replace before: ".$username) if $c;
        ## no critic
        eval('$username =~ '.$config->{'make_auth_replace_regex'});
        ## use critic
        _error("authentication regex replace error: ".$@) if ($c && $@);
        _debug("authentication regex replace after : ".$username) if $c;
    }
    return($username);
}

########################################

=head2 grant

    grant('role')

grant role to user

=cut

sub grant {
    my($self, $role) = @_;
    if($role eq 'admin') {
        push @{$self->{'roles'}}, @{$Thruk::Constants::possible_roles};
        # remove read only role
        $self->{'roles'} = [ grep({ $_ ne 'authorized_for_read_only' } @{$self->{'roles'}}) ];
        $self->{'roles'} = Thruk::Base::array_uniq($self->{'roles'});
    } else {
        confess('role '.$role.' not implemented');
    }
    return;
}

=head2 has_group

 has_group(<$group>)

 returns 1 if user has all given groups.

 for example:
  $c->user->has_group('Admin')
  $c->user->has_group(['Admin', 'Location XY'])

=cut

sub has_group {
    my($self, $groups) = @_;
    my $groups_hash = Thruk::Base::array2hash($self->{'groups'});
    $groups = Thruk::Base::list($groups);
    for my $g (@{$groups}) {
        return(0) unless defined $groups_hash->{$g};
    }
    return(1);
}

=head2 js_data

 js_data()

 returns user data exposed to javascript

=cut

sub js_data {
    my($self) = @_;
    return({
        name                => $self->{'username'},
        groups              => $self->{'groups'},
        roles               => $self->{'roles'},
        can_submit_commands => $self->{'can_submit_commands'} ? Cpanel::JSON::XS::true  : Cpanel::JSON::XS::false,
        readonly            => $self->{'can_submit_commands'} ? Cpanel::JSON::XS::false : Cpanel::JSON::XS::true,
    });
}

########################################
# expand roles from teams definitions
sub _expand_teams {
    my($self, $c, $teams, $already_included) = @_;
    $already_included = {} unless defined $already_included;

    my $added = 0;
    for my $t (@{$teams}) {
        next if $already_included->{$t};
        if(Thruk::Base::check_for_nasty_filename($t)) {
            _warn("invalid team name: $t");
            next;
        }

        my $role_data = read_team_data($c, $t, 1);
        if($role_data->{'includes'}) {
            for my $i (@{$role_data->{'includes'}}) {
                next if $already_included->{$i};
                push @{$teams}, $i;
                $already_included->{$i} = $i;
                $added++;
            }
        }

        if($role_data->{'roles'}) {
            for my $i (@{$role_data->{'roles'}}) {
                my $r = "$i";
                $r =~ s/^authorized_for_//gmx;
                $r = "authorized_for_".$r;
                $self->{'roles_from_teams'}->{$r} = [] unless defined $self->{'roles_from_teams'}->{$r};
                push @{$self->{'roles_from_teams'}->{$r}}, $t;
                push @{$self->{'roles'}}, $r;
            }
        }
        if($role_data->{'permissions'}) {
            push @{$self->{'permissions'}}, @{$role_data->{'permissions'}};
        }
    }

    if($added > 0) {
        $self->_expand_teams($c, $teams, $already_included);
    }

    return;
}

########################################

=head2 read_team_data

 read_team_data($c, $team, [$with_fallback])

 returns team data including optional fallback

=cut
sub read_team_data {
    my($c, $team, $with_fallback) = @_;

    my $role_data;
    if(-e $c->config->{'var_path'}."/teams/".$team.".json") {
        _debug("reading teams data from: ".$c->config->{'var_path'}."/teams/".$team.".json") if Thruk::Base->verbose;
        $role_data = Thruk::Utils::IO::json_lock_retrieve($c->config->{'var_path'}."/teams/".$team.".json");
        return $role_data;
    }

    my $team_script = $c->config->{'var_path'}."/teams/".$team;
    if(-x $team_script) {
        _debug("running teams data script from: ".$team_script) if Thruk::Base->verbose;
        my($rc, undef, $stdout, $stderr) = Thruk::Utils::IO::cmd([$team_script], { timeout => 10, env => { 'THRUK_TEAM' => $team } });
        if($rc == 0 && $stdout) {
            my $jsonreader = Thruk::Utils::IO::json_reader();
            $role_data = $jsonreader->decode($stdout);
        } else {
            _error("%s team script failed (rc:%d): %s%s", $team_script, $rc, $stdout, $stderr);
        }
        $role_data->{'scripted_team'} = 1;
        return $role_data;
    }

    return unless $with_fallback;

    # no specific team found, try fallback json
    if(-e $c->config->{'var_path'}."/teams/_fallback.json") {
        _debug("reading fallback teams data from: ".$c->config->{'var_path'}."/teams/_fallback.json") if Thruk::Base->verbose;
        $role_data = Thruk::Utils::IO::json_lock_retrieve($c->config->{'var_path'}."/teams/_fallback.json");
        $role_data->{'real_file'} = '_fallback.json';
        return $role_data;
    }

    my $fallback_script = $c->config->{'var_path'}."/teams/_fallback";
    if(-x $fallback_script) {
        _debug("running teams data fallback script from: ".$fallback_script) if Thruk::Base->verbose;
        my($rc, undef, $stdout, $stderr) = Thruk::Utils::IO::cmd([$fallback_script, $team], { timeout => 10, env => { 'THRUK_TEAM' => $team } });
        if($rc == 0 && $stdout) {
            my $jsonreader = Thruk::Utils::IO::json_reader();
            $role_data = $jsonreader->decode($stdout);
        } else {
            _error("%s team script failed (rc:%d): %s%s", $fallback_script, $rc, $stdout, $stderr);
        }
        $role_data->{'scripted_team'} = 1;
        $role_data->{'real_file'}     = '_fallback';
        return $role_data;
    }

    return;
}

########################################
# returns true if permissions contain * permissions
sub _has_star_permissions {
    my($permissions, $with_commands, $with_services) = @_;

    return 0 unless $permissions;

    for my $p (@{$permissions}) {
        # must have * hosts or hostgroups
        next unless(grep(/^\*$/mx, @{$p->{'hosts'}}) || grep(/^\*$/mx, @{$p->{'hostgroups'}}));

        if($with_services) {
            next if $p->{'with_services'} ne '1'; # 1 = incl. all services
            next if ($with_commands && (!$p->{'svc_commands'} || $p->{'allowed_commands'})); # not if list of commands is limited
        } else {
            next if $p->{'with_services'} eq '2'; # 2 = specific services. Star permission not when there is a service filter involved
            next if ($with_commands && (!$p->{'hst_commands'} || $p->{'allowed_commands'})); # not if list of commands is limited
        }
        return 1;
    }
    return 0;
}

########################################

1;
