當今常見的BPM趨勢是集中化整個公司或公司內大部門的BPM執行.這意味著,單個BPM伺服器(集群)運行著整個公司的許多流程定義.這種方式的挑戰在 於,雖然BPM引擎(包括jBPM)提供了對於任務訪問的授權,但它們一般都不支持這些功能的授權:流程定義的查看和刪除,流程實例的啟動、結束、查看和刪除等.在這篇文章中,我們將描述如何對jBPM引擎進行擴展 (基於jBPM 4.3)來實現這一功能.
整體實現方法
整個實現方式相當直接了當——對於每個流程定義引入一組可以授權的用戶/用戶組(類似任務定義),作用於定義、實例和給定流程的歷史.此外,我們還想對給 定的用戶/用戶組支持多重授權級別——目前我們打算引入2個角色:「starter」和「user」.這裡的「starter」是允許對流程定義/實例 /歷史進行任何操作的角色,而「user」角色的許可權僅限於查詢流程/歷史.
這種方式的實現需要對jBPM進行以下改造:
◆流程定義:
給流程定義增加流程訪問許可權
◆流程部署:
擴展當前的流程部署器,增加流程授權定義的解析和流程訪問列表的生成
引入額外的類/資料庫表,存放每個流程定義的訪問許可權
◆流程執行(Execution)
引入已授權命令(authorized command)——要求用戶經過授權才能執行的命令
修改現有的jBPM中我們期望基於當前用戶證書進行授權的部分.這包括啟動、結束和刪除流程實例,以及刪除部署定義.
修改現有的jBPM查詢,考慮現有用戶的證書.這包括部署和流程定義查詢、流程實例查詢,以及歷史流程實例、活動和細節的查詢.
除了以上更改,我們還想擴展流程實例查詢,好讓用戶可以通過指定某些流程變數的值來縮小查詢結果.這種搜索的一個常見情況就是查詢「由我啟動的」流程.為 了確保這種查詢總是可用,我們更改了啟動流程實例命令的實現,顯式地把當前用戶ID加到了流程變數值的集合中.
最后,為了支持多種用戶認證方法,我們實現了一個自定義的身份會話,它支持用程序來設置和訪問當前用戶的證書.其目的在於,把用戶證書(ID和參與的組) 的獲得和jBPM運行時對這種信息的使用分離開來.
我們的實現利用了非常強大和靈活的jBPM 4的配置機制,它讓我們可以:
通過擴展現有jBPM類,最小化了自定義代碼的數量,只實現我們擴展所需的額外功能
將我們的擴展實現成可以與jBPM 4類庫一起使用的單獨jar包,無需對現有庫進行任何改變.
在深入我們的實現細節之前,我們要討論一下我們大量使用的jBPM 4的配置.
jBPM 4的配置機制
jBPM的基礎是流程虛擬機(PVM)[2],它建立在自定義的依賴注入實現之上.依賴注入由非常強大的、基於XML的配置機制控制,這種機制用於創建標籤和預定義介面相關的特定實現之間的綁定 (binding).
這種機制的核心是jbpm.wire.bindings.xml文件,它描述了[3] jBPM PVM的主要組件,包括:
◆基本類型
◆對象及引用
◆環境引用
◆Hibernate綁定
◆會話
◆服務
◆部署器
◆攔截器
◆等
該文件是jBPM分發包的一部分.如果用戶想增加自己的綁定(binding),他可以創建jbpm.user.wire.bindings.xml描述 它們,而不用修改jbpm.wire.bindings.xml文件.
這兩個文件會被jBPM PVM在啟動時讀入並解析,為定義在jbpm.cfg.xml中的基礎PVM執行(execution)配置而服務.jbpm.cfg.xml一般會包含 多個部分,描述了PVM執行的特定組件的配置.
jBPM PVM由一組提供PVM功能的服務組成[4].主要的PVM服務包括:
◆倉儲服務,提供一組查看和管理部署倉儲的方法
◆執行服務,提供一組查看和管理運行中流程執行(execution)的方 法.
◆管理服務,提供一組查看和管理工作(job)的方法
◆任務服務,提供一組查看和管理用戶任務(task)的方法.
◆歷史服務,提供一組訪問運行中和已完成流程執行的歷史信息的方法.
這組可用服務和實現這些服務的類(使用前面說的綁定)被配置成流程引擎的上下文.
服務執行被實現成一組命令(command),它們作為服務方法執行的一部分被調用.命令的實際執行由命令服務控制.
命令服務在命令服務上下文中被配置成一組攔截器,實現橫切關注點,環繞(around)命令調用(命令執行管線).預設的jBPM分發包在命令執行管線中 攜帶了以下攔截器:
◆重試(Retry)攔截器負責重試命令執行
◆環境(Environment)攔截器負責在必要時把jBPM上下文注入命 令執行中
◆事務(Transaction)攔截器負責介入命令調用的事務邊界劃分.
攔截器是將jBPM移植到不同環境以及引入其他橫切關注點的核心機制.
命令執行一般會利用環境,它也是可配置的.典型的環境組件有:
◆倉儲會話
◆DB會話
◆消息會話
◆定時器會話
◆歷史會話
◆郵件會話
可以添加其他會話來擴展PVM的功能.
最后,部署管理器配置允許指定一組部署器,它們依次執行,把業務流程部署到PVM.這種方法是的擴展流程定義可以通過實現額外的部署步驟完成,無需覆蓋 jBPM分發包自帶的部署器.
整個PVM的架構如圖1示:
圖 1 PVM架構
在流程定義中引入授權
我們在圖2中看到,可以給流程定義添加任意屬性.利用這種擴展選項,我們現在定義以下流程屬性,描述授權策略:
◆starter-users,具有「starter」角色的用戶列表
◆starter-groups,具有 「starter」角色的組列表
◆user-users,具有「user」角色的用戶列表
◆user-groups,具 有「user」角色的組列表
每個屬性的值是逗號分隔的組/用戶id列表.
圖 2 流程定義模式
此外,我們還定義了一個特殊的用戶類型--「any」和兩個用戶組--「all」和「admin」.任何用戶,不論其真實ID是什麼,都是「any」用 戶.任何組,不論其ID是什麼,也都是「all」.最后,「admin」組的成員被認為是任意組的成員.
流程授權定義由以下規則驅動:
◆如果user-users和user-groups都未被指定,則user-users=」all」
◆如果 starter-users和starte-groups都未被指定,則流程用戶被額外地分配「starter」角色.
按照這個規則,清單1中的流程可以被任何人啟動和使用.
1.<process package="com.navteq.jbpm"
2. key="NO_AUTHORIZATION"
3. name="Test Authorization not required"
4. version="1"
5. xmlns="http://jbpm.org/4.0/jpdl">
6. <start g="68,14,48,48" name="start">
7. <transition to="end"/>
8. </start>
9. <end g="78,383,48,48" name="end"/>
10.</process>
11.
清單 1 沒有授權信息的流程定義
清單2的流程可以被mark或tomcat組中的任何人使用和啟動.
12.<process package="com.navteq.jbpm"
13. key="AUTHORIZATION"
14. name="Test Authorization Required"
15. version="1"
16. xmlns="http://jbpm.org/4.0/jpdl"
17. user-users="mark"
18. user-groups="tomcat">
19. <start g="68,14,48,48" name="start" >
20. <transition to="end"/>
21. </start>
22. <end g="78,383,48,48" name="end"/>
23.</process>
清單 2 具有用戶授權信息的流程定義
我們引入了一個新類--ACL,針對給定流程 (processDefinitionID,processDefinitionKey,DeploymentID),它包含一個單獨的訪問列表(用戶或 組,以及類型);同時還引入了相應的Hibernate定義.
圖3中,清單1的流程部署為具有兩個角色(「user」和「starter」)的用戶「any」創建了2個ACL;而在圖4中,清單2的流程部署將創建4 個--用戶「mark」和組「tomcat」,每個都具有2個角色:「user」和「starter」.
圖 3 無授權信息的流程的ACL
圖 4 有用戶授權信息的流程的ACL
這些ACL的生成是通過引入額外的部署器完成的,它將在「標準」jBPM部署器之後運行,抽取上面描述的授權屬性,為給定流程構建ACL.
保護jBPM命令
我們採用了一種通用的方法來保護jBPM命令,包括實現用於定義命令所需授權信息的自定義的註解,以及處理這個註解的自定義的授權會話(命令攔截器)實現.
授權註解(清單3)可以指定所需的用戶角色和表示某個流程的方法.
1.@Retention(value=RetentionPolicy.RUNTIME)
2.@Target(value=ElementType.METHOD)
3.public @interface AuthorizedCommand {
4. /** Access type */
5. public String role();
6. String key();
7.}
8.
清單 3 授權註解
對於某個流程,用戶角色--「starter」或「user」--指向某個用戶應該擁有的角色[6].由於不同命令既可以引用部署ID,也可以引用流程ID或者流程鍵值,因此註解支持多種指定鍵值的方式,允許將合適的引用指定為鍵值.
清單4的授權攔截器檢查是否有命令的方法被授權註解修飾.如果有,則執行適當的查詢,確定出哪些用戶和用戶組集合被授權給了這個命令,然後檢查當前用戶是 否屬於他們.
9.…………
10.
11.@SuppressWarnings("unchecked")
12.public void checkPermission(Command<?> command, EnvironmentImpl environment) {
13. environment.setAuthenticatedUserId(environment.get(AuthorizationIdentitySession.class).getAuthenticatedUserId());
14. for( Method method : command.getClass().getMethods()) {
15. AuthorizedCommand sc = method.getAnnotation(AuthorizedCommand.class);
16. if(sc != null){
17. log.debug("Checking Class based Secured Function");
18. String ID = environment.get(AuthorizationIdentitySession.class).getAuthenticatedUserId();
19. Object value = null;
20. try {
21. log.debug("Checking authorization: " command.getClass().getName());
22. Session session = environment.get(SessionImpl.class);
23. value = method.invoke(command, (Object[])null);
24. Query uQ = session.createQuery(userQuery.get(sc.key())).
25. setString("role", sc.role()).setString("value",(String) value);
26. Query gQ = session.createQuery(groupQuery.get(sc.key())).
27. setString("role", sc.role()).setString("value", (String) value);
28. List<String> userIds = (List<String>)uQ.list();
29. List<String> groups = (List<String>)gQ.list();
30. if(!isAuthorized(environment, userIds, groups))
31. throw new AccessControlException(ID " attempted access to ProcessDefinition #" value);
32. } catch (IllegalArgumentException e) {
33. log.error("Caught " IllegalArgumentException.class, e);
34. throw new AccessControlException(ID " attempted access to ProcessDefinition #" value);
35. } catch (IllegalAccessException e) {
36. log.error("Caught " IllegalAccessException.class, e);
37. throw new AccessControlException(ID " attempted access to ProcessDefinition #" value);
38. } catch (InvocationTargetException e) {
39. log.error("Caught " InvocationTargetException.class, e);
40. throw new AccessControlException(ID " attempted access to ProcessDefinition #" value);
41. }
42. }
43. }
44. return;
45.}
46.
47.……………………
48.
49.public boolean isAuthorized(EnvironmentImpl env, List<String> authorizedUserIds, List<String> authorizedGroupIds) {
50. AuthorizationIdentitySession identitySession = env.get(AuthorizationIdentitySession.class);
51. if (authorizedUserIds.contains(AuthorizationIdentitySession.ANONYMOUS_USER_ID))
52. return true;
53. if (authorizedUserIds.contains(identitySession.getAuthenticatedUserId()) )
54. return true;
55. //check if any of userGroups is an authorized group. if so then return true
56. List<Group> groups = identitySession.findGroupsByUser(identitySession.getAuthenticatedUserId());
57. for(Group group : groups){
58. String g = group.getId();
59. //admin is allowed to execute any command
60. if(g.equals(AuthorizationIdentitySession.ADMINISTRATORS_GROUP))
61. return true;
62. if(authorizedGroupIds.contains(g))
63. return true;
64. }
65. return false;
66.}
67.
清單 4 授權攔截器
為了保護命令實現,我們創建了一個新類,它擴展了現有的命令,增加了一個帶註解的方法(清單5),返回給定命令可用的鍵值.
68.@AuthorizedCommand(role = ACL.STARTER, key = NavteqAuthorizationSession.PROCESSID)
69.public String getProcessDefinitionKey() {
70. return processDefinitionId;
71.}
清單 5 給啟動流程實例命令引入授權信息
根據所提議的方法,我們對下列命令進行了註解:
◆刪除部署
◆啟動流程實例
◆啟動最近的流程實例
◆結束流程實例
◆刪除流程實例
擴展查詢
引入授權意味著查詢結果應該只返回用戶被授權查看的信息[7].這可以通過擴展現有查詢的where語句實現(清單6).
1.//check authorization
2.String userId = EnvironmentImpl.getCurrent().getAuthenticatedUserId();
3.List<Group> userGroups = EnvironmentImpl.getCurrent().get(IdentitySession.class).findGroupsByUser(userId);
4.
5.hql.append(", " ACL.class.getName() " as acl ");
6.appendWhereClause("acl.deployment=deployment and acl.type='"
7. ACL.STARTER
8. "' ", hql);
9.appendWhereClause("((acl.userId in "
10. Utils.createHqlUserString(userId)
11. ") or "
12. "(acl.groupId in "
13. Utils.createHqlGroupString(userGroups) "))
14. ", hql);
清單 6 為流程定義查詢提供授權支持的額外where語句
額外的where語句是通過擴展現有查詢實現和覆蓋hlq方法實現的.
按照這種方法,擴展了以下查詢:
◆部署查詢
◆流程定義查詢
◆流程實例查詢
◆歷史流程實例查詢
◆歷史活動 查詢
◆歷史細節查詢
為了能夠增加字元串實例變數,以縮小查詢結果,我們還額外擴展了一個流程實例查詢.
支持多種用戶管理方式
以上給出的實現依賴使用執行環境來獲得當前用戶ID和使用IdentitySession來獲得用戶組成員關係.jBPM分發包提供了這個介面的2個實現:
◆IdentitySessionImpl,基於jBPM的用戶/組資料庫
◆JBossIdmIdentitySessionImpl, 基於JBoss Identity IDM組件
不同於大量依賴其他技術的實現,對於我們的實現,我們決定把用戶ID/組的獲取同這些信息的保存分離開來,使之可以被其他的jBPM實現利用(圖5).
圖 5 用戶管理實現
為了確保在設定和重新設定用戶證書的時候環境是可用的,我們把這兩個操作實現成了命令(清單7),這樣,藉助jBPM命令執行服務就可以正確設置執行環境.
1.public static class SetPrincipalCommand extends AbstractCommand<Void> {
2. private static final long serialVersionUID = 1L;
3. private String userId;
4. private String[] groups;
5. public SetPrincipalCommand(String u, String...groups) {
6. this.userId=u; this.groups=groups;
7. }
8. public Void execute(Environment environment) throws Exception {
9. environment.get(AuthorizationIdentitySession.class).setPrincipal(userId,groups);
10. return null;
11. }
12.}
13.
14.public static class ResetPrincipalCommand extends AbstractCommand<Void> {
15. private static final long serialVersionUID = 1L;
16. public Void execute(Environment environment) throws Exception {
17. environment.get(AuthorizationIdentitySession.class).reset();
18. return null;
19. }
20.}
把新命令和查詢引入到jBPM執行中
由於jBPM並沒有提供任何配置「命令--服務」關係的支持,為了能改變給定服務中的命令,就必須使用調用新命令的新服務實現覆蓋舊實現.清單8給出了一個使用新命令服務覆蓋歷史服務的例子.
1.public class NavteqHistoryServiceImpl extends HistoryServiceImpl {
2. @Override
3. public HistoryActivityInstanceQuery createHistoryActivityInstanceQuery() {
4. return new AuthorizedHistoryActivityInstanceQueryImpl(commandService);
5. }
6.
7. @Override
8. public HistoryDetailQuery createHistoryDetailQuery() {
9. return new AuthorizedHistoryDetailQueryImpl(commandService);
10. }
11.
12. @Override
13. public HistoryProcessInstanceQuery createHistoryProcessInstanceQuery() {
14. return newAuthorizedHistoryProcessInstanceQuery(commandService);
15. }
16.}
17.
清單 8 在歷史服務中引入授權命令
總結
本文給出的這部分實現[8]並沒有擴展JPDL,而是擴展了被jBPM支持的所有語言都使用的JBoss PVM[9].結果是,這些語言都能夠使用這個實現.
[火星人 ] 使用JBoss jBPM實現流程訪問和執行的授權已經有1118次圍觀