内容


使用 Informix 进行事件驱动的细粒度审计

Comments

IBM® Informix® 允许通过使用回调函数执行触发器来生成一些事件。您可以编写这样一个泛型函数:它能够生成通过 SQL 操作并通过触发器捕获的信息的审计线索。

本文介绍如何在不同的上下文最后使用这些工具来生成审计记录。

触发器

触发器 是当一个操作在一个表上执行时执行的操作。所有版本的 Informix 支持触发器进行进行以下 SQL 操作:INSERTUPDATEDELETE

Informix 发布版 9.2x 添加了对 SELECT 触发器的支持,版本 9.40 添加了对 “Instead of ” 触发器的支持,该触发器允许您使用触发器中的操作替代当前操作。这种高效支持您更新一个不可更新的视图。

触发器语法

我们通过下面的简化语法图表来描述触发器语法。

在这些语法图表中,您可以从大括号(“{” 和 “}”)中通过管道符号(“|”)分隔的列表中选择一个项目。方括号(“[” 和 “]”)包括的表达式表示一些可选项目,是否需要取决于前面的选择。我们将通过一些示例来详细解释。

清单 1. 样例触发器语法
CREATE TRIGGER trigger_name
{INSERT | UPDATE | DELETE | SELECT} ON table_name
[REFERENCING OLD AS old_name] [REFERENCING NEW AS new_name]
[BEFORE [WHEN (condition)] (action)]
[FOR EACH ROW [WHEN(condition)] (action)]
[AFTER [WHEN (condition)] (action)][ENABLED | DISABLED]

CREATE TRIGGER trigger_name INSTEAD OF 
{INSERT ON | UPDATE ON | DELETE ON} view_name
[REFERENCING OLD AS old_name] [REFERENCING NEW AS new_name]
FOR EACH ROW [WHEN(condition)] (action) [ENABLED | DISABLED]

您可以选择 3 种操作类型,它们都可以在触发器中使用:

  • Before:该操作在触发操作执行之前执行。
  • For each row:该操作在每行处理之后执行。
  • After:该操作在触发操作执行之后执行,即使没有行被处理。

注意,每个触发操作都包含一个可选条件,用于评估操作是否将被执行。当您只想对可疑处理(比如超过 10% 的工资变更)生成审计记录时,这个特性可能有用。每种操作类型(before、for each row、after)都可以包含多个条件和操作,包括每个条件多个操作。

清单 2 提供了一个触发器创建示例,该示例来自 Informix SQL 语法手册。

清单 2. 来自 Informix SQL 语法手册的样例触发器创建
CREATE TRIGGER up_trigger
UPDATE OF unit_price ON stock REFERENCING OLD AS pre NEW AS post
FOR EACH ROW WHEN (post.unit_price > pre.unit_price * 2)
(INSERT INTO warn_tab VALUES (pre.stock_num, pre.order_num, pre.unit_price, post.unit_price,
CURRENT) );

这个语句在 warn_tab 表中插入一行,条件是新的 unit_price 超过旧单价的两倍。

操作不必是 SQL 语句。可以是 EXECUTE PROCEDURE 语句或 EXECUTE FUNCTION 语句。

处理行

上面的语法图表和示例显示,我们可以引用正在处理的行的 before 和 after 图像。例如,下面的创建语句将失败:

清单 3. 失败的创建语句
CREATE TRIGGER tab1instrig INSERT ON tab1
REFERENCING NEW AS post
FOR EACH ROW (EXECUTE PROCEDURE do_auditing('INSERT', post))

可以通过在向函数传递参数时定义一行来解决这个问题。假设有一个两列组成的表,上一个语句将变为:

清单 4. 成功的创建语句
CREATE TRIGGER tab1instrig INSERT ON tab1
REFERENCING NEW AS post
FOR EACH ROW (EXECUTE PROCEDURE do_auditing('INSERT', 
         ROW(post.pkid, post.col2)::ROW(pkid integer, col2 varchar(30)) ) )

这个示例展示,尽管不足以创建一行,但我们必须通过设置操作符(::)来包含它的定义。在解决这个问题之前,我们必须先看看 Informix 9.x 中引入的几个可扩展性概念。

对象关系特性

Informix 9.x 引入了几个对象关系特性。这些特性中的几个促进了审计函数的实现。它们是新数据类型 ROW 和用户定义函数(UDF)。

ROW 数据类型可以等同于一个表定义:它定义可以组成一个元组的多个列。ROW 类型可以已命名,也可以未命名。例如,可以将一个行类型定义如下:

清单 5. 定义行类型
CREATE ROW TYPE zipcode_t (
  state		CHAR(2),
  code		CHAR(5)
);

我们可以使用 zipcode_t 名称创建几个那种类型的元素。我们还创建了几个未命名的行类型,如上面的触发器示例所示。未命名的行类型使用下面的表达式创建:

ROW(post.pkid, post.col2)::ROW(pkid integer, col2 varchar(30))

ROW 类型可以用于已创建的类型表,或作为一个表中的一列的数据类型。对于 zipcode_t 类型,它可以用于一个表定义中:

清单 6. 表定义
CREATE TABLE customer (
  FirstName		varchar(30),
. . .
  zip			zipcode_t,
. . .
);

用户定义函数(或流程)可以接受 ROW 类型参数。下面的示例以 XML 格式格式化返回的每一行:

SELECT genxml2('customer', customer) FROM customer;

这个语句传递一行,作为 genxml2() 函数的第二个参数。这个参数与其从中选择的表的名称相同,表示 customer 表中的一行。作为参数传递的是一个未命名的行类型。因此,genxml2() 定义向该行提供名称的第一个参数。这然后用作 XML 表示中的顶级名称。要了解关于从 Informix 生成 XML 的更多信息,请参阅本文末尾的 “参考资料” 部分中列示的文章 “Generating XML from Informix”

ROW 类型是自我描述型。当一个 UDF 接收一个 ROW 参数时,它可以查明已定义的列数、列名、列类型、以及它们的内容。UDF 可以定义为接收一个泛型行。运行时,它能够从该行提取足够的信息来确定处理类型。

生成审计记录

前面介绍的 ROW 类型和 UDF 相关知识表明,可以创建一个可用于您的数据库中的任意表的审计函数。如果我们正在计划编写一个审计表,我们必须确保我们匹配审计表定义,不管正在审计的是哪个表。

一个简单的方法是生成一个 XML 表示的审计记录。然后,我们可以使用下列格式的审计表:

清单 7. 审计表格式
CREATE TABLE auditTable (
  id		SERIAL PRIMARY KEY,
  tabname	VARCHAR(128),
  log		LVARCHAR(30000)
);

我们创建了一个函数 do_auditing(),它最多接收 4 个参数:表名、触发器类型(INSERTUPDATEDELETESELECT)、以及该行的 before 和 after 图像。

触发器自检

Informix 版本 9.40.xC4 在 DataBlade API 中引入了一组函数,用于从 UDF 检索上下文信息。DataBlade API 是用于与 “C” 语言数据库服务器接口的编程接口。这意味着,触发器自检功能仅当在触发器中调用的函数或流程是用 “C” 语言编写时才适用。

从现在起,示例代码将假定使用 stores7 演示数据库。如果我们想对 customer 表中的几个插入创建一个审计,则可以创建一个函数 do_auditing1(),并以如下方式在 CREATE TRIGGER 中使用它:

CREATE TRIGGER custinstrig INSERT ON customer 
FOR EACH ROW (EXECUTE PROCEDURE do_auditing1() )

do_auditing1() 函数检索行信息,以及可能对审计有用的其他任何信息。触发器自检函数包括:

  • mi_integer mi_hdr_status():返回的状态表明函数是否在一个 HDR 环境中执行,是在主 HDR 上执行还是在辅助 HDR 上执行。
  • mi_string *mi_trigger_tabname(mi_integer flags):返回触发表或视图。flags 参数表明表名的格式:它是否包含架构名称、所有者姓名,等等。
  • mi_integer mi_trigger_event():触发器信息(操作,before/after/foreach/instead)。
  • mi_integer mi_trigger_level():触发器的嵌套级别。
  • mi_string *mi_trigger_name():返回触发器的名称。
  • MI_ROW *mi_trigger_get_old_row():行的 before 图像。
  • MI_ROW *mi_trigger_get_new_row():行的 after 图像。

通过这些函数,我们可以查明关于触发器及其上下文的所有信息。现在,我们可以编写 do_auditing1() “C” 流程来提供数据库修改审计。

第一件事是确保我们位于一个触发器中且正在处理每一行:

清单 8. 确保我们位于一个触发器中且正在处理每一行
trigger_operation = mi_trigger_event();
  if (trigger_operation & MI_TRIGGER_NOT_IN_EVENT) {
    /* not in a trigger! generate an exception */
    mi_db_error_raise(NULL, MI_EXCEPTION, 
    	"do_auditing1() can only be called within a trigger!", NULL);
    return;
  }
  /* Make sure this is in a FOR EACH type of trigger */
  if (0 == (trigger_operation & MI_TRIGGER_FOREACH_EVENT) ){
    /* not in a for each trigger! generate an exception */
    mi_db_error_raise(NULL, MI_EXCEPTION, 
    	"do_auditing1() must be in a FOR EACH trigger operation", NULL);
	return;
  }

一旦我们知道我们位于正确的上下文中,我们就可以准备一个基于被处理的操作类型的日志记录。下面的代码节选演示了如何操作:

清单 9. 准备一个日志记录
trigger_operation &= (MI_TRIGGER_INSERT_EVENT | MI_TRIGGER_UPDATE_EVENT | MI_TRIGGER_DELETE_EVENT |
		MI_TRIGGER_SELECT_EVENT);

  /* Call the appropriate function */
  switch (trigger_operation) {
    case MI_TRIGGER_INSERT_EVENT:
	  pdata = doInsertCN();
	  break;
. . .

一旦日志记录已创建,我们必须做的最后一件事就是将其插入到审计表中:

清单 10. 插入到审计表中
. . .
sprintf(psql, "INSERT INTO auditTable VALUES(0, '%s', '%s')",tabname, pdata);
sessionConnection = mi_get_session_connection();
ret = mi_exec(sessionConnection, psql, MI_QUERY_NORMAL);
. . .

要了解 do_auditing1() 实现的全部细节,请参阅本文提供的示例代码 。

获取其他有用信息

do_auditing1() 函数记录表名和正被添加、修改和移除的行。DataBlade API 提供三个函数,允许您进一步识别语句:

  • mi_get_database_info():检索数据库名和用户名等基本信息。
  • mi_get_id():检索语句 ID 或会话 ID。
  • mi_get_transaction_id():获取当前事务 ID。

也可以通过使用 SQL 内置函数 USER(参见 Informix 11.50 SQL Syntax 手册 4-71 页)来检索执行触发器的用户名。

事务边界

do_auditing1() 的实现在当前事务上下文中运行。这意味着如果事务最终回滚,记录将从 auditTable 表中移除。这对于这个实现没有问题。在这种情况下,如果一个审计程序需要知道 auditTable 表的更改,它必须以一定的时间间隔读取该表。根据 它必须以多快的速度对这些记录做出反应,这个时间间隔可能是几秒钟,或者可以以更宽松的间隔处理,比如几分钟或几小时。

如果我们想写入数据库外部的一个文件,或者发送一个消息队列上的审计记录,又该如何做呢?在这种情况下,在我们知道事务已被提交之前,操作不能结束。为此,我们需要能够对事件做出反应。

事件处理

DataBlade API 提供了一些方法来注册等待特定事件的回调函数。这个机制允许我们在事务结束时完成我们的审计操作。要执行触发器,我们遵循下图中展示的一般方法。

图 1. 事件处理
事件处理
事件处理

当一个语句执行时(1),触发器被调用(2)。触发器注册一个回调函数(3),以便将结果写入一个文件。它还创建审计记录并将其存储在内存中(4)。这是一种通过 DataBlade API 可用的特殊内存类型,其中,您向它赋予一个名称,并可以根据名称获取对它的一个引用。

一个事务可以在一行被处理之后结束,也可以在多行被处理之后结束。这种情况发生时(5),Informix 调用回调函数(6)。回调函数可以读取保存在已命名内存中的记录(7)并将每条记录写入一个文件(8)。

这种方法的处理类似与 do_auditing1()。我们称其为 do_auditing2()。它添加了已命名内存段的创建、一个回调函数的注册、以及文件的写入。内存分配如清单 11 所示:

清单 11. 内存分配
sessionId = mi_get_id(sessionConnection, MI_SESSION_ID);
/* Retrieve or create session memory */
sprintf(buffer, "logger%d", sessionId);
if (MI_OK != mi_named_get(buffer, PER_SESSION, &pmem)) {
  /* wasn't there, allocate it */
  if (MI_OK != mi_named_zalloc(sizeof(NamedMemory_t), buffer, PER_SESSION, &pmem)) {
    mi_db_error_raise(NULL, MI_EXCEPTION, "Logger memory allocation error", NULL);
  }
}

我们首先检索会话 ID,以便为我们的已命名内存创建一个惟一名称。如果操作失败,则意味着这是会话首次调用这个触发器函数。然后我们分配内存。注意,mi_named_zalloc() 函数的第三个参数是 PER_SESSION。这意味着内存使用一个 PER_SESSION 期间分配。一旦用户从数据库服务器断开连接,会话将消失。由于已命名内存在一个 PER_SESSION 期间上分配,因此这个已命名内存也被释放。

代码的第二个添加涉及一个回调函数的注册。

清单 12. 注册回调函数
/* Register the callback */
if (pmem->gothandle == 0) {
  cbhandle = mi_register_callback(NULL, MI_EVENT_END_XACT, cbfunc,(void *)pmem, NULL);
  if (cbhandle == NULL)
	mi_db_error_raise(NULL, MI_EXCEPTION, "Callback registration failed", NULL);
     pmem->gothandle = 1;
  }
}

这个代码注册一个名为 cbfunc() 的回调函数。已命名内存的一个指针作为 mi_register_callback() 的参数传递。cbfunc() 然后可以在它的代码中直接使用它。函数定义为:

MI_CALLBACK_STATUS MI_PROC_CALLBACK
  cbfunc(MI_EVENT_TYPE event_type, MI_CONNECTION *conn, void *event_data, void *user_data);

cbfunc() 中做出的关键决定是决定我们是否应该写入审计文件。这通过两个测试完成。一个测试查看事件类型(MI_EVENT_END_XACT),另一个测试查看它是以一个提交(MI_NORMAL_END)还是一个回滚(MI_ABORT_END)结束。清单 13 提供一些代码来演示这一点:

清单 13. 测试我们是否应该写入审计文件
if (event_type == MI_EVENT_END_XACT) {
. . .
change_type = mi_transition_type(event_data);
switch(change_type) {
  case MI_NORMAL_END:
   . . .
  case MI_ABORT_END:
  . . .

注意,即便是回滚情况,回调函数也必须执行一些清理:从已命名内存删除作为事务一部分的所有记录。

DataBlade API 提供一些函数来写入操作系统文件。回调函数创建一个惟一文件名来写入已命名内存中存储的审计记录。

清单 14. 写入操作系统文件
sprintf(buffer, "%s%d_%d.xml", LOGGERFILEPREFIX, pmem->sessionId, pcur->seq);
fd = mi_file_open(buffer, O_WRONLY | O_APPEND | O_CREAT, 0644);
ret = mi_file_write(fd, pcur->xml, strlen(pcur->xml));
mi_file_close(fd);

fastpath 接口

DataBlade API 提供一些称为 fastpath 接口的函数来调用另一个 UDR。这个接口的描述超出了本文的范围。您可以在本文末尾的 “参考资料” 部分提供的文档中了解关于 fastpath 接口的更多信息。

这个接口可用于调用其他函数,比如 MQSeries® DataBlade 中定义的函数。

Java 如何?

本文描述的细粒度审计功能最好使用 “C” 语言实现,以便您可以利用自检特性,该特性允许您实现一个适用任何表的泛型函数。但这并不意味着 Java™ 不能用于处理的一部分。

使用 Java 用户定义函数和流程的优势是可以访问 Java 环境的所有功能,包括套接字连接等通信类、HTTP 协议等。

为了解在我们的审计函数中使用 Java 的一种方法,请看看一个新函数 do_auditing3()。这个函数提供的处理与 do_auditing2() 相同,但略微修改了回调函数。

这个回调函数不使用 DataBlade API 函数来写入一个文件,而是使用 fastpath 接口来调用一个将写入一个文件的 Java 用户定义流程。这个 Java 函数定义如下:

清单 15. Java 函数
CREATE PROCEDURE writeFile(lvarchar, lvarchar)
EXTERNAL NAME
'audit_jar:RecordAudit.writeFile(java.lang.String, java.lang.String)'
LANGUAGE JAVA;

第一个参数表示文件名,第二个参数表示审计记录。回调函数执行使用 fastpath 接口的 Java 流程。它首先发现对这个函数的一个引用,然后使用适当的参数执行它。这使用以下代码演示:

清单 16. 找到并执行适当的参数
fn = mi_routine_get(conn, 0, "writeFile(lvarchar, lvarchar)");
. . .
ret = mi_routine_exec(conn, fn, &ret, buffer, pcur->xml);
. . .

mi_routine_exec() 函数中,参数 bufferpcur->xmlwriteFile() 函数的参数。函数引用 fn 必须在我们使用完它后释放:

mi_routine_end(conn, fn);

示例代码

本文附带实现这里描述的所有函数的示例代码。有 4 个函数实现不同的审计模式:

  • do_auditing1()
  • do_auditing2()
  • do_auditing3()
  • writeFile(lvarchar, lvarchar)

示例代码附带两个 make 文件:用于 Windows® 环境的 WinNT.mak 以及用作一个泛型 UNIX® makefile 的 Unix.mak。安装假定您拥有一个名为 $INFORMIXDIR/extend/auditing 的目录。要了解更多信息,请参阅示例代码包含的 README.txt 文件。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Information Management
ArticleID=57803
ArticleTitle=使用 Informix 进行事件驱动的细粒度审计
publish-date=12202010