Skip to content

Commit

Permalink
feat(auth): add header based authentication
Browse files Browse the repository at this point in the history
This makes it possible to use API Tokens as headers instead of having to set JWTs from some OAuth2
flow. I also removed the "PollinationAuth" method because it doesn't work now that we migrated to
Google Identity Platform. I will aim to add a nice OAuth2 style login for Pollination in the new
year (much like the gcloud API logs in users).
  • Loading branch information
AntoineDao committed Dec 15, 2020
1 parent fef4520 commit 266a589
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 84 deletions.
15 changes: 8 additions & 7 deletions queenbee/base/request.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pathlib
import os
from urllib import request
from typing import Union
from typing import Union, Dict
from .basemodel import BaseModel

USER_AGENT_STRING = 'Queenbee'
Expand Down Expand Up @@ -36,7 +36,7 @@ def urljoin(*args):
return url


def make_request(url: str, auth_header: str = '') -> str:
def make_request(url: str, auth_header: Dict[str, str] = {}) -> str:
"""Fetch data from a url to a local file or using the http protocol
Args:
Expand All @@ -46,11 +46,12 @@ def make_request(url: str, auth_header: str = '') -> str:
Returns:
str: [description]
"""
auth_header = auth_header or ''
headers = {
'Authorization': auth_header,
if auth_header == None:
auth_header = {}

auth_header.update({
'User-Agent': USER_AGENT_STRING
}
})

req = request.Request(url=url, headers=headers)
req = request.Request(url=url, headers=auth_header)
return request.urlopen(req)
23 changes: 15 additions & 8 deletions queenbee/cli/config/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ...config.auth import PollinationAuth
from ...config.auth import PollinationAuth, HeaderAuth

try:
import click
Expand All @@ -20,25 +20,32 @@ def add():
"""add auth domains and methods to your queenbee config"""
pass


@add.command('pollination')
@add.command('api_token')
@click.argument('api_token')
@click.option(
'-n',
'--header-name',
help='API token header name',
show_default=True,
default='x-pollination-token'
)
@click.option(
'-d',
'--domain',
help='The domain to use this credential with',
show_default=True,
default='api.pollination.cloud'
)
def add_pollination(api_token, domain):
"""add a pollination auth domain"""
def add_api_token(api_token, header_name, domain):
"""add an API token auth config"""
ctx = click.get_current_context()

auth_conf = PollinationAuth(
auth_conf = HeaderAuth(
domain=domain,
api_token=api_token,
header_name=header_name,
access_token=api_token,
)

ctx.obj.config.add_auth(auth=auth_conf)

ctx.obj.write_config()

12 changes: 6 additions & 6 deletions queenbee/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from typing import List, Union
from typing import List, Union, Dict
from urllib.parse import urlparse
from pydantic import Field, SecretStr, constr

from ..base.basemodel import BaseModel
from .auth import JWTAuth, PollinationAuth
from .auth import JWTAuth, HeaderAuth
from .repositories import RepositoryReference


class Config(BaseModel):
type: constr(regex='^Config$') = 'Config'

auth: List[Union[PollinationAuth, JWTAuth]] = Field(
auth: List[Union[JWTAuth, HeaderAuth]] = Field(
[],
description='A list of authentication configurations for different repository domains'
)
Expand All @@ -20,7 +20,7 @@ class Config(BaseModel):
description='A list of repositories used for local execution'
)

def get_auth_header(self, repository_url: str) -> str:
def get_auth_header(self, repository_url: str) -> Dict[str, str]:
"""Get auth headers for the given repository url
Args:
Expand All @@ -45,11 +45,11 @@ def refresh_tokens(self):
"""
[auth.refresh_token() for auth in self.auth]

def add_auth(self, auth: Union[PollinationAuth, JWTAuth]):
def add_auth(self, auth: Union[JWTAuth, HeaderAuth]):
"""add an authentication method for a specific repository domain
Args:
auth (Union[PollinationAuth, JWTAuth]): An authentication config object
auth (Union[JWTAuth, HeaderAuth]): An authentication config object
"""
found = False

Expand Down
75 changes: 17 additions & 58 deletions queenbee/config/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from enum import Enum
from urllib import request
from urllib.error import HTTPError
from typing import Union
from pydantic import Field, SecretStr
from typing import Union, Dict
from pydantic import Field, SecretStr, constr


from ..base.basemodel import BaseModel
Expand All @@ -12,6 +12,8 @@

class BaseAuth(BaseModel):

type: constr(regex='^BaseAuth$') = 'BaseAuth'

domain: str = Field(
...,
description='The host domain to authenticate to',
Expand All @@ -24,84 +26,41 @@ class BaseAuth(BaseModel):
)

@property
def auth_header(self) -> str:
def auth_header(self) -> Dict[str, str]:
"""the auth header string for this auth model
Returns:
str: a bearer token auth header string
Dict[str, str]: a bearer token auth header string
"""
if self.access_token is None:
return ''
return f'Bearer {self.access_token.get_secret_value()}'
return {}
return {'Authorization': f'Bearer {self.access_token.get_secret_value()}'}

def refresh_token(self):
pass


class JWTAuth(BaseAuth):
class HeaderAuth(BaseAuth):

type: Enum('JWTAuth', {'type': 'jwt'}) = 'jwt'


class PollinationAuth(BaseAuth):

type: Enum('PollinationAuth', {'type': 'pollination'}) = 'pollination'

api_token: SecretStr = Field(
header_name: str = Field(
...,
description='The API token to use to authenticate to Pollination'
description='The HTTP header to user'
)

@property
def auth_endpoint(self):
return f'https://{self.domain}/user/login'

def check_cached_token(self) -> bool:
"""Check whether the cached auth token is still valid
Raises:
err: could not verify the token with the auth server
def auth_header(self) -> Dict[str, str]:
"""the auth header string for this auth model
Returns:
bool: whether the token is still valid
Dict[str, str]: a header with an API token
"""
auth_header = self.auth_header
return {self.header_name: self.access_token}

try:
make_request(
url='https://api.pollination.cloud/user',
auth_header=auth_header
)
except HTTPError as err:
if err.code >= 400 or err.code < 500:
return False
else:
raise err

return True

def refresh_token(self):
"""Refresh the cached token if it is invalid
"""
if self.check_cached_token():
return

payload = json.dumps(
{'api_token': self.api_token.get_secret_value()}).encode('utf-8')

headers = {
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': USER_AGENT_STRING
}

req = request.Request(
url=self.auth_endpoint,
method='POST',
data=payload
)
class JWTAuth(BaseAuth):

res = request.urlopen(req)
type: Enum('JWTAuth', {'type': 'jwt'}) = 'jwt'

res_data = json.loads(res.read())

self.access_token = SecretStr(res_data.get('access_token'))
3 changes: 2 additions & 1 deletion queenbee/config/repositories.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from typing import Dict
from urllib.parse import urlparse
from pydantic import Field, validator
from ..base.basemodel import BaseModel
Expand All @@ -22,7 +23,7 @@ def remote_or_local(cls, v):
"""Determine whether the path is local or remote (ie: http)"""
return get_uri(v)

def fetch(self, auth_header: str = '') -> 'RepositoryIndex':
def fetch(self, auth_header: Dict[str, str] = {}) -> 'RepositoryIndex':
"""Fetch the referenced repository index
Returns:
Expand Down
5 changes: 3 additions & 2 deletions queenbee/recipe/dependency.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Queenbee dependency class."""
import os
from typing import Dict
from enum import Enum
from pydantic import Field, constr

Expand Down Expand Up @@ -85,7 +86,7 @@ def ref_name(self) -> str:
return self.alias
return self.name

def _fetch_index(self, auth_header: str = ''):
def _fetch_index(self, auth_header: Dict[str, str] = {}):
"""Fetch the source repository index object.
Returns:
Expand All @@ -107,7 +108,7 @@ def _fetch_index(self, auth_header: str = ''):
raw_bytes = res.read()
return RepositoryIndex.parse_raw(raw_bytes)

def fetch(self, verify_digest: bool = True, auth_header: str = '') -> 'PackageVersion':
def fetch(self, verify_digest: bool = True, auth_header: Dict[str, str] = {}) -> 'PackageVersion':
"""Fetch the dependency from its source
Keyword Arguments:
Expand Down
4 changes: 2 additions & 2 deletions queenbee/repository/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from io import BytesIO
from datetime import datetime
from tarfile import TarInfo, TarFile
from typing import Union, Tuple
from typing import Union, Tuple, Dict

from pydantic import Field, constr

Expand Down Expand Up @@ -252,7 +252,7 @@ def from_package(cls, package_path: str):
return version

def fetch_package(self, source_url: str = None, verify_digest: bool = True,
auth_header: str = '') -> 'PackageVersion':
auth_header: Dict[str, str] = {}) -> 'PackageVersion':
if source_url.startswith('file:'):
source_path = source_url.split('file:///')[1]
if os.path.isabs(source_path):
Expand Down

0 comments on commit 266a589

Please sign in to comment.