HAVANA KEYSTONE新建用户API过程分析

这篇文章里小秦我会跟踪一下添加用户这个API的调用过程。

1.API使用方法

[root@OS_DEV certs]#  curl -X POST -H "Content-type: application/json" -H "X-Auth-Token: MIIDnQYJKoZIhvcNAQcCoIIDjjCCA4oCAQExCTAHBgUrDgMCGjCCAfMGCSqGSIb3DQEHAaCCAeQEggHgeyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAiMjAxNC0wNC0xNVQxNDozMzo0MC4zMTE0NTIiLCAiZXhwaXJlcyI6ICIyMDE0LTA0LTE1VDE1OjMzOjQwWiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7ImRlc2NyaXB0aW9uIjogIlNlcnZpY2UgVGVuYW50IiwgImVuYWJsZWQiOiB0cnVlLCAiaWQiOiAiYmQyYTFjYzQ5MzY0NDk5ZGE0ZDRiODljN2Y1NTdlZWUiLCAibmFtZSI6ICJzZXJ2aWNlIn19LCAic2VydmljZUNhdGFsb2ciOiBbXSwgInVzZXIiOiB7InVzZXJuYW1lIjogIm5vdmEiLCAicm9sZXNfbGlua3MiOiBbXSwgImlkIjogImVlYmU5ZjMxN2VlMjQ3YjQ5YmU0MjUwNjg3NzcxMDVlIiwgInJvbGVzIjogW3sibmFtZSI6ICJhZG1pbiJ9XSwgIm5hbWUiOiAibm92YSJ9LCAibWV0YWRhdGEiOiB7ImlzX2FkbWluIjogMCwgInJvbGVzIjogWyI0MGE5ZDlhNGQwNjE0MWY3YjUzMzgzYzY4ZTVmZDQxYyJdfX19MYIBgTCCAX0CAQEwXDBXMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVW5zZXQxDjAMBgNVBAcMBVVuc2V0MQ4wDAYDVQQKDAVVbnNldDEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tAgEBMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIBANV1tBkejTeH8l87vHL+T+nbL23kumgvMJ2ncl+dSArV9l8VjZo-Duwe9TmxYVh9pWjvRyzhC++Q9V0gVqwNOqdgUVKCEWcyOpZZ7qFVmakxZ1Doij++t8h8ZwLU-3jfLdE7Kqxru1Gf0vWtWdAelmufdnhk4LOFIdZ0yh5ZycUPjERDoyjT651DlTiyItLGQtmzFkXVrQTfGNrpYtUgkgVpfg-fyVeiQKo9vOyoVbVndBQIFrPTbW6TyhPrQtY1MJOl+UPG4niMQx+0Ffoih8FiyA6J42qcPniY5Uh7ygVK+3v3Gvm8Ys650yPmeajz1futWr3FxIU1dRSmWO6ueKg=" -d '{"user": {"name": "thuanqin","OS-KSADM:password": "password","enabled": true,"tenantId": "d742f3f76e5b43c986f411c68b74e089","email": "646543317@qq.com"}}'  http://192.168.19.96:35357/v2.0/users | jq .

这里的X-Auth-Token需要加上keystone认证得到的token。然后-d指定的body则包括了添加的新账户的具体的信息。

2.根据这篇文章,我们很容易就能找到对应的mapping:
[keystone/contrib/admin_crud/core.py]

        mapper.connect(
            '/users',
            controller=user_controller,
            action='create_user',
            conditions=dict(method=['POST']))

user_controller来自于:

user_controller = identity.controllers.User()

对应的create_user是:

    # CRUD extension
    @controller.v2_deprecated
    def create_user(self, context, user):
        user = self._normalize_OSKSADM_password_on_request(user)
        user = self.normalize_username_in_request(user)
        user = self._normalize_dict(user)
        self.assert_admin(context)

        if 'name' not in user or not user['name']:
            msg = _('Name field is required and cannot be empty')
            raise exception.ValidationError(message=msg)
        if 'enabled' in user and not isinstance(user['enabled'], bool):
            msg = _('Enabled field must be a boolean')
            raise exception.ValidationError(message=msg)

        default_project_id = user.pop('tenantId', None)
        if default_project_id is not None:
            # Check to see if the project is valid before moving on.
            self.assignment_api.get_project(default_project_id)
            user['default_project_id'] = default_project_id

        user_id = uuid.uuid4().hex
        user_ref = self._normalize_domain_id(context, user.copy())
        user_ref['id'] = user_id
        new_user_ref = self.identity_api.v3_to_v2_user(
            self.identity_api.create_user(user_id, user_ref))

        if default_project_id is not None:
            self.assignment_api.add_user_to_project(default_project_id,
                                                    user_id)
        return {'user': new_user_ref}

看这段代码的时候先大概猜测一下具体要做什么:先认证token,看看token是不是没有问题,如果token没有问题,那么就去调用API添加用户。
一开始的几行是修改请求中的一些格式,比如把name改成username啥的:

    # CRUD extension
    @controller.v2_deprecated
    def create_user(self, context, user):
        user = self._normalize_OSKSADM_password_on_request(user)
        user = self.normalize_username_in_request(user)
        user = self._normalize_dict(user)

然后会做一个认证request的操作,由于这个操作是大部分controller都要做的,所以对应的方法在爷爷类中:
[keystone/common/wsgi.py]

    def assert_admin(self, context):
        if not context['is_admin']:
            try:
                user_token_ref = self.token_api.get_token(context['token_id'])
            except exception.TokenNotFound as e:
                raise exception.Unauthorized(e)

            validate_token_bind(context, user_token_ref)
            creds = user_token_ref['metadata'].copy()

            try:
                creds['user_id'] = user_token_ref['user'].get('id')
            except AttributeError:
                LOG.debug('Invalid user')
                raise exception.Unauthorized()

            try:
                creds['tenant_id'] = user_token_ref['tenant'].get('id')
            except AttributeError:
                LOG.debug('Invalid tenant')
                raise exception.Unauthorized()

            # NOTE(vish): this is pretty inefficient
            creds['roles'] = [self.assignment_api.get_role(role)['name']
                              for role in creds.get('roles', [])]
            # Accept either is_admin or the admin role
            self.policy_api.enforce(creds, 'admin_required', {})

按照小秦我的猜想,这个token是由一个类似于{user:XXX,password:YYY}这样字典通过keystone的私钥签名而成的。那么我这里要做的就是用keystone的公钥去解密这个加密后的字符串,看看能否解析回那个字典。我们来看看是不是这样:
首先是要判断是不是admin的token,是的话就不看了,因为admin是通过conf文件来看是不是那个token的:

if not context['is_admin']:

然后是获取user_token_ref,我们先看看这个的输出应该是啥:

{'expires': datetime.datetime(2014, 4, 15, 16, 42, 49), u'user': {u'username': u'nova', u'enabled': True, u'email': u'nova@example.com', u'name': u'nova', u'id': u'eebe9f317ee247b49be425068777105e'}, u'key': u'MIIDnQYJKoZIhvcNAQcCoIIDjjCCA4oCAQExCTAHBgUrDgMCGjCCAfMGCSqGSIb3DQEHAaCCAeQEggHgeyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAiMjAxNC0wNC0xNVQxNTo0Mjo0OS42OTQ1NzMiLCAiZXhwaXJlcyI6ICIyMDE0LTA0LTE1VDE2OjQyOjQ5WiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7ImRlc2NyaXB0aW9uIjogIlNlcnZpY2UgVGVuYW50IiwgImVuYWJsZWQiOiB0cnVlLCAiaWQiOiAiYmQyYTFjYzQ5MzY0NDk5ZGE0ZDRiODljN2Y1NTdlZWUiLCAibmFtZSI6ICJzZXJ2aWNlIn19LCAic2VydmljZUNhdGFsb2ciOiBbXSwgInVzZXIiOiB7InVzZXJuYW1lIjogIm5vdmEiLCAicm9sZXNfbGlua3MiOiBbXSwgImlkIjogImVlYmU5ZjMxN2VlMjQ3YjQ5YmU0MjUwNjg3NzcxMDVlIiwgInJvbGVzIjogW3sibmFtZSI6ICJhZG1pbiJ9XSwgIm5hbWUiOiAibm92YSJ9LCAibWV0YWRhdGEiOiB7ImlzX2FkbWluIjogMCwgInJvbGVzIjogWyI0MGE5ZDlhNGQwNjE0MWY3YjUzMzgzYzY4ZTVmZDQxYyJdfX19MYIBgTCCAX0CAQEwXDBXMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVW5zZXQxDjAMBgNVBAcMBVVuc2V0MQ4wDAYDVQQKDAVVbnNldDEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tAgEBMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIBANyPZUENgx-OfdGhHLO-hkz6T8zCAULcwoARtT7z9zw2uYwoeDqZOHfZK8A-dVfVB7B03c56C7A4x84-ucBf3EN2BHucFrCqZ2kx3cMXal9f5mylaxDbAuO6dQoVaXEWPIYF+NhZHabYogl0A30R24DIHE22-JfiDv-B4Bs19ulBjbVFslOiyCihI1S-r9v4tkzyu2EeCYILUd-nMEj7usUmCH-hKw+JRkK5c5a6itZZxHDHGApw-IaTxggdVZMFlw87ev-nuGt0QaYzt-Yx9y1lWlAc0of4uRioJvjO-6IWdV8XqnJIWwbUQG-ZJBQ03DsA5UL4Vwj1HcgJ6ZQFges=', u'token_version': u'v2.0', 'id': u'ca1919c69eb5bc083496ab5989ac42f1', 'trust_id': None, 'user_id': u'eebe9f317ee247b49be425068777105e', u'bind': None, u'token_data': {u'access': {u'token': {u'issued_at': u'2014-04-15T15:42:49.694573', u'expires': u'2014-04-15T16:42:49Z', u'id': u'MIIDnQYJKoZIhvcNAQcCoIIDjjCCA4oCAQExCTAHBgUrDgMCGjCCAfMGCSqGSIb3DQEHAaCCAeQEggHgeyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAiMjAxNC0wNC0xNVQxNTo0Mjo0OS42OTQ1NzMiLCAiZXhwaXJlcyI6ICIyMDE0LTA0LTE1VDE2OjQyOjQ5WiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7ImRlc2NyaXB0aW9uIjogIlNlcnZpY2UgVGVuYW50IiwgImVuYWJsZWQiOiB0cnVlLCAiaWQiOiAiYmQyYTFjYzQ5MzY0NDk5ZGE0ZDRiODljN2Y1NTdlZWUiLCAibmFtZSI6ICJzZXJ2aWNlIn19LCAic2VydmljZUNhdGFsb2ciOiBbXSwgInVzZXIiOiB7InVzZXJuYW1lIjogIm5vdmEiLCAicm9sZXNfbGlua3MiOiBbXSwgImlkIjogImVlYmU5ZjMxN2VlMjQ3YjQ5YmU0MjUwNjg3NzcxMDVlIiwgInJvbGVzIjogW3sibmFtZSI6ICJhZG1pbiJ9XSwgIm5hbWUiOiAibm92YSJ9LCAibWV0YWRhdGEiOiB7ImlzX2FkbWluIjogMCwgInJvbGVzIjogWyI0MGE5ZDlhNGQwNjE0MWY3YjUzMzgzYzY4ZTVmZDQxYyJdfX19MYIBgTCCAX0CAQEwXDBXMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVW5zZXQxDjAMBgNVBAcMBVVuc2V0MQ4wDAYDVQQKDAVVbnNldDEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tAgEBMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIIBANyPZUENgx-OfdGhHLO-hkz6T8zCAULcwoARtT7z9zw2uYwoeDqZOHfZK8A-dVfVB7B03c56C7A4x84-ucBf3EN2BHucFrCqZ2kx3cMXal9f5mylaxDbAuO6dQoVaXEWPIYF+NhZHabYogl0A30R24DIHE22-JfiDv-B4Bs19ulBjbVFslOiyCihI1S-r9v4tkzyu2EeCYILUd-nMEj7usUmCH-hKw+JRkK5c5a6itZZxHDHGApw-IaTxggdVZMFlw87ev-nuGt0QaYzt-Yx9y1lWlAc0of4uRioJvjO-6IWdV8XqnJIWwbUQG-ZJBQ03DsA5UL4Vwj1HcgJ6ZQFges=', u'tenant': {u'enabled': True, u'id': u'bd2a1cc49364499da4d4b89c7f557eee', u'name': u'service', u'description': u'Service Tenant'}}, u'serviceCatalog': [], u'user': {u'username': u'nova', u'roles_links': [], u'id': u'eebe9f317ee247b49be425068777105e', u'roles': [{u'name': u'admin'}], u'name': u'nova'}, u'metadata': {u'is_admin': 0, u'roles': [u'40a9d9a4d06141f7b53383c68e5fd41c']}}}, u'tenant': {u'enabled': True, u'id': u'bd2a1cc49364499da4d4b89c7f557eee', u'name': u'service', u'description': u'Service Tenant'}, u'metadata': {u'roles': [u'40a9d9a4d06141f7b53383c68e5fd41c']}}

可以看到,我们给了他一个token_id,其解析(或者说解密)出了其中的相关信息。看下是怎么做到的吧:
[keystone/token/core.py]

    def get_token(self, token_id):
        if not token_id:
            # NOTE(morganfainberg): There are cases when the
            # context['token_id'] will in-fact be None. This also saves
            # a round-trip to the backend if we don't have a token_id.
            raise exception.TokenNotFound(token_id='')
        unique_id = self.unique_id(token_id)
        token_ref = self._get_token(unique_id)
        # NOTE(morganfainberg): Lift expired checking to the manager, there is
        # no reason to make the drivers implement this check. With caching,
        # self._get_token could return an expired token. Make sure we behave
        # as expected and raise TokenNotFound on those instances.
        self._assert_valid(token_id, token_ref)
        return token_ref

看下unique_id:

    def unique_id(self, token_id):
        """Return a unique ID for a token.

        The returned value is useful as the primary key of a database table,
        memcache store, or other lookup table.

        :returns: Given a PKI token, returns it's hashed value. Otherwise,
                  returns the passed-in value (such as a UUID token ID or an
                  existing hash).
        """
        return cms.cms_hash_token(token_id)

根据注释,对于PKI,该方法会的返回一个primary key,这个primary key在数据库中的token表里可以找到对应的token的信息。(看来之前的猜测是有问题的。token的加密不是对字典加密,而是对数据库的主键加密)。至于这个cms_hash_token小秦我以后会专门写个文章来分析这些和PKI有关的东西。先往下看:

        token_ref = self._get_token(unique_id)

unique_id就是我们的主键,而token_ref就是我们上面的user_token_ref了。猜测一下这个方法应该就是查数据库,看看是不是这样:

    @cache.on_arguments(should_cache_fn=SHOULD_CACHE,
                        expiration_time=EXPIRATION_TIME)
    def _get_token(self, token_id):
        # Only ever use the "unique" id in the cache key.
        return self.driver.get_token(token_id)

这里的driver是:

<keystone.identity.backends.sql.Identity object at 0x375d4d0>

ok啦,就是查个数据库而已。而剩下的这个则是看下有没有过期:

    def _assert_valid(self, token_id, token_ref):
        """Raise TokenNotFound if the token is expired."""
        current_time = timeutils.normalize_time(timeutils.utcnow())
        expires = token_ref.get('expires')
        if not expires or current_time > timeutils.normalize_time(expires):
            raise exception.TokenNotFound(token_id=token_id)

从这里来讲,服务器的时间最好不要乱动。务必切记。

再回到assert_admin中,下面的一些代码主要就是信息的一个重新整理,然后来看这个:
[keystone/common/wsgi.py]

            # Accept either is_admin or the admin role
            self.policy_api.enforce(creds, 'admin_required', {})

这里的policy_api在我们这里是这个:

driver = keystone.policy.backends.sql.Policy

看下enforce:

def enforce(credentials, action, target, do_raise=True):
    """Verifies that the action is valid on the target in this context.

       :param credentials: user credentials
       :param action: string representing the action to be checked, which
                      should be colon separated for clarity.
       :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': object.project_id}
       :raises: `exception.Forbidden` if verification fails.

       Actions should be colon separated for clarity. For example:

        * identity:list_users

    """
    init()

    # Add the exception arguments if asked to do a raise
    extra = {}
    if do_raise:
        extra.update(exc=exception.ForbiddenAction, action=action,
                     do_raise=do_raise)

    return _ENFORCER.enforce(action, target, credentials, **extra)

可以看到这个是一个很关键的方法。其根据我们的policy.json的文件,来决定某个用户或角色是否有权限做某个操作。
关于这个policy.json的解析小秦我会单独写个文章来分析,这里我们只要知道这个是在干什么就行了。

继续看我们的create_user方法。看完了上面对token的认证,下面的代码就很简单啦:

        new_user_ref = self.identity_api.v3_to_v2_user(
            self.identity_api.create_user(user_id, user_ref))

        if default_project_id is not None:
            self.assignment_api.add_user_to_project(default_project_id,
                                                    user_id)

这里的self.identity_api和self.assignment_api在我之前的文章里都大概分析过(调用sql的driver,然后写数据库),这里就不再看了。

最后再把数据返回就一切ok了:

        return {'user': new_user_ref}

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*