Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New authentication #12

Merged
merged 15 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,40 @@ Once the package is installed, you can access the provider from Qiskit via the f
from qiskit.providers.honeywell import Honeywell
```

You will need credentials for the Honeywell Quantum Service. This can either be
set via the `HQS_API_KEY` environment variable, or you can save that token to
disk with:
You will need credentials for the Honeywell Quantum Service. Credentials are
tied to an e-mail address that can be stored on disk with:

```python3
Honeywell.save_account('MYToken')
Honeywell.save_account('username@company.com')
```

After the initial saving of your account information, you will be prompted to enter
your password which will be used to acquire a token that will enable continuous
interaction until it expires. Your password will **not** be saved to disk and will
be required infrequently to update the credentials stored on disk or when a new
machine must be authenticated.

The credentials will then be loaded automatically on calls that return Backends,
or you can manually load the credentials with:
or can be manually loaded with:

```python3
Honeywell.load_account()
```

which will first check if the env variable is set and use that token and if not
it will load any save credentials from disk.
This will load the most recently saved credentials from disk so that they can be provided
for each interaction with Honeywell's devices.

Storing a new account will **not** invalidate your other stored credentials. You may have an arbitrary
number of credentials saved. To delete credentials you can use:

```python3
Honeywell.delete_credentials()
```

Which will delete the current accounts credentials from the credential store. Please keep in mind
this only deletes the current accounts credentials, and not all credentials stored.

With credentials loaded then you can access the backends from the provider:
With credentials loaded you can access the backends from the provider:

```python3
backends = Honeywell.backends()
Expand All @@ -69,13 +84,13 @@ print(result.get_counts(qc))
To configure a proxy include it in the save account configuration:

```python3
Honeywell.save_account('MYToken', proxies = {'urls': {'http': 'http://user:password@myproxy:8080', 'https': 'http://user:password@myproxy:8080'}})
Honeywell.save_account('username@company.com', proxies = {'urls': {'http': 'http://user:password@myproxy:8080', 'https': 'http://user:password@myproxy:8080'}})
```

To remove the proxy you can save with an empty dictionary:

```python3
Honeywell.save_account('MYToken', proxies = {})
Honeywell.save_account('username@company.com', proxies = {})
```

The 'urls' field must be a dictionary that maps a protocol type or url to a specific proxy. Additional information/details can be found [here](https://requests.readthedocs.io/en/master/user/advanced/#proxies).
Expand Down
2 changes: 1 addition & 1 deletion qiskit/providers/honeywell/VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.2.0
31 changes: 22 additions & 9 deletions qiskit/providers/honeywell/api/honeywellclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,37 +39,50 @@ class HoneywellClient:
"""Client for programmatic access to the Honeywell API."""

def __init__(self,
credentials,
proxies: dict = None):
""" HoneywellClient constructor """
self.credentials = credentials
self.client_api = self._init_service_client(proxies, self.credentials.api_url)

self.client_api = self._init_service_client(proxies)
@property
def api_url(self):
""" Returns the api url that the credential object is targeting

Returns:
str: API URL
"""
return self.credentials.api_url

def _init_service_client(self,
proxies: dict = None):
proxies: dict = None,
api_url: str = None):
"""Initialize the client used for communicating with the API.

Returns:
Api: client for the api server.
"""
service_url = urljoin(_API_URL, _API_VERSION)
service_url = urljoin(api_url, _API_VERSION)

# Create the api server client
client_api = Api(RetrySession(service_url,
credentials=self.credentials,
proxies=proxies))

return client_api

def has_token(self):
"""Check if a token has been aquired."""
return bool(self.client_api.session.access_token)
return bool(self.client_api.session.credentials.access_token)

def authenticate(self, credentials):
def authenticate(self, credentials=None):
"""Authenticate against the API and aquire a token."""
service_url = urljoin(_API_URL, _API_VERSION)
service_url = urljoin(self.credentials.api_url, _API_VERSION)
if credentials:
self.credentials = credentials
self.client_api = Api(RetrySession(service_url,
proxies=credentials.proxies))
self.client_api.session.access_token = credentials.token
self.client_api.session.proxies = credentials.proxies
credentials=self.credentials,
proxies=self.credentials.proxies))

# Backend-related public functions.
def list_backends(self):
Expand Down
8 changes: 5 additions & 3 deletions qiskit/providers/honeywell/api/rest/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def __init__(self, session, job_id):

def status(self):
"""Return the status of a job."""
url = "{url}?websocket={proxies}".format(url=self.get_url("status"),
proxies="false" if self.session.proxies
else "true")
if isinstance(self.session.proxies, dict) and 'urls' in self.session.proxies:
print('Using proxy, websockets not supported, falling back to polling')
url = "{url}?websocket={use_ws}".format(url=self.get_url("status"), use_ws="false")
else:
url = "{url}?websocket={use_ws}".format(url=self.get_url("status"), use_ws="true")
return self.session.get(url).json()
40 changes: 25 additions & 15 deletions qiskit/providers/honeywell/api/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ class RetrySession(Session):
``requests.Session``.
"""

def __init__(self, base_url, access_token=None,
def __init__(self, base_url, credentials=None,
retries=5, backoff_factor=0.5,
verify=True, proxies=None, auth=None):
"""RetrySession constructor.

Args:
base_url (str): base URL for the session's requests.
access_token (str): access token.
credentials (Credentials): credential class that allows for access token refresh.
retries (int): number of retries for the requests.
backoff_factor (float): backoff factor between retry attempts.
verify (bool): enable SSL verification.
Expand All @@ -77,25 +77,30 @@ def __init__(self, base_url, access_token=None,
super().__init__()

self.base_url = base_url
self._access_token = access_token
self.access_token = access_token
self._credentials = credentials

self._initialize_retry(retries, backoff_factor)
self._initialize_session_parameters(verify, proxies or {}, auth)

def update_auth(self):
""" Updates the headers with updated authorization, or removes
the authorization if credentials have been cleared. """
if self._credentials is not None:
self.headers.update({'Authorization': self._credentials.access_token})
else:
self.headers.pop('Authorization', None)

@property
def access_token(self):
def credentials(self):
"""Return the session access token."""
return self._access_token
self.update_auth()
return self._credentials

@access_token.setter
def access_token(self, value):
@credentials.setter
def credentials(self, value):
"""Set the session access token."""
self._access_token = value
if value:
self.headers.update({'x-api-key': value})
else:
self.headers.pop('x-api-key', None)
self._credentials = value
self.update_auth()

def _initialize_retry(self, retries, backoff_factor):
"""Set the Session retry policy.
Expand Down Expand Up @@ -128,6 +133,11 @@ def _initialize_session_parameters(self, verify, proxies, auth):
self.proxies = proxies or {}
self.verify = verify

def prepare_request(self, request):
# Before making the request use this as an opportunity to check/update authorization field
self.update_auth()
return super().prepare_request(request)

def request(self, method, url, **kwargs): # pylint: disable=arguments-differ
"""Constructs a Request, prepending the base url.

Expand All @@ -154,8 +164,8 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ
# Wrap the requests exceptions into a Honeywell custom one, for
# compatibility.
message = str(ex)
if self.access_token:
message = message.replace(self.access_token, '...')
if self.credentials:
message = message.replace(self.credentials.access_token, '...')

raise RequestsApiError(ex, message) from None

Expand Down
106 changes: 1 addition & 105 deletions qiskit/providers/honeywell/credentials/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,108 +25,4 @@
# limitations under the License.

""" Module for handling credentials """
from configparser import ConfigParser, ParsingError
import logging
import os
import json
from pathlib import Path

from ..exceptions import HoneywellError

DEFAULT_QISKITRC_FILE = Path.home()/'.qiskit'/'qhprc'
SECTION_NAME = 'qiskit-honeywell-provider'
logger = logging.getLogger(__name__)


class HoneywellCredentialsError(HoneywellError):
""" Base class for errors raised during credential management """
pass


class Credentials:
""" Class implementing all credential handling for id/refresh tokens """
def __init__(self,
token: str = None,
proxies: dict = None):
"""
Credentials Constructor
"""
# Default empty config
self.token = None
self.proxies = None

# Load configuration from env/config file
self.load_config(DEFAULT_QISKITRC_FILE)

# Allow overrides if provided
if token is not None:
self.token = token
if proxies is not None:
self.proxies = proxies

def load_from_environ(self):
""" Attempts to read credentials from environment variable """
self.token = os.getenv('HON_QIS_API')

def load_from_qiskitrc(self, filename):
""" Attempts to read credentials from qiskitrc file
The default qiskitrc location is in ``$HOME/.qiskit/qhprc``
"""
config_parser = ConfigParser()
try:
config_parser.read(filename)
except ParsingError as ex:
raise HoneywellCredentialsError(str(ex))

setattr(self, 'token',
config_parser.get(SECTION_NAME, 'API_KEY', fallback=self.token))
setattr(self, 'proxies',
json.loads(config_parser.get(SECTION_NAME, 'proxies', fallback='{}')))

def load_config(self, filename):
""" Load config information from environment or configuration file """
self.load_from_environ()
self.load_from_qiskitrc(filename)

def save_config(self, filename=DEFAULT_QISKITRC_FILE, overwrite=False):
""" Save configuration to resource file. """
self.save_qiskitrc(overwrite, filename)

def save_qiskitrc(self, overwrite=False, filename=DEFAULT_QISKITRC_FILE):
""" Stores the credentials and proxy information to qiskitrcc file
The default qiskitrc location is in ``$HOME/.qiskitrc/qhprc``
"""
config_parser = ConfigParser()
try:
config_parser.read(filename)
except ParsingError as ex:
raise HoneywellCredentialsError(str(ex))

if not config_parser.has_section(SECTION_NAME):
config_parser[SECTION_NAME] = {}
for k, v in {'API_KEY': self.token, 'proxies': self.proxies}.items():
if k not in config_parser[SECTION_NAME] or not overwrite:
if isinstance(v, dict):
config_parser[SECTION_NAME].update({k: json.dumps(v)})
else:
config_parser[SECTION_NAME].update({k: v})
(Path(filename).parent).mkdir(parents=True, exist_ok=True)
with open(filename, 'w') as conf_file:
config_parser.write(conf_file)

def remove_creds_from_qiskitrc(self, filename=DEFAULT_QISKITRC_FILE):
""" Removes the credentials from the configuration file
The default qiskitrc location is in ``$HOME/.qiskitrc/qhprc``
"""
config_parser = ConfigParser()
try:
config_parser.read(filename)
except ParsingError as ex:
raise HoneywellCredentialsError(str(ex))
if not (config_parser.has_section(SECTION_NAME) and config_parser.get(SECTION_NAME,
'API_KEY')):
return
config_parser.remove_option(SECTION_NAME, 'API_KEY')
(Path(filename).parent).mkdir(parents=True, exist_ok=True)
with open(filename, 'w') as conf_file:
config_parser.write(conf_file)
from .credentials import Credentials, HoneywellCredentialsError
Loading