1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5#      http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13from keystoneauth1 import discover
14
15# NOTE(jamielennox): The AUTH_INTERFACE is a special value that can be
16# requested from get_endpoint. If a plugin receives this as the value of
17# 'interface' it should return the initial URL that was passed to the plugin.
18AUTH_INTERFACE = object()
19
20IDENTITY_AUTH_HEADER_NAME = 'X-Auth-Token'
21
22
23class BaseAuthPlugin(object):
24    """The basic structure of an authentication plugin.
25
26    .. note::
27        See :doc:`/authentication-plugins` for a description of plugins
28        provided by this library.
29
30    """
31
32    def __init__(self):
33        self._discovery_cache = {}
34
35    def get_token(self, session, **kwargs):
36        """Obtain a token.
37
38        How the token is obtained is up to the plugin. If it is still valid
39        it may be re-used, retrieved from cache or invoke an authentication
40        request against a server.
41
42        There are no required kwargs. They are passed directly to the auth
43        plugin and they are implementation specific.
44
45        Returning None will indicate that no token was able to be retrieved.
46
47        This function is misplaced as it should only be required for auth
48        plugins that use the 'X-Auth-Token' header. However due to the way
49        plugins evolved this method is required and often called to trigger an
50        authentication request on a new plugin.
51
52        When implementing a new plugin it is advised that you implement this
53        method, however if you don't require the 'X-Auth-Token' header override
54        the `get_headers` method instead.
55
56        :param session: A session object so the plugin can make HTTP calls.
57        :type session: keystoneauth1.session.Session
58
59        :return: A token to use.
60        :rtype: string
61        """
62        return None
63
64    def get_auth_ref(self, session, **kwargs):
65        """Return the authentication reference of an auth plugin.
66
67        There are no required kwargs. They are passed directly to the auth
68        plugin and they are implementation specific.
69
70        :param session: A session object to be used for communication
71        :type session: keystoneauth1.session.session
72        """
73        return None
74
75    def get_headers(self, session, **kwargs):
76        """Fetch authentication headers for message.
77
78        This is a more generalized replacement of the older get_token to allow
79        plugins to specify different or additional authentication headers to
80        the OpenStack standard 'X-Auth-Token' header.
81
82        How the authentication headers are obtained is up to the plugin. If the
83        headers are still valid they may be re-used, retrieved from cache or
84        the plugin may invoke an authentication request against a server.
85
86        The default implementation of get_headers calls the `get_token` method
87        to enable older style plugins to continue functioning unchanged.
88        Subclasses should feel free to completely override this function to
89        provide the headers that they want.
90
91        There are no required kwargs. They are passed directly to the auth
92        plugin and they are implementation specific.
93
94        Returning None will indicate that no token was able to be retrieved and
95        that authorization was a failure. Adding no authentication data can be
96        achieved by returning an empty dictionary.
97
98        :param session: The session object that the auth_plugin belongs to.
99        :type session: keystoneauth1.session.Session
100
101        :returns: Headers that are set to authenticate a message or None for
102                  failure. Note that when checking this value that the empty
103                  dict is a valid, non-failure response.
104        :rtype: dict
105        """
106        token = self.get_token(session)
107
108        if not token:
109            return None
110
111        return {IDENTITY_AUTH_HEADER_NAME: token}
112
113    def get_endpoint_data(self, session,
114                          endpoint_override=None,
115                          discover_versions=True,
116                          **kwargs):
117        """Return a valid endpoint data for a the service.
118
119        :param session: A session object that can be used for communication.
120        :type session: keystoneauth1.session.Session
121        :param str endpoint_override: URL to use for version discovery.
122        :param bool discover_versions: Whether to get version metadata from
123                                       the version discovery document even
124                                       if it major api version info can be
125                                       inferred from the url.
126                                       (optional, defaults to True)
127        :param kwargs: Ignored.
128
129        :raises keystoneauth1.exceptions.http.HttpError: An error from an
130                                                         invalid HTTP response.
131
132        :return: Valid EndpointData or None if not available.
133        :rtype: `keystoneauth1.discover.EndpointData` or None
134        """
135        if not endpoint_override:
136            return None
137        endpoint_data = discover.EndpointData(catalog_url=endpoint_override)
138
139        if endpoint_data.api_version and not discover_versions:
140            return endpoint_data
141
142        return endpoint_data.get_versioned_data(
143            session, cache=self._discovery_cache,
144            discover_versions=discover_versions)
145
146    def get_api_major_version(self, session, endpoint_override=None, **kwargs):
147        """Get the major API version from the endpoint.
148
149        :param session: A session object that can be used for communication.
150        :type session: keystoneauth1.session.Session
151        :param str endpoint_override: URL to use for version discovery.
152        :param kwargs: Ignored.
153
154        :raises keystoneauth1.exceptions.http.HttpError: An error from an
155                                                         invalid HTTP response.
156
157        :return: Valid EndpointData or None if not available.
158        :rtype: `keystoneauth1.discover.EndpointData` or None
159        """
160        endpoint_data = self.get_endpoint_data(
161            session, endpoint_override=endpoint_override,
162            discover_versions=False, **kwargs)
163        if endpoint_data is None:
164            return
165
166        if endpoint_data.api_version is None:
167            # No version detected from the URL, trying full discovery.
168            endpoint_data = self.get_endpoint_data(
169                session, endpoint_override=endpoint_override,
170                discover_versions=True, **kwargs)
171
172        if endpoint_data and endpoint_data.api_version:
173            return endpoint_data.api_version
174
175        return None
176
177    def get_endpoint(self, session, **kwargs):
178        """Return an endpoint for the client.
179
180        There are no required keyword arguments to ``get_endpoint`` as a plugin
181        implementation should use best effort with the information available to
182        determine the endpoint. However there are certain standard options that
183        will be generated by the clients and should be used by plugins:
184
185        - ``service_type``: what sort of service is required.
186        - ``service_name``: the name of the service in the catalog.
187        - ``interface``: what visibility the endpoint should have.
188        - ``region_name``: the region the endpoint exists in.
189
190        :param session: The session object that the auth_plugin belongs to.
191        :type session: keystoneauth1.session.Session
192
193        :returns: The base URL that will be used to talk to the required
194                  service or None if not available.
195        :rtype: string
196        """
197        endpoint_data = self.get_endpoint_data(
198            session, discover_versions=False, **kwargs)
199        if not endpoint_data:
200            return None
201        return endpoint_data.url
202
203    def get_connection_params(self, session, **kwargs):
204        """Return any additional connection parameters required for the plugin.
205
206        :param session: The session object that the auth_plugin belongs to.
207        :type session: keystoneauth1.session.Session
208
209        :returns: Headers that are set to authenticate a message or None for
210                  failure. Note that when checking this value that the empty
211                  dict is a valid, non-failure response.
212        :rtype: dict
213        """
214        return {}
215
216    def invalidate(self):
217        """Invalidate the current authentication data.
218
219        This should result in fetching a new token on next call.
220
221        A plugin may be invalidated if an Unauthorized HTTP response is
222        returned to indicate that the token may have been revoked or is
223        otherwise now invalid.
224
225        :returns: True if there was something that the plugin did to
226                  invalidate. This means that it makes sense to try again. If
227                  nothing happens returns False to indicate give up.
228        :rtype: bool
229        """
230        return False
231
232    def get_user_id(self, session, **kwargs):
233        """Return a unique user identifier of the plugin.
234
235        Wherever possible the user id should be inferred from the token however
236        there are certain URLs and other places that require access to the
237        currently authenticated user id.
238
239        :param session: A session object so the plugin can make HTTP calls.
240        :type session: keystoneauth1.session.Session
241
242        :returns: A user identifier or None if one is not available.
243        :rtype: str
244        """
245        return None
246
247    def get_project_id(self, session, **kwargs):
248        """Return the project id that we are authenticated to.
249
250        Wherever possible the project id should be inferred from the token
251        however there are certain URLs and other places that require access to
252        the currently authenticated project id.
253
254        :param session: A session object so the plugin can make HTTP calls.
255        :type session: keystoneauth1.session.Session
256
257        :returns: A project identifier or None if one is not available.
258        :rtype: str
259        """
260        return None
261
262    def get_sp_auth_url(self, session, sp_id, **kwargs):
263        """Return auth_url from the Service Provider object.
264
265        This url is used for obtaining unscoped federated token from remote
266        cloud.
267
268        :param sp_id: ID of the Service Provider to be queried.
269        :type sp_id: string
270
271        :returns: A Service Provider auth_url or None if one is not available.
272        :rtype: str
273
274        """
275        return None
276
277    def get_sp_url(self, session, sp_id, **kwargs):
278        """Return sp_url from the Service Provider object.
279
280        This url is used for passing SAML2 assertion to the remote cloud.
281
282        :param sp_id: ID of the Service Provider to be queried.
283        :type sp_id: str
284
285        :returns: A Service Provider sp_url or None if one is not available.
286        :rtype: str
287
288        """
289        return None
290
291    def get_cache_id(self):
292        """Fetch an identifier that uniquely identifies the auth options.
293
294        The returned identifier need not be decomposable or otherwise provide
295        anyway to recreate the plugin. It should not contain sensitive data in
296        plaintext.
297
298        This string MUST change if any of the parameters that are used to
299        uniquely identity this plugin change.
300
301        If get_cache_id returns a str value suggesting that caching is
302        supported then get_auth_cache and set_auth_cache must also be
303        implemented.
304
305        :returns: A unique string for the set of options
306        :rtype: str or None if this is unsupported or unavailable.
307        """
308        return None
309
310    def get_auth_state(self):
311        """Retrieve the current authentication state for the plugin.
312
313        Retrieve any internal state that represents the authenticated plugin.
314
315        This should not fetch any new data if it is not present.
316
317        :raises NotImplementedError: if the plugin does not support this
318            feature.
319
320        :returns: raw python data (which can be JSON serialized) that can be
321                  moved into another plugin (of the same type) to have the
322                  same authenticated state.
323        :rtype: object or None if unauthenticated.
324        """
325        raise NotImplementedError()
326
327    def set_auth_state(self, data):
328        """Install existing authentication state for a plugin.
329
330        Take the output of get_auth_state and install that authentication state
331        into the current authentication plugin.
332
333        :raises NotImplementedError: if the plugin does not support this
334            feature.
335        """
336        raise NotImplementedError()
337
338
339class FixedEndpointPlugin(BaseAuthPlugin):
340    """A base class for plugins that have one fixed endpoint."""
341
342    def __init__(self, endpoint=None):
343        super(FixedEndpointPlugin, self).__init__()
344        self.endpoint = endpoint
345
346    def get_endpoint(self, session, **kwargs):
347        """Return the supplied endpoint.
348
349        Using this plugin the same endpoint is returned regardless of the
350        parameters passed to the plugin. endpoint_override overrides the
351        endpoint specified when constructing the plugin.
352        """
353        return kwargs.get('endpoint_override') or self.endpoint
354
355    def get_endpoint_data(self, session,
356                          endpoint_override=None,
357                          discover_versions=True,
358                          **kwargs):
359        """Return a valid endpoint data for a the service.
360
361        :param session: A session object that can be used for communication.
362        :type session: keystoneauth1.session.Session
363        :param str endpoint_override: URL to use for version discovery.
364        :param bool discover_versions: Whether to get version metadata from
365                                       the version discovery document even
366                                       if it major api version info can be
367                                       inferred from the url.
368                                       (optional, defaults to True)
369        :param kwargs: Ignored.
370
371        :raises keystoneauth1.exceptions.http.HttpError: An error from an
372                                                         invalid HTTP response.
373
374        :return: Valid EndpointData or None if not available.
375        :rtype: `keystoneauth1.discover.EndpointData` or None
376        """
377        return super(FixedEndpointPlugin, self).get_endpoint_data(
378            session,
379            endpoint_override=endpoint_override or self.endpoint,
380            discover_versions=discover_versions,
381            **kwargs)
382