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