Skip to content

Commit

Permalink
Google Cloud oauth authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Victor Garcia committed Oct 5, 2016
1 parent 2a712e3 commit edb323d
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 47 deletions.
16 changes: 16 additions & 0 deletions pykube/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,22 @@ def user(self):
raise exceptions.PyKubeError("current context not set; call set_current_context")
return self.users.get(self.contexts[self.current_context].get("user", ""), {})

def persist_doc(self):
if not hasattr(self, "filename") or not self.filename:
# Config was provided as string, not way to persit it
return
with open(self.filename, "w") as f:
yaml.safe_dump(self.doc, f, encoding='utf-8',
allow_unicode=True, default_flow_style=False)

def reload(self):
if hasattr(self, "_users"):
delattr(self, "_users")
if hasattr(self, "_contexts"):
delattr(self, "_contexts")
if hasattr(self, "_clusters"):
delattr(self, "_clusters")


class BytesOrFile(object):
"""
Expand Down
47 changes: 12 additions & 35 deletions pykube/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import re
import sys
import warnings
import requests

from six.moves.urllib.parse import urlparse

from .session import build_session
from .exceptions import HTTPError


Expand All @@ -21,16 +21,25 @@ class HTTPClient(object):
Client for interfacing with the Kubernetes API.
"""

def __init__(self, config):
_session = None

def __init__(self, config, gcloud_file=None):
"""
Creates a new instance of the HTTPClient.
:Parameters:
- `config`: The configuration instance
- `gcloud_file`: For GCP deployments, override gcloud credentials file location
"""
self.config = config
self.gcloud_file = gcloud_file
self.url = self.config.cluster["server"]
self.session = self.build_session()

@property
def session(self):
if not self._session:
self._session = build_session(self.config, self.gcloud_file)
return self._session

@property
def url(self):
Expand All @@ -43,38 +52,6 @@ def url(self, value):
warnings.warn("IP address hostnames are not supported with Python < 3.5. Please see https://github.com/kelproject/pykube/issues/29 for more info.", RuntimeWarning)
self._url = pr.geturl()

def _set_bearer_token(self, session, token):
"""
Set the bearer authorization token for the session.
"""
session.headers["Authorization"] = "Bearer {}".format(token)

def build_session(self):
"""
Creates a new session for the client.
"""
s = requests.Session()
if "certificate-authority" in self.config.cluster:
s.verify = self.config.cluster["certificate-authority"].filename()
elif "insecure-skip-tls-verify" in self.config.cluster:
s.verify = not self.config.cluster["insecure-skip-tls-verify"]
if "token" in self.config.user and self.config.user["token"]:
self._set_bearer_token(s, self.config.user["token"])
elif "auth-provider" in self.config.user:
token = self.config.user['auth-provider'].get('config', {}).get('access-token')
if token is not None:
self._set_bearer_token(s, token)
elif "client-certificate" in self.config.user:
s.cert = (
self.config.user["client-certificate"].filename(),
self.config.user["client-key"].filename(),
)
elif self.config.user.get("username") and self.config.user.get("password"):
s.auth = (self.config.user["username"], self.config.user["password"])
else: # no user present; don't configure anything
pass
return s

def get_kwargs(self, **kwargs):
"""
Creates a full URL to request based on arguments.
Expand Down
125 changes: 125 additions & 0 deletions pykube/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import os
import json
import requests
import datetime

from tzlocal import get_localzone
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient

from .exceptions import PyKubeError


def build_session(config, gcloud_file=None):
"""
Creates a new session for the client.
"""
if "token" in config.user and config.user["token"]:
s = _session_object("token")
_set_bearer_token(s, config.user["token"])
elif "auth-provider" in config.user:
s = _session_object("gcp", config, gcloud_file)
elif "client-certificate" in config.user:
s = _session_object("client-certificate")
s.cert = (
config.user["client-certificate"].filename(),
config.user["client-key"].filename(),
)
elif config.user.get("username") and config.user.get("password"):
s = _session_object("basic-auth")
s.auth = (config.user["username"], config.user["password"])
else: # no user present; don't configure anything
s = _session_object()

if "certificate-authority" in config.cluster:
s.verify = config.cluster["certificate-authority"].filename()
elif "insecure-skip-tls-verify" in config.cluster:
s.verify = not config.cluster["insecure-skip-tls-verify"]
return s


def _session_object(strategy=None, config=None, gcloud_file=None):
if strategy in ["token", "client-certificate", "basic-auth"]:
return requests.Session()
elif strategy in ["gcp"]:
return GCPSession(config, gcloud_file).create()
else:
return requests.Session()


def _set_bearer_token(session, token):
"""
Set the bearer authorization token for the session.
"""
session.headers["Authorization"] = "Bearer {}".format(token)


class GCPSession(object):

oauth = None
token_url = u'https://www.googleapis.com/oauth2/v4/token'
userinfo_url = u'https://www.googleapis.com/oauth2/v1/userinfo'
gcloud_well_known_file = os.path.join(os.path.expanduser('~'),
".config/gcloud/application_default_credentials.json")

scope = ["https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/appengine.admin",
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/plus.me"]

def __init__(self, config, gcloud_file=None):
self.config = config
if gcloud_file:
self.gcloud_well_known_file = gcloud_file
self.client_id, self.client_secret, self.refresh_token = self._load_default_gcloud_credentials()
client = BackendApplicationClient(client_id=self.client_id)
self.oauth = OAuth2Session(client=client, scope=self.scope)
token = {
'access_token': self.access_token,
'refresh_token': self.refresh_token,
'token_type': 'Bearer',
'expires_in': '3600',
}

self.oauth.token = token

@property
def access_token(self):
return self.config.user['auth-provider'].get('config', {}).get('access-token')

@property
def expired_token(self):
return self.oauth.get(self.userinfo_url).status_code == 401

def create(self):
if not self.access_token or self.expired_token:
# Getting access token from gcp
self._update_token()

return self.oauth

def _update_token(self):
tok = self.oauth.refresh_token(self.token_url, client_id=self.client_id,
client_secret=self.client_secret,
refresh_token=self.refresh_token)
self._persist_token(tok)

def _persist_token(self, tok):
user_name = self.config.contexts[self.config.current_context]['user']
user = [u['user'] for u in self.config.doc['users'] if u['name'] == user_name][0]
if 'config' not in user['auth-provider']:
user['auth-provider']['config'] = {}
user['auth-provider']['config']['access-token'] = tok['access_token']
date_expires = datetime.datetime.fromtimestamp(tok['expires_at'])
local_tz = get_localzone()
user['auth-provider']['config']['expiry'] = local_tz.localize(date_expires).isoformat()
self.config.persist_doc()
self.config.reload()

def _load_default_gcloud_credentials(self):
if not os.path.exists(self.gcloud_well_known_file):
raise PyKubeError('Google cloud well known file missing, configure your gcloud session')
with open(self.gcloud_well_known_file) as f:
data = json.loads(f.read())
return data['client_id'], data['client_secret'], data['refresh_token']
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
packages=find_packages(),
install_requires=[
"requests",
"requests-oauthlib",
"PyYAML",
"six",
"tzlocal",
],
)
2 changes: 1 addition & 1 deletion test/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ def tearDown(self):
def test_build_session_basic(self):
"""
"""
session = HTTPClient(self.cfg).build_session()
session = HTTPClient(self.cfg).session
self.assertEqual(session.auth, ('adm', 'somepassword'))
15 changes: 4 additions & 11 deletions test/test_httpclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,15 @@ def test_no_auth_with_no_user(self):
client = pykube.HTTPClient(pykube.KubeConfig(doc=config))
self.ensure_no_auth(client)

def test_build_session_auth_provider(self):
"""Test that HTTPClient correctly parses the auth-provider config.
Observed in GKE with kubelet v1.3.
def test_build_session_bearer_token(self):
"""Test that HTTPClient correctly parses the token
"""
self.config.update({
'users': [
{
'name': 'test-user',
'user': {
'auth-provider': {
'config': {
'access-token': 'abc',
'expiry': '2016-08-24T16:19:17.19878675-07:00',
},
},
'token': 'test'
},
},
]
Expand All @@ -127,4 +120,4 @@ def test_build_session_auth_provider(self):
client = pykube.HTTPClient(pykube.KubeConfig(doc=self.config))
_log.debug('Checking headers %s', client.session.headers)
self.assertIn('Authorization', client.session.headers)
self.assertEqual(client.session.headers['Authorization'], 'Bearer abc')
self.assertEqual(client.session.headers['Authorization'], 'Bearer test')
93 changes: 93 additions & 0 deletions test/test_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
pykube.http unittests
"""
import os
import copy
import logging
import tempfile

import pykube

from . import TestCase

BASE_CONFIG = {
"clusters": [
{
"name": "test-cluster",
"cluster": {
"server": "http://localhost:8080",
}
}
],
"contexts": [
{
"name": "test-cluster",
"context": {
"cluster": "test-cluster",
"user": "test-user",
}
}
],
"users": [
{
'name': 'test-user',
'user': {},
}
],
"current-context": "test-cluster",
}

_log = logging.getLogger(__name__)


class TestSession(TestCase):

def setUp(self):
self.config = copy.deepcopy(BASE_CONFIG)

def test_build_session_auth_provider(self):
"""Test that HTTPClient correctly parses the auth-provider config.
Observed in GKE with kubelet v1.3.
"""
self.config.update({
'users': [
{
'name': 'test-user',
'user': {
'auth-provider': {
'config': {
'access-token': 'abc',
'expiry': '2016-08-24T16:19:17.19878675-07:00',
},
},
},
},
]
})

gcloud_content = """
{
"client_id": "myclientid",
"client_secret": "myclientsecret",
"refresh_token": "myrefreshtoken",
"type": "authorized_user"
}
"""

_log.info('Built config: %s', self.config)
try:
tmp = tempfile.mktemp()
with open(tmp, 'w') as f:
f.write(gcloud_content)

session= pykube.session.GCPSession(pykube.KubeConfig(doc=self.config), tmp)
self.assertEquals(session.oauth.token['access_token'], 'abc')
self.assertEquals(session.oauth.token['refresh_token'], 'myrefreshtoken')
self.assertEquals(session.client_id, 'myclientid')
self.assertEquals(session.client_secret, 'myclientsecret')
finally:
if os.path.exists(tmp):
os.remove(tmp)

0 comments on commit edb323d

Please sign in to comment.