Skip to content

Commit

Permalink
Merged PR 187: CLI Authentication Bugfix
Browse files Browse the repository at this point in the history
- Add CLI "config" command that outputs the current CLI configuration
- Add endpoint_url to the .env file with CLI
- Add auto-timeout to CLI authentication
- Fix cant connect to atrioumdb api from cli on non standard port
  • Loading branch information
William Dixon authored and bgreer101 committed Dec 4, 2023
1 parent 92ae2fa commit 5bf6c95
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 43 deletions.
2 changes: 1 addition & 1 deletion sdk/atriumdb/atrium_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class AtriumSDK:
>>> sdk = AtriumSDK(dataset_location="./example_dataset", metadata_connection_type=metadata_connection_type, connection_params=connection_params)
>>> # Remote API Mode
>>> api_url = "http://example.com/api/v1"
>>> api_url = "http://example.com/v1"
>>> token = "4e78a93749ead7893"
>>> metadata_connection_type = "api"
>>> sdk = AtriumSDK(api_url=api_url, token=token, metadata_connection_type=metadata_connection_type)
Expand Down
96 changes: 87 additions & 9 deletions sdk/atriumdb/cli/atriumdb_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
import urllib3
import os
import time
from urllib.parse import urlparse

from auth0.authentication.token_verifier import TokenVerifier, AsymmetricSignatureVerifier

from dotenv import load_dotenv
from dotenv import load_dotenv, set_key, get_key

from atriumdb import AtriumSDK
from atriumdb.adb_functions import parse_metadata_uri
Expand All @@ -43,7 +44,8 @@

_LOGGER = logging.getLogger(__name__)

load_dotenv(dotenv_path="./.env", override=True)
dotenv_path = "./.env"
load_dotenv(dotenv_path=dotenv_path, override=True)

cli_help_text = """
The atriumdb command is a command line interface for the Atrium database,
Expand Down Expand Up @@ -95,11 +97,26 @@ def cli(ctx, dataset_location, metadata_uri, database_type, endpoint_url, api_to


@click.command(help="Endpoint to login with QR code.")
@click.option("--endpoint-url", type=str, required=True, help="The endpoint to connect to for a remote AtriumDB server")
@click.pass_context
def login(ctx):
endpoint_url = ctx.obj["endpoint_url"]
def login(ctx, endpoint_url):
parsed_url = urlparse(endpoint_url)

# Check if a port is included in the URL
if parsed_url.port:
base_url = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}"
else:
base_url = f"{parsed_url.scheme}://{parsed_url.hostname}"

endpoint_url = endpoint_url.rstrip('/')
path = parsed_url.path.rstrip('/')

# Construct the final endpoint URL
endpoint_url = f"{base_url}{path}"

# Write endpoint URL to .env file
load_dotenv(dotenv_path=dotenv_path)
set_key(dotenv_path, "ATRIUMDB_ENDPOINT_URL", endpoint_url)
load_dotenv(dotenv_path=dotenv_path, override=True)

auth_conf_res = requests.get(f'{endpoint_url}/auth/cli/code')

Expand Down Expand Up @@ -148,8 +165,12 @@ def validate_token(id_token):
}

authenticated = False
while not authenticated:
# click.echo('Checking if the user completed the flow...')

# Calculate absolute expiration time
expires_in = time.time() + device_code_data['expires_in']
expiration_time = time.time() + int(device_code_data['expires_in'])

while not authenticated and time.time() < expiration_time:
token_response = requests.post(f'https://{auth0_domain}/oauth/token', data=token_payload)

token_data = token_response.json()
Expand All @@ -160,8 +181,11 @@ def validate_token(id_token):

authenticated = True

set_env_var_in_dotenv("ATRIUMDB_API_TOKEN", token_data['access_token'])
set_env_var_in_dotenv("ATRIUMDB_DATABASE_TYPE", "api")
load_dotenv(dotenv_path=dotenv_path)
set_key(dotenv_path, "ATRIUMDB_API_TOKEN", token_data['access_token'])
set_key(dotenv_path, "ATRIUMDB_DATABASE_TYPE", "api")
load_dotenv(dotenv_path=dotenv_path, override=True)

click.echo("Your API Token is:\n")
click.echo(token_data['access_token'])
click.echo("The variable ATRIUMDB_API_TOKEN has been set in your .env file and")
Expand All @@ -173,6 +197,58 @@ def validate_token(id_token):
else:
time.sleep(device_code_data['interval'])

# Check if the authentication process timed out
if not authenticated:
click.echo("Authentication Request Timed-Out")


@click.command(help="Refresh the API token using the stored endpoint URL.")
@click.pass_context
def refresh_token(ctx):
# Load .env file
load_dotenv(dotenv_path=dotenv_path)

# Check if endpoint URL is set
endpoint_url = get_key(dotenv_path, "ATRIUMDB_ENDPOINT_URL")

if not endpoint_url:
click.echo("Endpoint URL not set. Please use 'atriumdb login --endpoint-url MY_URL'.")
exit(1)

# Call the login function with the stored endpoint URL
ctx.invoke(login, endpoint_url=endpoint_url)


@click.command(help="Displays the current CLI configuration.")
@click.pass_context
def config(ctx):
# Retrieve the configuration from the context
config_data = {
"Endpoint URL": "Not Set" if not ctx.obj.get("endpoint_url") else ctx.obj.get("endpoint_url"),
"API Token": "Not Set" if not ctx.obj.get("api_token") else ctx.obj.get("api_token"),
"Dataset Location": "Not Set" if not ctx.obj.get("dataset_location") else ctx.obj.get("dataset_location"),
"Metadata URI": "Not Set" if not ctx.obj.get("metadata_uri") else ctx.obj.get("metadata_uri"),
"Database Type": "Not Set" if not ctx.obj.get("database_type") else ctx.obj.get("database_type"),
}

if config_data["API Token"] != "Not Set":
config_data["API Token"] = config_data["API Token"][:15] + "..."

# Determine the mode (remote or local)
mode = "Remote" if config_data["Database Type"] == "api" else "Local"

# Prepare data for tabulation
data = [
["Mode", mode],
*config_data.items()
]

# Print the tabulated data
click.echo(tabulate(data, headers=["Configuration", "Value"], tablefmt="grid"))
if config_data["API Token"] != "Not Set":
click.echo("Full API Token:")
click.echo(ctx.obj.get("api_token", "Not Set"))


@click.command()
@click.pass_context
Expand Down Expand Up @@ -459,6 +535,8 @@ def patient_ls(ctx, skip, limit, age_years_min, age_years_max, gender, source_id
cli.add_command(atriumdb_measure)
cli.add_command(atriumdb_device)
cli.add_command(login)
cli.add_command(refresh_token)
cli.add_command(config)


def set_env_var_in_dotenv(name, value):
Expand Down
45 changes: 28 additions & 17 deletions sdk/docs/source/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,41 +42,52 @@ Once installed, you can access the CLI by running the `atriumdb` command. For he
Authentication
=========================

To use the CLI for authentication and remote access, you will need to install the `atriumdb` package with the `cli` and `remote` optional dependency.
To use the AtriumDB CLI for authentication and remote access, you only need to specify the `endpoint-url` when logging in. The CLI will now automatically detect the URL from the last login for all subsequent commands.

To log in and set the endpoint URL, simply use the `atriumdb login` command with the `--endpoint-url` option. This option now supports login with an optional port:

.. code-block:: bash
pip install atriumdb[cli,remote]
# Login with a port
atriumdb login --endpoint-url "https://example.com:443/v1"
# Login without specifying a port
atriumdb login --endpoint-url "https://example.com/v1"
You can then use the `atriumdb` CLI to set the endpoint URL and log in to the remote API.
After logging in, the authentication credentials, including the API token and endpoint URL, are securely stored. If you need to refresh your token, you can now do so with the `atriumdb refresh-token` command, which uses the stored endpoint URL:

.. code-block:: bash
atriumdb --endpoint-url http://example.com/api/v1 login
atriumdb refresh-token
To view your current CLI configuration, including the endpoint URL and API token, use the `atriumdb config` command:

If the endpoint URL is already set in the .env file or as an environment variable, you can simply log in like this:
.. code-block:: bash
Create a file named `.env` in the same directory as your script and add the following content:
atriumdb config
.. code-block:: ini
Authentication Timeout
-----------------------

ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1
If a certain period of inactivity passes or the CLI detects that the authentication has timed out, the CLI will prompt you to reauthenticate using the `atriumdb refresh-token` or re-run the `atriumdb login` command with your endpoint URL.

Now, you can log in using the CLI:
Modifying the `.env` File
---------------------------

.. code-block:: bash
Directly editing the `.env` file is no longer recommended. The AtriumDB CLI will manage all necessary environment variables for you, ensuring secure and effective handling of authentication credentials.

atriumdb login
Connecting to an Existing Dataset
----------------------------------

After logging in, the `atriumdb` CLI will store the API token in the `.env` file. You can update your `.env` file to include the API token as well:
After successfully logging in using the updated methods described above, you can continue to use the AtriumSDK in remote mode with any Python scripts. The synchronization with your current login state occurs automatically.

.. code-block:: ini
Remember that while instantiating the `AtriumSDK`, there's no need to explicitly provide the API token, as it will be read from the stored credentials:

ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1
ATRIUMDB_API_TOKEN=4e78a93749ead7893
.. code-block:: python
Now, you can access the remote dataset using the AtriumSDK object, as shown in the "Connecting to an Existing Dataset" section.
sdk = AtriumSDK(metadata_connection_type="api", api_url="https://example.com/v1")
# The SDK will now use the stored API token from the last successful login.
========================================================
Using AtriumSDK in Remote Mode with CLI Authentication
Expand All @@ -100,7 +111,7 @@ To use the AtriumSDK in remote mode, follow these steps:

.. code-block:: python
sdk = AtriumSDK(metadata_connection_type="api", api_url="http://example.com/api/v1")
sdk = AtriumSDK(metadata_connection_type="api", api_url="http://example.com/v1")
By setting `metadata_connection_type` to `"api"`, the AtriumSDK will automatically detect and use the API token stored in the `.env` file for remote API calls (alternatively you can specify the token in the `token` parameter).

Expand Down
24 changes: 9 additions & 15 deletions sdk/docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ If it's a MariaDB dataset you will also have to specify the connection parameter
sdk = AtriumSDK(dataset_location=dataset_location, metadata_connection_type="mysql", connection_params=connection_params)
# Connect to a remote dataset using the API
api_url = "http://example.com/api/v1"
api_url = "http://example.com/v1"
token = "4e78a93749ead7893"
sdk = AtriumSDK(api_url=api_url, token=token, metadata_connection_type="api")
Expand Down Expand Up @@ -130,28 +130,22 @@ You can then use the `atriumdb` CLI to set the endpoint URL and log in to the re

.. code-block:: bash
atriumdb --endpoint-url http://example.com/api/v1 login
atriumdb login --endpoint-url "https://example.com/v1"
If the endpoint URL is already set in the .env file or as an environment variable, you can simply log in like this:

Create a file named `.env` in the same directory as your script and add the following content:
This command, after authenticating your API connection, will save your URL, token, auth expiration time, and connection mode in the `.env`:

.. code-block:: ini
ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1
ATRIUMDB_ENDPOINT_URL=https://example.com/v1
ATRIUMDB_API_TOKEN='aBcD012345eFgHI'
ATRIUMDB_AUTH_EXPIRATION_TIME=1234567890.1234567
ATRIUMDB_DATABASE_TYPE='api'
Now, you can log in using the CLI:
Once these variables have been set after running `login`, you can refresh the token using:

.. code-block:: bash
atriumdb login
After logging in, the `atriumdb` CLI will store the API token in the `.env` file. You can update your `.env` file to include the API token as well:

.. code-block:: ini
ATRIUMDB_ENDPOINT_URL=http://example.com/api/v1
ATRIUMDB_API_TOKEN=4e78a93749ead7893
atriumdb refresh-token
Now, you can access the remote dataset using the AtriumSDK object, as shown in the "Connecting to an Existing Dataset" section.

Expand Down
2 changes: 1 addition & 1 deletion sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mypkg = ["*.so", "*.dll"]

[project]
name = "atriumdb"
version = "2.0.0"
version = "2.1.1"
description = "Timeseries Database"
readme = "README.md"
authors = [{name = "Robert Greer, William Dixon, Spencer Vecile"}, { name = "Robert Greer", email = "[email protected]"}, { name = "William Dixon", email = "[email protected]" }, { name = "Spencer Vecile", email = "[email protected]"}]
Expand Down

0 comments on commit 5bf6c95

Please sign in to comment.