? ? ? ? 眾所周知,OpenStack使用基于角色的權(quán)限訪問控制(RBAC),在RBAC中,權(quán)限與角色相關(guān)聯(lián),用戶通過成為適當(dāng)角色的成員而得到這些角色的權(quán)限。這就極大地簡化了權(quán)限的管理。在一個(gè)組織中,角色是為了完成各種工作而創(chuàng)造,用戶則依據(jù)它的責(zé)任和資格來被指派相應(yīng)的角色,用戶可以很容易地從一個(gè)角色被指派到另一個(gè)角色。角色可依新的需求和系統(tǒng)的合并而賦予新的權(quán)限,而權(quán)限也可根據(jù)需要而從某角色中回收。角色與角色的關(guān)系可以建立起來以囊括更廣泛的客觀情況。為了更好的適應(yīng)OpenStack的角色權(quán)限管理,oslo項(xiàng)目創(chuàng)建了oslo.policy子項(xiàng)目為所有OpenStack服務(wù)提供RBAC策略實(shí)施支持。本文將詳細(xì)介紹oslo.policy的實(shí)現(xiàn)與使用。
在通用的策略引擎實(shí)現(xiàn)中,策略規(guī)則表達(dá)式一般包含一個(gè)目標(biāo)和一個(gè)相關(guān)聯(lián)的規(guī)則。具體示例如下:
"<target>": <rule> ?
其中,target指定了正在執(zhí)行策略的服務(wù);通常,一個(gè)target指的是一個(gè)API調(diào)用。rule代表了具體的策略規(guī)則,一般可以用以下兩種形式表示:一個(gè)使用新策略語法寫成的字符串或者一個(gè)策略規(guī)則的列表。OpenStack推薦使用字符串格式,因?yàn)樗菀桌斫狻?/p>
在策略語法中,每個(gè)規(guī)則檢查都被指定為一個(gè)簡單的"a:b"對形式,該"a:b"對通過匹配與之對應(yīng)的類來執(zhí)行檢查訪問權(quán)限。這些"a:b"對的類型與格式可以歸納為表1所示。
表1 策略語法類型與格式
類型格式
用戶的角色role:admin
policy中定義的規(guī)則rule:admin_required
拒絕訪問的URL(URL檢查返回True才有效)http://my-url.org/check
用戶屬性(可通過token獲得,包括user_id、domain_id和project_id等)project_id:%(target.project.id)s
字符串:’xpto2035abc’
‘myproject’:
字面量project_id:xpto2035abc
domain_id:20
True:%(user.enabled)s
在字符串格式的策略規(guī)則表達(dá)式中,如果需要使用多個(gè)規(guī)則,可以使用連接運(yùn)算符and或or,and表示與,or表示或。下面的例子表示允許角色為admin的用戶或項(xiàng)目ID為%(project_id)s且角色為projectadmin的用戶訪問。
"role:admin?or?(project_id:%(project_id)s?and?role:projectadmin)"
另外,策略規(guī)則表達(dá)式中還可以使用not運(yùn)算符表取反。下面的例子表示允許項(xiàng)目ID為%(project_id)s且角色不是dunce的用戶訪問。
"project_id:%(project_id)s?and?not?role:dunce"
在策略規(guī)則表達(dá)式中,各運(yùn)算符的優(yōu)先級(jí)如表2所示。其中,數(shù)字越大代表優(yōu)先級(jí)越高。
表2 策略規(guī)則表達(dá)式運(yùn)算符優(yōu)先級(jí)
優(yōu)先級(jí)類型表達(dá)式
4組運(yùn)算(...)
3邏輯否運(yùn)算not ...
2邏輯與運(yùn)算... and ...
1邏輯或運(yùn)算... or ...
在列表形式的策略規(guī)則表達(dá)式中,使用"[]"表示邏輯與運(yùn)算,在"[]"內(nèi)的多個(gè)規(guī)則使用","連接,在進(jìn)行權(quán)限檢查時(shí),只有"[]"中的所有規(guī)則都滿足才可以通過檢查;而同一級(jí)的不同"[]"表示邏輯或運(yùn)算,用","連接,表示只要滿足其中一個(gè)"[]"定義的規(guī)則即可訪問;另外,使用"@"表示始終允許訪問,使用"!"表示拒絕訪問。基于此,上述示例表達(dá)式使用列表形式可以表示如下:
[["role:admin"],?["project_id:%(project_id)s","role:projectadmin"]]
需要注意的是,如果一個(gè)規(guī)則定義為一個(gè)空列表[]或一個(gè)空字符串"",表示該target始終允許訪問。
在oslo.policy中,所有規(guī)則的封裝與檢查操作都定義在oslo_policy._checks模塊中,該模塊首先定義了一個(gè)BaseCheck抽象類,所有對規(guī)則檢查的封裝都需要繼承該抽象類。在繼承BaseCheck類時(shí),每個(gè)規(guī)則檢查類都需要覆寫__str__()方法,用于顯示該檢查的含義;還需要覆寫__call__()方法,在執(zhí)行具體的檢查操作時(shí)通過調(diào)用具體規(guī)則檢查類的__call__()方法實(shí)現(xiàn)。接下來,本文首先介紹oslo.policy中定義了規(guī)則檢查類。而針對"a:b"格式的策略規(guī)則表達(dá)式,oslo.policy定義了一個(gè)繼承BaseCheck類的Check類來表示,并為其定義了kind屬性表示該條規(guī)則檢查的類型,即a,match屬性表示具體匹配的值,即b。
GenericCheck類通常用于匹配與API調(diào)用一起發(fā)送的屬性。通過以下語法,策略引擎可以使用這些屬性(位于策略規(guī)則表達(dá)式的右側(cè)):
user_id:%(user.id)s
在上述語法中,右側(cè)的值是一個(gè)字符串或者使用規(guī)定的Python字符串替換??捎玫膶傩院椭等Q于使用公共策略引擎的程序。所有這些屬性(與用戶、API調(diào)用和上下文相關(guān)的)都可以相互檢查,或?qū)潭ǖ某A窟M(jìn)行檢查。
GenericCheck類可以對通過令牌獲取的以下幾種用戶屬性執(zhí)行策略檢查:
user_id
domain_id和project_id(取決于token指定的范圍)
給定的token范圍內(nèi)的角色role列表
特殊檢查類是相較于GenericCheck類而言的,特殊檢查類可以提供更加靈活的檢查機(jī)制。oslo.policy內(nèi)置的特殊檢查類包括RoleCheck、RuleCheck、HTTPCheck等。
RoleCheck類:該類用于檢查提供的憑證中是否存在指定的角色。一個(gè)角色檢查的表達(dá)式如下:
"role:<role_name>" ?
RuleCheck類:該類用于通過名稱引用另一個(gè)已定義的規(guī)則。這樣,一個(gè)普通的規(guī)則可以定義為可重用的規(guī)則,然后在其他規(guī)則中引用該規(guī)則。它還適用于將一組檢查定義為一個(gè)更具描述性的命名的情況,這樣便于策略的可讀性。一個(gè)規(guī)則檢查的表達(dá)式如下,在這個(gè)示例中,將定義好的admin_required規(guī)則進(jìn)行了重用。
"admin_required":"role:admin"
"<target>": "rule:admin_required" ?
HTTPCheck類:該類用于向遠(yuǎn)程服務(wù)器發(fā)送HTTP請求,以確定檢查結(jié)果。target和憑證將傳遞到遠(yuǎn)程服務(wù)器進(jìn)行檢查,如果遠(yuǎn)程服務(wù)器返回的響應(yīng)結(jié)果為True,則表示該操作通過權(quán)限驗(yàn)證。HTTP檢查的表達(dá)式如下,預(yù)期目標(biāo)URL包含字符串格式的關(guān)鍵字,這個(gè)關(guān)鍵字是目標(biāo)字典的key鍵。
"http:<target URI>" ?
"http://server.test/%(name)s"?
oslo.policy為第1節(jié)中提到的各種運(yùn)算符也提供了相應(yīng)的檢查類,主要包含以下幾種檢查類:
FalseCheck類:相當(dāng)于運(yùn)算符"!",即總是返回False,表示始終不允許訪問。
TrueCheck類:相當(dāng)于運(yùn)算符"@",即總是返回True,表示始終允許訪問。
NotCheck類:相當(dāng)于取反運(yùn)算符"not ..."。
AndCheck類:相當(dāng)于邏輯與運(yùn)算符"... and ..."。
OrCheck類:相當(dāng)于邏輯或運(yùn)算符"... or ..."。
除了上述的三類檢查類,用戶也可以自定義檢查類。在自定義檢查類時(shí),首先需要繼承BaseCheck類或Check類,然后覆寫__str__()和__call__()方法:__str__()方法用于返回以此節(jié)點(diǎn)為根的Check樹的字符串表示形式,用于打?。籣_call__()方法實(shí)現(xiàn)了具體的匹配算法。另外,還需要注意的是自定義檢查類時(shí)還應(yīng)該使用oslo_policy._checks模塊下的register(name, func=None)裝飾器將其緩存在檢查類字典中,便于查找。
3 oslo.policy的權(quán)限檢查實(shí)現(xiàn)原理
oslo.policy中權(quán)限檢查的實(shí)現(xiàn)主要定義在oslo_policy.policy模塊下。該模塊中定義了用于保存規(guī)則,加載和檢查規(guī)則以及定義策略的多個(gè)類。這些類的實(shí)現(xiàn)如下:
Rules類:該類用于緩存所有的規(guī)則,其可以直接處理default_rule的設(shè)置。該類提供了load(data, default_rule=None)和load(data, default_rule=None)可以從策略的YAML或JSON配置文件中加載所有規(guī)則;還提供了from_dict(rules_dict, default_rule=None)一個(gè)指定的字典中加載所有規(guī)則。
Enforcer類:該類負(fù)責(zé)加載和執(zhí)行規(guī)則。該類提供了load_rules(force_reload=False)從該類的實(shí)例化對象綁定的策略文件路徑加載所有規(guī)則,如果force_reload為True,表示從配置文件重新加載數(shù)據(jù);提供了register_default(default)注冊一個(gè)默認(rèn)的RuleDefault對象,提供了register_defaults(defaults)注冊一組RuleDefault對象;提供了enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs)根據(jù)目標(biāo)target和憑證檢查規(guī)則的權(quán)限,該方法通常需要結(jié)合authorize(self, rule, target, creds, do_raise=False, exc=None, *args, **kwargs)裝飾器使用,以保證待檢查的策略規(guī)則已經(jīng)被注冊;該類還提供了check_rules(raise_on_violation=False)檢查是否存在明顯不正確的規(guī)則,如未定義的規(guī)則或循環(huán)引用的規(guī)則等。
RuleDefault類:該類用于定義一個(gè)權(quán)限檢查策略,創(chuàng)建時(shí)需要指定名稱name和值check_str,建議對該策略定義一個(gè)詳細(xì)的說明description。
DocumentedRuleDefault類:該類用于定義一個(gè)policy-in-code的策略對象。該類的功能與RuleDefault類似,但它還需要一些與注冊的策略規(guī)則有關(guān)的額外的數(shù)據(jù)。這樣就可以根據(jù)這個(gè)類的屬性來呈現(xiàn)與之對應(yīng)的文檔。最終,oslo.policy都會(huì)使用該類而棄用RuleDefault。創(chuàng)建該類的對象時(shí),必須指定名稱name、值check_str、描述信息description;另外,還需要定義一個(gè)operations屬性,這個(gè)屬性包含了每個(gè)API的URL和相應(yīng)HTTP請求方法的字典。下面是一個(gè)operations屬性的示例。
operations=[{'path': '/foo', 'method': 'GET'}, ?
? ? ? ? ? ? {'path': '/some', 'method': 'POST'}]?
DeprecatedRule類:該類主要用于表示一個(gè)被廢棄的策略或規(guī)則。
本節(jié)結(jié)合1-3節(jié)的內(nèi)容以及nova組件介紹oslo.policy的使用方法。一般地,OpenStack其他組件可以直接使用oslo.policy庫實(shí)現(xiàn)RBAC策略,也可以對oslo.policy庫進(jìn)行擴(kuò)展,實(shí)現(xiàn)適合自身使用的RBAC策略。nova組件就是對oslo.policy的實(shí)現(xiàn)進(jìn)行了擴(kuò)展。
首先為了實(shí)現(xiàn)nova自身的需求,nova組件實(shí)現(xiàn)了一個(gè)檢查類。
@policy.register('is_admin') ?
class IsAdminCheck(policy.Check): ?
? ? """An explicit check for is_admin.""" ?
? ? def __init__(self, kind, match): ?
? ? ? ? """Initialize the check.""" ?
? ? ? ? self.expected = (match.lower() == 'true') ?
? ? ? ? super(IsAdminCheck, self).__init__(kind, str(self.expected)) ?
? ? def __call__(self, target, creds, enforcer): ?
? ? ? ? """Determine whether is_admin matches the requested value.""" ?
? ? ? ? return creds['is_admin'] == self.expected ?
這個(gè)類用于判斷用戶是否是admin用戶。接著,nova組件實(shí)現(xiàn)了一個(gè)初始化方法init()。
fromoslo_configimportcfg
fromoslo_logimportlog?as?logging
fromoslo_policyimportpolicy
definit(policy_file=None,?rules=None,?default_rule=None,?use_conf=True):
"""Init?an?Enforcer?class.
:param?policy_file:?Custom?policy?file?to?use,?if?none?is?specified,
`CONF.policy_file`?will?be?used.
:param?rules:?Default?dictionary?/?Rules?to?use.?It?will?be
considered?just?in?the?first?instantiation.
:param?default_rule:?Default?rule?to?use,?CONF.default_rule?will
be?used?if?none?is?specified.
:param?use_conf:?Whether?to?load?rules?from?config?file.
"""
global_ENFORCER
globalsaved_file_rules
ifnot_ENFORCER:
_ENFORCER?=?policy.Enforcer(CONF,
policy_file=policy_file,
rules=rules,
default_rule=default_rule,
use_conf=use_conf)
register_rules(_ENFORCER)
_ENFORCER.load_rules()
#?Only?the?rules?which?are?loaded?from?file?may?be?changed.
current_file_rules?=?_ENFORCER.file_rules
current_file_rules?=?_serialize_rules(current_file_rules)
#?Checks?whether?the?rules?are?updated?in?the?runtime
ifsaved_file_rules?!=?current_file_rules:
_warning_for_deprecated_user_based_rules(current_file_rules)
saved_file_rules?=?copy.deepcopy(current_file_rules)
該初始化方法創(chuàng)建了一個(gè)Enforcer對象,并將所有策略規(guī)則都加載到緩存中備用。然后,nova組件分別定義了兩個(gè)用于檢查規(guī)則的方法。
[python]view plaincopy
defauthorize(context,?action,?target,?do_raise=True,?exc=None):
"""Verifies?that?the?action?is?valid?on?the?target?in?this?context.
:param?context:?nova?context
:param?action:?string?representing?the?action?to?be?checked
this?should?be?colon?separated?for?clarity.
i.e.?``compute:create_instance``,
``compute:attach_volume``,
``volume:attach_volume``
:param?target:?dictionary?representing?the?object?of?the?action
for?object?creation?this?should?be?a?dictionary?representing?the
location?of?the?object?e.g.?``{'project_id':?context.project_id}``
:param?do_raise:?if?True?(the?default),?raises?PolicyNotAuthorized;
if?False,?returns?False
:param?exc:?Class?of?the?exception?to?raise?if?the?check?fails.
Any?remaining?arguments?passed?to?:meth:`authorize`?(both
positional?and?keyword?arguments)?will?be?passed?to
the?exception?class.?If?not?specified,
:class:`PolicyNotAuthorized`?will?be?used.
:raises?nova.exception.PolicyNotAuthorized:?if?verification?fails
and?do_raise?is?True.?Or?if?'exc'?is?specified?it?will?raise?an
exception?of?that?type.
:return:?returns?a?non-False?value?(not?necessarily?"True")?if
authorized,?and?the?exact?value?False?if?not?authorized?and
do_raise?is?False.
"""
init()
credentials?=?context.to_policy_values()
ifnotexc:
exc?=?exception.PolicyNotAuthorized
try:
result?=?_ENFORCER.authorize(action,?target,?credentials,
do_raise=do_raise,?exc=exc,?action=action)
exceptpolicy.PolicyNotRegistered:
with?excutils.save_and_reraise_exception():
LOG.exception(_LE('Policy?not?registered'))
exceptException:
with?excutils.save_and_reraise_exception():
LOG.debug('Policy?check?for?%(action)s?failed?with?credentials?'
'%(credentials)s',
{'action':?action,'credentials':?credentials})
returnresult
defcheck_is_admin(context):
"""Whether?or?not?roles?contains?'admin'?role?according?to?policy?setting.
"""
init()
#?the?target?is?user-self
credentials?=?context.to_policy_values()
target?=?credentials
return_ENFORCER.authorize('context_is_admin',?target,?credentials)
其中,authorize()方法對oslo.policy中的所有默認(rèn)規(guī)則進(jìn)行檢查;而check_is_admin()方法只對nova自定義的IsAdminCheck類的規(guī)則進(jìn)行檢查。緊接著,nova組件定義了一個(gè)用戶獲取Enforcer對象的方法。
defget_enforcer():
#?This?method?is?for?use?by?oslopolicy?CLI?scripts.?Those?scripts?need?the
#?'output-file'?and?'namespace'?options,?but?having?those?in?sys.argv?means
#?loading?the?Nova?config?options?will?fail?as?those?are?not?expected?to
#?be?present.?So?we?pass?in?an?arg?list?with?those?stripped?out.
conf_args?=?[]
#?Start?at?1?because?cfg.CONF?expects?the?equivalent?of?sys.argv[1:]
i?=1
whilei?<?len(sys.argv):
ifsys.argv[i].strip('-')in['namespace','output-file']:
i?+=2
continue
conf_args.append(sys.argv[i])
i?+=1
cfg.CONF(conf_args,?project='nova')
init()
return_ENFORCER
該方法通過讀取配置文件或輸入?yún)?shù)中的相關(guān)配置信息,獲取一個(gè)Enforcer對象。因此,為了實(shí)現(xiàn)oslo.policy的功能,需要在nova組件的配置文件中添加如下的配置信息:
[python]view plaincopy
[DEFAULT]
output_file?=?policy-sample.yaml
namespace?=?nova
其中,namespace為oslo.policy添加了一個(gè)命名空間,這樣可以隔離OpenStack的不同組件;output_file為默認(rèn)策略規(guī)則文件的輸出路徑,當(dāng)然你也可以自定義一個(gè)策略規(guī)則文件。
如果需要為定義的Enforcer對象添加RuleDefault對象,則可以使用如下的方式。首先,創(chuàng)建一個(gè)Enforcer對象,然后創(chuàng)建一個(gè)或多個(gè)RuleDefault對象,接著調(diào)用Enforcer對象的register_defaults()方法或register_default()方法將RuleDefault對象注冊到Enforcer對象中。
[python]view plaincopy
fromoslo_configimportcfg
CONF?=?cfg.CONF
enforcer?=?policy.Enforcer(CONF,?policy_file=_POLICY_PATH)
base_rules?=?[
policy.RuleDefault('admin_required','role:admin?or?is_admin:1',
description='Who?is?considered?an?admin'),
policy.RuleDefault('service_role','role:service',
description='service?role'),
]
enforcer.register_defaults(base_rules)
enforcer.register_default(policy.RuleDefault('identity:create_region',
'rule:admin_required',
description='helpful?text'))
最后,為了可以使用oslo.policy相關(guān)的命令行更好的管理權(quán)限策略規(guī)則,可以在nova組件安裝配置文件setup.cfg文件中進(jìn)行添加如下配置:
[python]view plaincopy
[entry_points]
oslo.policy.enforcer?=
nova?=?nova.policy:get_enforcer
這樣,在使用命令行管理nova組件的權(quán)限控制策略時(shí),便可以根據(jù)oslo.policy.enforcer配置項(xiàng)找到nova組件的Enforcer對象。