#!/usr/bin/env perl
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
package xCAT::xcatd;
BEGIN
{
  $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : -d '/opt/xcat' ? '/opt/xcat' : '/usr';
}
use strict;
use xCAT::Table;
use xCAT::MsgUtils;
use Data::Dumper;
use xCAT::NodeRange;
use xCAT::Utils;
#--------------------------------------------------------------------------------

=head1    xCAT::XCATD

=head2    Package Description

This program module file, is a set of utilities used by xCAT daemon. 

=cut


#------------------------------------------------------------------------------

=head3   validate 

  Here is where we check if  $peername is allowed to do $request in policy tbl.
   $peername, if set signifies client has a cert that the xCAT CA accepted.
   Logs to syslog and auditlog table all user commands, see site.auditskipcmds
   attribute and auditnosyslog attribute. 
    
    Arguments:
        
    Returns:
        returns 1 if policy engine allows the action, 0 if denied.
    Globals:
        none
    Error:
        none
    Example:
       if(xCAT::XCATd->validate($peername,$peerhost,$req,$peerhostorg,\@deferredmsgargs)) {
                .
                .
    Comments:
        none
=cut

#------------------------------------------------------------------------------

sub validate {


  my $class=shift;
  my $peername=shift;
  my $peerhost=shift;
  my $request=shift;
  my $peerhostorg=shift;
  my $deferredmsgargs=shift;
   
  # now check the policy table if user can run the command
  my $policytable = xCAT::Table->new('policy');
  unless ($policytable) {
     xCAT::MsgUtils->message("S","Unable to open policy data, denying");
    return 0;
  }
 
  my $policies = $policytable->getAllEntries;
  $policytable->close;
  my $rule;
  my $peerstatus="untrusted";
  # This sorts the policy table  rows based on the level of the priority field in the row.
  # note the lower the number in the policy table the higher the priority
  my @sortedpolicies = sort { $a->{priority} <=> $b->{priority} } (@$policies);
  # check to see if peerhost is trusted
  foreach $rule (@sortedpolicies) {
     
    if (($rule->{name} and $rule->{name} eq $peername)  && ($rule->{rule}=~ /trusted/i)) {
     $peerstatus="Trusted";
     last;
    }
  }
  RULE: foreach $rule (@sortedpolicies) {
    if ($rule->{name} and $rule->{name} ne '*') {
      #TODO: more complex matching (lists, wildcards)
      next unless ($peername and $peername eq $rule->{name});
    }
    if ($rule->{name} and $rule->{name} eq '*') { #a name is required, but can be any name whatsoever....
      next unless ($peername);
    }
    if ($rule->{time} and $rule->{time} ne '*') {
      #TODO: time ranges
    }
    if ($rule->{host} and $rule->{host} ne '*') {
      #TODO: more complex matching (lists, noderanges?, wildcards)
      next unless ($peerhost eq $rule->{host});
    }
    if ($rule->{commands} and $rule->{commands} ne '*') {
      my @commands = split(",", $rule->{commands});
      my $found =0;
      foreach my $cmd (@commands) {
        if ($request->{command}->[0] eq $cmd) {
           $found=1;
           last;
        }
      }
      if ($found == 0) {  # no command match
        next ;
      }
    }
    if ($rule->{parameters} and $rule->{parameters} ne '*') {
      my $parms;
      if ($request->{arg}) {
         $parms = join(' ',@{$request->{arg}});
      } else {
         $parms = "";
      }
      my $patt = $rule->{parameters};
      unless ($parms =~ /$patt/) {
         next;
      }
    }
    if ($rule->{noderange} and $rule->{noderange} ne '*') {
      my $matchall=0;
      if ($rule->{rule} =~ /allow/i or $rule->{rule} =~ /accept/i or $rule->{rule} =~ /trusted/i) {
          $matchall=1;
      }
      
      if (defined $request->{noderange}->[0]) {
        my @tmpn= xCAT::NodeRange::noderange($request->{noderange}->[0]);
        $request->{node}=\@tmpn;
      }
      unless (defined $request->{node}) {
          next RULE;
      }
      my @reqnodes = @{$request->{node}};
      my %matchnodes;
      foreach (noderange($rule->{noderange})) {
          $matchnodes{$_}=1;
      }
      REQN: foreach (@reqnodes) {
          if (defined ($matchnodes{$_})) {
              if ($matchall) {
                  next REQN;
              } else {
                last REQN;
              }
          } elsif ($matchall) {
                  next RULE;
          }
      }
    }
    # If we are still in, that means this rule is the first match and dictates behavior.
    # We are not going to log getdestiny,getbladecons,getipmicons commands, way
    # too many of them
    #print Dumper($request);
    if ($rule->{rule}) {
      my $logst;
      my $rc;
      my $status;
      if ($rule->{rule} =~ /allow/i or $rule->{rule} =~ /accept/i or $rule->{rule} =~ /trusted/i) {
         $logst = "xCAT: Allowing ".$request->{command}->[0];
         $status = "Allowed";
         $rc=1;
      } else {
         $logst = "xCAT: Denying ".$request->{command}->[0];
         $status = "Denied";
         $rc=0;
      }
     if (($request->{command}->[0] ne "getdestiny") && ($request->{command}->[0] ne "getbladecons") && ($request->{command}->[0] ne "getipmicons")) {
      # set username authenticated to run command
      # if from Trusted host, use input username,  else set from creds
      if (($request->{username}) && defined($request->{username}->[0])) {
         if ($peerstatus ne "Trusted" ) {  # then set to peername
            $request->{username}->[0] = $peername;
         }
      } else {
            $request->{username}->[0] = $peername;
      }
      if ($request->{noderange} && defined($request->{noderange}->[0]))
       {
          $logst .= " to ".$request->{noderange}->[0];
       } else { # no noderange maybe a nodes
          
           if ($request->{node} && defined($request->{node}->[0])) {
             my @reqnodes = @{$request->{node}};
             if (@reqnodes) {
               $logst .= " to ";
               foreach my $node (@reqnodes) {
                 $logst .= "$node,";
               }
               chop $logst;
             }
           }
       }
        # add each argument
      my $args = $request->{arg};
      my $arglist;
      foreach my $argument (@$args){

             $arglist .= " " . $argument;
      }
      if ($arglist) { $logst .= $arglist; }
      if($peername) { $logst .= " for " . $request->{username}->[0]};
      if ($peerhost) { $logst .= " from " . $peerhost };

      # read site.auditskipcmds and auditnosyslog attributes,
      # if set skip commands else audit all cmds.
      # is auditnosyslog, then only write to auditlog table and not to syslog
      my @skipcmds=($::XCATSITEVALS{auditskipcmds}); 
      my $auditnosyslog=($::XCATSITEVALS{auditnosyslog});
      my $skipsyslog = 0;  # default is to write all commands to auditlog and syslog
      if (defined($auditnosyslog)) { 
        $skipsyslog = $auditnosyslog; # take setting from site table,  1 means no syslog
      }
      # if not "ALL" and not a command from site.auditskipcmds 
      # and not getcredentials and not getcredentials ,
      # put in syslog and  auditlog
      my $skip = 0; 
      my $all = "all";
      if (defined($skipcmds[0])) { # if there are values
        if (grep(/$all/i, @skipcmds)) {  # skip all
           $skip = 1;
        } else {
          if (grep(/$request->{command}->[0]/, @skipcmds)) {  # skip the command 
             $skip = 1;
          }
          # if skip clienttype clienttype:value
          my $client="clienttype:";
          $client .= $request->{clienttype}->[0];
          if (grep(/$client/, @skipcmds)) { #skip the client 
             $skip = 1;
          }
        }
        
      }
      @$deferredmsgargs=(); #should be redundant, but just in case
      if (($request->{command}->[0] ne "getpostscript") && ($request->{command}->[0] ne "getcredentials") && ($skip == 0)) {
      
        # put in audit Table and syslog unless site.noauditsyslog=1
        my $rsp = {};
        
        if ($skipsyslog == 0){  # write to syslog and auditlog
          $rsp->{syslogdata}->[0] = $logst;  # put in syslog data
        }
        if ($peername) {
           $rsp->{userid} ->[0] = $request->{username}->[0];
        }
        if ($peerhost) {
          $rsp->{clientname} -> [0] = $peerhost;
        }
        if (defined $request->{clienttype}) {
          $rsp->{clienttype} -> [0] = $request->{clienttype} -> [0];
        } else {
           if (defined $request->{becomeuser}) {
             $rsp->{clienttype} -> [0] = "webui";
           } else {
             $rsp->{clienttype} -> [0] = "other";
           }
        }
        $rsp->{command} -> [0] = $request->{command}->[0];
        if ($request->{noderange} && defined($request->{noderange}->[0])) { 
            $rsp->{noderange} -> [0] = $request->{noderange}->[0];
        }
        $rsp->{args} -> [0] =$arglist; 
        $rsp->{status} -> [0] = $status;
        if ($skipsyslog == 0){  # write to syslog and auditlog
          @$deferredmsgargs = ("SA",$rsp);
        } else{  # only auditlog
          @$deferredmsgargs = ("A",$rsp);
        }
      } else { # getpostscript or getcredentials, just syslog
         if (($request->{command}->[0] eq "getpostscript")
            || ($request->{command}->[0] eq "getcredentials")) {
            unless ($::XCATSITEVALS{skipvalidatelog}) { @$deferredmsgargs=("S",$logst); }
         } else { #other skipped command syslog unless auditnosyslog
           if ($skipsyslog == 0){  # write to syslog
              @$deferredmsgargs=("S",$logst); 
           }
         }
      }
     } # end getbladecons,etc check
      return $rc;
    } else { #Shouldn't be possible....
       xCAT::MsgUtils->message("S","Impossible line in xcatd reached");
      return 0;
    }
  } # end RULE
  #Reached end of policy table, reject by default.
   xCAT::MsgUtils->message("S","Request matched no policy rule: peername=$peername, peerhost=$peerhost  ".$request->{command}->[0]);
  return 0;
}

my $tokentimeout = 86400;  # one day
# this subroutine search the token table 
# 1. find the existed token entry for the user and reset the expire time
# 1.1. if not find existed token, create a new one and add it to token table
# 2. clean up the expired token
#
# this subroutine is called after the account has been authorized 
sub gettoken {
    my $class=shift;
    my $req = shift;

    my $user = $req->{gettoken}->[0]->{username}->[0];
    my $tokentb = xCAT::Table->new('token');
    unless ($tokentb) {
        return undef;
    }
    my $tokens = $tokentb->getAllEntries;
    my $expiretime = time() + $tokentimeout;
    foreach my $token (@{$tokens}) {
        if ($token->{username} eq $user) {
            #delete old token
            $tokentb->delEntries({'tokenid'=>$token->{tokenid}});
        } else {
            #clean the expired token
            if ($token->{expire} > $expiretime) {
                $tokentb->delEntries({'tokenid'=>$token->{tokenid}});
            }
        }
    }
    
    # create a new token for this request
    my $uuid = xCAT::Utils->genUUID();
    $tokentb->setAttribs({tokenid=>$uuid, username => $user}, {expire => $expiretime});
    $tokentb->close();
   
    return ($uuid, $expiretime);
}

# verify the token has correct entry in token table and expire time is not exceeded.
sub verifytoken {
    my $class=shift;
    my $req = shift;

    my $tokenid = $req->{tokens}->[0]->{tokenid}->[0];
    my $tokentb = xCAT::Table->new('token');
    unless ($tokentb) {
        return undef;
    }
    my $token = $tokentb->getAttribs({'tokenid' => $tokenid}, ('username', 'expire'));
    if (defined ($token) && defined ($token->{'username'}) && defined ($token->{'expire'})) {
        my $expiretime = time() + $tokentimeout;
        if ($token->{'expire'} < time()) {
            $tokentb->delEntries({'tokenid'=>$token->{tokenid}});
            return undef;
        } else {
            return $token->{'username'};
        }
    } else {
        return undef;
    }
}

1;