Skip to content

Commit

Permalink
Merge pull request #161 from descarteslabs/feat/auth-injection
Browse files Browse the repository at this point in the history
Support multiple authenticated users in the same environment
  • Loading branch information
nikhaldi authored Aug 24, 2017
2 parents 4c8760f + 0bd0880 commit d09e265
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 39 deletions.
2 changes: 1 addition & 1 deletion descarteslabs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

__version__ = "0.4.4"
from .auth import Auth
descartes_auth = Auth()
descartes_auth = Auth.from_environment_or_token_json()

from .services.metadata import Metadata
from .services.places import Places
Expand Down
64 changes: 43 additions & 21 deletions descarteslabs/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

from descarteslabs.exceptions import AuthError, OauthError

DEFAULT_TOKEN_INFO_PATH = os.path.join(
os.path.expanduser("~"), '.descarteslabs', 'token_info.json')


def base64url_decode(input):
"""Helper method to base64url_decode a string.
Expand All @@ -42,34 +45,55 @@ def base64url_decode(input):

class Auth:
def __init__(self, domain="https://iam.descarteslabs.com",
scope=None, leeway=500):
scope=None, leeway=500, token_info_path=DEFAULT_TOKEN_INFO_PATH,
client_id=None, client_secret=None, jwt_token=None):
"""
Helps retrieve JWT from a client id and refresh token for cli usage.
:param client_id: str
:param refresh_token: str generated through IAM interface
:param url: endpoint for auth0
:param domain: endpoint for auth0
:param scope: the JWT fields to be included
:param leeway: JWT expiration leeway
:param token_info_path: path to a JSON file optionally holding auth information
:param client_id: JWT client id
:param client_secret: JWT client secret
:param jwt_token: the JWT token, if we already have one
"""
self.token_info_path = token_info_path
self.client_id = client_id
self.client_secret = client_secret
self._token = jwt_token

token_info = {}
self.domain = domain
self.scope = scope
self.leeway = leeway

if self.scope is None:
self.scope = ['openid', 'name', 'groups']

@classmethod
def from_environment_or_token_json(cls, domain="https://iam.descarteslabs.com",
scope=None, leeway=500,
token_info_path=DEFAULT_TOKEN_INFO_PATH):
"""
Creates an Auth object from environment variables CLIENT_ID, CLIENT_SECRET,
JWT_TOKEN if they are set, or else from a JSON file at the given path.
:param domain: endpoint for auth0
:param scope: the JWT fields to be included
:param leeway: JWT expiration leeway
:param token_info_path: path to a JSON file optionally holding auth information
"""
token_info = {}
try:
with open(os.path.join(os.path.expanduser("~"), '.descarteslabs', 'token_info.json')) as fp:
with open(token_info_path) as fp:
token_info = json.load(fp)
except:
pass

self.client_id = os.environ.get('CLIENT_ID', token_info.get('client_id', None))
self.client_secret = os.environ.get('CLIENT_SECRET', token_info.get('client_secret', None))
self._token = os.environ.get('JWT_TOKEN', token_info.get('jwt_token', None))

self.domain = domain
self.scope = scope
self.leeway = leeway
client_id = os.environ.get('CLIENT_ID', token_info.get('client_id', None))
client_secret = os.environ.get('CLIENT_SECRET', token_info.get('client_secret', None))
jwt_token = os.environ.get('JWT_TOKEN', token_info.get('jwt_token', None))

if self.scope is None:
self.scope = ['openid', 'name', 'groups']
return cls(domain=domain, scope=scope, leeway=leeway, token_info_path=token_info_path,
client_id=client_id, client_secret=client_secret, jwt_token=jwt_token)

@property
def token(self):
Expand Down Expand Up @@ -134,7 +158,7 @@ def _get_token(self, timeout=100):
token_info = {}

try:
with open(os.path.join(os.path.expanduser("~"), '.descarteslabs', 'token_info.json')) as fp:
with open(self.token_info_path) as fp:
token_info = json.load(fp)
except:
pass
Expand All @@ -148,15 +172,13 @@ def _get_token(self, timeout=100):

os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)

file = os.path.join(os.path.expanduser("~"), '.descarteslabs', 'token_info.json')

with open(file, 'w+') as fp:
with open(self.token_info_path, 'w+') as fp:
json.dump(token_info, fp)

os.chmod(file, stat.S_IRUSR | stat.S_IWUSR)
os.chmod(self.token_info_path, stat.S_IRUSR | stat.S_IWUSR)


if __name__ == '__main__':
auth = Auth()
auth = Auth.from_environment_or_token_json()

print(auth.token)
14 changes: 6 additions & 8 deletions descarteslabs/scripts/parser/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
import six
from six.moves import input

from descarteslabs.auth import Auth, base64url_decode
from descarteslabs.auth import Auth, base64url_decode, DEFAULT_TOKEN_INFO_PATH

import descarteslabs as dl


def auth_handler(args):
auth = Auth()
auth = Auth.from_environment_or_token_json()

if args.command == 'login':

Expand All @@ -41,22 +41,20 @@ def auth_handler(args):

token_info = json.loads(base64url_decode(s).decode('utf-8'))

path = os.path.join(os.path.expanduser("~"), '.descarteslabs')
path = os.path.dirname(DEFAULT_TOKEN_INFO_PATH)

if not os.path.exists(path):
os.makedirs(path)

os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)

file = os.path.join(os.path.expanduser("~"), '.descarteslabs', 'token_info.json')

with open(file, 'w+') as fp:
with open(DEFAULT_TOKEN_INFO_PATH, 'w+') as fp:
json.dump(token_info, fp)

os.chmod(file, stat.S_IRUSR | stat.S_IWUSR)
os.chmod(DEFAULT_TOKEN_INFO_PATH, stat.S_IRUSR | stat.S_IWUSR)

# Get a fresh Auth token
auth = dl.Auth()
auth = dl.Auth.from_environment_or_token_json()
dl.metadata.auth = auth
keys = dl.metadata.keys()

Expand Down
4 changes: 2 additions & 2 deletions descarteslabs/services/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Metadata(Service):
TIMEOUT = (9.5, 120)
"""Image Metadata Service"""

def __init__(self, url=None, token=None):
def __init__(self, url=None, token=None, auth=dl.descartes_auth):
"""The parent Service class implements authentication and exponential
backoff/retry. Override the url parameter to use a different instance
of the backing service.
Expand All @@ -42,7 +42,7 @@ def __init__(self, url=None, token=None):
url = os.environ.get("DESCARTESLABS_METADATA_URL",
"https://platform-services.descarteslabs.com/metadata/v1")

Service.__init__(self, url, token)
Service.__init__(self, url, token, auth)

def sources(self):
"""Get a list of image sources.
Expand Down
5 changes: 3 additions & 2 deletions descarteslabs/services/places.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@
from cachetools.keys import hashkey
from .service import Service
from six import string_types
import descarteslabs


class Places(Service):
TIMEOUT = (9.5, 360)
"""Places and statistics service https://iam.descarteslabs.com/service/waldo"""

def __init__(self, url=None, token=None, maxsize=10, ttl=600):
def __init__(self, url=None, token=None, auth=descarteslabs.descartes_auth, maxsize=10, ttl=600):
"""The parent Service class implements authentication and exponential
backoff/retry. Override the url parameter to use a different instance
of the backing service.
"""
if url is None:
url = os.environ.get("DESCARTESLABS_PLACES_URL", "https://platform-services.descarteslabs.com/waldo/v1")

Service.__init__(self, url, token)
Service.__init__(self, url, token, auth)
self.cache = TTLCache(maxsize, ttl)

def placetypes(self):
Expand Down
5 changes: 3 additions & 2 deletions descarteslabs/services/raster.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import json
import warnings

import descarteslabs
from descarteslabs.addons import numpy as np
from descarteslabs.utilities import as_json_string
from .service import Service
Expand All @@ -35,7 +36,7 @@ class Raster(Service):
"""Raster"""
TIMEOUT = (9.5, 300)

def __init__(self, url=None, token=None):
def __init__(self, url=None, token=None, auth=descarteslabs.descartes_auth):
"""The parent Service class implements authentication and exponential
backoff/retry. Override the url parameter to use a different instance
of the backing service.
Expand All @@ -44,7 +45,7 @@ def __init__(self, url=None, token=None):
if url is None:
url = os.environ.get("DESCARTESLABS_RASTER_URL", "https://platform-services.descarteslabs.com/raster/v1")

Service.__init__(self, url, token)
Service.__init__(self, url, token, auth)

def get_bands_by_key(self, key):
"""
Expand Down
4 changes: 2 additions & 2 deletions descarteslabs/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ class Service:

ADAPTER = HTTPAdapter(max_retries=RETRY_CONFIG)

def __init__(self, url, token):
self.auth = descarteslabs.descartes_auth
def __init__(self, url, token, auth):
self.auth = auth
self.base_url = url
if token:
self.auth._token = token
Expand Down
2 changes: 1 addition & 1 deletion descarteslabs/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class TestAuth(unittest.TestCase):
def test_get_token(self):
# get a jwt
auth = Auth()
auth = Auth.from_environment_or_token_json()
self.assertIsNotNone(auth.token)

# validate the jwt
Expand Down

0 comments on commit d09e265

Please sign in to comment.