OpenStack源码分析【2021-11-29】

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源码分析【2021-11-29】_第1张图片

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中定义)。

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

你可能感兴趣的:(源码阅读分析,OpenStack,openstack)