ABAP权限检查到SELECT-OPTIONS边界的RANGES实现
做过几年 ABAP 报表开发的人大概率都碰过这种需求一张报表选择屏幕上有几十个 SELECT-OPTIONS权限对象里明明只放行了一部分组织范围但前台条件框却允许任意输入。比如采购报表里s_bukrs能填 1000也能填 9000而用户的 AUTHORITY-CHECK 在 START-OF-SELECTION 里一检查填 9000 时直接弹一个无权限的报错。用户一脸懵开发也被动。这篇文章想说的就是怎么把 AUTHORITY-CHECK 的结果直接变成 SELECT-OPTIONS 的边界权限允许哪些值条件框就默认这些值用户想追加的条件必须落在权限范围内即使有人强行改内存、改变式SQL 查询里也始终有一条权限范围在兜底。无论你是刚接触 ABAP RANGES 的新手还是正在为某个报表加权限的资深开发这套思路都能直接拿去用。1. 用户有权限但报表还是报错传统 AUTHORITY-CHECK 的痛点1.1 一个真实场景筛选条件可以填满一执行就报错我在一个成本管控项目里做过一张费用分析报表选择屏幕上有成本中心范围、公司代码范围、期间范围。上线前半个月财务关键用户跑过来说我明明是成本中心 1000 到 3000 的负责人为什么在报表里输入 1000 到 3000 没问题输 4000 就报无权限当时代码里的权限检查非常粗糙只在 START-OF-SELECTION 里做了两步先取一个主成本中心如果有再对这个值调一次 AUTHORITY-CHECK没过就直接 MESSAGE EXIT。这种写法的直接后果是权限检查的粒度跟用户输入条件的粒度完全对不上。用户填了一个完整的条件权限判断却只能针对某一个值给结果结果报错也报得莫名其妙。后来我去看那个报表发现选择屏幕上的成本中心是一个多值 SELECT-OPTIONS用户可能填三个单值也可能填一个区间甚至可能为了看全部而留空。传统AUTHORITY-CHECK根本没法覆盖这些情况。1.2 传统 AUTHORITY-CHECK 的有或无逻辑很多人写权限检查用的都是最基础的写法AUTHORITY-CHECK OBJECT Z_PUR_REPORT ID ACTVT FIELD 03 ID BUKRS FIELD lv_bukrs. IF sy-subrc 0. 允许访问 ELSE. MESSAGE e001(zreport) WITH 无权访问该数据. ENDIF.这段代码的逻辑是对一个具体字段值做验证。sy-subrc 0代表用户对这个值有权限其他返回值代表没有权限或者授权对象配置有问题。它的结果本质上是一个布尔值——这个值能不能访问。问题就在于SELECT-OPTIONS 天生是集合不是单值。用户在屏幕上填的是一组范围甚至包含 EXCLUDE 排除项、多个区间、通配符。你拿一个布尔值去校验一个集合怎么都对不齐。于是项目里最常见的结果就是条件填得越多权限误报越频繁条件留空想查全部反而绕过了权限检查因为代码里往往只对非空值做检查。1.3 为什么报表需要的是范围而不是布尔值权限管理的本质是给用户划定一个数据范围。比如公司代码 1000、2000 可查看这句话翻译过来就是一个 RANGES 条件BUKRS 1000或BUKRS 2000。SELECT-OPTIONS 同样是一个 RANGES 条件。两者天生是一类东西只是开发时经常把它们割裂开一个放在权限校验里一个放在 SQL 条件里。所以正确的思路不是查之前拿某个值去 check 一下而是先把用户的授权值翻译成一个 RANGES 内表然后把这个内表作为 SELECT-OPTIONS 的默认值、校验基准、以及 SQL 的强制过滤条件。这样权限边界和筛选条件才能严丝合缝。下面我把这条链路拆开讲。2. 把授权值翻译成 RANGES权限字段与主数据表的映射2.1 关键认知AUTHORITY-CHECK 只能验证单个值要构造权限 RANGES第一件事是搞清楚AUTHORITY-CHECK 本身不能替你枚举用户有哪些值有权限。它只能回答这个值有没有权限。很多开发同事问过我既然授权对象里配了一堆值能不能直接读取授权记录拿列表理论上有AGR_USERS、AGR_1251这些表可以查但直接查授权表的水很深后面专门讲。标准做法是先找到候选值列表再用 AUTHORITY-CHECK 逐个验证。注意这里有个常见误区AUTHORITY-CHECK OBJECT ... ID BUKRS DUMMY只是在做检查时跳过这个字段值并不代表能拿到所有有权限的值。DUMMY 适合判断其他条件满足时这个授权对象是否可用不能解决枚举问题。2.2 权限字段对应主数据表先找到能枚举的候选值既然要枚举就得知道候选值从哪来。原则是优先取主数据表因为主数据表里的值才是有业务意义、报表里真实会出现的值。下面是项目里最常用的一组映射权限字段含义候选值来源表BUKRS公司代码T001KOKRS成本控制范围TKA01WERKS工厂T001WKOSTL成本中心CSKSEKORG采购组织T024EVKORG销售组织TVKOPRCTR利润中心CEPCMATKL物料组T023这些表基本都是主键或者组织维度主数据量级通常不会太大适合作为枚举来源。如果权限字段是自定义的配置值比如报表类型、审批状态那就从对应的配置表或者域固定值里取也可以用DOMAIN_VALUES_GET拿域的固定值列表。2.3 逐值检查并构造 RANGES核心代码有了候选值接下来就是循环 AUTHORITY-CHECK。这里以公司代码为例写一个通用的构造 FORMFORM build_auth_bukrs CHANGING ct_auth_bukrs TYPE RANGE OF bukrs. DATA: ls_auth LIKE LINE OF ct_auth_bukrs. SELECT bukrs FROM t001 INTO DATA(lv_bukrs) ORDER BY bukrs. AUTHORITY-CHECK OBJECT Z_PUR_REPORT ID ACTVT FIELD 03 ID BUKRS FIELD lv_bukrs. IF sy-subrc 0. ls_auth-sign I. ls_auth-option EQ. ls_auth-low lv_bukrs. APPEND ls_auth TO ct_auth_bukrs. ENDIF. ENDSELECT. ENDFORM.这段代码的意思是把所有公司代码遍历一遍能通过授权检查的值就放进去。最终ct_auth_bukrs就是用户的公司代码权限范围。字段SIGN统一为I包含OPTION为EQ等于业务上最安全不会出现排除条件和区间条件带来的歧义。一个小提醒如果枚举的表带有效期字段比如 CSKS成本中心、CEPC利润中心记得在查询里加日期过滤否则可能把一大堆过期主数据也拿进来做权限判断导致 RANGES 里出现一堆永远不会有业务数据的历史值既影响性能又让用户看不清授权范围。3. 空授权和 * 通配符构造权限范围时最容易写错的两种边界3.1 权限范围为空是无任何数据权限不是放行全部这个坑我亲眼见过不下三次。有些开发在构造完 RANGE 后看到结果是空的下意识认为既然这个用户没有特定的公司代码权限那就不用限制他让他查全部。这是完全错误的逻辑。权限范围为空在 ABAP 权限语义里就是零授权应当理解为该用户对任何公司代码都没有访问权限。正确做法是在报表执行前判断权限 RANGES 是否为空为空就明确提示用户没有数据访问权限而不是进入 SQL 查询然后返回一大堆越权数据。从权限设计角度看空授权代表的是无跟全部是天壤之别这个认知必须从一开始就建立。3.2 * 通配符全部授权的识别与放行另一个更隐蔽的问题是通配符*。用户可能在权限对象里被分配了BUKRS *意思是全部公司代码都能看。如果只依赖主数据表循环加上 AUTHORITY-CHECK 判断你会遇到一个尴尬局面当前 T001 里的公司代码全部检查通过你就把所有现有公司代码放进了权限 RANGE。但明年新增了一个公司代码线上系统还没刷新权限新增公司代码就不在权限范围内用户反而看不到本该看到的组织。遇到*授权最稳妥的是在代码里识别这个特殊值并打上标记。我一般用一个全局标志位IF ls_auth-low *. gv_all_auth abap_true. ENDIF.如果gv_all_auth abap_true在后续 SQL 拼接权限条件时直接跳过该公司代码字段的IN限制或者把权限 RANGE 设置为SIGN I OPTION CP LOW *。我更推荐前者因为标志位在代码阅读和排错时都更直白。但要识别*通常不能只靠主数据循环因为他如果配了*主数据里的值也全会通过你根本分不清这是全部授权还是恰好有全部现有公司代码授权。项目里比较务实的做法有两种一种是在权限配置层面约定不允许使用*通过权限维护工具限制另一种是在初始化权限范围时用AUTH_CHECK相关标准逻辑或权限维护出口把*识别出来置上标志位。具体用哪种取决于你们项目的权限治理粒度但至少要意识到这个边界存在。3.3 区间/不连续授权RANGES 的表示与合并除了单值和*还有一种情况是用户被授权了一个连续区间。比如公司代码从 1000 到 1999。如果逐值循环出来你会得到一千条EQ条件。虽然 ABAP 处理一千条 RANGES 也没太大问题但 SQL 传输到数据库时IN列表太长查询计划会变差性能不好看。项目里常见的优化是在生成 RANGES 后做一个连续区间合并。把排好序的单值序列检查下一个值是否等于当前值 1如果是就继续否则把当前连续段收拢成一个I BT low-high。这样一千条单值就能合并成一条区间条件。注意这种合并只适用于备份表值在逻辑上连续的字段比如公司代码、工厂这类编码有连续性的组织单位不适合用在物料号这种没有连续性含义的字段上。4. 自动填充和强制拦截让 SELECT-OPTIONS 与权限严丝合缝的落地组合4.1 INITIALIZATION 里填充默认权限范围拿到权限 RANGES 之后就要想办法让它出现在选择屏幕上。最简单直观的方式是在INITIALIZATION事件里把权限 RANGE 填进 SELECT-OPTIONS 对应的内部表INITIALIZATION. PERFORM build_auth_bukrs CHANGING lt_auth_bukrs. IF s_bukrs[] IS INITIAL. s_bukrs[] lt_auth_bukrs[]. ENDIF.这样做的好处是用户一进画面就能看到自己被授权的公司代码范围不用再手动猜。如果用户没填任何条件系统默认就会按权限范围查询如果用户填了条件后续由 PAI 和 SQL 做双重保障。这里有一个细节要注意INITIALIZATION里填充时要先判断s_bukrs[]是否为空不要无条件覆盖。因为用户可能已经通过保存的变式带入了自定义条件这时候直接覆盖会惹恼用户。当然变式里带入的条件是否越权是另一层校验的问题放到 5.3 讲。4.2 PAI 里对用户输入做越权校验如果只做默认填充用户还是可以在屏幕上把默认值删掉再填越权值所以在AT SELECTION-SCREEN阶段要再拦一道。针对 SEELECT-OPTIONS 的多值、多区间场景我通常写一个简单的校验 FORMAT SELECTION-SCREEN. IF gv_all_auth abap_false. PERFORM check_so_against_auth USING s_bukrs[] lt_auth_bukrs[]. ENDIF. FORM check_so_against_auth USING it_so TYPE RANGE OF bukrs it_auth TYPE RANGE OF bukrs. IF it_auth[] IS INITIAL. MESSAGE e001(zreport) WITH 您没有任何公司代码的查询权限. ENDIF. LOOP AT it_so INTO DATA(ls_so). CASE ls_so-option. WHEN EQ OR NE OR GE OR GT OR LE OR LT. IF NOT ( ls_so-low IN it_auth ). MESSAGE e001(zreport) WITH 筛选条件中存在无权访问的值: ls_so-low. ENDIF. WHEN BT. IF NOT ( ls_so-low IN it_auth ) OR NOT ( ls_so-high IN it_auth ). MESSAGE e001(zreport) WITH 区间 ls_so-low - ls_so-high 包含无权访问的范围. ENDIF. ENDCASE. ENDLOOP. ENDFORM.这个校验是有权限才给过做的是正向来校验。它有一个已知的限制区间BT只检查了起点和终点如果用户输的区间起点和终点都在授权范围内但中间恰好隔着几个无权值这种中间空洞靠这个简单校验是拦不住的。业务上如果确实存在不连续授权就需要更精细的 RANGES 交集运算那是另一个复杂度等级的话题。我自己的经验是大多数业务报表的权限边界不会碎到那种程度但你要知道这个限制避免在需求评审时拍胸脯保证绝对精确。4.3 SQL 终点防线无论屏幕条件如何查询永远带上权限范围前面填充默认值、PAI 校验都是在引导和拦截。真正能保证权限边界不破的是在最终查询 SQL 里强制拼接权限范围SELECT k~ebeln, k~bukrs, k~ekorg, ... FROM ekko AS k WHERE k~ebeln IN s_ebeln[] AND k~bukrs IN lt_auth_bukrs[] INTO TABLE DATA(lt_result).注意lt_auth_bukrs[]和s_bukrs[]是两个独立条件一个来自授权计算一个来自屏幕输入。这样做的意义是哪怕屏幕条件为空、变式被动手脚、或者某个 ABAP 内存被直接修改只要进入查询数据库层面就会强制把结果限制在授权范围内。这一层是整条权限链路的最终防线没有它的报表权限再怎么做都有漏洞。我见过有同事只用 INITIALIZATION 填充权限值然后就不管了结果用户在前台把默认值删掉照样能查出权限外的数据。所以 SQL 条件里的强制IN lt_auth_bukrs[]永远不能省。5. 多权限对象、性能与变式复杂项目里值得注意的三个坑5.1 多权限对象组合AND 和 OR 的语义差异单字段权限做完后报表权限复杂在多个权限对象组合。假设授权对象里同时包含了公司代码、采购组织、物料组三个 ID 在一个对象里它们之间是 AND 关系——必须同时满足。那 SQL 里的拼接就是三个IN条件BUKRS IN auth_bukrs AND EKORG IN auth_ekorg AND MATKL IN auth_matkl构造逻辑跟单字段一模一样分别构造三个 RANGES 就行。麻烦的是 OR 关系。业务上可能出现销售组织 1000 有权限或者工厂 2000 有权限有任一权限就算有权限。如果直接在 SQL 里写WHERE bukrs IN auth_bukrs OR vkorg IN auth_vkorg结果集会包含公司代码有权限但销售组织没权限和销售组织有权限但公司代码没权限的行这些行到底是越权还是可访问取决于报表的数据主体到底是什么。遇到这种多对象 OR我最直接的建议是先跟权限管理员盘点清楚数据主体的归属维度别在 ABAP 里硬拼 OR 条件很容易拼出一个权限漏洞。如果实在避免不了建议在数据层面把由哪个权限对象决定可见性这件事固化下来比如构造一个授权组合视图再去做 JOIN。5.2 为什么我不建议直接查 AGR_USERS/AGR_1251很多开发看到需要枚举授权值第一反应是直接读授权表更快。我也试过查AGR_USERS、AGR_1251这些底层授权记录表但后来放弃了。原因有三第一角色和授权的关系不止一层有单角色、复合角色、派生角色、用户组直接查表很容易漏掉某条继承链第二授权对象里的字段组合校验逻辑比如多个 ID 之间的 AND、ACTVT 值SAP 都没有公开的稳定 SQL自己拼非常容易和标准 AUTHORITY-CHECK 结果不一致第三系统升级或者客户做权限增强后底层表行为可能变化你敢硬查后面升级就敢给你埋雷。所以我始终坚持一个原则能把校验交给 AUTHORITY-CHECK 的就绝不自己造轮子。唯一可能考虑读授权表的场景是前面提到的检测*通配符而且要小心加注释并锁定版本后续做好回归测试。5.3 性能优化与变式覆盖最后的落地心得性能方面逐值循环 AUTHORITY-CHECK 在最开始会让人担心。实际项目里主数据组织维度一般就几百到几千条每次报表初始化做一次权限 RANGES 构造耗时基本可以忽略。但需要注意两点一是把权限 RANGES 放进全局变量不要在 INITIALIZATION、PAI、SQL 拼接的各个阶段各算一遍否则白消耗性能二是如果公司代码、工厂这类候选值特别大比如集团层面的 WERKS 有上万条可以考虑先按业务维度把范围缩小再做权限过滤。变式Variant是另一个容易翻车的地方。用户保存变式后选择屏幕的值在 INITIALIZATION 之后会被变式覆盖。也就是说你在 INITIALIZATION 里填的权限默认值可能被保存在变式里的越权值覆盖掉。所以千万不要觉得填充完就万事大吉PAI 校验和 SQL 强制过滤才是兜底。我见过一张报表因为没做 SQL 兜底用户把带越权条件的变式一保存每次进来都是越权数据问题排查了很久才发现是变式覆盖的锅。做权限报表这几年我最大的体会是把权限当作数据的一部分去设计而不是当作执行前的一关。SELECT-OPTIONS 本身就是天然的权限边界展示窗口AUTHORITY-CHECK 则负责把这些边界翻译成机器能理解的 RANGES。只要把两者接上用户、开发、权限管理员三方都能轻松不少。最后分享一个小技巧给报表加权限时先在测试环境用 SU53 看完当前用户到底命中哪个授权对象再决定用哪几个 ID 去做 RANGES 构造这个顺序能帮你少写很多无用代码。