2021SC@SDUSC
KeyStone概述
身份服务通常是用户与之交互的第一个服务。通过身份验证后,最终用户可以使用他们的身份访问其他 OpenStack 服务。同样,其他 OpenStack 服务利用身份服务来确保用户是他们所声称的人,并发现其他服务在部署中的位置。 Identity 服务还可以与一些外部用户管理系统(例如 LDAP)集成。
用户和服务可以使用由 Identity 服务管理的服务目录来定位其他服务。顾名思义,服务目录是 OpenStack 部署中可用服务的集合。每个服务可以有一个或多个端点,每个端点可以是以下三种类型之一:管理、内部或公共。在生产环境中,出于安全原因,不同的端点类型可能位于向不同类型用户公开的不同网络上。例如,公共 API 网络可能在 Internet 上可见,因此客户可以管理他们的云。管理 API 网络可能仅限于组织内管理云基础架构的操作员。内部 API 网络可能仅限于包含 OpenStack 服务的主机。此外,OpenStack 支持多个区域以实现可扩展性。在身份服务中创建的区域、服务和端点共同构成了部署的服务目录。您部署中的每个 OpenStack 服务都需要一个服务条目,其中相应的端点存储在 Identity 服务中。这一切都可以在安装和配置 Identity 服务后完成。
身份服务包含以下组件:
服务器
中央服务器使用 RESTful 接口提供身份验证和授权服务。
驱动程序
驱动程序或服务后端集成到中央服务器。它们用于访问 OpenStack 外部存储库中的身份信息,并且可能已经存在于部署 OpenStack 的基础设施中(例如,SQL 数据库或 LDAP 服务器)。
模块
中间件模块在使用身份服务的 OpenStack 组件的地址空间中运行。这些模块拦截服务请求,提取用户凭证,并将其发送到中央服务器进行授权。中间件模块和 OpenStack 组件之间的集成使用 Python Web 服务器网关接口。
架构图
Identity服务提供授权资质验证,并提供关于用户和组的数据。在比较复杂的情况下,数据由一个授权后端服务来管理,而Identity服务作为LDAP的前端。
User: 代表API用户个体。一个user必须在它自己的域中,因此用户名不必要全局唯一,但在自己的域中是唯一的。
Groups: 代表一些API用户的集合。一个group必须在它自己的域中,因此组名不必要全局唯一,但在自己的域中是唯一的。
Resource服务提供关于项目和域的数据。
Project: Project是OpenStack所有制的基本单位,所有OpenStack中的资源都必须从属于某个project,所有project必须从属于某个域。因此,项目名不必要全局唯一,但在自己的域中是唯一的。如果某个项目的域没有特别指定, 则它将被添加到默认域中。
Domains: Domains是projects、users、groups的高层容器。每个域都定义了一个命名空间。
Assignment服务提供关于角色和角色分配的数据。
Roles: Roles规定了一个终端用户可以获得的授权级别。可以授予域或项目级别的角色,可以给个体用户或组授予角色。角色名在它自己的域中是唯一的。
Role Assignments: 一个(角色,资源,身份)三元组。
Token服务验证已验证资质的用户的授权请求,并管理tokens
Catalog服务提供用来发现接入点的接入点注册机制。
着手了解keystone系统
Keystone是一系列服务的HTTP前端。它也使用REST API接口。每个服务都有相应的后端实现。这些服务后端在setup.cfg中的名字都带有“base”或“backend”,事实上,它们也都继承自Base类。
setup.cfg的entry_pointers模块指明了keystone提供的每一项服务的实现类。观察服务的名字,发现第二个字段给出该服务所属的大类(例如auth: 授权,identity: 认证等)。
Keystone访问流程
以创建一个虚拟机(server)为例,简述keystone在openstack的访问流程。
- 首先用户向 Keystone 提供自己的身份验证信息,如用户名和密码。Keystone 会从数据库中读取数据对其验证,如验证通过,会向用户返回一个 token,此后用户所有的请求都会使用该 token 进行身份验证。如用户向 Nova 申请虚拟机服务,nova 会将用户提供的 token 发给 Keystone 进行验证,Keystone 会根据 token 判断用户是否拥有进行此项操作的权限,若验证通过那么 nova 会向其提供相对应的服务。其它组件和 Keystone 的交互也是如此。
下面结合源码来看keystone是如何完成这一流程:
- 认证用户名密码,并返回token(id)
验证机制的后端是在identity/backends/ldap/core.py中Identity类的一个方法authenticate(user_id,password)
def authenticate(self, user_id, password):
try:
user_ref = self._get_user(user_id)
except exception.UserNotFound:
raise AssertionError(_('Invalid user / password'))
if not user_id or not password:
raise AssertionError(_('Invalid user / password'))
conn = None
try:
conn = self.user.get_connection(user_ref['dn'],
password, end_user_auth=True)
if not conn:
raise AssertionError(_('Invalid user / password'))
except Exception:
raise AssertionError(_('Invalid user / password'))
finally:
if conn:
conn.unbind_s()
return self.user.filter_attributes(user_ref)
它先尝试获取user_id所示user,如果不成功或user_id和password存在空值,则报错’Invalid user / password’;如果成功,则用password尝试连接LDAP服务器,这里调用的是ldap/common.py中BaseLdap类的方法get_connection(…)
def get_connection(self, user=None, password=None, end_user_auth=False):
use_pool = self.use_pool
pool_size = self.pool_size
pool_conn_lifetime = self.pool_conn_lifetime
if end_user_auth:
if not self.use_auth_pool:
use_pool = False
else:
pool_size = self.auth_pool_size
pool_conn_lifetime = self.auth_pool_conn_lifetime
conn = _get_connection(self.LDAP_URL, use_pool,
use_auth_pool=end_user_auth)
# 未完待续
仔细读这段代码,可以看到它里面又调用了_get_connection(…)方法来获取Handler,如果给出的conn_url的前缀对应的handler已经在注册表中了,则直接返回该handler,如果use_pool选项为true,则返回PooledLDAPHandler,否则,返回PythonLDAPHandler。
def _get_connection(conn_url, use_pool=False, use_auth_pool=False):
for prefix, handler in _HANDLERS.items():
if conn_url.startswith(prefix):
return handler()
if use_pool:
return PooledLDAPHandler(use_auth_pool=use_auth_pool)
else:
return PythonLDAPHandler()
无论那种类型的handler,在下一句中都转换为KeystoneLDAPHandler
conn = KeystoneLDAPHandler(conn=conn)
然后,调用conn.connect(…)尝试连接LDAP服务器
try:
conn.connect(self.LDAP_URL,
page_size=self.page_size,
alias_dereferencing=self.alias_dereferencing,
use_tls=self.use_tls,
tls_cacertfile=self.tls_cacertfile,
tls_cacertdir=self.tls_cacertdir,
tls_req_cert=self.tls_req_cert,
chase_referrals=self.chase_referrals,
debug_level=self.debug_level,
conn_timeout=self.conn_timeout,
use_pool=use_pool,
pool_size=pool_size,
pool_retry_max=self.pool_retry_max,
pool_retry_delay=self.pool_retry_delay,
pool_conn_timeout=self.pool_conn_timeout,
pool_conn_lifetime=pool_conn_lifetime)
然后判断,如果user是空的,则使用conf中的user,如果password是空的,则使用conf中的password,然后把user和password打包放入conn,返回conn。最后处理一些异常。
if user is None:
user = self.LDAP_USER
if password is None:
password = self.LDAP_PASSWORD
# not all LDAP servers require authentication, so we don't bind
# if we don't have any user/pass
if user and password:
conn.simple_bind_s(user, password)
else:
conn.simple_bind_s()
return conn
except ldap.INVALID_CREDENTIALS:
raise exception.LDAPInvalidCredentialsError()
except ldap.SERVER_DOWN:
raise exception.LDAPServerConnectionError(
url=self.LDAP_URL)
回到Ldap/core.py,正确拿到conn表示password正确,则将user和password解包,返回user除了password、tenant、groups的其它属性(在identity/backends/base.py中定义)。
- 验证token,并响应token携带的请求
验证token的代码在auth/plugins/token.py中
def authenticate(self, auth_payload):
if 'id' not in auth_payload:
raise exception.ValidationError(attribute='id',
target='token')
token = self._get_token_ref(auth_payload)
if token.is_federated and PROVIDERS.federation_api:
response_data = mapped.handle_scoped_token(
token, PROVIDERS.federation_api,
PROVIDERS.identity_api
)
else:
response_data = token_authenticate(token)
# NOTE(notmorgan): The Token auth method is *very* special and sets the
# previous values to the method_names. This is because it can be used
# for re-scoping and we want to maintain the values. Most
# AuthMethodHandlers do no such thing and this is not required.
response_data.setdefault('method_names', []).extend(token.methods)
return base.AuthHandlerResponse(status=True, response_body=None,
response_data=response_data)
其中,核心验证过程是调用token_authenticate(token)完成的。
def token_authenticate(token):
response_data = {}
try:
# Do not allow tokens used for delegation to
# create another token, or perform any changes of
# state in Keystone. To do so is to invite elevation of
# privilege attacks
json_body = flask.request.get_json(silent=True, force=True) or {}
project_scoped = 'project' in json_body['auth'].get(
'scope', {}
)
domain_scoped = 'domain' in json_body['auth'].get(
'scope', {}
)
if token.oauth_scoped:
raise exception.ForbiddenAction(
action=_(
'Using OAuth-scoped token to create another token. '
'Create a new OAuth-scoped token instead'))
elif token.trust_scoped:
raise exception.ForbiddenAction(
action=_(
'Using trust-scoped token to create another token. '
'Create a new trust-scoped token instead'))
elif token.system_scoped and (project_scoped or domain_scoped):
raise exception.ForbiddenAction(
action=_(
'Using a system-scoped token to create a project-scoped '
'or domain-scoped token is not allowed.'
)
)
if not CONF.token.allow_rescope_scoped_token:
# Do not allow conversion from scoped tokens.
if token.project_scoped or token.domain_scoped:
raise exception.ForbiddenAction(
action=_('rescope a scoped token'))
# New tokens maintain the audit_id of the original token in the
# chain (if possible) as the second element in the audit data
# structure. Look for the last element in the audit data structure
# which will be either the audit_id of the token (in the case of
# a token that has not been rescoped) or the audit_chain id (in
# the case of a token that has been rescoped).
try:
token_audit_id = token.parent_audit_id or token.audit_id
except IndexError:
# NOTE(morganfainberg): In the case this is a token that was
# issued prior to audit id existing, the chain is not tracked.
token_audit_id = None
# To prevent users from never having to re-authenticate, the original
# token expiration time is maintained in the new token. Not doing this
# would make it possible for a user to continuously bump token
# expiration through token rescoping without proving their identity.
response_data.setdefault('expires_at', token.expires_at)
response_data['audit_id'] = token_audit_id
response_data.setdefault('user_id', token.user_id)
return response_data
except AssertionError as e:
LOG.error(e)
raise exception.Unauthorized(e)
通过了解整个过程,对keystone文件结构也有了初步认识。auth包中都是与“用户拿来一个东西,我验证一下它对不对”有关的,其中,plugins子包中是验证各种东西的代码;core.py中method相关函数负责加载用于验证指定东西的方法,定义了AuthContext类用于在集成各个验证plugins所需属性,AuthInfo用于封装授权请求,UserMFARulesValidator用于帮忙验证MFA Rules。