Skip to content

Commit

Permalink
Fix/rest (#17083)
Browse files Browse the repository at this point in the history
* fixing Rest layer

* wip

* fix test

* caching localdb creds and capabilities
  • Loading branch information
memsharded authored Oct 7, 2024
1 parent 64daf86 commit 4917b9b
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 270 deletions.
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

@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

0 comments on commit 4917b9b

Please sign in to comment.