LoopIn

From Request Tracker Wiki
Revision as of 20:54, 13 August 2016 by Tharn (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Update:

The code below didn't worked out of the box for me. I did some corrections, the code is available at: https://github.com/zito/rt-action-loopin. I'm have prepared it for RT 4.2.2.

Overview

We had a set of users who, when using our RT, would like to say they were "looping in" folks when they responded to tickets. This was a common practice in their regular mail, where the Cc list would be extended with new addresses and everyone would be sure to use Reply All for the latest message. After explaining repeatedly that RT doesn't work like that, I decided to find a way to make it work like that.

I started with the existing AddWatchersOnCorrespond. That action itsef points out there are security problems with letting people send mail with additional addresses that then are automagically added as watchers, so this action takes some care to limit who is allowed to loop addresses into the ticket. The addresses authorized include:

  • Requestor
  • Cc and AdminCc addresses

Additionally, there is a lookaside file with rules to permit addresses to loop in others within a specific domain. The permitted list can include specific authorized addresses or anyone at a domain.

RT::Action::LoopIn Module

 
package RT::Action::LoopIn;
 
 # SEE: http://wiki.bestpractical.com/view/AddWatchersOnCorrespond
 
 # This scrip will add new watchers based on message headers, but
 # only if the actor is authorized (is a requestor, cc, or admincc).
 
 use strict;
 
 use base qw(RT::Action);
 
 my $scrip = 'Scrip:LoopIn';
 
 # {{{ sub Describe
    sub Describe  {
       my $self = shift;
       return (ref $self . " add new watchers from to/cc list if actor is authorized.");
    }
    # }}}
 
 # {{{ sub Prepare
    sub Prepare  {
       # nothing to prepare
       return 1;
    }
    # }}}
 
 sub Commit {
    my $self = shift;
    my $Transaction = $self->TransactionObj;
    my $ActorAddr = $Transaction->CreatorObj->EmailAddress;
    my $Queue = $self->TicketObj->QueueObj;
    my $Ticket = $self->TicketObj;
    my $Id = $self->TicketObj->id;
    my @Authorized;
    my @Unauthorized;
 
    # assume it is NOT valid to loop in additional addresses
    my $loopin_authorized = 0;
 
    $RT::Logger->debug("$scrip: about to check if creator is authorized");
 
    # load email alias file, if present
    my %loopauth;
    my $loopauthfile = RT->Config->Get('WM_LoopAuth');
    if ($loopauthfile and my $fh = IO::File->new($loopauthfile)) {
       while (<$fh>) {
          chomp;
          s/^\s*//;        # strip leading whitespace
          s/\s*$//;        # strip trailing whitespace
          next if /^$/;    # skip blank lines
          next if /^#/;    # skip comment lines
          if (/^equiv\s+(\S+)\s+(\S+)$/i) {
             $loopauth{equiv}{lc($1)} = $2;
          }
          elsif (/^domain\s+(\S+)\s+(\S+)$/i) {
             $loopauth{domain}{lc($1)}->{lc($2)} = 1;
          }
          else {
             $RT::Logger->error("$scrip: unknown loopauth directive: $_");
          }
       }
    }
 
    # if actor is a requestor, cc or admincc, loopin is authorized
    if (my $Creator = $Transaction->CreatorObj) {
       my $Principal = $Creator->PrincipalId if $Creator->Id;
       $RT::Logger->debug("$scrip: creator principal ID: #$Principal");
       if (($Queue->IsCc($Principal) or
            $Queue->IsAdminCc($Principal) or
            $Ticket->IsCc($Principal) or
            $Ticket->IsAdminCc($Principal) or
            $Ticket->IsRequestor($Principal)
           )) {
          $loopin_authorized = 1;
          $RT::Logger->debug("$scrip: $ActorAddr is authorized to loop in additional watchers");
       }
    }
 
    $RT::Logger->debug("$scrip: about to extract candidate address list");
 
    # extract a list of to and cc addresses associated with this transaction
    for my $h (qw(To Cc)) {
       my $header = $Transaction->Attachments->First->GetHeader($h);
       for my $addrObj (Mail::Address->parse($header)) {
          # extract and normalize email address
          my $addr = lc $RT::Nobody->UserObj->CanonicalizeEmailAddress($addrObj->address);
 
          # ignore the specific addresses for this queue:
          next if lc $Queue->CorrespondAddress eq $addr or lc $Queue->CommentAddress eq $addr;
 
          # ignore any email address that looks like one for ANY of our queues:
          next if RT::EmailParser::IsRTAddress('', $addr);''
 
          # normalize address if equivalence is defined
          if (defined($loopauth{equiv}{$addr})) {
             $RT::Logger->debug("$scrip: normalizing $addr to $loopauth{equiv}{$addr}");
             $addr = $loopauth{equiv}{$addr};
          }
 
          # ignore any email address that is already a watcher
          my $User = RT::User->new($RT::SystemUser);
          $User->LoadByEmail($addr);     # NOT LoadOrCreateByEmail
          my $Principal = $User->PrincipalId if $User->Id;
          next if ($Queue->IsCc($Principal) or
                   $Queue->IsAdminCc($Principal) or
                   $Ticket->IsCc($Principal) or
                   $Ticket->IsAdminCc($Principal) or
                   $Ticket->IsRequestor($Principal)
                  );
 
          # extend additional watchers list if authorized
          if ($loopin_authorized or domainauth($loopauth{domain}, $ActorAddr, $addr)) {
             $RT::Logger->debug("$scrip: Ticket #$Id correspondence contains header - $h: $addr");
             push @Authorized, $addr;
          }
          else {
             push @Unauthorized, $addr;
          }
       }
    }
 
    my $comment = "";
 
    if (@Unauthorized) {
       $comment .= "$ActorAddr made an unauthorized attempt to loop in the following:\n " . join("\n ", @Unauthorized) . "\n";
    }
 
    $RT::Logger->debug("$scrip: about to add candidate addresses as watchers");
 
    # add authorized candidates not already listed as watchers
    if (@Authorized) {
       my @looped;       # list of looped addresses
       my @failed;       # list of failed addresses
       for my $addr (@Authorized) {
          my $User = RT::User->new($RT::SystemUser);
          $User->LoadOrCreateByEmail($addr);
          my $Principal = $User->PrincipalId if $User->Id;
          # add the new watcher and check for errors
          my ($ret, $msg) = $Ticket->AddWatcher(
             Type  => 'Cc',
             Email => $addr,
             PrincipalId => $Principal,
          );
          if ($ret) {
             $RT::Logger->info("$scrip: New watcher added to ticket #$Id: $addr (#$Principal)");
             push(@looped, $addr);
          } else {
             $RT::Logger->error("$scrip: Failed to add new watcher to ticket #$Id: $addr (#$Principal) - $msg");
             push(@failed, $addr);
          }
       }
       if (@looped) {
          $comment .= "$ActorAddr successfully looped in the following:\n  " . join("\n  ", @looped) . "\n";
       }
       if (@failed) {
          $comment .= "$ActorAddr failed to loop in the following:\n  " . join("\n  ", @failed) . "\n";
       }
    }
 
    $Ticket->Comment(Content => $comment) if $comment;
 }
 
 sub domainauth {
    my $domainauth = shift;
    my $actor = shift;
    my $loopin = shift;
    my $loopin_domain;
    my $actor_domain;
 
    $RT::Logger->debug("$scrip: checking domainauth for $actor looping in $loopin");
    ($loopin_domain = $loopin) =~ s/^[^@]+@//;
    ($actor_domain = $actor) =~ s/^[^@]+@//;
    $RT::Logger->debug("$scrip: actor domain: $actor_domain, loopin domain: $loopin_domain");
    return 0 unless defined $domainauth;
    return 0 unless defined $domainauth->{$loopin_domain};
    return 1 if $domainauth->{lc($loopin_domain)}->{lc($actor)};
    return 1 if $domainauth->{lc($loopin_domain)}->{'*@'.lc($actor_domain)};
    return 0;
 }
 
 eval "require RT::Action::LoopIn_Vendor";
 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/LoopIn_Vendor.pm});
 eval "require RT::Action::LoopIn_Local";
 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Action/LoopIn_Local.pm});
 
 return 1;

LoopIn External Configuration

Additional rules can be defined for controlling access to LoopIn. The filename is defined in RT_SiteConfig.pm as follows:

Set($WM_LoopAuth, "/opt/rt3/etc/loopauth.cfg");

Note that the variable name includes a WM_ prefix for "Willing Minds", since I don't want to stomp on anything that might be defined by Best Practical or others later.

A sample copy of this file with directive documentation is included below.

 # loopin address equivalences
 #
 # This defines address normalization rules to avoid adding duplicate 
 # watchers.
 #
 # Format:
 #
 # equiv ORIGADDR NORMADDR
 #
 # If ORIGADDR is being looped in, it will be normalized to NORMADDR
 # before the existing watcher list is checked.
 
 # normalize foo.com users with example.com addresses
 equiv   user1@example.com   user1@foo.com
 equiv   user2@example.com   user2@foo.com
 
 
 # loopin domain permissions
 # 
 # Format:
 #
 # domain DOMAIN FROMADDR
 #
 # If fromaddr includes To or Cc addresses that is in DOMAIN not on the
 # ticket, then those addresses will be looped into the ticket.  The
 # special case '*@domain' format may be used for FROMADDR to indicate
 # that anyone in the domain is authorized.
 
 # anyone at foo.com can loop in foo.com addresses
 domain  foo.com    *@foo.com
 
 # joe@bar.com can loop in baz.com addresses
 domain  baz.com    joe@bar.com