Skip to content

Commit 442751e

Browse files
authored
user/password login support (#399)
TODO: Followup with tests
1 parent 5c049ec commit 442751e

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ API servers or Custom Resource Definitions.
2222
* [Create a Route](#create-a-route)
2323
* [List Projects](#list-projects)
2424
* [Custom Resources](#custom-resources)
25+
* [OpenShift Login with username and password](#openshift-login-with-username-and-password)
2526
* [Available Methods for Resources](#available-methods-for-resources)
2627
* [Get](#get)
2728
* [Create](#create)
@@ -232,6 +233,44 @@ for item in foo_resources.get().items:
232233
print(item.metadata.name)
233234
```
234235

236+
## OpenShift Login with username and password
237+
238+
```python
239+
from kubernetes import client
240+
from openshift.dynamic import DynamicClient
241+
from openshift.helper.userpassauth import OCPLoginConfiguration
242+
243+
apihost = 'https://api.cluster.example.com:6443'
244+
username = 'demo-user'
245+
password = 'insecure'
246+
247+
kubeConfig = OCPLoginConfiguration(ocp_username=username, ocp_password=password)
248+
kubeConfig.host = apihost
249+
kubeConfig.verify_ssl = True
250+
kubeConfig.ssl_ca_cert = './ocp.pem' # use a certificate bundle for the TLS validation
251+
252+
# Retrieve the auth token
253+
kubeConfig.get_token()
254+
255+
print('Auth token: {0}'.format(kubeConfig.api_key))
256+
print('Token expires: {0}'.format(kubeConfig.api_key_expires))
257+
258+
k8s_client = client.ApiClient(kubeConfig)
259+
260+
dyn_client = DynamicClient(k8s_client)
261+
v1_projects = dyn_client.resources.get(api_version='project.openshift.io/v1', kind='Project')
262+
project_list = v1_projects.get()
263+
264+
for project in project_list.items:
265+
print(project.metadata.name)
266+
267+
# Renew the auth token
268+
kubeConfig.get_token()
269+
270+
print('Auth token: {0}'.format(kubeConfig.api_key))
271+
print('Token expires: {0}'.format(kubeConfig.api_key_expires))
272+
```
273+
235274
# Available Methods for Resources
236275

237276
The generic Resource class supports the following methods, though every resource kind does not support every method.

openshift/helper/userpassauth.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# OpenShift User-Password Login helper module
2+
# This module extends the `kubernetes.client.Configuration` object.
3+
# `OCPLoginConfiguration` uses `username` and `password` params to authenticate
4+
# with the OAuth OpenShift component, after the autentication the `api_key` value is set
5+
# with the Bearer token.
6+
#
7+
# IMPORTANT: the Bearer token is designed to expire, si up to the user to renew the token.
8+
# the valitity (in secods) is saved into the `token['expires_in`]` attribute.
9+
# The value refers to when the token has been created and to not change overtime.
10+
11+
# Related discussion on GitHub https://github.com/openshift/openshift-restclient-python/issues/249
12+
# Most part of the code has been taken from the k8s_auth ansible module:
13+
# https://github.com/ansible/ansible/blob/stable-2.9/lib/ansible/modules/clustering/k8s/k8s_auth.py
14+
15+
import requests
16+
from requests_oauthlib import OAuth2Session
17+
from urllib3.util import make_headers
18+
from urllib.parse import parse_qs, urlencode, urlparse
19+
from kubernetes.client import Configuration as KubeConfig
20+
21+
class OCPLoginException(Exception):
22+
"""The base class for the OCPLogin exceptions"""
23+
24+
class OCPLoginRequestException(OCPLoginException):
25+
def __init__(self, msg, **kwargs):
26+
self.msg = msg
27+
self.req_info = {}
28+
for k, v in kwargs.items():
29+
self.req_info['req_' + k] = v
30+
31+
def __str__(self):
32+
error_msg = self.msg
33+
for k, v in self.req_info.items():
34+
error_msg += '\t{0}: {1}\n'.format(k, v)
35+
return error_msg
36+
37+
class OCPLoginConfiguration(KubeConfig):
38+
def __init__(self, host="http://localhost",
39+
api_key=None, api_key_prefix=None,
40+
ocp_username=None, ocp_password=None,
41+
discard_unknown_keys=False,
42+
):
43+
44+
self.ocp_username = ocp_username
45+
self.ocp_password = ocp_password
46+
47+
super(OCPLoginConfiguration, self).__init__(host=host, api_key=None,
48+
api_key_prefix=None, username=None,
49+
password=None, discard_unknown_keys=discard_unknown_keys)
50+
51+
def get_token(self):
52+
# python-requests takes either a bool or a path to a ca file as the 'verify' param
53+
if self.verify_ssl and self.ssl_ca_cert:
54+
self.con_verify_ca = self.ssl_ca_cert # path
55+
else:
56+
self.con_verify_ca = self.verify_ssl # bool
57+
58+
self.discover()
59+
self.token = self.login()
60+
self.api_key = {"authorization": "Bearer " + self.token['access_token']}
61+
self.api_key_expires = self.token['expires_in']
62+
self.api_key_scope = self.token['scope']
63+
64+
def discover(self):
65+
url = '{0}/.well-known/oauth-authorization-server'.format(self.host)
66+
ret = requests.get(url, verify=self.con_verify_ca)
67+
68+
if ret.status_code != 200:
69+
raise OCPLoginRequestException("Couldn't find OpenShift's OAuth API", method='GET', url=url,
70+
reason=ret.reason, status_code=ret.status_code)
71+
72+
oauth_info = ret.json()
73+
self.openshift_auth_endpoint = oauth_info['authorization_endpoint']
74+
self.openshift_token_endpoint = oauth_info['token_endpoint']
75+
76+
def login(self):
77+
os_oauth = OAuth2Session(client_id='openshift-challenging-client')
78+
authorization_url, state = os_oauth.authorization_url(self.openshift_auth_endpoint,
79+
state="1", code_challenge_method='S256')
80+
auth_headers = make_headers(basic_auth='{0}:{1}'.format(self.ocp_username, self.ocp_password))
81+
# Request authorization code using basic auth credentials
82+
ret = os_oauth.get(
83+
authorization_url,
84+
headers={'X-Csrf-Token': state, 'authorization': auth_headers.get('authorization')},
85+
verify=self.con_verify_ca,
86+
allow_redirects=False
87+
)
88+
89+
if ret.status_code != 302:
90+
raise OCPLoginRequestException("Authorization failed.", method='GET', url=authorization_url,
91+
reason=ret.reason, status_code=ret.status_code)
92+
93+
qwargs = {}
94+
for k, v in parse_qs(urlparse(ret.headers['Location']).query).items():
95+
qwargs[k] = v[0]
96+
qwargs['grant_type'] = 'authorization_code'
97+
98+
# Using authorization code given to us in the Location header of the previous request, request a token
99+
ret = os_oauth.post(
100+
self.openshift_token_endpoint,
101+
headers={
102+
'Accept': 'application/json',
103+
'Content-Type': 'application/x-www-form-urlencoded',
104+
# This is just base64 encoded 'openshift-challenging-client:'
105+
'Authorization': 'Basic b3BlbnNoaWZ0LWNoYWxsZW5naW5nLWNsaWVudDo='
106+
},
107+
108+
data=urlencode(qwargs),
109+
verify=self.con_verify_ca
110+
)
111+
if ret.status_code != 200:
112+
raise OCPLoginRequestException("Failed to obtain an authorization token.", method='POST',
113+
url=self.openshift_token_endpoint,
114+
reason=ret.reason, status_code=ret.status_code)
115+
return ret.json()
116+
117+
def logout(self):
118+
url = '{0}/apis/oauth.openshift.io/v1/oauthaccesstokens/{1}'.format(self.host, self.api_key)
119+
headers = {
120+
'Accept': 'application/json',
121+
'Content-Type': 'application/json',
122+
'Authorization': 'Bearer {0}'.format(self.api_key)
123+
}
124+
json = {
125+
"apiVersion": "oauth.openshift.io/v1",
126+
"kind": "DeleteOptions"
127+
}
128+
requests.delete(url, headers=headers, json=json, verify=self.con_verify_ca)
129+
# Ignore errors, the token will time out eventually anyway

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ kubernetes
33
python-string-utils
44
ruamel.yaml
55
six
6+
requests
7+
requests-oauthlib

0 commit comments

Comments
 (0)