1#   Copyright 2012-2013 OpenStack Foundation
2#
3#   Licensed under the Apache License, Version 2.0 (the "License"); you may
4#   not use this file except in compliance with the License. You may obtain
5#   a copy of the License at
6#
7#        http://www.apache.org/licenses/LICENSE-2.0
8#
9#   Unless required by applicable law or agreed to in writing, software
10#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#   License for the specific language governing permissions and limitations
13#   under the License.
14#
15
16"""Common identity code"""
17
18from keystoneclient import exceptions as identity_exc
19from keystoneclient.v3 import domains
20from keystoneclient.v3 import groups
21from keystoneclient.v3 import projects
22from keystoneclient.v3 import users
23from osc_lib import exceptions
24from osc_lib import utils
25
26from openstackclient.i18n import _
27
28
29def find_service_in_list(service_list, service_id):
30    """Find a service by id in service list."""
31
32    for service in service_list:
33        if service.id == service_id:
34            return service
35    raise exceptions.CommandError(
36        "No service with a type, name or ID of '%s' exists." % service_id)
37
38
39def find_service(identity_client, name_type_or_id):
40    """Find a service by id, name or type."""
41
42    try:
43        # search for service id
44        return identity_client.services.get(name_type_or_id)
45    except identity_exc.NotFound:
46        # ignore NotFound exception, raise others
47        pass
48
49    try:
50        # search for service name
51        return identity_client.services.find(name=name_type_or_id)
52    except identity_exc.NotFound:
53        pass
54    except identity_exc.NoUniqueMatch:
55        msg = _("Multiple service matches found for '%s', "
56                "use an ID to be more specific.")
57        raise exceptions.CommandError(msg % name_type_or_id)
58
59    try:
60        # search for service type
61        return identity_client.services.find(type=name_type_or_id)
62    except identity_exc.NotFound:
63        msg = _("No service with a type, name or ID of '%s' exists.")
64        raise exceptions.CommandError(msg % name_type_or_id)
65    except identity_exc.NoUniqueMatch:
66        msg = _("Multiple service matches found for '%s', "
67                "use an ID to be more specific.")
68        raise exceptions.CommandError(msg % name_type_or_id)
69
70
71def get_resource(manager, name_type_or_id):
72    # NOTE (vishakha): Due to bug #1799153 and for any another related case
73    # where GET resource API does not support the filter by name,
74    # osc_lib.utils.find_resource() method cannot be used because that method
75    # try to fall back to list all the resource if requested resource cannot
76    # be get via name. Which ends up with NoUniqueMatch error.
77    # This new function is the replacement for osc_lib.utils.find_resource()
78    # for resources does not support GET by name.
79    # For example: identity GET /regions.
80    """Find a resource by id or name."""
81
82    try:
83        return manager.get(name_type_or_id)
84    except identity_exc.NotFound:
85        # raise NotFound exception
86        msg = _("No resource with name or id of '%s' exists")
87        raise exceptions.CommandError(msg % name_type_or_id)
88
89
90def _get_token_resource(client, resource, parsed_name, parsed_domain=None):
91    """Peek into the user's auth token to get resource IDs
92
93    Look into a user's token to try and find the ID of a domain, project or
94    user, when given the name. Typically non-admin users will interact with
95    the CLI using names. However, by default, keystone does not allow look up
96    by name since it would involve listing all entities. Instead opt to use
97    the correct ID (from the token) instead.
98    :param client: An identity client
99    :param resource: A resource to look at in the token, this may be `domain`,
100                     `project_domain`, `user_domain`, `project`, or `user`.
101    :param parsed_name: This is input from parsed_args that the user is hoping
102                        to find in the token.
103    :param parsed_domain: This is domain filter from parsed_args that used to
104                          filter the results.
105
106    :returns: The ID of the resource from the token, or the original value from
107              parsed_args if it does not match.
108    """
109
110    try:
111        token = client.auth.client.get_token()
112        token_data = client.tokens.get_token_data(token)
113        token_dict = token_data['token']
114
115        # NOTE(stevemar): If domain is passed, just look at the project domain.
116        if resource == 'domain':
117            token_dict = token_dict['project']
118        obj = token_dict[resource]
119
120        # user/project under different domain may has a same name
121        if parsed_domain and parsed_domain not in obj['domain'].values():
122            return parsed_name
123        if isinstance(obj, list):
124            for item in obj:
125                if item['name'] == parsed_name:
126                    return item['id']
127                if item['id'] == parsed_name:
128                    return parsed_name
129            return parsed_name
130        return obj['id'] if obj['name'] == parsed_name else parsed_name
131    # diaper defense in case parsing the token fails
132    except Exception:  # noqa
133        return parsed_name
134
135
136def _get_domain_id_if_requested(identity_client, domain_name_or_id):
137    if not domain_name_or_id:
138        return None
139    domain = find_domain(identity_client, domain_name_or_id)
140    return domain.id
141
142
143def find_domain(identity_client, name_or_id):
144    return _find_identity_resource(identity_client.domains, name_or_id,
145                                   domains.Domain)
146
147
148def find_group(identity_client, name_or_id, domain_name_or_id=None):
149    domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id)
150    if not domain_id:
151        return _find_identity_resource(identity_client.groups, name_or_id,
152                                       groups.Group)
153    else:
154        return _find_identity_resource(identity_client.groups, name_or_id,
155                                       groups.Group, domain_id=domain_id)
156
157
158def find_project(identity_client, name_or_id, domain_name_or_id=None):
159    domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id)
160    if not domain_id:
161        return _find_identity_resource(identity_client.projects, name_or_id,
162                                       projects.Project)
163    else:
164        return _find_identity_resource(identity_client.projects, name_or_id,
165                                       projects.Project, domain_id=domain_id)
166
167
168def find_user(identity_client, name_or_id, domain_name_or_id=None):
169    domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id)
170    if not domain_id:
171        return _find_identity_resource(identity_client.users, name_or_id,
172                                       users.User)
173    else:
174        return _find_identity_resource(identity_client.users, name_or_id,
175                                       users.User, domain_id=domain_id)
176
177
178def _find_identity_resource(identity_client_manager, name_or_id,
179                            resource_type, **kwargs):
180    """Find a specific identity resource.
181
182    Using keystoneclient's manager, attempt to find a specific resource by its
183    name or ID. If Forbidden to find the resource (a common case if the user
184    does not have permission), then return the resource by creating a local
185    instance of keystoneclient's Resource.
186
187    The parameter identity_client_manager is a keystoneclient manager,
188    for example: keystoneclient.v3.users or keystoneclient.v3.projects.
189
190    The parameter resource_type is a keystoneclient resource, for example:
191    keystoneclient.v3.users.User or keystoneclient.v3.projects.Project.
192
193    :param identity_client_manager: the manager that contains the resource
194    :type identity_client_manager: `keystoneclient.base.CrudManager`
195    :param name_or_id: the resources's name or ID
196    :type name_or_id: string
197    :param resource_type: class that represents the resource type
198    :type resource_type: `keystoneclient.base.Resource`
199
200    :returns: the resource in question
201    :rtype: `keystoneclient.base.Resource`
202
203    """
204
205    try:
206        identity_resource = utils.find_resource(identity_client_manager,
207                                                name_or_id, **kwargs)
208        if identity_resource is not None:
209            return identity_resource
210    except exceptions.Forbidden:
211        pass
212
213    return resource_type(None, {'id': name_or_id, 'name': name_or_id})
214
215
216def add_user_domain_option_to_parser(parser):
217    parser.add_argument(
218        '--user-domain',
219        metavar='<user-domain>',
220        help=_('Domain the user belongs to (name or ID). '
221               'This can be used in case collisions between user names '
222               'exist.'),
223    )
224
225
226def add_group_domain_option_to_parser(parser):
227    parser.add_argument(
228        '--group-domain',
229        metavar='<group-domain>',
230        help=_('Domain the group belongs to (name or ID). '
231               'This can be used in case collisions between group names '
232               'exist.'),
233    )
234
235
236def add_project_domain_option_to_parser(parser, enhance_help=lambda _h: _h):
237    parser.add_argument(
238        '--project-domain',
239        metavar='<project-domain>',
240        help=enhance_help(_('Domain the project belongs to (name or ID). This '
241                            'can be used in case collisions between project '
242                            'names exist.')),
243    )
244
245
246def add_role_domain_option_to_parser(parser):
247    parser.add_argument(
248        '--role-domain',
249        metavar='<role-domain>',
250        help=_('Domain the role belongs to (name or ID). '
251               'This must be specified when the name of a domain specific '
252               'role is used.'),
253    )
254
255
256def add_inherited_option_to_parser(parser):
257    parser.add_argument(
258        '--inherited',
259        action='store_true',
260        default=False,
261        help=_('Specifies if the role grant is inheritable to the sub '
262               'projects'),
263    )
264