Repository: conan-io/conan

Issue:
Artifactory without enabled "anonymous access" doesn't use revisions
If the "anonymous access" is not enabled in Artifactory, the "ping" Conan endpoint returns a 401 if no authentication is sent, so the capabilities are never read, but the 401 is currently ignored by the client, storing an empty set of capabilities for the remote.

To fix this, we have to raise an exception if we receive a 401 in the "ping" endpoint so we force the authentication process in the client. But there is another issue to solve: the authenticating process in the client is trying to get the capabilities to know if it should use v1 or v2 so it is like a snake biting its own tail. 

It can be fixed by using always "v1" to authenticate and not hiding the 401 in the ping endpoint.



Discussion:

Resolving patch:
diff --git a/conans/client/rest/rest_client.py b/conans/client/rest/rest_client.py
--- a/conans/client/rest/rest_client.py
+++ b/conans/client/rest/rest_client.py
@@ -4,6 +4,7 @@
 from conans.client.rest.rest_client_v1 import RestV1Methods
 from conans.client.rest.rest_client_v2 import RestV2Methods
 from conans.errors import OnlyV2Available
+from conans.util.log import logger
 
 
 class RestApiClient(object):
@@ -33,6 +34,7 @@ def _get_api(self):
                                 self.requester, self.verify_ssl, self._put_headers)
             _, _, cap = tmp.server_info()
             self._cached_capabilities[self.remote_url] = cap
+            logger.debug("REST: Cached capabilities for the remote: %s" % cap)
             if not self._revisions_enabled and ONLY_V2 in cap:
                 raise OnlyV2Available(self.remote_url)
 
@@ -83,7 +85,11 @@ def upload_package(self, pref, files_to_upload, deleted, retry, retry_wait):
         return self._get_api().upload_package(pref, files_to_upload, deleted, retry, retry_wait)
 
     def authenticate(self, user, password):
-        return self._get_api().authenticate(user, password)
+        apiv1 = RestV1Methods(self.remote_url, self.token, self.custom_headers, self._output,
+                              self.requester, self.verify_ssl, self._put_headers)
+        # Use v1 for the auth because the "ping" could be also protected so we don't know if
+        # we can use v2
+        return apiv1.authenticate(user, password)
 
     def check_credentials(self):
         return self._get_api().check_credentials()
diff --git a/conans/client/rest/rest_client_common.py b/conans/client/rest/rest_client_common.py
--- a/conans/client/rest/rest_client_common.py
+++ b/conans/client/rest/rest_client_common.py
@@ -111,9 +111,9 @@ def server_info(self):
 
         ret = self.requester.get(url, auth=self.auth, headers=self.custom_headers,
                                  verify=self.verify_ssl)
-        if ret.status_code == 404:
-            raise NotFoundException("Not implemented endpoint")
 
+        if not ret.ok:
+            raise get_exception_from_error(ret.status_code)("")
         version_check = ret.headers.get('X-Conan-Client-Version-Check', None)
         server_version = ret.headers.get('X-Conan-Server-Version', None)
         server_capabilities = ret.headers.get('X-Conan-Server-Capabilities', "")


Test patch:
diff --git a/conans/test/functional/old/conan_trace_file_test.py b/conans/test/functional/old/conan_trace_file_test.py
--- a/conans/test/functional/old/conan_trace_file_test.py
+++ b/conans/test/functional/old/conan_trace_file_test.py
@@ -123,7 +123,7 @@ def test_trace_actions(self):
             self.assertEqual(num_post, 2)  # 2 get urls
 
         num_get = len([it for it in actions if "REST_API_CALL" in it and "GET" in it])
-        self.assertEqual(num_get, 10)
+        self.assertEqual(num_get, 9)
 
         # Check masked signature
         for action in actions:
diff --git a/conans/test/functional/remote/server_error_test.py b/conans/test/functional/remote/server_error_test.py
--- a/conans/test/functional/remote/server_error_test.py
+++ b/conans/test/functional/remote/server_error_test.py
@@ -14,7 +14,8 @@ def __init__(self, *args, **kwargs):
 
             def get(self, *args, **kwargs):  # @UnusedVariable
                 # Response must be binary, it is decoded in RestClientCommon
-                return namedtuple("Response", "status_code headers content")(200, {}, b'<>')
+                return namedtuple("Response", "status_code headers content ok")(200, {}, b'<>',
+                                                                                True)
 
         # https://github.com/conan-io/conan/issues/3432
         client = TestClient(servers={"default": TestServer()},
@@ -33,7 +34,8 @@ def __init__(self, *args, **kwargs):
             def get(self, *args, **kwargs):  # @UnusedVariable
                 # Response must be binary, it is decoded in RestClientCommon
                 headers = {"Content-Type": "application/json"}
-                return namedtuple("Response", "status_code headers content")(200, headers, b'<>')
+                return namedtuple("Response", "status_code headers content ok")(200, headers,
+                                                                                b'<>', True)
 
         # https://github.com/conan-io/conan/issues/3432
         client = TestClient(servers={"default": TestServer()},
@@ -51,8 +53,8 @@ def __init__(self, *args, **kwargs):
             def get(self, *args, **kwargs):  # @UnusedVariable
                 # Response must be binary, it is decoded in RestClientCommon
                 headers = {"Content-Type": "application/json"}
-                return namedtuple("Response", "status_code headers content")(200, headers,
-                                                                             b'[1, 2, 3]')
+                return namedtuple("Response", "status_code headers content ok")(200, headers,
+                                                                                b'[1, 2, 3]', True)
 
         # https://github.com/conan-io/conan/issues/3432
         client = TestClient(servers={"default": TestServer()},