Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
48 changes: 44 additions & 4 deletions gen3/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,28 @@


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:
import gen3.configure as config_tool
Comment thread
jacob50231 marked this conversation as resolved.
Outdated
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 +64,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 +110,7 @@ def main(
ctx,
auth_config,
endpoint,
profile_name,
verbose_logs,
very_verbose_logs,
only_error_logs,
Expand All @@ -99,10 +119,28 @@ def main(
):
"""Gen3 Command Line Interface"""
ctx.ensure_object(dict)

if profile_name and not auth_config:
try:
import gen3.configure as config_tool
Comment thread
jacob50231 marked this conversation as resolved.
Outdated
profile_creds = config_tool.get_profile_credentials(profile_name)
import tempfile
import json
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 +177,8 @@ def main(
main.add_command(wss.wss)
main.add_command(discovery.discovery)
main.add_command(configure.configure)
main.add_command(configure.list_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
72 changes: 64 additions & 8 deletions gen3/cli/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,76 @@


@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)
profile_title, new_lines = config_tool.get_profile_from_creds(
profile, cred, apiendpoint
)
lines = config_tool.get_current_config_lines()
config_tool.update_config_lines(lines, profile_title, new_lines)

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_profiles():
Comment thread
jacob50231 marked this conversation as resolved.
Outdated
"""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
120 changes: 113 additions & 7 deletions gen3/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,50 @@

"""
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")
Comment thread
jacob50231 marked this conversation as resolved.
Outdated
GEN3_CLIENT_CONFIG_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()

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"]
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"] = ""

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_key"] = auth_tool.get_access_token_with_key(credentials)
except Exception as e:
logging.warning(f"Could not validate credentials with endpoint: {e}")
credentials["access_key"] = ""

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.
Expand Down Expand Up @@ -76,3 +96,89 @@ def update_config_lines(lines, profile_title, new_lines):
with open(CONFIG_FILE_PATH, "a+") as configFile:
configFile.write(profile_title)
configFile.writelines(new_lines)


def parse_profile_from_config(profile_name):
"""Parse profile configuration from config file, checking both gen3sdk and cdis-data-client formats"""

try:
with open(CONFIG_FILE_PATH, 'r') as f:
lines = f.readlines()
profile_line = f"[{profile_name}]\n"
if profile_line in lines:
profile_idx = lines.index(profile_line)
profile_data = {}
for i in range(profile_idx + 1, len(lines)):
if lines[i].startswith('[') or lines[i].strip() == '':
break
if '=' in lines[i]:
key, value = lines[i].strip().split('=', 1)
profile_data[key] = value
return profile_data
except FileNotFoundError:
pass

try:
if os.path.exists(GEN3_CLIENT_CONFIG_PATH):
config = configparser.ConfigParser()
config.read(GEN3_CLIENT_CONFIG_PATH)
if profile_name in config:
profile_data = dict(config[profile_name])
if 'access_token' in profile_data:
profile_data['access_key'] = profile_data['access_token']
return profile_data
except Exception as e:
logging.warning(f"Error reading cdis-data-client config: {e}")

return None


def list_profiles():
"""List all available profiles from both config files"""
profiles = set()

try:
with open(CONFIG_FILE_PATH, 'r') as f:
lines = f.readlines()
for line in lines:
if line.startswith('[') and line.endswith(']\n'):
profile_name = line[1:-2]
profiles.add(profile_name)
except FileNotFoundError:
pass

try:
if os.path.exists(GEN3_CLIENT_CONFIG_PATH):
config = configparser.ConfigParser()
config.read(GEN3_CLIENT_CONFIG_PATH)
for section in config.sections():
profiles.add(section)
except Exception as e:
logging.warning(f"Error reading cdis-data-client config: {e}")

return sorted(list(profiles))


def get_profile_credentials(profile_name):
"""Get credentials for a specific profile, compatible with both config formats"""
profile_data = parse_profile_from_config(profile_name)
if not profile_data:
raise ValueError(f"Profile '{profile_name}' not found in any 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 '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']

return credentials
Loading