Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 43 additions & 4 deletions gen3/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,33 @@
import gen3
from gen3 import logging as sdklogging
from gen3.cli import nih
import gen3.configure as config_tool
import tempfile
import json


class AuthFactory:
def __init__(self, refresh_file):
def __init__(self, refresh_file, profile_name=None):
self.refresh_file = refresh_file
self.profile_name = profile_name
self._cache = None

def get(self):
"""Lazy factory"""
"""Lazy factory with profile support"""
if self._cache:
return self._cache
self._cache = gen3.auth.Gen3Auth(refresh_file=self.refresh_file)

if self.profile_name and not self.refresh_file:
try:
profile_creds = config_tool.get_profile_credentials(self.profile_name)
self._cache = gen3.auth.Gen3Auth(
endpoint=profile_creds['api_endpoint'],
refresh_token=profile_creds.get('access_token')
)
except Exception as e:
raise ValueError(f"Error creating auth from profile '{self.profile_name}': {e}")
else:
self._cache = gen3.auth.Gen3Auth(refresh_file=self.refresh_file)
return self._cache


Expand All @@ -51,6 +66,12 @@ def get(self):
default=os.getenv("GEN3_ENDPOINT", None),
help="commons hostname - optional if API Key given in `auth`",
)
@click.option(
"--profile",
"profile_name",
default=os.getenv("GEN3_PROFILE", None),
help="Profile name to use for authentication (compatible with cdis-data-client profiles)",
)
@click.option(
"-v",
"verbose_logs",
Expand Down Expand Up @@ -91,6 +112,7 @@ def main(
ctx,
auth_config,
endpoint,
profile_name,
verbose_logs,
very_verbose_logs,
only_error_logs,
Expand All @@ -99,10 +121,25 @@ def main(
):
"""Gen3 Command Line Interface"""
ctx.ensure_object(dict)

if profile_name and not auth_config:
try:
profile_creds = config_tool.get_profile_credentials(profile_name)
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump(profile_creds, f)
auth_config = f.name
ctx.obj["temp_cred_file"] = f.name
if not endpoint:
endpoint = profile_creds.get('api_endpoint')
except Exception as e:
click.echo(f"Error loading profile '{profile_name}': {e}")
ctx.exit(1)

ctx.obj["auth_config"] = auth_config
ctx.obj["endpoint"] = endpoint
ctx.obj["commons_url"] = commons_url
ctx.obj["auth_factory"] = AuthFactory(auth_config)
ctx.obj["profile_name"] = profile_name
ctx.obj["auth_factory"] = AuthFactory(auth_config, profile_name)

if silent:
# we still need to define the logger, the log_level here doesn't
Expand Down Expand Up @@ -139,6 +176,8 @@ def main(
main.add_command(wss.wss)
main.add_command(discovery.discovery)
main.add_command(configure.configure)
main.add_command(configure.list_all_profiles, name="list-profiles")
main.add_command(configure.show_profile, name="show-profile")
main.add_command(objects.objects)
main.add_command(drs_pull.drs_pull)
main.add_command(file.file)
Expand Down
75 changes: 65 additions & 10 deletions gen3/cli/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,75 @@


@click.command()
@click.option("--profile", help="name of the profile to name for this credentials")
@click.option("--cred", help="path to the credentials.json")
def configure(profile, cred):
"""[unfinished] Commands to configure multiple profiles with corresponding credentials

./gen3 configure --profile=<profile-name> --cred=<path-to-credential/cred.json>
@click.option("--profile", required=True, help="name of the profile to name for this credentials")
@click.option("--cred", help="path to the credentials.json file")
@click.option("--apiendpoint", help="API endpoint URL (optional if derivable from credentials)")
def configure(profile, cred, apiendpoint):
"""Configure multiple profiles with corresponding credentials

Compatible with cdis-data-client profile format.

Examples:
./gen3 configure --profile=<profile-name> --cred=<path-to-credential/cred.json>
./gen3 configure --profile=<profile-name> --cred=<cred.json> --apiendpoint=https://data.mycommons.org
"""

if not cred:
click.echo("Error: --cred option is required")
return

logging.info(f"Configuring profile [ {profile} ] with credentials at {cred}")

try:
profile_title, new_lines = config_tool.get_profile_from_creds(profile, cred)
lines = config_tool.get_current_config_lines()
config_tool.update_config_lines(lines, profile_title, new_lines)
profile_name, credentials = config_tool.get_profile_from_creds(
profile, cred, apiendpoint
)
config_tool.update_config_profile(profile_name, credentials)

click.echo(f"Profile '{profile}' has been configured successfully.")

profiles = config_tool.list_profiles()
if len(profiles) > 1:
click.echo(f"Available profiles: {', '.join(profiles)}")

except Exception as e:
logging.error(str(e))
click.echo(f"Error configuring profile: {str(e)}")
raise e


@click.command()
def list_all_profiles():
"""List all available profiles from both gen3sdk and cdis-data-client configs"""
try:
profiles = config_tool.list_profiles()
if profiles:
click.echo("Available profiles:")
for profile in profiles:
click.echo(f" - {profile}")
else:
click.echo("No profiles found. Use 'gen3 configure' to create one.")
except Exception as e:
click.echo(f"Error listing profiles: {str(e)}")
raise e


@click.command()
@click.option("--profile", required=True, help="Profile name to show details for")
def show_profile(profile):
"""Show details for a specific profile"""
try:
profile_data = config_tool.parse_profile_from_config(profile)
if profile_data:
click.echo(f"Profile '{profile}' details:")
for key, value in profile_data.items():
if key in ['api_key', 'access_key', 'access_token']:
masked_value = value[:8] + '...' if len(value) > 8 else '***'
click.echo(f" {key}: {masked_value}")
else:
click.echo(f" {key}: {value}")
else:
click.echo(f"Profile '{profile}' not found")
except Exception as e:
logging.warning(str(e))
click.echo(f"Error showing profile: {str(e)}")
raise e
152 changes: 112 additions & 40 deletions gen3/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,60 +19,132 @@

"""
import json
import os
from os.path import expanduser
from pathlib import Path
from collections import OrderedDict
import gen3.auth as auth_tool
import configparser

from cdislogging import get_logger

logging = get_logger("__name__")

CONFIG_FILE_PATH = expanduser("~/.gen3/config")
CONFIG_FILE_PATH = expanduser("~/.gen3/gen3_client_config.ini")


def get_profile_from_creds(profile, cred):
def get_profile_from_creds(profile, cred, apiendpoint=None):
"""Create profile configuration from credentials file with validation"""
with open(expanduser(cred)) as f:
creds_from_json = json.load(f)
credentials = OrderedDict()
credentials["key_id"] = creds_from_json["key_id"]
credentials["api_key"] = creds_from_json["api_key"]
credentials["api_endpoint"] = auth_tool.endpoint_from_token(
credentials["api_key"]
)
credentials["access_key"] = auth_tool.get_access_token_with_key(credentials)
credentials["use_shepherd"] = ""
credentials["min_shepherd_version"] = ""
profile_line = "[" + profile + "]\n"
new_lines = [key + "=" + value + "\n" for key, value in credentials.items()]
new_lines.append("\n") # Adds an empty line between two profiles.
return profile_line, new_lines


def get_current_config_lines():
"""Read lines from the config file if exists in ~/.gen3 folder, else create new config file"""

if "key_id" not in creds_from_json:
raise ValueError("key_id not found in credentials file")
if "api_key" not in creds_from_json:
raise ValueError("api_key not found in credentials file")

credentials = {
"key_id": creds_from_json["key_id"],
"api_key": creds_from_json["api_key"]
}

if apiendpoint:
if not apiendpoint.startswith(('http://', 'https://')):
raise ValueError("API endpoint must start with http:// or https://")
credentials["api_endpoint"] = apiendpoint.rstrip('/')
else:
credentials["api_endpoint"] = auth_tool.endpoint_from_token(
credentials["api_key"]
)

try:
credentials["access_token"] = auth_tool.get_access_token_with_key(credentials)
except Exception as e:
logging.warning(f"Could not validate credentials with endpoint: {e}")
credentials["access_token"] = ""

return profile, credentials


def ensure_config_dir():
"""Ensure the ~/.gen3 directory exists"""
config_dir = os.path.dirname(CONFIG_FILE_PATH)
if config_dir:
os.makedirs(config_dir, exist_ok=True)


def update_config_profile(profile_name, credentials):
"""Update config file with new profile using configparser"""
ensure_config_dir()

config = configparser.ConfigParser()
if os.path.exists(CONFIG_FILE_PATH):
config.read(CONFIG_FILE_PATH)

if profile_name not in config:
config.add_section(profile_name)

for key, value in credentials.items():
config.set(profile_name, key, str(value))

with open(CONFIG_FILE_PATH, 'w') as configfile:
config.write(configfile)

logging.info(f"Profile '{profile_name}' saved to {CONFIG_FILE_PATH}")


def parse_profile_from_config(profile_name):
"""Parse profile configuration from config file using configparser"""
if not os.path.exists(CONFIG_FILE_PATH):
return None

try:
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
if profile_name in config:
profile_data = dict(config[profile_name])
# Normalize access_token vs access_key
if 'access_token' in profile_data and 'access_key' not in profile_data:
profile_data['access_key'] = profile_data['access_token']
return profile_data
except Exception as e:
logging.warning(f"Error reading config file: {e}")

return None


def list_profiles():
"""List all available profiles from config file"""
if not os.path.exists(CONFIG_FILE_PATH):
return []

try:
with open(CONFIG_FILE_PATH) as configFile:
logging.info(f"Reading existing config file at {CONFIG_FILE_PATH}")
return configFile.readlines()
except FileNotFoundError:
Path(CONFIG_FILE_PATH).touch()
logging.info(f"Config file doesn't exist at {CONFIG_FILE_PATH}, creating one")
config = configparser.ConfigParser()
config.read(CONFIG_FILE_PATH)
return sorted(config.sections())
except Exception as e:
logging.warning(f"Error reading config file: {e}")
return []


def update_config_lines(lines, profile_title, new_lines):
"""Update config file contents with the new profile values"""
def get_profile_credentials(profile_name):
"""Get credentials for a specific profile"""
profile_data = parse_profile_from_config(profile_name)
if not profile_data:
raise ValueError(f"Profile '{profile_name}' not found in config file")

required_fields = ['key_id', 'api_key', 'api_endpoint']
for field in required_fields:
if field not in profile_data or not profile_data[field]:
raise ValueError(f"Profile '{profile_name}' missing required field: {field}")

credentials = {
'key_id': profile_data['key_id'],
'api_key': profile_data['api_key'],
'api_endpoint': profile_data['api_endpoint']
}

if profile_title in lines:
profile_line_index = lines.index(profile_title)
next_profile_index = len(lines)
for i in range(profile_line_index, len(lines)):
if lines[i][0] == "[":
next_profile_index = i
break
del lines[profile_line_index:next_profile_index]
if 'access_key' in profile_data:
credentials['access_token'] = profile_data['access_key']
elif 'access_token' in profile_data:
credentials['access_token'] = profile_data['access_token']

with open(CONFIG_FILE_PATH, "a+") as configFile:
configFile.write(profile_title)
configFile.writelines(new_lines)
return credentials
Loading