Repository: conan-io/conan
Pull request #17127 (2024-10-07 22:47:04)

ask for creds again when token was expired
Changelog: Fix: Get credentials and re-authenticate when an expired token gives AuthenticationException
Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/13858
Close https://github.com/conan-io/conan/issues/11925





Patch:
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index 959462acf..000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-# Configuration for probot-stale - https://github.com/probot/stale
-
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 60
-
-# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
-# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 7
-
-# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
-onlyLabels:
-    - "type: question"
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
-    - "stage: triaging"
-
-# Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
-
-# Set to true to ignore issues in a milestone (defaults to false)
-exemptMilestones: false
-
-# Set to true to ignore issues with an assignee (defaults to false)
-exemptAssignees: false
-
-# Label to use when marking as stale
-staleLabel: staled
-
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: >
-  This issue has been automatically marked as stale because it has not had
-  recent activity. It will be closed if no further activity occurs. Thank you
-  for your contributions.
-
-# Comment to post when removing the stale label.
-# unmarkComment: >
-#   Your comment here.
-
-# Comment to post when closing a stale Issue or Pull Request.
- closeComment: >
-   This issue has been automatically closed because it has not had recent activity. Feel
-     free to reopen it if necessary. Thank you for your contributions.
-
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 30
-
-# Limit to only `issues` or `pulls`
-only: issues
-
-# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
-# pulls:
-#   daysUntilStale: 30
-#   markComment: >
-#     This pull request has been automatically marked as stale because it has not had
-#     recent activity. It will be closed if no further activity occurs. Thank you
-#     for your contributions.
-
-# issues:
-#   exemptLabels:
-#     - confirmed
diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py
index ee0c4164b..60e71f0d0 100644
--- a/conan/cli/commands/graph.py
+++ b/conan/cli/commands/graph.py
@@ -10,6 +10,7 @@ from conan.cli.commands.list import prepare_pkglist_compact, print_serial
 from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot
 from conan.cli.formatters.graph.build_order_html import format_build_order_html
 from conan.cli.formatters.graph.graph_info_text import format_graph_info
+from conan.cli.printers import print_profiles
 from conan.cli.printers.graph import print_graph_packages, print_graph_basic
 from conan.errors import ConanException
 from conan.internal.deploy import do_deploys
@@ -209,6 +210,7 @@ def graph_info(conan_api, parser, subparser, *args):
                                                partial=args.lockfile_partial,
                                                overrides=overrides)
     profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)
+    print_profiles(profile_host, profile_build)
 
     if path:
         deps_graph = conan_api.graph.load_graph_consumer(path, args.name, args.version,
diff --git a/conans/client/graph/install_graph.py b/conans/client/graph/install_graph.py
index d51f9082c..cc7db6587 100644
--- a/conans/client/graph/install_graph.py
+++ b/conans/client/graph/install_graph.py
@@ -1,5 +1,6 @@
 import json
 import os
+import shlex
 import textwrap
 
 from conan.api.output import ConanOutput
@@ -72,7 +73,8 @@ class _InstallPackageReference:
         compatible = "compatible:" if self.info and self.info.get("compatibility_delta") else ""
         cmd += f" --build={compatible}{self.ref}"
         if self.options:
-            cmd += " " + " ".join(f"-o {o}" for o in self.options)
+            scope = "" if self.context == "host" else ":b"
+            cmd += " " + " ".join(f'-o{scope}="{o}"' for o in self.options)
         if self.overrides:
             cmd += f' --lockfile-overrides="{self.overrides}"'
         return cmd
@@ -283,7 +285,8 @@ class _InstallConfiguration:
         compatible = "compatible:" if self.info and self.info.get("compatibility_delta") else ""
         cmd += f" --build={compatible}{self.ref}"
         if self.options:
-            cmd += " " + " ".join(f"-o {o}" for o in self.options)
+            scope = "" if self.context == "host" else ":b"
+            cmd += " " + " ".join(f'-o{scope}="{o}"' for o in self.options)
         if self.overrides:
             cmd += f' --lockfile-overrides="{self.overrides}"'
         return cmd
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index e67745ba8..ca2dc3617 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -60,18 +60,10 @@ class ConanApiAuthManager:
             raise ForbiddenException(f"Permission denied for user: '{user}': {e}")
         except AuthenticationException:
             # User valid but not enough permissions
-            if user is None or token is None:
-                # token is None when you change user with user command
-                # 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)
-                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)
+            # token is None when you change user with user command
+            # Anonymous is not enough, ask for a user
+            ConanOutput().info(f"Remote '{remote.name}' needs authentication, obtaining credentials")
+            if self._get_credentials_and_authenticate(rest_client, user, remote):
                 return self.call_rest_api_method(remote, method_name, *args, **kwargs)
 
     def _get_credentials_and_authenticate(self, rest_client, user, remote):
@@ -95,14 +87,6 @@ class ConanApiAuthManager:
                 return True
         raise AuthenticationException("Too many failed login attempts, bye!")
 
-    def _clear_user_tokens_in_db(self, user, remote):
-        try:
-            self._creds.set(remote, user, token=None)
-        except Exception as e:
-            out = ConanOutput()
-            out.error('Your credentials could not be stored in local cache', error_type="exception")
-            out.debug(str(e) + '\n')
-
     def _authenticate(self, rest_client, remote, user, password):
         try:
             token = rest_client.authenticate(user, password)


Test patch:
diff --git a/test/integration/command_v2/test_info_build_order.py b/test/integration/command_v2/test_info_build_order.py
index 21f6b86a4..bb9c688c7 100644
--- a/test/integration/command_v2/test_info_build_order.py
+++ b/test/integration/command_v2/test_info_build_order.py
@@ -237,13 +237,13 @@ def test_info_build_order_options():
                   'context': 'build', 'depends': [], "overrides": {},
                   'binary': 'Build', 'options': ['tool/0.1:myopt=2'], 'filenames': [],
                   'info': {'options': {'myopt': '2'}},
-                  'build_args': '--tool-requires=tool/0.1 --build=tool/0.1 -o tool/0.1:myopt=2'},
+                  'build_args': '--tool-requires=tool/0.1 --build=tool/0.1 -o:b="tool/0.1:myopt=2"'},
                  {'package_id': 'a9035d84c5880b26c4b44acf70078c9a7dd37412', 'prev': None,
                   'context': 'build', 'depends': [], "overrides": {},
                   'info': {'options': {'myopt': '1'}},
                   'binary': 'Build', 'options': ['tool/0.1:myopt=1'],
                   'filenames': [],
-                  'build_args': '--tool-requires=tool/0.1 --build=tool/0.1 -o tool/0.1:myopt=1'}
+                  'build_args': '--tool-requires=tool/0.1 --build=tool/0.1 -o:b="tool/0.1:myopt=1"'}
              ]]}
         ],
         [
@@ -765,3 +765,22 @@ class TestBuildOrderReduce:
         tc.run("graph build-order-merge --file=order.json --file=order.json --format=json", assert_error=True)
         assert "dep/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: Invalid configuration" in tc.out
         assert "IndexError: list index out of range" not in tc.out
+
+def test_build_order_space_in_options():
+    tc = TestClient(light=True)
+    tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0")
+                .with_option("flags", ["ANY", None])
+                .with_option("extras", ["ANY", None]),
+             "conanfile.txt": textwrap.dedent("""
+             [requires]
+             dep/1.0
+
+             [options]
+             dep/*:flags=define=FOO define=BAR define=BAZ
+             dep/*:extras=cxx="yes" gnuext='no'
+             """)})
+
+    tc.run("create dep")
+    tc.run("graph build-order . --order-by=configuration --build=dep/1.0 -f=json", redirect_stdout="order.json")
+    order = json.loads(tc.load("order.json"))
+    assert order["order"][0][0]["build_args"] == '''--requires=dep/1.0 --build=dep/1.0 -o="dep/*:extras=cxx="yes" gnuext='no'" -o="dep/*:flags=define=FOO define=BAR define=BAZ"'''
diff --git a/test/integration/remote/auth_test.py b/test/integration/remote/auth_test.py
index 80aacdcb8..da585c94e 100644
--- a/test/integration/remote/auth_test.py
+++ b/test/integration/remote/auth_test.py
@@ -202,9 +202,9 @@ def test_token_expired():
        */*@*/*: admin
        """)
     save(os.path.join(server_folder, ".conan_server", "server.conf"), server_conf)
-    server = TestServer(base_path=server_folder, users={"admin": "password"})
+    server = TestServer(base_path=server_folder, users={"admin": "password", "other": "pass"})
 
-    c = TestClient(servers={"default": server}, inputs=["admin", "password"])
+    c = TestClient(servers={"default": server}, inputs=["admin", "password", "other", "pass"])
     c.save({"conanfile.py": GenConanfile()})
     c.run("create . --name=pkg --version=0.1 --user=user --channel=stable")
     c.run("upload * -r=default -c")
@@ -216,13 +216,12 @@ def test_token_expired():
     import time
     time.sleep(3)
     c.users = {}
-    conan_conf = "core:non_interactive=True"
-    c.save_home({"global.conf": conan_conf})
     c.run("remove * -c")
     c.run("install --requires=pkg/0.1@user/stable")
+    assert "Remote 'default' needs authentication, obtaining credentials" in c.out
     user, token, _ = localdb.get_login(server.fake_url)
-    assert user == "admin"
-    assert token is None
+    assert user == "other"
+    assert token is not None
 
 
 def test_auth_username_space():