Technical Blog Post
Abstract
QIBM_QDB_OPEN: 打开数据库文件出口程序
Body
如何使用打开数据库文件出口程序来保护你的数据库
本文将为你介绍QIBM_QDB_OPEN - 数据库文件打开退出点,这是在IBM i 5.3版本中新追加的功能。这些退出点可以帮助IBM i程序员和管理员来配置他们的数据库安全并强化IBM i安全策略。通过本文,你可以了解在设计出口程序时如何使用IBM DB2或者IBM i 系统的退出点设置选项、方法和需要考虑的要点。
简介
黑客、入侵、 萨班斯法案和责任法案这些术语在当今的IT技术领域越来越普遍。如果你对你当前的IBM i安全部署不是很有自信的话,即使你很熟悉这些术语,它们依然会给你带来很多的心理压力。安全是当今社会一个非常火的话题,因为对一个企业来说最重要的资源就是存在数据库中的信息,于是实施、部署、维护一个可靠的数据库安全策略便成为了一个企业最重要的工作。你需要经常自省下面的这些问题:你已经尽最大努力去维护数据的安全了吗?你的安全策略还有瑕疵吗?如果有的话有什么办法可以解决掉这些瑕疵呢?IBM i平台提供了很多安全功能以及一些可以利用的方法来帮助你保护数据的安全。本文将为你介绍如何使用打开数据库文件退出点来保护你IBM i数据库安全中可能的薄弱点。
注:本文曾经被发布在2007年的IBM白皮书中。后来又被发布在IBM《开发手册》的IBM i的部分中,由此扩大了他的影响范围并让更多的DB2 i的用户发现并使用它。尽管这篇文章是几年前发布的,但他的细节部分仍然非常精确。
退出点
如果你是一个IBM i程序员或者是管理员,你可能已经意识到甚至曾经使用过退出点来管理IBM i应用环境及数据的安全。退出点是被定义在系统函数或者是程序上的一个特殊的点,它可以用来调用一个或者是多个特殊的出口程序。 通过使用退出点你可以编写程序,使得在系统发生某些特殊事件的时候可以进行特定的处理。
QIBM_QDB_Open
IBM i 从V5R3开始支持QIBM_QDB_OPEN,增加了打开数据库文件退出点功能。退出点功能使得在系统中打开数据库表时调用一段程序成为可能。在退出点被正常设置之后,系统中无论是什么程序试图去打开一个物理文件、逻辑文件、SQL表、SQL视图都会调用其对应的出口程序。这个行为的出现跟最原始的的打开请求无关,而发出打开请求的来源可以包含程序接口(例如记录级别的访问RLA或是SQL)、以及非程序接口(例如IBM i 导航器、IBM Query/400、IBM DB2 Web Query for i、IBM数据文件(DFU)甚至于IBM i命令打开请求文件(OPNQRYF)或是显示物理文件成员(DSPPFM)。当打开数据库文件的动作开始调用出口程序时,出口程序将会被运行在打开数据库文件的相同作业中,并且它可以为打开数据库文件的请求发送一个返回值以阻止打开数据库文件的后续动作。
理解数据库的打开操作
当完全打开请求出现时其上的退出点可以唤起对应的出口程序。完全打开操作可以创建一个打开数据的路径(ODP),ODP可以为程序访问表提供一个直接路径,由此程序可以访问表中的数据做一些输入输出的操作。如果你要使用退出点你就必须了解完全打开的操作会在什么时候发生。这个完全打开事件发生的时机跟很多条件有关,其中就包括你访问的数据库的类型是RLA还是SQL。
纪录级访问
纪录级访问(也叫作native I/O)是高级编程语言(HLL)访问文本数据的通用方法。当今社会很多的程序仍然是使用纪录级访问来读取数据。在纪录级访问中完全打开操作发生在程序真正去调用打开操作或方法的时候,而不会发生在程序企图去访问数据的动作(比方说read 或chain操作)。
当RPG程序使用纪录级访问时,数据库表一般会被定义在程序的文件说明区(F-spec)。完全打开操作发生的时间点与USROPN关键字是否被定义在F-spec有关。如果在F-spec中没有USROPN的关键字定义,隐式的完全打开操作将会发生在程序被第一次调用的时候。然而如果USROPN被指定时,程序必须使用OPEN操作或者%OPEN嵌入程序显式的去调用打开表的操作。不管是哪种场景(隐式或是显式),RPG程序赋予的LR标示器的值决定了其后续程序调用的过程中表打开操作的工作方式。如果LR被打开时,表将会在程序结束前被关闭,所有对此程序的每次后续调用都会激发完全打开的操作。但如果LR是关闭状态,所有的程序将会维持在打开状态以供后续程序使用,这样可以降低因为完全打开操作所带来的压力,在这种情况下,ODP将会被一直维护,而这种程序被称为可再入模式。
另外一个记录级访问的打开方式被称为:共享打开。这个技术可以帮助我们将一个文件的ODP共享给同一个任务或者整段操作中的多个程序,其中一个程序调用文件完全打开操作,这个任务中的其他程序就可以使用这个文件的ODP来访问此文件,这样可以节省系统资源,比方说处理器、内存和磁盘。共享打开功能可以通过在打开文件前重载(OVRDBF)或是创建文件命令的方式指定SHARE(*YES)参数来控制。共享打开功能通常被使用在需要调用OPNQRYF命令的应用中。
SQL访问
HLL程序或远程接口(如ODBC、JDBC和ADO.NET)可以使用SQL去访问数据表。显然,完全打开操作发生在游标执行SQL OPEN语句时,SELECT INTO、INSERT、 DELETE以及UPDATE语句开始时将会隐式的去打开数据表。对于所有的SQL语句,这些打开的操作都是由数据库管理器来控制的,它会通过内部的调用为应用程序打开所需的数据表。在执行这些打开游标或者SQL语句时,数据库管理器将会自动执行完全打开操作。数据表将会在游标关闭或是SQL语句执行结束后被自动关闭,如果相同的语句在同一个任务中被第二次调用时,这个完全打开的操作将会被再次调用,但在这次调用结束后,数据库管理器将不会再次关闭此文件而是将它留给此任务中的后续的程序或是链接使用。所有在第三次相同的SQL语句被调用时,数据库管理器可以直接使用已经被打开的数据表,而不用再次调用完全打开操作。这个功能被称为:模拟打开。从这点来看,SQL语句通常处于ODP重利用模式。模拟打开功能跟native I/O操作的共享打开功能很类似。
调用出口程序
理解各种打开操作是非常重要的,因为出口程序只有在完全打开操作的过程中才可能被调用。一个模拟打开的动作,或者是共享打开,或者是数据表被程序使用再打开模式进行访问都不会调用出口程序。那些被定义为多表链接的SQL视图,逻辑文件,或者是SQL请求/400请求中出口程序仅会被调用一次,但是他的输入参数列表中包含所有将被打开的视图或者逻辑文件,以及这些视图和逻辑文件所包涵的其它所有的表。在出口程序中将会对这所有的对象单独进行处理。
另外,一些表(IBM提供)或者是临时表将不会受退出点的控制,因此即使这是一个完全打开的请求,如果这些存在于QTEMP,QSYS,QSYS2或者是其它一些系统库中的文件被打开出口程序也不会被激活。请参考IBM I 信息中心的QIBM_QDB_OPEN退出点一节查看这些系统库列表
使用案例
在实际使用中大多数的退出点在IBM i的环境中经常会被用来增强系统安全、数据库安全。他可以通过下面的方式来提高安全性:
。辅助执行安全策略
。在一些安全漏洞可能存在的地方完成某些等级的检查工作
。执行数据库安全策略
在过去,加密数据是一件非常简单的事情,那时可以根据用户属性的设置内容,如程序的初始数据或任务的描述,将应用用户限制在5250绿屏终端应用以内。例如可以将用户属性中的权限限制参数设置为*YES以防用户使用命令行。应用程序可以设置权限以使得特殊用户可以访问特殊数据表。应用级别的安全策略完全可以满足数据的安全要求,同时因为没有关于数据的接口,对象级别的安全也不需要被设置。而在现今的环境中,应用级别的安全策略不再能满足需求,你必须负责阻止那些未经授权的用户或进程在通过主程序之外的途径访问远程数据库对象并读取敏感数据。访问扩展应用或接口例如IBMiNavigator以及其他基于ODBC或JDBC的工具,允许用户获得访问系统的权限,这些访问将不再受主应用的控制和影响,也不再受主应用中的安全策略所控制。
如果你使用应用级别安全策略来管理你的环境,强烈推荐你至少要为你的环境部署对象级别的安全策略,在很长一段时间,这是一种最好的并且高效的保证数据安全的方法。想要了解更多关于对象级别安全策略的信息您可以访问IBM i信息中心或IBM i安全手册。
补充应用级的安全
如果你觉得实施对象级的安全策略对于一个企业过于庞大,或投入产出比确实不合适,你就可以考虑使用退出点来帮助你解决一些安全策略中的漏洞。对于你的5250应用,你仍然可以靠应用级安全策略以及设置用户域来阻止那些未被认证的请求访问原始的5250基础应用接口,除此以外你可以使用退出点功能来阻止那些未被安全策略和用户域限制住的请求使用上面列出的远程接口来访问数据表。
提供安全控制粒度
解决方案供应商可以使用这个退出点(或是其他系统的退出点)来支持他们的用户来定制化、管理、控制本地以及远程的数据库对象访问的安全策略。例如:出口程序可以使用安全表来管理那些敏感数据表或是认证用户,以及这些用户本地或远程访问权限(表一)
表一: 出口程序安全表
| Database table | User profile | Local access | Remote access |
| ORDERS | COBBG | N | Y |
| ORDERS | KMILL | Y | N |
| ORDERS | JAREK | Y | Y |
当接收到一个对表ORDERS的打开数据库请求时,出口程序可以判断这个请求的类型(本地、远程),再去查看安全表来得出发出此请求的用户可以拥有哪种访问权限,以此来同意或拒绝这个数据库打开的请求。
使用表一中的数据,用户可以通过出口程序完成下面的工作:
1) 防止用户COBBG通过基于5250应用接口打开ORDERS表,但是允许这个用户通过ODBC接口访问ORDERS表
2)允许用户KMILL通过基于5250的应用打开ORDERS表,但是可以组织此用户通过远程接口访问数据。
3) 允许用户JAREK通过任何方式(本地、远程)打开ORDERS表
你甚至可以更进一步: 仅在一周中的某些天或一天中的某个时刻允许远程访问。因为你可以编写出口程序,可以控制特定的用户在特定情况下访问表的权限。正如你所见到的,退出点功能可以提供安全粒度等级是对象级安全测试所不可能提供的。
审计
从审计的角度来看,退出点可以帮助你捕获数据库安全相关的信息,例如:
1) 那些用户其他访问敏感数据表
2) 一个SQL语句是否真正打开了数据表
3) 打开表操作的类型(读、插入、更新或是删除)
4) 其他打开数据表操作所使用的程序或接口
比较出口程序和对象审计
如果你熟悉IBM i 上的安全特点,你应该知道“审计日志”能够使系统抓取并记录各种安全相关的事件。这些事件将会被记录在叫“日志接收器”的特殊系统实体中。一个“审计日志”的功能是记录对某个对象的访问,比如敏感的表,这就被称作“对象审计”。
例如,在FLIGHT400C中为AGENTS表启用对象审计,在IBM iOS命令行中执行下面的命令:
CHGSYSVAL SYSVAL(QAUDCTL) VALUE(*OBJAUD)
CHGOBJAUD OBJ(FLGHT400C/AGENTS) OBJTYPE(*FILE) BJAUD(*ALL)
任何访问表的尝试都被记录进审计日志,审计信息能够通过IBM i OS 显示日志(DSPJRN)或显示审计日志(DSPAUDJRNE)命令显示或者提取出来。
从审计的角度来说,对象审计和出口程序的功能看起来是相似的,但其实存在一些关键的不同:
1)管理: 在你设置了对象审计以后,系统将会接管并负责抓取并记录每一个对此对象的访问。通过出口程序,IBM i 程序员会负责写程序去抽取和记录详细的访问信息,并且创建日志表和其他支持的对象。
2)对象级别授权:如果对象级别的授权阻止了用户访问表,出口程序是不会被触发的,因为对象访问被限制了,表本身并没有打开,出口程序并没有被调用。在某种程度上的负面影响就是出口程序不能记录这个非授权的访问尝试。然而,对象审计能够监测的这样的访问请求,尽管他们是没有成功的访问
3)采取行动的能力:对象审计只能记录对象的访问,它不能阻止访问的尝试或者立即触发工作流去向某人发布尝试访问的警告。只要对象级别的授权没有阻止出口程序被触发,出口程序允许程序员去编程阻止访问或者采取必要的工作流任务。
为了获得更多的关于IBM i 审计日志和对象审计,请参考IBM i 信息中心的IBM i Security reference手册
比较退出点和数据库触发器
也许你需要熟悉数据库触发器的概念,它是数据库用户写的关联到数据库表的程序或者SQL例程。当读一行数据或者表上数据发生改变,而不管这个改变操作来自于哪里,数据库触发器都将被数据库管理器自动触发 。数据库触发器的主要目的是监控数据库变化以及执行跟这些变化相关的任务。当需要评估是要用退出点还是触发器来完成安全方面的需求时,下面是几个需要考虑的点:
1)管理:退出点是一个系统级的设置,你只能在一个地方设置它以及写出口程序。依赖于你想要用触发器的表的数量,数据库触发器需要更多的介入,这是 因为你必须为每一个需要审计的表加触发器。另外,为了保证每种数据库访问都被检测到,你需要给每个表加四个触发器,对应于READ, UPDATE, INSERT 和 DELETE(尽管同一个程序)。为了完成审计任务,两种方法都需要写程序。
2)对象级别授权:同出口程序一样,如果对象级别的授权阻止了用户对表的访问,数据库触发器是不会触发的
3)性能:因为每一个被处理的行都会触发调用触发器,这从性能来考虑是很昂贵的,尤其是需要处理很多行时。另外读触发器是非常影响性能的,用的时候必须很小心。在某些用读触发器的用例中,比如需要某些字段的组合,就需要创建一个表的临时备份。而如上面提到的,出口程序只会在打开请求时被触发,同触发器进行比较时,通常会有比较少的程序调用和处理。
4)提供的信息:触发器的优势是可以访问到被审计的实际数据。在一个改变操作中,一行数据的改之前和之后都可以在触发器内获得。然而,出口程序是在打开请求时触发的,不能获得被访问的行数据信息。
5)采取行动的能力:同退出点一样,数据库触发器能够阻止对数据库的访问尝试以及采取一些其他工作流任务(如果对象级别的授权没有阻止用户的访问)。去阻止访问,触发器可以简单的抛出一个错误信息去成功的完全阻止这个操作。
当决定要用那个方法时,更多的是依赖于审计的需求。一些审计规则(比如HIPAA)规定应用必须记录每个访问敏感数据的访问已经要访问的数据行,这样就不得不用触发器来实现这个审计的需求了。
请参考IBM红皮书Stored Procedures, Triggers and User Defined Functions on DB2 Universal Database for iSeries.获得更多数据库触发器相关的信息。
下面的表提供了各种统计方法之间的比较。
Table 2: 统计方法对比
| Method | Administration | Programming required | Disabled by object-level authority | Performance | Row content provided | Ability to take action |
| Exit point | System-wide setting | Yes | Yes | Only called on full opens | No | Yes |
| Object-level auditing | System-wide setting | No | No | Logs the event | No | No |
| Database triggers | Per table setting | Yes | Yes | Called for every row | Yes | Yes |
编写出口程序
正如前面所提到的,注册了退出点以后,出口程序成为了一个系统范围的设置,他将在系统中的每一次表的完全打开时被触发。需要重点理解作业在继续打开请求之前要等待出口程序先完成, 依赖于出口程序是如何写的以及做了什么, 他对整个应用的性能潜在的会有负面的影响, 一个粗心实现的出口程序可能对应用的性能产生极其严重的影响。
如果你决定退出点,你需要在设计阶段仔细考虑以下几个方面:
1)确保你的程序能够处理多个表被打开的情况(有的情况是多表联合被定义在一个逻辑文件中,视图或者查询中,并且多表或者视图在一个SQL语句中)。比如,如果一个多表联合被打开,出口程序只被调用一次,但是视图以及视图里面对应的表会以列表形式传进来,为了保证完备的处理,你的程序需要能够处理每一个视图和每一个表。
2)如果你的出口程序自己也要打开数据库表,要确保你检查并处理了潜在的递归调用,否则,你的程序将可能处在无限递归循环中。另外,如果你的出口程序用记录级访问一个数据库表,你必须在F-spec中指定USROPN关键字。如果确定了导致出口程序被调用的表不是在出口程序中打开的表,你可以在程序里继续打开了。再次强调,如果没有考虑这些情况就可能导致一个递归循环。如何处理递归循环的问题稍后会在这个文章详细讨论。
3)如果出口程序改变返回值为0,数据库打开就是失败;如果程序没有改变返回值或者改变为1,那么打开请求被接受,可以继续其他操作。
4)出口程序必须是线程安全的。
一个简单的RPG例子
考虑了这些情况,这里是一个用RPG写的简单的出口程序例子,这个程序会被传入一个包含头信息的数据结构,比如当前用户,打开的表数,打开的类型(输入,输出,更新,删除)和一个偏移值(offset value)。偏移值包含了列表结构开始地址,列表包含了每个被打开的表的信息。程序执行下面的任务:
1)获取进行数据库打开请求的当前用户
2)用头结构中的偏移值计算表列表的开始地址
3)从表列表,找到所有被打开的表
4)如果被打开的表是“FLIGHTS”并且当前请求的用户是COBBG,返回值被设置成0并出口程序。设置返回值为0会导致数据库阻止打开请求。
5)如果没有打开的表是FLIGHTS,程序不改变任何返回值,直接退出,这样就允许数据库请求可以继续。
例1: 一个简单的RPG程序
h dftactgrp(*no) actgrp(*caller)
*
d DBOP0100 ds qualified
d HeaderSize 10i 0
d FormatName 8
d ArrayOffset 10i 0
d FileCount 10i 0
d ElementLength 10i 0
d JobName 10
d UserName 10
d JobNumber 6
d CurrentUser 10
d QueryOpen 1
d DBOPFile ds qualified based(DBOPFilePtr)
d FileName 10
d Library 10
d Member 10
d 2
d FileType 10i 0
d UnderlyingPF 10i 0
d InputOption 1
d OutputOption 1
d UpdateOption 1
d DeleteOption 1
d returnCode s 10i 0
d i s 10i 0
d userNam s like(dbop0100.UserName)
d fileNam s like(dbopFile.fileName)
d curUser s like(dbop0100.currentUser)
c *entry plist
c parm DBOP0100
c parm ReturnCode
/free
// Extract current user from passed in structure
curUser = dbop0100.CurrentUser;
// Process each table being opened by the request
for i = 1 to DBOP0100.FileCount;
DBOPFilePtr = %addr(DBOP0100) + DBOP0100.ArrayOffset +
(i - 1) * DBOP0100.ElementLength;
FileNam = dbopFile.FileName;
// Don't allow user COBBG to open table FLIGHTS
if curUser = 'COBBG'
and fileNam = 'FLIGHTS';
returnCode = 0;
return;
endif;
endfor;
return;
/end-free
递归相关考虑
当只组织某些用户访问制定的表时,RPG例子(如例1)可以很好的工作,这里需要注意的是“退出点”程序本身没有任何数据库操作, 用户配置和需要检查的数据库表都是硬编码在程序中的。如果需要更动态的配置会如何呢?如果出口程序自己也要从数据库表读取或插入数据(比如写入一个审计表)会如何呢?改变程序去实现这些需求需要打开一个数据库表的操作(结果就是出口程序又触发了新的出口程序调用):这样一个出口程序的调用已经存在于程序栈中,一个递归的程序调用发生了,这就有问题了,因为RPG不支持递归程序调用。
你也许想通过参数“ACTGRP(*NEW)来编译RPG出口程序, 这将使每个调用都需要放在一个新的激活组中,这在一些情况下是可以工作的,但这基于性能因素是不被提倡的(创建一个新的激活组是一个非常昂贵的,而每次打开数据库都需要)。 另外, 如果你指定了“ACTGRP(*NEW)”,当从一个外部的用户定义方法(UDF)的SQL中的打开表操作触发时,出口程序会失败。
为了避免RPG的递归问题,下面是几个可选择的方案:
1)硬编码:正如前面例子,你可以把一些不常变化的数据硬编码到程序里,并且不用审计日志。也就是说,出口程序完全避免的访问数据库表的操作。不好的地方就是任何数据的变化都需要更新源代码,重新编译并重新通过变更管理系统更新程序
2)用非数据库的存储:为了避免用数据库表去存储动态数据(比如需要阻止的用户以及需要安全保护的表名),可以用其他替代方式去存储,比如数据区或者用户空间。审计日志可以通过其他机制,比如发送消息给一个消息队列或者数据队列。
3)用不同的编程语言:如果你的出口程序需要数据库的I/O, 建议用一个支持递归的编程语言(比如C或者控制语言CL)。整个程序可以用C语言来写或者写一个CL程序去做一个受约束的过程调用(CALLPRC)调用RPG过程去做大部分工作(尽管你不能递归调用RPG过程),比如一个C或者CL语言程序处理递归部分。(请参考附录)
添加出口程序到退出点
出口程序创建以后,就可以使系统在打开database时调用它, 这就需要按照下面步骤把出口程序添加到QIBM_QDB_OPEN退出点:
1)登录一个IBM I 5250会话。
2)在命令行,输入作业及注册信息(WRKREGINF)命令,并回车。
3)作业及注册信息界面就会出来,显示系统退出点的列表,通过这个列表去显示关于一个退出点的信息以及这个退出点关联的的出口程序。
4)在QIBM_QDB_OPEN退出点之后输入8,并回车。
在作业及注册信息界面,输入选项1,退出“EXITPGM”程序和QGPL库(如表1所示),并回车。
图1:在Work with Exit Programs screen中使用Add选项
Add Exit Program screen页如图2所示。
图2: Add Exit Program 页
输入回车键去添加出口程序,出口程序就如图3所示加到退出点了
图3: 出口程序已被添加
作为另一个可选择的方法,一个快速的添加出口程序到退出点是执行下面的IBM i OS 添加出口程序(ADDEXITPGM)命令:
ADDEXITPGM EXITPNT(QIBM_QDB_OPEN) FORMAT(DBOP0100) PGMNBR(*LOW) PGM(QGPL/DBOEXIT1)
注意:The PGM参数必须是是一个合法的库名字或者*CURLIB(作业的当前库)
验证出口程序
把出口程序加载到QIBM_QDB_OPEN退出点以后,你可以用以下方式验证他的有效性:
1)如果你的出口程序记录日志,检查日志表
2)检查你的作业(job)日志。 如果你的出口程序因为某种原因失败了(比如没找到程序,程序没有被授权,或者有一个功能区检查程序), 相应的信息就会留在作业日志里,但出口程序本身会继续处理
3)调试你的作业。也许最好的方式去验证程序是否正确就是启动调试会话,设置断点,打开一个表(去触发出口程序),然后进行单步调试
出口程序的安全
在出口程序被编写完成,加载并验证了以后,显然你要考虑安全性。否则可能出现对退出点的规避情况。一个恶意用户能够更改或者删除,或者更改出口程序使用的安全表(或者其他数据库对象)。这样的行为就潜在的允许用户获得对敏感表的访问(其实应该被访问点程序阻止的)。
即使你没有对其他应用对象实现对象级别的安全,但强烈建议要对出口程序和它所访问的所有对象进行设置。下面是一个基本的面向出口程序以及相关的实体的对象级别安全的建议:
1)用USRPRF(*OWNER)参数编译出口程序。当用这种方法编译后,运行时就会用拥有这个程序的用户的授权
2)把出口程序的拥有者改成一个只对出口程序相关的对象有授权的用户。程序运行时将只访问这个用户的授权了的对象。
3)授予出口程序公共的“*USE”授权。这样只给用户调用程序的授权()但不能修改或者删除它)
4)给予拥有出口程序的用户充分的授权去访问出口程序用到的数据库对象以及其他非数据库对象(比如数据队列和消息队列)。这保证了程序有合适的授权在这些对象中去读写该删数据
5)撤销对出口程序用到的数据库对象和非数据库对象的所有 *PUBLIC授权,这样就阻止其他用户通过任何非出口程序的接口访问这些对象。
6)在动态调用中用合法的库名字。如果出口程序做任何对其他程序的动态调用,保证库名字在调用语句中是合法的。这样阻止了一个插在库列表中前面的同名程序(但不是所要调用的)的调用。
其他一些考虑
这里是其他一些关于退出点你需要考虑的内容
1)如果出口程序返回了0,数据库打开请求失败,下面内容将会出现的作业日志中:
1.CPF428E The open failed because exit program DBOEXIT in library
2.DBOEXIT1 associated with exit point QIBM_QDB_OPEN returned a reason
3.code that ended the request.
另外,如果用了SQL访问, SQL错误信息SQL0953也将出现在日志中
1.SQL State: 57014
2.Vendor Code: -952
3.Message: [SQL0952] Processing of the SQL statement ended. Reason code 11.
4.Cause . . . . . : The SQL operation was ended before normal completion.
5.The reason code is 11.
2)如果查询引擎在处理过程中创建了临时表,出口程序是不会因为这些临时表而被调用的。这对所有的访问接口都是一样的(SQL, OPNQRYF, Query/400).
3)如果你跟踪了SQL查询引擎最近的功能上的改进,你应该会熟悉物化查询表(MQT)。你也会想到查询引擎可以自由的去重写SQL查询语句,并能用一个存在的MQT来代替请求的表。这样的打开请求是对MQT的(而不是MQT所基于的下面的实际请求的表)
4)当注册或者取消注册出口程序,要小心时间点。出口程序应该不会被那些在出口程序加入前已经启动的作业所调用。相反的,如果你移除出口程序,那些已经在执行的作业应该会继续调用出口程序。
5)出口程序必须被定义在辅助存储池(ASP)中
总结
有经验的IBM i 程序员使用退出点来管理他们的应用、系统和安全策略已经有许多年了。当系统中发生某些特殊事件时,退出点提供了一个有效的方法去调用定制的程序。 退出点QIBM_QDB_OPEN在IBM iOS V5R3中已经被介绍了,去帮助IBM i 程序员和管理员去处理各种安全问题。这篇文章应该会帮助你理解退出点做什么以及如何被用作管理数据库安全以及增强IBM i 的安全策略。
附录
下面的C和CL程序代码例子说明了如何去处理递归。
处理递归的例子:C程序
C程序是可以进行递归的调用的。下面的例程比较了将要打开的表的名字和审计日志表DBLOG(去避免无限的递归循环):如果他们是不同的,程序将会在审计表中记下打开请求的细节。程序只记下传进来的第一个表的信息,并没有进行任何处理来验证这个用户是否被授权打开这个表
去创建所描述的出口程序,需要进行如下步骤:
1)创建日志表用以记录审计信息。日志表包含出口程序处理的每个数据库打开的细节
2)在IBM i Navigator Run SQL 脚本窗口,执行下面的语句去创建表。
CREATE TABLE QGPL.DBOLOG (QRYOPN CHAR(1), USER CHAR(10), LIBRARY CHAR(10), FILE CHAR(10), JOBNAME CHAR(10), CURNAME CHAR(10), CURDATE CHAR(8), CURTIME CHAR(8));
3)通过执行下面的语句来创建C模块。
CRTSQLCI OBJ(QTEMP/DBOEXIT) SRCFILE(QGPL/QCSRC) SRCMBR(DBOEXIT1) COMMIT(*NONE) DBGVIEW(*SOURCE)
4)执行以下语句去创建C程序。
CRTPGM PGM(QGPL/DBOEXIT1) MODULE(QTEMP/DBOEXIT1) ACTGRP(*CALLER)
例2: 一个简单的C程序
#include <stdio.h>
#include <string.h>
exec sql include sqlca;
main(int argc, char **argv)
{
int i;
int *returnCode = (int *) argv[2];
struct header
{
int len;
char eyecatcher[8];
int fileOff;
int numFiles;
int fileSpecLen;
char jName[10];
char userName[10];
char jobNumber[6];
char currentName[10];
char isQueryOpen;
char reserved[3];
} *inHdr;
struct fileDef
{
char name[10];
char lib[10];
char mbr[10];
char reserved[2];
int fileType;
int lowerFileType;
char inpOpt;
char outOpt;
char updOpt;
char dltOpt;
char reserved2[4];
} *fileInfo;
char user[11];
char qryopn;
char inputOpt;
char outputOpt;
char updateOpt;
char deleteOpt;
char filename[10];
char library[10];
char jobName[10];
char curName[10];
inHdr = (struct header *) argv[1];
fileInfo = (struct fileDef *) (argv[1] + inHdr->fileOff);
/* If the open is for the SQL table, return to avoid recursion */
/* Also, don't collect information on QSYS and QTCP users */
if (!strncmp(fileInfo->name,"DBOLOG",6) ||
!strncmp(inHdr->userName,"QSYS",4) ||
!strncmp(inHdr->userName,"QTCP",4))
return;
/* Get the user and the type of access */
strncpy(user,inHdr->userName,10);
qryopn = inHdr->isQueryOpen;
/* Get the file and library */
strncpy(filename,fileInfo->name,10);
strncpy(library,fileInfo->lib,10);
inputOpt = fileInfo->inpOpt;
outputOpt = fileInfo->outOpt;
updateOpt = fileInfo->updOpt;
deleteOpt = fileInfo->dltOpt;
/* Get the job and the current name */
strncpy(jobName,inHdr->jName,10);
strncpy(curName,inHdr->currentName,10);
/* Insert the info into the SQL table */
exec sql INSERT INTO qgpl/dbolog VALUES(:qryopn,:user,:library,:filename,
:jobName, :curName, :inputOpt, :outputOpt, :updateOpt, :deleteOpt,
CURRENT DATE, CURRENT TIME);
}
处理递归的例子:CL程序并用RPG过程
如果C语言不是你的选择,你还可以用RPG去完成大多数处理,因为CL程序能够被递归调用,一个出口程序能用CL写并调用RGP过程。这之所以能工作就是因为它能递归的调用RPG程序。下面的例子中,一个数据区,一个表,两个模块和一个程序被创建。这个出口程序执行两个主要功能: 它阻止任何非应用接口的打开应用表请求, 并且详细记录对一个表的请求。
通过执行下面步骤去创建如上所描述的出口程序:
1)创建一个数据区去存放数据打开退出点开关(data-open exit-point switch).如果开关是打开的(1),处理过程继续;如果是关闭的(0),将没有进一步的处理(也就是允许数据库打开)。这个增加的新特性即使不是一个必要的功能,但是他可以让你方便的启用或者停用出口程序。
CRTDTAARA DTAARA(QGPL/DBOEXITSW) TYPE(*CHAR) LEN(1) TEXT('DB Open exit point switch (1=On,0=Off)')
2)创建数据表来存储需要保护的数据库的程序库。出口程序在这些程序库中为所有远程访问检查所有数据表。当出口程序被调用时,他获取正在打开表的程序库的名字,并在表中查询是否有这个程序库名字。如果没有,被打开的表不需要加密面向外部访问而是立刻停止此外部调用。如果有,那么这个表将会被加密,出口程序需要继续后续的处理。在IBM i Navigator Run SQL脚本窗口,执行下面命令创建一个表:
CREATE TABLE QGPL.SECDBLIB (DB_LIB CHAR(10) CCSID 37 DEFAULT NULL);
3)创建一个存储用户列表的数据库表,这些用户是被授权远程访问存在于SECDBLBFL表中的数据库的程序库。出口程序查询这个表去验证打开表的程序库和进行打开请求的用户:如果这个表中存在这样的记录并且远程访问是打开的,这个用户就是被授权的通过远程接口访问数据库表。在IBM i Navigator Run SQL脚本窗口,执行下面语句去创建表:
CREATE TABLE QGPL.AUTUSRFIL(USER_NAME CHAR(10), DB_LIB CHAR(10), LCL_ACS_FL CHAR(1),
RMT_ACS_FL CHAR(1));
4)创建一个用于审计的表。日志表存储出口程序处理的每一个数据库打开。在IBM i Navigator Run SQL脚本窗口,执行下面命令去创建一个表:
CREATE TABLE QGPL.DBOLOGFIL (QRYOPN CHAR(1), ACCESSED INTEGER, USER CHAR(10),
LIBRARY CHAR(10), FILE CHAR(10), JOBNAME CHAR(10), CURNAME CHAR(10),
CURDATE CHAR(8), CURTIME CHAR(8), PGMSTK CHAR(1000));
5)创建CL DBOEXIT1模块。这个模块首先读取数据区(data area)去检查数据打开退出点开关(database-open exit-point switch)。如果开关是打开的,将会调用RPG CHKOPEN过程。这个模块也是保护程序入口过程的模块。因为这是一个CL程序,它能够被递归调用。在IBM i OS 命令行,执行下面命令去创建这个模块。
CRTCLMOD MODULE(QTEMP/DBOEXIT1) SRCFILE(DBOEXITPT/QCLSRC) DBGVIEW(*ALL)
例3: DBOEXIT1 源代码(一个简单的CL程序)
PGM PARM(&DBOP0100 &RETURNCODE)
DCL VAR(&DBOP0100) TYPE(*CHAR) LEN(2000)
DCL VAR(&RETURNCODE) TYPE(*INT)
DCL VAR(&LOGFLAG) TYPE(*INT)
DCL VAR(&MSG) TYPE(*CHAR) LEN(500)
DCL VAR(&DBOEXITSW) TYPE(*CHAR) LEN(1)
/* If exit point switch is on, issue bound call to procedure */
RTVDTAARA DTAARA(QGPL/DBOEXITSW) RTNVAR(&DBOEXITSW)
IF COND(&DBOEXITSW = '1') THEN(DO)
CALLPRC PRC(CHKOPEN) PARM((&DBOP0100) (&RETURNCODE))
ENDDO
ENDPGM
6)创建RPG DBOEXIT2模块,它包含的CHKOPN过程执行下面的任务。
A)获取被打开的表
B)如果被打开的表是出口程序打开的三个表中的任何一个,返回值设为1,并且出口程序立即退出去避免递归循环
C)如果表的应用程序库没有定义在SECDBLIBF表中,这个表就不需要保护,返回值设为1并出口程序。
D)这个模块发出API调用去获取当前作业的程序栈。程序栈中的每一项都会被检查。如果程序栈的项(正在被检查的)是与打开表的项在同一个程序库中,打开请求就是来自于应用。如果没有程序栈的项在数据库的程序库中,打开请求就来自于一个远程访问(相对于应用来说的外部接口)。
E)如果打开表请求来自于应用接口,返回值设为1
F)如果下面的条件为真,返回值设为1:
打开表的请求是一个远程访问请求
打开表的程序库以及请求打开的用户被定义在AUTUSRFIL表
这一行的远程访问开关被设为1
G)打开请求的细节(包含全部的程序栈)被记录在DBOLOGFIL表中用来审计
H)返回值返回给调用者。如果返回值是1,打开请求就会继续;如果返回值是0,打开请求就会被拒绝。
注意:这个例子中所有的I/O通过RPG自由格式来用嵌入式的SQL,这是IBM i V5R4中的一个新特色。在以前的发布中,程序员不得不停止使用自由格式来嵌入SQL语句。
7)去创建这个模块,在IBM i OS命令行执行下面命令:
CRTSQLRPGI OBJ(QTEMP/DBOEXIT2) SRCFILE(QGPL/QRPGLESRC) COMMIT(*NONE) OBJTYPE(*MODULE)
例4: DBOEXIT2 源代码(一个RPG的例子)
h nomain
//------------------------------------------
// Prototypes
//------------------------------------------
d chkOpen pr
d dbopParm likeds(dbop0100)
d returnCode 10i 0
D rtvPgmStk PR extpgm('QWVRCSTK')
D 2000
D 10I 0
D 8 CONST
D 56
D 8 CONST
D 15
//------------------------------------------
// Data structures
//------------------------------------------
d DBOP0100 ds qualified
d headerSize 10i 0
d formatName 8
d arrayOffset 10i 0
d fileCount 10i 0
d elementLength 10i 0
d jobName 10
d userName 10
d jobNumber 6
d currentUser 10
d queryOpen 1
//******************************************
p chkOpen b export
//******************************************
d chkOpen pi
d dbopParm likeds(dbop0100)
d returnCode 10i 0
//------------------------------------------
// Data structures
//------------------------------------------
d DBOPFile ds qualified based(DBOPFilePtr)
d fileName 10
d fileLibrary 10
d member 10
d 2
d fileType 10i 0
d underlyingPF 10i 0
d inputOption 1
d outputOption 1
d updateOption 1
d deleteOption 1
D rcvVar DS 2000
D bytAvl 10I 0
D bytRtn 10I 0
D entries 10I 0
D offset 10I 0
D stkEntryCnt 10I 0
D stkEntThrd 10I 0
D jobIdInf DS
D jIDJobNam 10 inz('*')
D jIDJUsram 10 inz(*blanks)
D jIDJobNbr 6 inz(*blanks)
D jIDIntJobID 16 inz(*blanks)
D jIDRsrvd 2 inz(*loval)
D jIDThrdInd 10I 0 inz(1)
D jIDThrdID 8 inz(*loval)
D stkEntry DS 256
D stkEntryLen 10I 0
D procOffset 10I 0 Overlay(stkEntry:13)
D procLen 10I 0 Overlay(stkEntry:17)
D pgmNam 10 Overlay(stkEntry:25)
D pgmLib 10 Overlay(stkEntry:35)
d fullEntry 1 256
//------------------------------------------
// Standalone variables
//------------------------------------------
d qryOpn s like(dbopParm.queryOpen)
d userNam s like(dbopParm.UserName)
d jobNam s like(dbopParm.jobName)
d curUser s like(dbopParm.currentUser)
d fileNam s like(dbopFile.fileName)
d fileLib s like(dbopFile.fileLibrary)
d i s 10i 0
d j s 10i 0
d fullStack s 1000a
D rcvVarLen s 10i 0 Inz(%size(rcvVar))
D
D apiErr s 15a
D procNam s 10a
D rmtAcsFlg s 1a
D dbLib s 10a
/free
// Don't perform checks on QSYS and QTCP users
curUser= dbopParm.currentUser;
if dbopParm.currentUser = 'QSYS'
or dbopParm.currentUser = 'QTCP';
return;
endif;
returnCode = 1;
QryOpn = dbopParm.queryOpen;
userNam = dbopParm.userName;
jobNam = dbopParm.jobName;
for i = 1 to dbopParm.fileCount;
DBOPFilePtr = %addr(dbopParm) + dbopParm.arrayOffset +
(i - 1) * dbopParm.ElementLength;
fileNam = dbopFile.fileName;
fileLib = dbopFile.fileLibrary;
// Avoid an infinite recursion condition by not continuing if the
// file being opened is one of the files that this program will be
// opening.
if fileNam = 'DBOLOGFIL' or
fileNam = 'SECDBLIBFL' or
fileNam = 'AUTUSRFIL';
returnCode = 1;
return;
endif;
// See if the file being opened is in a library that has
// been defined in the table SECDBLIBFL. Entries in this
// table are ones that need to be secured from unauthorized
// remote access attempts
exec sql SELECT db_lib INTO :dbLib
FROM dboexit/secDBLibFl
WHERE db_lib = :fileLib;
// If not not found, we do not need to secure this table.
// Execute next iteration to check the next table in the list
if %subst(sqlstate:1:2) <> '00';
iter;
endif;
// At this point, we know the file being opened is one we need
// to secure. To determine if the file is being accessed by a
// remote interface, we need to examine the entries in the
// program stack.
// In this case we are going to assume that application programs
// are located in the same library as the database files. So
// if any of the progam stack entries has a library that matches
// the library of the file being opened, we can assume that the
// file is being opened via a local, application interface.
// Otherwise the inteface is external to the appication, so we
// need to continue processing.
returnCode = 0;
fullStack = *blanks;
// Call API to retrieve the program stack
rtvPgmStk(rcvVar:rcvVarLen:'CSTK0100':JobIdInf:
'JIDF0100':apiErr);
// While checking each pgm stack entry,
// save it in varable fullStack so that the
// full stack can be included in the log file.
for j = 1 to stkEntryCnt;
stkEntry = %subst(rcvVar:Offset + 1);
procNam = %subst(stkEntry:
procOffset + 1:
procLen);
if procNam <> *blanks;
fullStack = %trim(fullStack) + ', '
+ %trim(pgmLib) + '/'
+ %trim(pgmNam) + ':'
+ procNam;
else;
fullStack = %trim(fullStack) + ', '
+ %trim(pgmLib) + '/'
+ %trim(pgmNam);
endif;
// If the program library is same as file lib, this
// indicates that the interface opening the file is
// part of the secured application. Set return code
// to 1 to allow the open request to continue.
if pgmLib = fileLib;
returnCode = 1;
endif;
offset = offset + stkEntryLen;
endfor;
fullStack = 'Stack count is ' + %char(stkEntryCnt) + ': ' +
%subst(fullStack : 3 : %len(fullstack)-2);
// At this point, if return code is 0, we know that the user
// is attempting to access the file via an interface external
// to the application. Check the table of authorized users to
// determine if this user has remote access authorization
if returnCode = 0;
exec sql SELECT remote_access_flag INTO :rmtAcsFlg
FROM autUsrFil
WHERE user_Name = :userNam AND
application_library = :fileLib AND
remote_access_flag = '1';
// If state is ok, match was found. Set return code to
// 1 to indicate that file open request can continue
if %subst(sqlstate:1:2) = '00';
returnCode = 1;
endif;
endif;
// Keep an audit log entry of this db open request
exec sql INSERT INTO dboLogFil
VALUES(:qryOpn, :returnCode, :userNam,:fileLib,
:fileNam, :jobNam,:curUser, CURRENT DATE,
CURRENT TIME, :fullStack);
endfor;
return;
/end-free
p chkOpen e
8)当这些模块都被创建了,执行下面命令去创建程序.
CRTPGM PGM(DBOEXITPT/DBOEXIT1) MODULE(DBOEXIT1 DBOEXIT2) ENTMOD(DBOEXIT1) ACTGRP(*CALLER)
资源
IBM i and System i Information Center
DB2 Information Center
IBM Redbooks
原文地址:https://www.ibm.com/developerworks/ibmi/library/i-open-db-file-exit-program/index.html#ibm-content
原文作者:Gene Cobb (cobbg@us.ibm.com)
Gene Cobb 是高级技术人员并且是DB2 for IBM i 和 DB2 Web Query 的专家。如果有需要,请发邮件到cobbg@us.ibm.com
译者:Zhao Li Yan
UID
ibm11144426


