本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承上啟下完成最終的工作的。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

