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

Fix/rest #17083

Merged
merged 5 commits into from
Oct 7, 2024
Merged

Fix/rest #17083

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
2 changes: 1 addition & 1 deletion conan/cli/commands/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def remote_login(conan_api, parser, subparser, *args):
if args.username is not None and args.password is not None:
user, password = args.username, args.password
else:
user, password = creds.auth(r, args.username)
user, password, _ = creds.auth(r, args.username)
if args.username is not None and args.username != user:
raise ConanException(f"User '{args.username}' doesn't match user '{user}' in "
f"credentials.json or environment variables")
Expand Down
1 change: 0 additions & 1 deletion conans/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
CHECKSUM_DEPLOY = "checksum_deploy" # Only when v2
REVISIONS = "revisions" # Only when enabled in config, not by default look at server_launcher.py
OAUTH_TOKEN = "oauth_token"

__version__ = '2.9.0-dev'
83 changes: 35 additions & 48 deletions conans/client/rest/auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
get_conan with the new token.
"""

import hashlib
from uuid import getnode as get_mac

from conan.api.output import ConanOutput
from conans.client.rest.remote_credentials import RemoteCredentials
from conans.client.rest.rest_client import RestApiClient
Expand All @@ -22,22 +19,38 @@
LOGIN_RETRIES = 3


class RemoteCreds:
def __init__(self, localdb):
self._localdb = localdb

def get(self, remote):
creds = getattr(remote, "_creds", None)
if creds is None:
user, token, _ = self._localdb.get_login(remote.url)
creds = user, token
setattr(remote, "_creds", creds)
return creds

def set(self, remote, user, token):
setattr(remote, "_creds", (user, token))
self._localdb.store(user, token, None, remote.url)


class ConanApiAuthManager:

def __init__(self, requester, cache_folder, localdb, global_conf):
self._requester = requester
self._localdb = localdb
self._creds = RemoteCreds(localdb)
self._global_conf = global_conf
self._cache_folder = cache_folder
self._cached_capabilities = {} # common to all RestApiClient

def call_rest_api_method(self, remote, method_name, *args, **kwargs):
"""Handles AuthenticationException and request user to input a user and a password"""
user, token, refresh_token = self._localdb.get_login(remote.url)
rest_client = self._get_rest_client(remote)
user, token = self._creds.get(remote)
rest_client = RestApiClient(remote, token, self._requester, self._global_conf)

if method_name == "authenticate":
return self._authenticate(remote, *args, **kwargs)
return self._authenticate(rest_client, remote, *args, **kwargs)

try:
ret = getattr(rest_client, method_name)(*args, **kwargs)
Expand All @@ -51,75 +64,49 @@ def call_rest_api_method(self, remote, method_name, *args, **kwargs):
# Anonymous is not enough, ask for a user
ConanOutput().info('Please log in to "%s" to perform this action. '
'Execute "conan remote login" command.' % remote.name)
return self._retry_with_new_token(user, remote, method_name, *args, **kwargs)
elif token and refresh_token:
# If we have a refresh token try to refresh the access token
try:
self._authenticate(remote, user, None)
except AuthenticationException:
# logger.info("Cannot refresh the token, cleaning and retrying: {}".format(exc))
self._clear_user_tokens_in_db(user, remote)
return self.call_rest_api_method(remote, method_name, *args, **kwargs)
if self._get_credentials_and_authenticate(rest_client, user, remote):
return self.call_rest_api_method(remote, method_name, *args, **kwargs)
else:
# Token expired or not valid, so clean the token and repeat the call
# (will be anonymous call but exporting who is calling)
# logger.info("Token expired or not valid, cleaning the saved token and retrying")
self._clear_user_tokens_in_db(user, remote)
return self.call_rest_api_method(remote, method_name, *args, **kwargs)

def _retry_with_new_token(self, user, remote, method_name, *args, **kwargs):
def _get_credentials_and_authenticate(self, rest_client, user, remote):
"""Try LOGIN_RETRIES to obtain a password from user input for which
we can get a valid token from api_client. If a token is returned,
credentials are stored in localdb and rest method is called"""
creds = RemoteCredentials(self._cache_folder, self._global_conf)
for _ in range(LOGIN_RETRIES):
creds = RemoteCredentials(self._cache_folder, self._global_conf)
input_user, input_password = creds.auth(remote)
input_user, input_password, interactive = creds.auth(remote)
try:
self._authenticate(remote, input_user, input_password)
self._authenticate(rest_client, remote, input_user, input_password)
except AuthenticationException:
out = ConanOutput()
if user is None:
out.error('Wrong user or password', error_type="exception")
else:
out.error(f'Wrong password for user "{user}"', error_type="exception")
if not interactive:
raise AuthenticationException(f"Authentication error in remote '{remote.name}'")
else:
return self.call_rest_api_method(remote, method_name, *args, **kwargs)

return True
raise AuthenticationException("Too many failed login attempts, bye!")

def _get_rest_client(self, remote):
username, token, refresh_token = self._localdb.get_login(remote.url)
custom_headers = {'X-Client-Anonymous-Id': self._get_mac_digest(),
'X-Client-Id': str(username or "")}
return RestApiClient(remote, token, refresh_token, custom_headers, self._requester,
self._global_conf, self._cached_capabilities)

def _clear_user_tokens_in_db(self, user, remote):
try:
self._localdb.store(user, token=None, refresh_token=None, remote_url=remote.url)
self._creds.set(remote, user, token=None)
except Exception as e:
out = ConanOutput()
out.error('Your credentials could not be stored in local cache\n', error_type="exception")
out.error('Your credentials could not be stored in local cache', error_type="exception")
out.debug(str(e) + '\n')

@staticmethod
def _get_mac_digest():
sha1 = hashlib.sha1()
sha1.update(str(get_mac()).encode())
return str(sha1.hexdigest())

def _authenticate(self, remote, user, password):
rest_client = self._get_rest_client(remote)
if user is None: # The user is already in DB, just need the password
prev_user = self._localdb.get_username(remote.url)
if prev_user is None:
raise ConanException("User for remote '%s' is not defined" % remote.name)
else:
user = prev_user
def _authenticate(self, rest_client, remote, user, password):
try:
token, refresh_token = rest_client.authenticate(user, password)
token = rest_client.authenticate(user, password)
except UnicodeDecodeError:
raise ConanException("Password contains not allowed symbols")

# Store result in DB
self._localdb.store(user, token, refresh_token, remote.url)
self._creds.set(remote, user, token)
3 changes: 0 additions & 3 deletions conans/client/rest/client_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ def search_packages(self, ref):
url = _format_ref(route, ref)
return self.base_url + url

def oauth_authenticate(self):
return self.base_url + self.routes.oauth_authenticate

def common_authenticate(self):
return self.base_url + self.routes.common_authenticate

Expand Down
8 changes: 4 additions & 4 deletions conans/client/rest/remote_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ def auth(self, remote, user=None):
msg = scoped_traceback(msg, e, scope="/extensions/plugins")
raise ConanException(msg)
if plugin_user and plugin_password:
return plugin_user, plugin_password
return plugin_user, plugin_password, False

# Then prioritize the cache "credentials.json" file
creds = self._urls.get(remote.name)
if creds is not None:
try:
return creds["user"], creds["password"]
return creds["user"], creds["password"], False
except KeyError as e:
raise ConanException(f"Authentication error, wrong credentials.json: {e}")

Expand All @@ -58,12 +58,12 @@ def auth(self, remote, user=None):
if env_passwd is not None:
if env_user is None:
raise ConanException("Found password in env-var, but not defined user")
return env_user, env_passwd
return env_user, env_passwd, False

# If not found, then interactive prompt
ui = UserInput(self._global_conf.get("core:non_interactive", check_type=bool))
input_user, input_password = ui.request_login(remote.name, user)
return input_user, input_password
return input_user, input_password, True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like UserInput throws if core:non_interactive is True, so at this point we're always interactive 👍


@staticmethod
def _get_env(remote, user):
Expand Down
48 changes: 13 additions & 35 deletions conans/client/rest/rest_client.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
from conans import CHECKSUM_DEPLOY, REVISIONS, OAUTH_TOKEN
from conans import CHECKSUM_DEPLOY, REVISIONS
from conans.client.rest.rest_client_v2 import RestV2Methods
from conans.errors import AuthenticationException, ConanException
from conans.errors import ConanException


class RestApiClient:
"""
Rest Api Client for handle remote.
"""

def __init__(self, remote, token, refresh_token, custom_headers, requester,
config, cached_capabilities):

# Set to instance
def __init__(self, remote, token, requester, config):
self._token = token
self._refresh_token = refresh_token
self._remote_url = remote.url
self._custom_headers = custom_headers
self._requester = requester

self._verify_ssl = remote.verify_ssl
self._config = config

# This dict is shared for all the instances of RestApiClient
self._cached_capabilities = cached_capabilities
self._remote = remote

def _capable(self, capability, user=None, password=None):
capabilities = self._cached_capabilities.get(self._remote_url)
# Caching of capabilities per-remote
capabilities = getattr(self._remote, "_capabilities", None)
if capabilities is None:
tmp = RestV2Methods(self._remote_url, self._token, self._custom_headers,
tmp = RestV2Methods(self._remote_url, self._token,
self._requester, self._config, self._verify_ssl)
capabilities = tmp.server_capabilities(user, password)
self._cached_capabilities[self._remote_url] = capabilities
setattr(self._remote, "_capabilities", capabilities)
return capability in capabilities

def _get_api(self):
Expand All @@ -41,7 +34,7 @@ def _get_api(self):
"Conan 2.0 is no longer compatible with "
"remotes that don't accept revisions.")
checksum_deploy = self._capable(CHECKSUM_DEPLOY)
return RestV2Methods(self._remote_url, self._token, self._custom_headers,
return RestV2Methods(self._remote_url, self._token,
self._requester, self._config, self._verify_ssl,
checksum_deploy)

Expand All @@ -61,26 +54,11 @@ def upload_package(self, pref, files_to_upload):
return self._get_api().upload_package(pref, files_to_upload)

def authenticate(self, user, password):
api_v2 = RestV2Methods(self._remote_url, self._token, self._custom_headers,
# BYPASS capabilities, in case v1/ping is protected
api_v2 = RestV2Methods(self._remote_url, self._token,
self._requester, self._config, self._verify_ssl)

if self._refresh_token and self._token:
token, refresh_token = api_v2.refresh_token(self._token, self._refresh_token)
else:
try:
# Check capabilities can raise also 401 until the new Artifactory is released
oauth_capable = self._capable(OAUTH_TOKEN, user, password)
except AuthenticationException:
oauth_capable = False

if oauth_capable:
# Artifactory >= 6.13.X
token, refresh_token = api_v2.authenticate_oauth(user, password)
else:
token = api_v2.authenticate(user, password)
refresh_token = None

return token, refresh_token
token = api_v2.authenticate(user, password)
return token

def check_credentials(self):
return self._get_api().check_credentials()
Expand Down
Loading