opengrok 权限管控
en:https://github.com/oracle/opengrok/wiki/Authorization#setting-up-tomcat
使用OpenGrok授权
本指导提供关于opengrok授权的授权框架的信息。你可以和project groupings结合使用该模块, project groupings允许你创建各种项目结构。
兼容性
如果插件目录不存在或者目录下没有任何插件,这个框架允许每一个请求。
配置
你可以配置:
-
插件目录
这个配置已经从web.xml移动到OpenGrok通用xml配置文件中。提供新插件目录的方式是创建一个新的xml配置文件,“read-only configuration” 文件,该文件将与合并索引器生成的配置合并。
在这个文件中,提供一个新的名为pluginDirectory的变量,带有一个字符串参数,参数值指向插件所在的绝对路径。
默认情况下,这个目录指向OPENGROK_DATA_ROOT
/plugins。OPENGROK_DATA_ROOT
是在主配置文件configuration.xml设置的
dataRoot
目录。如果插件不可见/目录不存在,那么框架会允许每一个请求。Opengrok授权在该目录中使用插件类,它将这些类作为插件加载到内存中,然后对每个请求执行授权。
-
监听服务
类似监控服务在配置中都有一个专用的配置选项,你可以按照上面的描述进行设置。配置项的名称是
authorizationWatchdogEnabled,
值为boolean类型。该服务监听插件目录的修改(包含递归子目录),如果发生了如下修改 (modifing, deleting),它会重新加载插件。内存中所有的的旧插件都会被清除,并且只有插件目录的插件会被加载。这个是实时的。
注意:修改插件目录下的任何文件都会被作为一个事件广播出来。这就意味着,在插件目录大批量修改文件,会导致多次重加载发生。
默认情况下,该值为flase。
-
授权堆栈
在下面的高级配置中讨论。
-
提供一个插件
你可以将编译后的插件类(.class或.jar)放到插件目录来提供你自己的插件。
注意:当使用.jar时,有很重要的经验,将在 故障诊断与排除 一节中被提到。
插件
例子
在git库的根目录的plugins文件夹中包含了一个 TruePlugin.java 的例子。
接口实现
每个插件必须实现 IAuthorizationPlugin
接口,它包含了四个方法:
-
void load ( void );
每当插件被框架加载到内存时,这个方法就会被调用
- 监听服务启用,并且有变更(强制重加载)
- 部署web应用
- 配置变更(变更插件目录.....)
-
void unload ( void );
每当框架销毁插件时,该方法被调用。
- 监听服务启用,并且有变更(强制重加载)(重载的顺序是先卸载旧的实例,然后加载新的)
- web应用取消部署
-
boolean isAllowed(HttpServletRequest request, Project project);
该project是否应该允许当前请求,这个方法给一个决策。
-
boolean isAllowed(HttpServletRequest request, Group group);
类似的,对该group给出决策。
所有这些方法在代码中也都有描述。
当为了用户授权所做的底层读取操作的成本很高时,都希望能为这些决策实现某种缓存。
HttpServletRequest
对象是当前请求,包含了所有特性:session, attributes, getUser()
, getPrincipal()
会话
如果你决定你的插件使用某种会话,当授权框架重加载插件时,你可能希望也会重载这个会话。框架会记录一个重加载的编号,这个对你都是可见的,通过 RuntimeEnvironment.getInstance().getPluginVersion() 可以获取到。你可以比对这个编号,如果与你的版本(假设在你的会话中记录了)不同,那重启你的会话。
限制条件
自定义类加载器限制插件只能加载org.opensolaris.opengrok包里的类:
private final static String[] classWhitelist = new String[]{ "org.opensolaris.opengrok.configuration.Group", "org.opensolaris.opengrok.configuration.Project", "org.opensolaris.opengrok.configuration.RuntimeEnvironment", "org.opensolaris.opengrok.authorization.IAuthorizationPlugin", "org.opensolaris.opengrok.util.*", "org.opensolaris.opengrok.logger.*", };
并且禁止这些包被继承。
private final static String[] packageBlacklist = new String[]{ "java", "javax", "org.w3c", "org.xml", "org.omg", "sun" };
并且JVM也会禁止你去继承一些不应被继承的包。
设置
插件类必须编译成 .class 文件(也可以打包成 .jar 文件)。框架同时支持 .class 和 .jar 文件。 为了编译,你必须在classpath中提供 opengrok.jar,以及 servlet api (HttpServletRequest
) 。
例如 (对于包含在仓库中的 TruePlugin
):$ javac -classpath dist/opengrok.jar -d . plugins/TruePlugin.java
然后,你可以将编译后的 .class 文件放到插件目录中,并部署web应用。如果插件是包的一部分,你必须复制编译器生成的相对源目录的整个文件夹。
如果发生错误,你应该从log中查找信息。
运行
框架由三部分组成
-
插件框架
它是插件的容器
- 执行授权决策
- 缓存授权结果,以便每个请求的授权,特定的插件的针对Project/Group的isAllowed方法只会被调用一次。
-
ProjectHelper
过滤(授权)后的projects/groups/repositories 的界面呈现,应该只用于在任何地方显示过滤后的信息。
- 提供获取过滤后的 projects/groups/repositories 的方法
- 为每个请求缓存过滤结果,这样就可以避免不必要的框架调用
-
AuthorizationFilter
标准的
javax.servlet.Filter
用于所有urls,判定当前请求是否有权限访问该url。- 限制用户访问被禁止的xref/history/download 等。返回403 HTTP错误。
- 只有当url包含关于项目的信息时才会发生反应(所以它可以决定每个项目)
每一个403错误都会被记录。但是关于用户的信息是丢失的,因为决策是由插件做的,过滤器不知道如何解析请求来从中获取用户信息。
高级配置
新1.0版本包含了一个如何在更多细节上配置插件调用的新特性。
一堆插件
这些插件构成了一个列表的线性结构(称为堆栈)。这些插件的isAllowed方法的调用顺序是由它们在堆栈中的位置决定的。另外,你可以为每一个插件配置不同的标志,以便在执行鉴权时受到重视。
有三种标志:
-
REQUIRED 这种插件的失败会最终导致框架的授权失败,但是要等剩下的插件都执行完毕之后。
-
REQUISITE 类似 required,不过,一旦这种插件返回授权失败,控件会直接返回到应用程序。与第一个required 或 requisite插件相关联的返回值置为失败。
-
SUFFICIENT 如果这种插件成功鉴权,并且之前没有失败的REQUIRED插件,授权框架立刻会给应用层返回成功信息,而不再调用堆栈中的其他插件。sufficient插件的失败会被忽略,然后继续处理插件列表,最终结果不受影响。
这些是受到了 PAM Authorization framework 的启发,这些定义也都是直接取自PAM配置的手册页。不过OpenGrok并没有实现所有的PAM标志。
向后兼容
你可以使用授权框架而不必提供这些高级配置,是因为所有已经加载的没有这个高级配置的插件,都被追加了REQUIRED到列表中。可是,由于类发现的特性,意味着这些插件被调用是非常随机的。
示例
这是一个 "read-only configuration"的例子。它定义了三个插件,每一个都有不同的规则,以此来从不同的方式影响堆栈的处理进程。作为授权规则,用类名来指定目标插件,把其中一种标志作为授权角色。这个堆栈稍后对每个请求从上到下进行处理(因为它是一个列表),对带有标志的特定插件的决策进行评估。
1 <void property="pluginStack"> 2 <void method="add"> 3 <object class="org.opensolaris.opengrok.authorization.AuthorizationPlugin"> 4 <void property="flag"> 5 <string>REQUISITE</string> 6 </void> 7 <void property="name"> 8 <string>some.my.package.RequisitePlugin</string> 9 </void> 10 </object> 11 </void> 12 <void method="add"> 13 <object class="org.opensolaris.opengrok.authorization.AuthorizationPlugin"> 14 <void property="flag"> 15 <string>SUFFICIENT</string> 16 </void> 17 <void property="name"> 18 <string>Plugin</string> 19 </void> 20 </object> 21 </void> 22 <void method="add"> 23 <object class="org.opensolaris.opengrok.authorization.AuthorizationPlugin"> 24 <void property="flag"> 25 <string>REQUIRED</string> 26 </void> 27 <void property="name"> 28 <string>ExamplePlugin</string> 29 </void> 30 </object> 31 </void> 32 </void>
这个配置的一个典型应用可能是:
RequisitePlugin
- 在这个requisite plugin 中确定用户标识,并将其保存在请求中。
- 当这个动作成功后返回true或false。
Plugin
- 使用在requisite plugin 中保存在请求中的确定了的用户标识。
- 执行一项授权检查,如果成功,该检查可以导致立刻从堆栈中返回。
- 如果检查是否定值,堆栈会继续执行第三个插件。
ExamplePlugin
- 使用在requisite plugin 中保存在请求中的确定了的用户标识。
- 过滤掉之前的插件执行时没有被启用的部分。
HTTP基础教程
这是一个使用http基础授权的例子。
这个例子有几个前提条件:
- 你有一个干净的正在工作中的OpenWork实例(无项目,无群组)
- 你会使用 Groups 子命令。(当你在tools下的克隆github仓时会用到)
- 你有权限修改Tomcat文件
- 你已经设置过插件目录或你知道它在哪 (见上面)
设置Tomcat
Tomcat支持HTTP,并且我们可以配置一个简单的示例,包含用户和角色。在本向导中,将使用我们自己定义的用户和角色来介绍如何设置Tomcat。
Tomcat users
我们需要修改一个Tomcat文件来提供该系统中的用户和角色信息。这个文件放置在 $CATALINA_BASE/conf/tomcat-users.xml
. 为了这个示例,添加这些行到该文件的 <tomcat-users>节点中。
<role rolename="users"/> <role rolename="admins"/> <role rolename="plugins"/> <role rolename="ghost"/> <user username="007" password="123456" roles="users"/> <user username="008" password="123456" roles="plugins"/> <user username="009" password="123456" roles="users,admins"/> <user username="00A" password="123456" roles="admins"/> <user username="00B" password="123456" roles="admins,plugins"/> <user username="00F" password="123456" roles="ghost"/>
通过这几行,我们告诉Tomcat设置4个角色和6个关联他们角色的用户。你可以根据你的需要修改用户名和密码,但是我们将在这个示例中用到这些。
Application deployment descriptor
现在,我们需要告诉应用程序,它应该使用HTTP Basic认证来保护它的资源。我们可以通过修改web.xml来做到,这个文件通常被放置在你应用程序的WEB-INF文件夹中。为了让它工作,下面这些行是必须的。
<security-constraint> <web-resource-collection> <url-pattern>/*</url-pattern> <!-- protect the whole application --> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>plugins</role-name> <!-- these are the roles from tomcat-users.xml --> <role-name>users</role-name> <!-- these are the roles from tomcat-users.xml --> <role-name>admins</role-name> <!-- these are the roles from tomcat-users.xml --> </auth-constraint> <user-data-constraint> <!-- transport-guarantee can be CONFIDENTIAL, INTEGRAL, or NONE --> <transport-guarantee>NONE</transport-guarantee> </user-data-constraint> </security-constraint> <security-role> <role-name>plugins</role-name> <role-name>users</role-name> <role-name>admins</role-name> </security-role> <login-config> <auth-method>BASIC</auth-method> </login-config>
监听服务
我们强烈建议你打开监听服务,它适合于开发插件,通过“read-only configuration”设置参数为true.
这会强制应用程序在当插件目录下有任何文件发生修改时,重新加载插件目录下的所有插件。
设置代码仓库
我们需要创建几个测试库来体现授权特性。
$ cd "$OPENGROK_SRC_ROOT" # navigate to the source folder $ for name in `seq 1 11`; do mkdir "test-project-$name"; cd "test-project-$name"; echo "Give it a try! Hello from $i" > README.md; git init; git config user.email "x"; git config user.name "y"; git add .; git commit -m "init commit"; cd ..; done;
这会在source目录下创建10个名为“test-project-$number”的测试库。
设置分组
为了真正的使用授权特性,建议创建Project群组。在授权插件中,你可以使用整个群组来代替单个项目。
我们将使用一个名为Groups的工具来创建它们。
$ export OPENGROK_READ_XML_CONFIGURATION=/var/opengrok/opt/myconf.xml $ ./tools/Groups empty > "$OPENGROK_READ_XML_CONFIGURATION" $ ./tools/Groups add admins "test-project-1|test-project-2|test-project-3|test-project-4" -u $ ./tools/Groups add users "test-project-5|test-project-6|test-project-7|test-project-8" -u $ ./tools/Groups add plugins "test-project-9|test-project-10" -p users -u
组名与之前在tamcat-users.xml中定义的角色对应。现在最终的群组结构应该看起来像这样:
$ ./tools/Groups list admins ~ "test-project-1|test-project-2|test-project-3|test-project-4" users ~ "test-project-5|test-project-6|test-project-7|test-project-8" plugins ~ "test-project-9|test-project-10"
OPENGROK_READ_XML_CONFIGURATION
这个变量包含了一个指向xml文件路径,该xml文件最终在index/reindex前与主配置文件(通常是 /var/opengrok/configuration.xml
)合并。如果你没有使用过这个,你可以用下面语句生成一个新的(示例):
$ export OPENGROK_READ_XML_CONFIGURATION=/var/opengrok/opt/myconf.xml $ ./tools/Groups add admins "test-project-1|test-project-2|test-project-3|test-project-4" > "$OPENGROK_READ_XML_CONFIGURATION"
这个命令生成了一个“admins”群组的配置项,并将其保存到了 "$OPENGROK_READ_XML_CONFIGURATION
" 文件中.
更多关于这个过程的描述,参考 Advanced Configuration
索引
现在像往常一样执行索引。不要忘了使用前一步中使用过的 OPENGROK_READ_XML_CONFIGURATION
变量,因为它会将群组传递到主配置文件中。这是如何在reindex时保存群组的唯一选择 - 使用 OPENGROK_READ_XML_CONFIGURATION
变量!
插件
现在进行主体部分 - 插件本身。
它包含三部分:
- 权限策略
- 发现群组
- 授权检查
权限策略
你可以使用你想使用的无论什么策略,甚至是java或你操作系统中的外置工具。在这个示例中,我们将使用静态映射表,定义哪些用户和群组有权限访问哪些项目。
private static final Map<String, Set<String>> userProjects = new TreeMap<>(); private static final Map<String, Set<String>> userGroups = new TreeMap<>(); static { // all have access to "test-project-11" and some to other "test-project-5" or "test-project-8" userProjects.put("007", new TreeSet<>(Arrays.asList(new String[]{"test-project-11", "test-project-5"}))); userProjects.put("008", new TreeSet<>(Arrays.asList(new String[]{"test-project-11", "test-project-8"}))); userProjects.put("009", new TreeSet<>(Arrays.asList(new String[]{"test-project-11"}))); userProjects.put("00A", new TreeSet<>(Arrays.asList(new String[]{"test-project-11"}))); userProjects.put("00B", new TreeSet<>(Arrays.asList(new String[]{"test-project-11"}))); } static { userGroups.put("007", new TreeSet<>(Arrays.asList(new String[]{}))); userGroups.put("008", new TreeSet<>(Arrays.asList(new String[]{}))); userGroups.put("009", new TreeSet<>(Arrays.asList(new String[]{}))); userGroups.put("00A", new TreeSet<>(Arrays.asList(new String[]{}))); userGroups.put("00B", new TreeSet<>(Arrays.asList(new String[]{}))); }
群组发现
运行在插件上层的插件框架在做决策的时候,无法知道我们的插件想做什么,所以没有途径可以在群组中自动发现groups/subgroups/projects。所有这些都必须在我们的插件中完成,最好是只在特定用户首次加载/使用插件时完成(本示例中不包含)。
非常重要的含义是,允许一个群组并不意味着允许它的子群组或项目。可以考虑将这个视为一个特性,因为它让你允许或不允许特定的群组和项目有更多的自由。
这是最重要的部分 - 如果你为一个用户发现了一个群组,那就把它的所有项目、仓库、子群组和底层对象都添加进去。
if ((g = Group.getByName(group)) != null) { // group discovery for (Project p : g.getRepositories()) { userProjects.get(request.getUserPrincipal().getName()).add(p.getDescription()); } for (Project p : g.getProjects()) { userProjects.get(request.getUserPrincipal().getName()).add(p.getDescription()); } for (Group grp : g.getDescendants()) { for (Project p : grp.getRepositories()) { userProjects.get(request.getUserPrincipal().getName()).add(p.getDescription()); } for (Project p : grp.getProjects()) { userProjects.get(request.getUserPrincipal().getName()).add(p.getDescription()); } descendants.add(grp.getName()); } while (g != null) { descendants.add(g.getName()); g = g.getParent(); } }
最后,userProjects和userGroups是User-Projects/Groups的一组映射,在这里我们可以通过只遍历映射表和对应的集合,快速的判断一个用户是否有指定实体的权限。
授权检查
最终的授权检查只是一个示例 - 检查一个这个map是否包含某个用户的Project/Group。
// for projects return userProjects.get(request.getUserPrincipal().getName()).contains(project.getDescription()); // or for groups return userGroups.get(request.getUserPrincipal().getName()).contains(group.getName());
运行
现在我们可以按照当前wiki页面上边介绍的方式来编译插件,并将编译结果放到插件目录 (默认 $OPENGROK_DATA_ROOT/plugins
)。如果你开启了监听服务,那你可以看到结果了,如果没有,你需要重启应用程序。
当你进入到应用程序中,页面会直接触发一个登录表单,在这里你可以输入认证信息(如在 tomcat users 中所写)。根据你进入时的输入,你可以在主页面看到过滤后的结果,并且即使在你搜索任何东西时,你也无法访问其他项目。
你可以退出登录,通过忘记密码会话(ctrl+shift+delete
+ 忘记当前会话/不同浏览器可能有所不同)来用其他账户登录。
完成的代码
HttpBasicAuthorizationPlugin.java
疑难问题解决
使用IDE
当使用IDE(NetBeans)编译你的插件时,你会面临一个问题,框架因为ClassFormatError而不会加载你的.jar文件导致ClassNotFoundException。
可能的解决方法是:
- 禁用 debugging 标志(project/properities/build/compile/uncheck 生成 debugging 信息)
- 手动通过javac编译 .java文件,然后手动打包到 .jar中(命令如上)
- 手动通过javac编译 .java文件,使用这些目录结构(不打包)
这些应该可以解决这个问题。如果不能,尝试修改代码。