本keystone源码分析系列基于Juno版Keystone,于2014年10月16日随Juno版OpenStack发布。
Keystone作为OpenStack中的身份管理与授权模块,主要实现系统用户的身份认证、基于角色的授权管理、其他OpenStack服务的地址发现和安全策略管理等功能。Keystone作为开源云系统OpenStack中至关重要的组成部分,与OpenStack中几乎所有的其他服务(如Nova, Glance, Neutron等)都有着密切的联系。同时,Keystone作为开源的身份管理和授权系统,对于其当前实现机制的探讨已经成为许多信息安全领域研究人员的一个重要方向,基于其提出的安全模型与扩展实现已经很多,这里我们并不赘述这些学术成果。
由于作者精力和能力有限,希望得到读者的反馈与指正,转载请注明出处。
在分析任何一个系统前安装并了解该系统的使用方法都是有益的,Keystone也不例外。关于如何安装和配置Keystone请参考本博客的其他随笔,这里我们假设读者已经熟悉OpenStack中每个服务的.conf和paste.ini配置文件的大体作用。
首先介绍我的系统环境,由于重点关注Keystone的相关机理,我在团队已有10台服务器组成的小型云外搭建了一个Keystone的开发调试环境。当前使用的是Ubuntu 14.04 LTS Desktop,系统中的Keystone采用Ubuntu的apt工具安装,源码采用git下载,并且都是使用的Juno版本的分支,配置文件集中于/etc/keystone目录下。
首先我们查看/etc/keystone/keystone-paste.ini文件,这里简要的介绍这个文件的大体结构与含义。
# /etc/keystone/keystone-paste.ini # filters [filter:token_auth] paste.filter_factory = keystone.middleware:TokenAuthMiddleware.factory
[filter:admin_token_auth] paste.filter_factory = keystone.middleware:AdminTokenAuthMiddleware.factory ... #applications [app:service_v3] paste.app_factory = keystone.service:v3_app_factory ...
# pipelines [pipeline:api_v3] pipeline = sizelimit url_normalize build_auth_context token_auth admin_token_auth xml_body_v3 json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension service_v3 ...
#composites [composite:main] use = egg:Paste#urlmap /v2.0 = public_api /v3 = api_v3 / = public_version_api [composite:admin] use = egg:Paste#urlmap /v2.0 = admin_api /v3 = api_v3 / = admin_version_api
可以发现这个文件事实上由若干filter, pipeline, application和composite的定义组成,在定义时并非必须严格按照这样的顺序进行隔离,这里为了分析的方便我们将其总结为这些段落。对于这些内容的具体含义,可以参考Paste Deploy和Python WSGI相关的介绍。
Keystone的代码生态圈包括keystone, python-keystoneclient和keystonemiddleware。其中keystone以WSGI服务器的方式实现,其监听python-keystoneclient和keystonemiddleware发送来的HTTP请求并作出相应响应。
keystone-paste.ini文件事实上是一个paste-deploy配置文件,该文件由application, filter, pipeline, composite等定义段落组成。composite是第一层调度者,它粗略地根据不同的url类型将请求分配到不同的pipeline上。每一个pipeline由若干filter和一个application组成。正如其名,filter按照其在pipeline中的先后顺序依次对http请求进行过滤,包括对参数进行格式化处理等操作。application位于pipeline的末尾,每一个pipeline只有一个application。最终通过所有filter的请求被application进一步调度到系统实现上每一个模块的路由层(routers),路由层根据HTTP请求方法和具体的请求路径,将HTTP请求分发给对应的控制层(controllers),controllers集中实现业务逻辑,并通过调用更低的驱动层完成底层的工作,如数据库的读写等等。这些构成了composite, pipeline, filter, application和keystone实现间的逻辑关系。
keystone-paste.ini文件从一个高层次定义了keystone所需的composite会将哪些类型的url交由由哪些pipeline, 每一个pipeline由哪些filter和app组成,以及具体到每一个filter和app是由系统源码的哪一个部分实现的,事实上,从下文的分析可以看出,每一个filter在实现上都对应着一个特定的类,而每一个application在实现上则对应着一个具体的方法。
以前文节选的部分keystone-paste.ini文件为例,我们以一个OpenStack系统要求的REST风格的HTTP请求视角看一下系统是如何实现的:
当用户以HTTP POST方式请求http://localhost:5000/v3/auth/tokens这个url时,意味着用户希望进行身份认证,同时获得keystone签发的Token。当然,用户需要在自己的HTTP请求中给出一些自己的身份信息,这样keystone才能据以判断该用户是否是系统合法的用户,并根据其拥有的角色和权限为其签发token。
下面是一个这样的HTTP请求例子,采用cURL命令行工具模拟,-d 代表了POST方法,-i参数说明我们要求系统显示未来获得的响应(response)的header,-H参数指明了我们的请求携带的header,这里向keystone指出我们接受的网络数据传输类型。
curl -i \ -H "Content-Type: application/json" \ -d ' { "auth": { "identity": { "methods": [ "password" ], "password": { "user": { "domain":{ "name": "Default" }, "name": "USER_NAME", "password": "PASSWORD" } } }, "scope": { "project": { "domain": { "name":"Default" }, "name": "PROJECT" } } } }' \ http://127.0.0.1:5000/v3/auth/tokens
系统首先根据paste-deploy配置文件结合请求的url类型(/v3)来判断处理该请求的pipeline即api_v3,接着系统将该请求交由该pipeline来处理。pipeline api_v3中的所有filter会依次对该请求的header以及data等内容进行处理,比如过滤器sizelimit在进行一定的操作后,将其传给下一个过滤器url_normalize,依次类推,直到给请求被传递给管道尽头的application。
我们以两个filter为例进行探讨,分别是token_auth和admin_token_auth,在该paste-deploy文件的filter定义字段,我们能够找到这两个filter的实现位置,事实上这也是系统搜寻每一个过滤器具体实现的依据。可以看到token_auth是由keystone.middleware:TokenAuthMiddleware.factory实现的,那么我们到keystone/middleware/core.py中找到TokenAuthMiddleware这个类:
#keystone/middleware/core.py
class TokenAuthMiddleware(wsgi.Middleware): def process_request(self, request): token = request.headers.get(AUTH_TOKEN_HEADER) context = request.environ.get(CONTEXT_ENV, {}) context['token_id'] = token if SUBJECT_TOKEN_HEADER in request.headers: context['subject_token_id'] = ( request.headers.get(SUBJECT_TOKEN_HEADER)) request.environ[CONTEXT_ENV] = context
发现该类继承了wsgi.Middleware,说明这个类实质上是一个WSGI中间件。该中间件提取了http请求中的X-Auth-Token字段和(可选的)X-Subject-Token字段的值,将context中相应的字段填充为这两个值,然后将修改后的上下文写回到请求携带的环境信息中,传递给下一个filter即中间件admin_token_auth。再来看filter admin_token_auth是如何实现的,首先定位其实现的位置:keystone.middleware:AdminTokenAuthMiddleware.factory,接着到源码keystone/middleware/core.py模块下找到相应的AdminTokenAuthMiddleware类,
#keystone/middleware/core.py
class AdminTokenAuthMiddleware(wsgi.Middleware): """A trivial filter that checks for a pre-defined admin token. Sets 'is_admin' to true in the context, expected to be checked by methods that are admin-only. """ def process_request(self, request): token = request.headers.get(AUTH_TOKEN_HEADER) context = request.environ.get(CONTEXT_ENV, {}) context['is_admin'] = (token == CONF.admin_token) request.environ[CONTEXT_ENV] = context、
可见这个过滤器的功能也非常简单,就是从http请求header中的X-Auth-Token字段提取附带的token,同时解析keystone主配置文件(keystone.conf)中[DEFAULT]字段下的admin_token选项,二者进行比对,如果结果相同,说明这个请求的发送者是我们系统默认的admin,在context字典的is_admin字段设1后写回到请求的环境信息,否则在context字典的is_admin字段置0。当然,熟悉keystone官方文档的用户会发现,在keystone的官方文档中强烈建议生产环境中删除该中间件,同时不设置keystone.conf文件中[DEFAULT]字段下的admin_token选项,因为该token的出示者将会获得系统的最高权限,因此禁用该账户能够避免一些不必要的攻击。
从上面的两个例子可以看到,每一个filter进行一个具体的操作,这些操作比较简单和独立,彼此按先后顺序串联起来,如本例中的过滤器token_auth放置在过滤器admin_token_auth之前,这就使得系统在对context的is_admin字段进行填充以前,会对token_id和subject_token_id字段进行填充。
最后我们看一下application是如何实现的。在pipeline api_v3的末端对应着最终的服务器应用service_v3,我们根据keystone-paste.ini文件中给出的位置paste.app_factory = keystone.service:v3_app_factory找到该app的具体实现:keystone/service.py,
#keystone/service.py ...
@fail_gracefully def v3_app_factory(global_conf, **local_conf): controllers.register_version('v3') mapper = routes.Mapper() sub_routers = [] _routers = [] router_modules = [assignment, auth, catalog, credential, identity, policy] if CONF.trust.enabled: router_modules.append(trust) for module in router_modules: routers_instance = module.routers.Routers() _routers.append(routers_instance) routers_instance.append_v3_routers(mapper, sub_routers) # Add in the v3 version api sub_routers.append(routers.VersionV3('admin', _routers)) sub_routers.append(routers.VersionV3('public', _routers)) return wsgi.ComposingRouter(mapper, sub_routers)
...
可以看到,该application将会实现第二层路由(第一层由keystone-paste.ini文件中的composite字段实现),此次路由将具体的请求处理工作进一步分发到系统的各个模块上,比如代码中的assignment,auth, catalog等等。由具体的模块根据请求的具体路径和内容完成具体功能。
装饰器@fail_gracefully在keystone/service.py中也有定义,主要功能是包裹住前文中的函数v3_app_factory,正如名称所示,让被装饰的v3_app_factory()函数能够“优雅”地执行,而由fail_gracefully()来处理可能的日志记录和异常处理等善后工作。
#keystone/service.py ... def fail_gracefully(f): """Logs exceptions and aborts.""" @functools.wraps(f) def wrapper(*args, **kw): try: return f(*args, **kw) except Exception as e: LOG.debug(e, exc_info=True) # exception message is printed to all logs LOG.critical(e) sys.exit(1) return wrapper ...
至此,我们从WSGI和paste-deploy的角度迈出了深入了解keystone实现的第一步,知道了keystone服务器是如何将一个HTTP请求粗略地归类分发到pipeline,再通过filter到达相应的app。下一步,我们将会看到每一个pipeline末端的app如何针对具体的HTTP请求方法和地址将其分发到对应的router,router再将其交给相应的controller,由controller承上启下完成最终的工作的。