[Rt-devel] RT::AuditLog preview

Todd Chapman todd at chaka.net
Wed May 31 17:02:26 EDT 2006


In case I die hiking the Grand View Trail this weekend. :)

This sets up RT to created more transactions so you have
an audit history for things like changes to queue and
custom fields.

Just drop this in $RTHOME/local/lib/RT/

and in RT_SiteConfig.pm: "use RT::AuditLog;"

The user interface bits aren't really there yet, so it
might not be that interesting for most people.

Not for production use.

-Todd
-------------- next part --------------
use strict;
use warnings;
use RT::Transactions;
use RT::Date;

package RT::AuditLog;
use Storable qw(dclone);

my ( $old_value, @save_args );

my @SkipClasses = ( 'RT::Transaction', );
#my @SkipClasses = ( 'RT::Transaction', 'RT::CachedGroupMember',  );

my %TransactionMap = (
    'RT::CustomFieldValue' => {
                                Create => { ObjectId   => sub { $_[0]->CustomField },
                                            ObjectType => sub { 'RT::CustomField' },
                                                Type   => sub { 'AddValue' },
                                            NewValue   => sub { $_[0]->Name },
                                          },
                                Delete => { ObjectId   => sub { $_[0]->CustomField },
                                            ObjectType => sub { 'RT::CustomField' },
                                                Type   => sub { 'DeleteValue' },
                                            OldValue   => sub { $_[0]->Name },
                                          },
                              },
    'RT::GroupMember'      => {
                                Create => { ObjectId   => sub { $_[0]->GroupId },
                                            ObjectType => sub { 'RT::Group' },
                                                Type   => sub { 'AddMember' },
                                            NewValue   => sub { $_[0]->MemberId },
                                          },
                                Delete => { ObjectId   => sub { $_[0]->GroupId },
                                            ObjectType => sub { 'RT::Group' },
                                                Type   => sub { 'DeleteMember' },
                                            OldValue   => sub { $_[0]->MemberId },
                                          },
                              },
    'RT::ACE'              => {
                                Create => {      ObjectId => sub { $_[0]->ObjectId },
                                               ObjectType => sub { $_[0]->ObjectType },
                                                     Type => sub { 'GrantRight' },
                                                 NewValue => sub { $_[0]->RightName },
                                             NewReference => sub { $_[0]->PrincipalId },
                                            ReferenceType => sub { 'RT::' . $_[0]->PrincipalType },
                                          },
                                Delete => {      ObjectId => sub { $_[0]->ObjectId },
                                               ObjectType => sub { $_[0]->ObjectType },
                                                     Type => sub { 'RevokeRight' },
                                                 OldValue => sub { $_[0]->RightName },
                                             OldReference => sub { $_[0]->PrincipalId },
                                            ReferenceType => sub { 'RT::' . $_[0]->PrincipalType },
                                          },
                              },
    'RT::CachedGroupMember' => {
                                Set    => {      ObjectId => sub { $_[0]->GroupId },
                                               ObjectType => sub { my $p = RT::Principal->new( $_[0]->CurrentUser );
                                                                   $p->Load($_[0]->GroupId);
                                                                   return 'RT::' . $p->PrincipalType 
                                                                 },
                                                     Type => sub { 'Set' },
                                                    Field => sub { $_[1] },
                                                 NewValue => sub { $_[2] },
                                          },
                              },
    'RT::Scrip'            => {
                                Create => {      ObjectId => sub { $_[0]->Queue },
                                               ObjectType => sub { 'RT::Queue' },
                                                     Type => sub { 'CreateScrip' },
                                                 NewValue => sub { $_[0]->Description },
                                             NewReference => sub { $_[0]->id },
                                            ReferenceType => sub { 'RT::Scrip' },
                                          },
                                Delete => {      ObjectId => sub { $_[0]->Queue },
                                               ObjectType => sub { 'RT::Queue' },
                                                     Type => sub { 'DeleteScrip' },
                                                 OldValue => sub { $_[0]->Description },
                                             OldReference => sub { $_[0]->id },
                                            ReferenceType => sub { 'RT::Scrip' },
                                          },
                              },
    'RT::Template'         => {
                                Create => {      ObjectId => sub { $_[0]->Queue },
                                               ObjectType => sub { 'RT::Queue' },
                                                     Type => sub { 'CreateTemplate' },
                                                 NewValue => sub { $_[0]->Name },
                                             NewReference => sub { $_[0]->id },
                                            ReferenceType => sub { 'RT::Template' },
                                          },
                                Delete => {      ObjectId => sub { $_[0]->Queue },
                                               ObjectType => sub { 'RT::Queue' },
                                                     Type => sub { 'DeleteTemplate' },
                                                 OldValue => sub { $_[0]->Name },
                                             OldReference => sub { $_[0]->id },
                                            ReferenceType => sub { 'RT::Template' },
                                          },
                              },
    'RT::ObjectCustomField' => {
                                Create => {      ObjectId => sub { $_[0]->ObjectId },
                                               ObjectType => sub { my $type = $_[0]->CustomFieldObj->LookupType;
                                                                   $type =~ /(RTx?::[^-]+)/;
                                                                   return $1; },
                                                     Type => sub { 'ApplyCF' },
                                                 NewValue => sub { $_[0]->CustomFieldObj->Name },
                                             NewReference => sub { $_[0]->CustomField },
                                            ReferenceType => sub { 'RT::CustomField' },
                                          },
                                Delete => {      ObjectId => sub { $_[0]->ObjectId },
                                               ObjectType => sub { my $type = $_[0]->CustomFieldObj->LookupType;
                                                                   $type =~ /(RTx?::[^-]+)/;
                                                                   return $1; },
                                                     Type => sub { 'RemoveCF' },
                                                 OldValue => sub { $_[0]->CustomFieldObj->Name },
                                             OldReference => sub { $_[0]->CustomField },
                                            ReferenceType => sub { 'RT::CustomField' },
                                          },
                              },
);

sub PreSaveArgs {

    @save_args = @_;
    pop @save_args;

}

sub PreDelete {

    $old_value = dclone $_[0];
    @save_args = @_;
    pop @save_args;

}

sub PreOldValue {

    my $self = shift;
    my $hook = pop;
    my %args = @_;

    $old_value = $self->__Value($args{Field});

}

sub PostSet {

    # RT::Group records it's own transactions by default after calling Record::_Set
    # so we do this to avoid recording duplicate transactions
    return if ref($save_args[0]) eq 'RT::Group';
    return if ref($save_args[0]) eq 'RT::User';
    &_PostSet;

}

sub _PostSet {

    @_ = ( @save_args, $_[-1] );
    my $self = shift;
    my $return = pop;
    my %args = @_;

    my $class = ref($self);
    return if grep { $class eq $_ } @SkipClasses;

    my $val = get_rv($return);
    return unless $val;

    return if DuplicateTransaction( ObjectType => $class,     ObjectId => $self->id,
                                          Type => 'Set',         Field => $args{Field},
                                      OldValue => $old_value, NewValue => $args{Value} );

    # transaction on actual object
    $self->_NewTransaction(
        Type => 'Set',
        Field => $args{Field},
        OldValue => $old_value,
        NewValue => $args{Value},
        Data => 'AuditLog',
    );
    
    my $ref = $TransactionMap{$class}{Set} if exists $TransactionMap{$class};
    $ref or return;

    # transaction of interest. for example, if you apply a custom field to
    # a queue, the CF gets the transaction above, but it is in the context
    # of the quue that we are interested in seeing transactions on
    my $trans = new RT::Transaction( $self->CurrentUser );
    my ( $transaction, $msg ) = $trans->Create(
        ObjectId   => $ref->{ObjectId}->($self, $args{Field}, $args{Value}),
        ObjectType => $ref->{ObjectType}->($self, $args{Field}, $args{Value}),
        Type       => $ref->{Type}->($self, $args{Field}, $args{Value}),
        NewValue   => $ref->{NewValue}->($self, $args{Field}, $args{Value}),
        Data       => 'AuditLog',
        exists $ref->{NewReference} ? ( NewReference => $ref->{NewReference}->($self, $args{Field}, $args{Value}),
                                        ReferenceType => $ref->{ReferenceType}->($self, $args{Field}, $args{Value}) )
                                    : (),
        exists $ref->{Field} ? ( Field => $ref->{Field}->($self, $args{Field}, $args{Value}) ) : (),
        OldValue   => $old_value,
    );
}

sub DuplicateTransaction {

    my %args = @_;
    my $seconds_back = delete $args{SecondsAgo} || 60;
    my $date = RT::Date->new( $RT::SystemUser );
    $date->SetToNow;
    $date->AddSeconds( -$seconds_back );

    # This makes sure some other part of RT hasn't already recorded this
    # transaction. This need to be smarted because if a field is set, say
    # from A to B, then from B to A, then from A to B again, only one
    # A to B transaction will be recorded. We should maybe check the time
    # on the previous transaction, and if it is more than some number of
    # seconds old, go ahead and record it again.
    my $Transactions = RT::Transactions->new( $RT::SystemUser );
    while ( my ( $key, $value ) = each %args ) {
        $Transactions->Limit( FIELD => $key, VALUE => $value );
    }
    $Transactions->Limit( FIELD => 'Created', VALUE => $date->ISO, OPERATOR => '>' );
    return $Transactions->Count;

}

sub PostGroupSet {

    # It's a matter of timing. By calling _PostSet after Group::_Set instead
    # of Record::_Set, _PostSet can check to make sure it doesn't record a
    # duplicate transaction
    &_PostSet;

}

sub PostCreate {

    @_ = ( @save_args, $_[-1] );
    my $self = shift;
    my $return = pop;
    my %args = @_;

    my $class = ref($self);
    return if grep { $class eq $_ } @SkipClasses;

    my $val = get_rv($return);
    return unless $val;

    return if
        DuplicateTransaction( ObjectType => $class, ObjectId => $self->id, Type => 'Create' );

    # transaction on actual object
    $self->_NewTransaction(
        Type => 'Create',
        Data => 'AuditLog',
        $self->can('Name') ? ( NewValue => $self->Name ) : (),
    );

    my $ref = $TransactionMap{$class}{Create} if exists $TransactionMap{$class};
    $ref or return;

    # transaction of interest. for example, if you apply a custom field to
    # a queue, the CF gets the transaction above, but it is in the context
    # of the quue that we are interested in seeing transactions on
    my $trans = new RT::Transaction( $self->CurrentUser );
    my ( $transaction, $msg ) = $trans->Create(
        ObjectId   => $ref->{ObjectId}->($self),
        ObjectType => $ref->{ObjectType}->($self),
        Type       => $ref->{Type}->($self),
        NewValue   => $ref->{NewValue}->($self),
        Data       => 'AuditLog',
        exists $ref->{NewReference} ? ( NewReference => $ref->{NewReference}->($self),
                                        ReferenceType => $ref->{ReferenceType}->($self) )
                                    : (),
    );

    
}

sub PostDelete {

    @_ = ( @save_args, $_[-1] );
    my $self = shift;
    my $return = pop;
    my %args = @_;

    my $class = ref($self);
    return if grep { $class eq $_ } @SkipClasses;

    my $val = get_rv($return);
    return unless $val;

    return if
        DuplicateTransaction( ObjectType => $class, ObjectId => $old_value->id, Type => 'Delete' );

    $old_value->_NewTransaction(
        Type => 'Delete',
        Data => 'AuditLog',
        $self->can('Name') ? ( OldValue => $self->Name ) : (),
    );
    
    my $ref = $TransactionMap{$class}{Delete} if exists $TransactionMap{$class};
    $ref or return;

    my $trans = new RT::Transaction( $self->CurrentUser );
    my ( $transaction, $msg ) = $trans->Create(
        ObjectId   => $ref->{ObjectId}->($self),
        ObjectType => $ref->{ObjectType}->($self),
        Type       => $ref->{Type}->($self),
        OldValue   => $ref->{OldValue}->($self),
        Data       => 'AuditLog',
        exists $ref->{OldReference} ? ( OldReference => $ref->{OldReference}->($self),
                                        ReferenceType => $ref->{ReferenceType}->($self) )
                                    : (),
    );
}

sub get_rv {

    my $return = shift;

    my $val;
    if ( ref($return) eq 'ARRAY' ) {
        $val = $return->[0];
    }
    elsif ( ref($return) eq 'Class::ReturnValue' ) {
        $val = ($return->as_array)[0];
    }
    else {
        $val = $return;
    }

    return $val;
}

package RT::Scrip;
use Hook::LexWrap;

wrap '_Set' => pre  => \&RT::AuditLog::PreSaveArgs;
wrap '_Set' => pre  => \&RT::AuditLog::PreOldValue,
               post => \&RT::AuditLog::PostSet;

package RT::Record;
use Hook::LexWrap;

wrap '_Set' => pre  => \&RT::AuditLog::PreSaveArgs;
wrap '_Set' => pre  => \&RT::AuditLog::PreOldValue,
               post => \&RT::AuditLog::PostSet;

wrap 'Create' => pre  => \&RT::AuditLog::PreSaveArgs,
                 post => \&RT::AuditLog::PostCreate;

wrap 'Delete' => pre  => \&RT::AuditLog::PreDelete,
                 post => \&RT::AuditLog::PostDelete;

package RT::User;
use Hook::LexWrap;

wrap '_Set' => pre  => \&RT::AuditLog::PreSaveArgs;
wrap '_Set' => pre  => \&RT::AuditLog::PreOldValue,
               post => \&RT::AuditLog::PostGroupSet;

package RT::Group;
use Hook::LexWrap;

wrap '_Set' => pre  => \&RT::AuditLog::PreSaveArgs;
wrap '_Set' => pre  => \&RT::AuditLog::PreOldValue,
               post => \&RT::AuditLog::PostGroupSet;

sub Content { "need to create _BriefDescriptions" }

package RT::Transaction;

$_BriefDescriptions{AddMember} = 
sub {

    my $self = shift;
    my $principal = RT::Principal->new( $self->CurrentUser );
    $principal->Load( $self->__Value('NewValue') );
    my $object = $principal->Object;
    my $type = $principal->IsUser ? 'user' : 'group';
    return $self->loc("Member added: [_1] '[_2]'", $type, $object->Name);


};

$_BriefDescriptions{DeleteMember} = 
sub {

    my $self = shift;
    my $principal = RT::Principal->new( $self->CurrentUser );
    $principal->Load( $self->__Value('OldValue') );
    my $object = $principal->Object;
    my $type = $principal->IsUser ? 'user' : 'group';
    return $self->loc("Member deleted: [_1] '[_2]'", $type, $object->Name);


};

$_BriefDescriptions{GrantRight} = 
sub {

    my $self = shift;
    my $principal = RT::Principal->new( $self->CurrentUser );
    $principal->Load( $self->NewReference );
    my $object = $principal->Object;
    if ( ref($object) eq 'RT::Group' and $object->Type eq 'UserEquiv' ) {
        $principal = RT::Principal->new( $self->CurrentUser );
        $principal->Load( $object->Instance );
        $object = $principal->Object;
    }
    if ( $principal->IsUser ) {
        return $self->loc("Right [_1] granted to [_2] '[_3]'", $self->__Value('NewValue'), 'user', $object->Name);
    }
    else {
        return $self->loc("Right [_1] granted to [_2] '[_3]'", $self->__Value('NewValue'), 'group', $object->Name || $object->Type);
    }

};

$_BriefDescriptions{RevokeRight} = 
sub {

    my $self = shift;
    my $principal = RT::Principal->new( $self->CurrentUser );
    $principal->Load( $self->OldReference );
    my $object = $principal->Object;
    if ( ref($object) eq 'RT::Group' and $object->Type eq 'UserEquiv' ) {
        $principal = RT::Principal->new( $self->CurrentUser );
        $principal->Load( $object->Instance );
        $object = $principal->Object;
    }
    if ( $principal->IsUser ) {
        return $self->loc("Right [_1] revoked from [_2] '[_3]'", $self->__Value('NewValue'), 'user', $object->Name);
    }
    else {
        return $self->loc("Right [_1] revoked from [_2] '[_3]'", $self->__Value('NewValue'), 'group', $object->Name || $object->Type);
    }

};

1;


More information about the Rt-devel mailing list