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 服务器网关接口。

架构图

openstack各个版本源码包怎么下载 openstack源码分析_服务器

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是如何完成这一流程:

  1. 认证用户名密码,并返回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中定义)。

  1. 验证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。