viernes, 12 de agosto de 2016

Como auditar cambios usando triggers en Harbour

Se nos ha planteado la posibilidad que indicar quién y qué es lo que se cambia en una serie de tablas.
Sabía desde hace tiempo que el driver rdd de sixdrive permite introducir un trigger para controlar el contenido.

Pero nosotros usamos DBFNTX, y no podemos cambiar así como así el sistema de RDD.
Por suerte para nosotros, la gente de Harbour le dio soporte también a los NTX.

Hemos diseñado un sistema de tablas, log y logid, que determina que tablas hacermos logs, y el campo de la PK, para encontrar los cambios de un registro de una tabla determinada.

La estructura de la tabla LOG;


TABLA: Nombre de la tabla que se modificado un valor
ID_VALUE: El valor que usaremos de relación, debería ser una PK
CAMPO: El campo de la dbf se está modificando
NEW_VALUE: Nuevo dato en el dbf
OLD_VALUE: Valor que tenia.
CODUSU, IP, HOSTNAME, FECHA, HORA; Datos de control para saber quien hizo y el cambio y desde donde.

Después , tenemos otra tabla LOGID, que determinará que TABLA vamos a auditar, en este caso, tenemos una especie de DBU integrado que necesitamos control de todas las tablas, porque como veremos , simplemente se activa en la apertura de la tabla si queremos que se disparen los triggers o no,y cual es el campo de la tabla que queremos que se guarde en el ID_VALUE de la tabla LOG.




En el ejemplo que veremos, simplemente he sustituido la tabla LOGID, por un Hash, en la llamada a la función __GetIdLog(), en mi código, simplemente abro la tabla logid, y creo el hash.

También, en el ejemplo, evito introducir los nuevos registros, y los campos que sean iguales, tampoco se guardarán. Esto programarlo como queráis, la imaginación es solamente vuestra meta ;-)

Despues, si estamos en un cliente, por ejemplo , donde la PK es el DNI y usamos el DNI como ID a la hora de guardar el log, simplemente haciendo un SCOPE de la tabla + el DNI del cliente, obtendremos todos los cambios aplicados a ese registro ;-)

Este ejemplo lo podéis ejecutar en el directorio de \harbour\tests, y usa la tabla test.dbf con
hbmk2 trigger.prg hbct.hbc xhb.hbc

 
Lo he modificado simplemente para que veais la salida a un fichero de log, audit.log

2016-08-12 12:38:53 -- trigger start --
2016-08-12 12:38:53 INFO: Ejemplo de auditar cambios a traves de triggers.
2016-08-12 12:38:53 INFO:  TABLE:TEST FIELD:FIRST ID VALUE:Homer               Simpson              OLD VALUE:Homer NEW VALUE:Homer_TEST DATETIME:20160812123853989 HOSTNAME:NEO64
2016-08-12 12:38:54 INFO:  TABLE:TEST2 FIELD:LAST ID VALUE: 99700 OLD VALUE:Dysert NEW VALUE:Dysert_TEST DATETIME:20160812123854031 HOSTNAME:NEO64
2016-08-12 12:38:54 INFO:  TABLE:TEST2 FIELD:NOTES ID VALUE: 99700 OLD VALUE:This is a test for record 500 NEW VALUE:Changes everything DATETIME:20160812123854055 HOSTNAME:NEO64
2016-08-12 12:38:54 -- trigger end -- El resultado del ejemplo es este;




A continación el codigo fuente;

#include "Hblog.ch"
#include "dbinfo.ch"
#include "hbsix.ch"

//REQUEST DBFNTX

Function Main()
 
   setmode( 25,80 )

   rddsetdefault( 'DBFNTX' )   // Forzamos RDD por defecto de HARBOUR

  /* Activa log */
  INIT LOG FILE( NIL, "audit.log", 1000, 999 ) // Tamaño a 100K y maximo 999 ficheros
  LOG "Ejemplo de auditar cambios a traves de triggers."

  /* Activar triggers*/
  rddInfo( RDDI_TRIGGER, "SX_DEFTRIGGER", "DBFNTX" )

  /*Llamar antes de abrir la tabla que queremos controlar*/
  sx_SetTrigger( TRIGGER_PENDING, "_trigger", "DBFNTX"  )
  USE "TEST" NEW SHARED

  /* O podemos hacer de esta manera*/
  USE TEST ALIAS "TEST2" NEW SHARED TRIGGER "_trigger"

  Select TEST
  go top
  if rlock()
     replace FIRST with alltrim( field->FIRST ) + "_TEST"
     unlock
     commit
  endif


  Select TEST2
  go bottom
  if rlock()
     replace LAST with alltrim( field->LAST ) + "_TEST"
     replace NOTES with "Changes everything"
     unlock
  endif


  CLOSE LOG
  CLOSE ALL

Return 0


function _trigger( nEvent, nArea, nFieldPos, xTrigVal )
     Local xIdValue, xValue, cIdValue
  
       DO CASE

        CASE nEvent == EVENT_PREUSE
        CASE nEvent == EVENT_POSTUSE
        CASE nEvent == EVENT_UPDATE
        CASE nEvent == EVENT_APPEND
        CASE nEvent == EVENT_DELETE
        CASE nEvent == EVENT_RECALL
        CASE nEvent == EVENT_PACK
        CASE nEvent == EVENT_ZAP
        CASE nEvent == EVENT_PUT

            if empty( cIdValue := __GetIdLog( ALIAS( nArea ) ) ) // Si no viene expresion, no controlaremos el log
               return .T.
            endif  
            Sx_SetTrigger( TRIGGER_DISABLE )
          
            if FieldType( nFieldPos ) = "C"                     // Solo en caso de cambios de Caracter, igualamos tamaño
               xValue   := (nArea)->( FieldGet( nFieldPos ) )
               xTrigVal := padr( alltrim( xTrigVal ), FieldSize( nFieldPos ) )
            else
               xValue := (nArea)->( FieldGet( nFieldPos ) )
            endif

            if xTrigVal != xValue
               xIdValue := cValtoChar( &( cIdValue ) )
               if !empty( xIdValue ) // Si hay algún valor, se guarda, en registros nuevos, el valor esta vacio, no hay que dejar log
                  LOG " TABLE:" + ALIAS( nArea ) +;
                      " FIELD:" + (nArea)->( FieldName( nFieldPos ) ) +;
                      " ID VALUE:" + xIdValue +;
                      " OLD VALUE:" + alltrim( cValtoChar( (nArea)->( FieldGet( nFieldPos ) ) ) ) +;
                      " NEW VALUE:" + alltrim( cValtoChar( xTrigVal ) )+;
                      " DATETIME:" + hb_ttos( hb_datetime() ) +;
                      " HOSTNAME:" + netname()
               endif   
            endif

            sx_SetTrigger( TRIGGER_ENABLE   )

        CASE nEvent == EVENT_GET

        CASE nEvent == EVENT_PRECLOSE

        CASE nEvent == EVENT_POSTCLOSE

        CASE nEvent == EVENT_PREMEMOPACK

        CASE nEvent == EVENT_POSTMEMOPACK

       ENDCASE

    Return( .T. )

/*
  Devuelve el ID a usar segun la tabla.
  hLogId es un hash que contiene la tabla y el valor de una expresion de esa tabla que usaremos
  para identificar el registro en el log.
  Generalmente, se debe usar un PK, una clave única.
*/
static function __GetIdLog( cTable )
     Local cId
     static hLogId := { "TEST" => "FIRST + LAST", "TEST2" => "SALARY"}

     hb_default( @cTable, "" )
   
     cId :=  HB_HGetDef( hLogId, cTable, "" )

return cId

function cValToChar( u ); return CStr( u )




Android y Git. Disponer del hash automáticamente.

Una de las cosas a las que estoy acostumbrado, es tener siempre en mi código, el hash/tag/versión del control de versiones que estoy usan...