本keystone源碼分析系列基于Juno版Keystone,于2014年10月16日隨Juno版OpenStack發(fā)布。
Keystone作為OpenStack中的身份管理與授權模塊,主要實現(xiàn)系統(tǒng)用戶的身份認證、基于角色的授權管理、其他OpenStack服務的地址發(fā)現(xiàn)和安全策略管理等功能。Keystone作為開源云系統(tǒng)OpenStack中至關重要的組成部分,與OpenStack中幾乎所有的其他服務(如Nova, Glance, Neutron等)都有著密切的聯(lián)系。同時,Keystone作為開源的身份管理和授權系統(tǒng),對于其當前實現(xiàn)機制的探討已經(jīng)成為許多信息安全領域研究人員的一個重要方向,基于其提出的安全模型與擴展實現(xiàn)已經(jīng)很多,這里我們并不贅述這些學術成果。
由于作者精力和能力有限,希望得到讀者的反饋與指正,轉載請注明出處。
在分析任何一個系統(tǒng)前安裝并了解該系統(tǒng)的使用方法都是有益的,Keystone也不例外。關于如何安裝和配置Keystone請參考本博客的其他隨筆,這里我們假設讀者已經(jīng)熟悉OpenStack中每個服務的.conf和paste.ini配置文件的大體作用。
首先介紹我的系統(tǒng)環(huán)境,由于重點關注Keystone的相關機理,我在團隊已有10臺服務器組成的小型云外搭建了一個Keystone的開發(fā)調試環(huán)境。當前使用的是Ubuntu 14.04 LTS Desktop,系統(tǒng)中的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
可以發(fā)現(xiàn)這個文件事實上由若干filter, pipeline, application和composite的定義組成,在定義時并非必須嚴格按照這樣的順序進行隔離,這里為了分析的方便我們將其總結為這些段落。對于這些內容的具體含義,可以參考Paste Deploy和Python WSGI相關的介紹。
? Keystone的代碼生態(tài)圈包括keystone, python-keystoneclient和keystonemiddleware。其中keystone以WSGI服務器的方式實現(xiàn),其監(jiān)聽python-keystoneclient和keystonemiddleware發(fā)送來的HTTP請求并作出相應響應。
keystone-paste.ini文件事實上是一個paste-deploy配置文件,該文件由application, filter, pipeline, composite等定義段落組成。composite是第一層調度者,它粗略地根據(jù)不同的url類型將請求分配到不同的pipeline上。每一個pipeline由若干filter和一個application組成。正如其名,filter按照其在pipeline中的先后順序依次對http請求進行過濾,包括對參數(shù)進行格式化處理等操作。application位于pipeline的末尾,每一個pipeline只有一個application。最終通過所有filter的請求被application進一步調度到系統(tǒng)實現(xiàn)上每一個模塊的路由層(routers),路由層根據(jù)HTTP請求方法和具體的請求路徑,將HTTP請求分發(fā)給對應的控制層(controllers),controllers集中實現(xiàn)業(yè)務邏輯,并通過調用更低的驅動層完成底層的工作,如數(shù)據(jù)庫的讀寫等等。這些構成了composite, pipeline, filter, application和keystone實現(xiàn)間的邏輯關系。
keystone-paste.ini文件從一個高層次定義了keystone所需的composite會將哪些類型的url交由由哪些pipeline, 每一個pipeline由哪些filter和app組成,以及具體到每一個filter和app是由系統(tǒng)源碼的哪一個部分實現(xiàn)的,事實上,從下文的分析可以看出,每一個filter在實現(xiàn)上都對應著一個特定的類,而每一個application在實現(xiàn)上則對應著一個具體的方法。
以前文節(jié)選的部分keystone-paste.ini文件為例,我們以一個OpenStack系統(tǒng)要求的REST風格的HTTP請求視角看一下系統(tǒng)是如何實現(xiàn)的:
當用戶以HTTP POST方式請求http://localhost:5000/v3/auth/tokens這個url時,意味著用戶希望進行身份認證,同時獲得keystone簽發(fā)的Token。當然,用戶需要在自己的HTTP請求中給出一些自己的身份信息,這樣keystone才能據(jù)以判斷該用戶是否是系統(tǒng)合法的用戶,并根據(jù)其擁有的角色和權限為其簽發(fā)token。
下面是一個這樣的HTTP請求例子,采用cURL命令行工具模擬,-d 代表了POST方法,-i參數(shù)說明我們要求系統(tǒng)顯示未來獲得的響應(response)的header,-H參數(shù)指明了我們的請求攜帶的header,這里向keystone指出我們接受的網(wǎng)絡數(shù)據(jù)傳輸類型。
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
系統(tǒng)首先根據(jù)paste-deploy配置文件結合請求的url類型(/v3)來判斷處理該請求的pipeline即api_v3,接著系統(tǒng)將該請求交由該pipeline來處理。pipeline api_v3中的所有filter會依次對該請求的header以及data等內容進行處理,比如過濾器sizelimit在進行一定的操作后,將其傳給下一個過濾器url_normalize,依次類推,直到給請求被傳遞給管道盡頭的application。
我們以兩個filter為例進行探討,分別是token_auth和admin_token_auth,在該paste-deploy文件的filter定義字段,我們能夠找到這兩個filter的實現(xiàn)位置,事實上這也是系統(tǒng)搜尋每一個過濾器具體實現(xiàn)的依據(jù)??梢钥吹絫oken_auth是由keystone.middleware:TokenAuthMiddleware.factory實現(xiàn)的,那么我們到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
發(fā)現(xiàn)該類繼承了wsgi.Middleware,說明這個類實質上是一個WSGI中間件。該中間件提取了http請求中的X-Auth-Token字段和(可選的)X-Subject-Token字段的值,將context中相應的字段填充為這兩個值,然后將修改后的上下文寫回到請求攜帶的環(huán)境信息中,傳遞給下一個filter即中間件admin_token_auth。再來看filter admin_token_auth是如何實現(xiàn)的,首先定位其實現(xiàn)的位置: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選項,二者進行比對,如果結果相同,說明這個請求的發(fā)送者是我們系統(tǒng)默認的admin,在context字典的is_admin字段設1后寫回到請求的環(huán)境信息,否則在context字典的is_admin字段置0。當然,熟悉keystone官方文檔的用戶會發(fā)現(xiàn),在keystone的官方文檔中強烈建議生產(chǎn)環(huán)境中刪除該中間件,同時不設置keystone.conf文件中[DEFAULT]字段下的admin_token選項,因為該token的出示者將會獲得系統(tǒng)的最高權限,因此禁用該賬戶能夠避免一些不必要的攻擊。
從上面的兩個例子可以看到,每一個filter進行一個具體的操作,這些操作比較簡單和獨立,彼此按先后順序串聯(lián)起來,如本例中的過濾器token_auth放置在過濾器admin_token_auth之前,這就使得系統(tǒng)在對context的is_admin字段進行填充以前,會對token_id和subject_token_id字段進行填充。
最后我們看一下application是如何實現(xiàn)的。在pipeline api_v3的末端對應著最終的服務器應用service_v3,我們根據(jù)keystone-paste.ini文件中給出的位置paste.app_factory = keystone.service:v3_app_factory找到該app的具體實現(xiàn):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將會實現(xiàn)第二層路由(第一層由keystone-paste.ini文件中的composite字段實現(xiàn)),此次路由將具體的請求處理工作進一步分發(fā)到系統(tǒng)的各個模塊上,比如代碼中的assignment,auth, catalog等等。由具體的模塊根據(jù)請求的具體路徑和內容完成具體功能。
裝飾器@fail_gracefully在keystone/service.py中也有定義,主要功能是包裹住前文中的函數(shù)v3_app_factory,正如名稱所示,讓被裝飾的v3_app_factory()函數(shù)能夠“優(yōu)雅”地執(zhí)行,而由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實現(xiàn)的第一步,知道了keystone服務器是如何將一個HTTP請求粗略地歸類分發(fā)到pipeline,再通過filter到達相應的app。下一步,我們將會看到每一個pipeline末端的app如何針對具體的HTTP請求方法和地址將其分發(fā)到對應的router,router再將其交給相應的controller,由controller承上啟下完成最終的工作的。
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
