1########################################################################
2#
3# (C) 2015, Chris Houseknecht <chouse@ansible.com>
4#
5# This file is part of Ansible
6#
7# Ansible is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Ansible is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
19#
20########################################################################
21from __future__ import (absolute_import, division, print_function)
22__metaclass__ = type
23
24import base64
25import os
26import json
27from stat import S_IRUSR, S_IWUSR
28
29import yaml
30
31from ansible import constants as C
32from ansible.galaxy.user_agent import user_agent
33from ansible.module_utils._text import to_bytes, to_native, to_text
34from ansible.module_utils.urls import open_url
35from ansible.utils.display import Display
36
37display = Display()
38
39
40class NoTokenSentinel(object):
41    """ Represents an ansible.cfg server with not token defined (will ignore cmdline and GALAXY_TOKEN_PATH. """
42    def __new__(cls, *args, **kwargs):
43        return cls
44
45
46class KeycloakToken(object):
47    '''A token granted by a Keycloak server.
48
49    Like sso.redhat.com as used by cloud.redhat.com
50    ie Automation Hub'''
51
52    token_type = 'Bearer'
53
54    def __init__(self, access_token=None, auth_url=None, validate_certs=True):
55        self.access_token = access_token
56        self.auth_url = auth_url
57        self._token = None
58        self.validate_certs = validate_certs
59
60    def _form_payload(self):
61        return 'grant_type=refresh_token&client_id=cloud-services&refresh_token=%s' % self.access_token
62
63    def get(self):
64        if self._token:
65            return self._token
66
67        # - build a request to POST to auth_url
68        #  - body is form encoded
69        #    - 'request_token' is the offline token stored in ansible.cfg
70        #    - 'grant_type' is 'refresh_token'
71        #    - 'client_id' is 'cloud-services'
72        #       - should probably be based on the contents of the
73        #         offline_ticket's JWT payload 'aud' (audience)
74        #         or 'azp' (Authorized party - the party to which the ID Token was issued)
75        payload = self._form_payload()
76
77        resp = open_url(to_native(self.auth_url),
78                        data=payload,
79                        validate_certs=self.validate_certs,
80                        method='POST',
81                        http_agent=user_agent())
82
83        # TODO: handle auth errors
84
85        data = json.loads(to_text(resp.read(), errors='surrogate_or_strict'))
86
87        # - extract 'access_token'
88        self._token = data.get('access_token')
89
90        return self._token
91
92    def headers(self):
93        headers = {}
94        headers['Authorization'] = '%s %s' % (self.token_type, self.get())
95        return headers
96
97
98class GalaxyToken(object):
99    ''' Class to storing and retrieving local galaxy token '''
100
101    token_type = 'Token'
102
103    def __init__(self, token=None):
104        self.b_file = to_bytes(C.GALAXY_TOKEN_PATH, errors='surrogate_or_strict')
105        # Done so the config file is only opened when set/get/save is called
106        self._config = None
107        self._token = token
108
109    @property
110    def config(self):
111        if self._config is None:
112            self._config = self._read()
113
114        # Prioritise the token passed into the constructor
115        if self._token:
116            self._config['token'] = None if self._token is NoTokenSentinel else self._token
117
118        return self._config
119
120    def _read(self):
121        action = 'Opened'
122        if not os.path.isfile(self.b_file):
123            # token file not found, create and chomd u+rw
124            open(self.b_file, 'w').close()
125            os.chmod(self.b_file, S_IRUSR | S_IWUSR)  # owner has +rw
126            action = 'Created'
127
128        with open(self.b_file, 'r') as f:
129            config = yaml.safe_load(f)
130
131        display.vvv('%s %s' % (action, to_text(self.b_file)))
132
133        return config or {}
134
135    def set(self, token):
136        self._token = token
137        self.save()
138
139    def get(self):
140        return self.config.get('token', None)
141
142    def save(self):
143        with open(self.b_file, 'w') as f:
144            yaml.safe_dump(self.config, f, default_flow_style=False)
145
146    def headers(self):
147        headers = {}
148        token = self.get()
149        if token:
150            headers['Authorization'] = '%s %s' % (self.token_type, self.get())
151        return headers
152
153
154class BasicAuthToken(object):
155    token_type = 'Basic'
156
157    def __init__(self, username, password=None):
158        self.username = username
159        self.password = password
160        self._token = None
161
162    @staticmethod
163    def _encode_token(username, password):
164        token = "%s:%s" % (to_text(username, errors='surrogate_or_strict'),
165                           to_text(password, errors='surrogate_or_strict', nonstring='passthru') or '')
166        b64_val = base64.b64encode(to_bytes(token, encoding='utf-8', errors='surrogate_or_strict'))
167        return to_text(b64_val)
168
169    def get(self):
170        if self._token:
171            return self._token
172
173        self._token = self._encode_token(self.username, self.password)
174
175        return self._token
176
177    def headers(self):
178        headers = {}
179        headers['Authorization'] = '%s %s' % (self.token_type, self.get())
180        return headers
181