Repository: conan-io/conan

Issue:
conan user command show which users are authenticated
cc / @Aalmann

PRobably use JSon output to it too.



Discussion:
danimtb: Could implement the json part but will only make sens for the user list output.
danimtb: JSON output example: `$ conan user --json user.json`

```
{  
    "users":[  
        {  
            "remote_name":"conan-center",
            "name":"danimtb",
            "authenticated":true
        },
        {  
            "remote_name":"bincrafters",
            "name":null,
            "authenticated":false
        },
        {  
            "remote_name":"upload_repo",
            "name":"danimtb",
            "authenticated":true
        },
        {  
            "remote_name":"conan-community",
            "name":"danimtb",
            "authenticated":true
        },
        {  
            "remote_name":"conan_artifactory",
            "name":"danielm",
            "authenticated":true
        },
        {  
            "remote_name":"szmyd",
            "name":null,
            "authenticated":false
        },
        {  
            "remote_name":"cli11",
            "name":"kkk",
            "authenticated":false
        },
        {  
            "remote_name":"conan_server",
            "name":"demo",
            "authenticated":true
        }
    ]
}
```

Patch:
diff --git a/conans/client/cmd/search.py b/conans/client/cmd/search.py
index 29ad716e6..77ee84fb8 100644
--- a/conans/client/cmd/search.py
+++ b/conans/client/cmd/search.py
@@ -1,7 +1,7 @@
 from conans.search.search import filter_outdated, search_recipes,\
     search_packages
-from collections import OrderedDict
-
+from collections import OrderedDict, namedtuple
+from conans.errors import NotFoundException
 
 class Search(object):
     def __init__(self, client_cache, remote_manager, remote_registry):
@@ -33,29 +33,74 @@ class Search(object):
         references[remote.name] = refs
         return references
 
+    remote_ref = namedtuple('remote_ref', 'ordered_packages recipe_hash')
     def search_packages(self, reference=None, remote_name=None, query=None, outdated=False):
         """ Return the single information saved in conan.vars about all the packages
             or the packages which match with a pattern
 
             Attributes:
                 pattern = string to match packages
-                remote = search on another origin to get packages info
+                remote_name = search on another origin to get packages info
                 packages_pattern = String query with binary
                                    packages properties: "arch=x86 AND os=Windows"
         """
-        if remote_name:
-            remote = self._registry.remote(remote_name)
-            packages_props = self._remote_manager.search_packages(remote, reference, query)
-            ordered_packages = OrderedDict(sorted(packages_props.items()))
-            manifest = self._remote_manager.get_conan_manifest(reference, remote)
-            recipe_hash = manifest.summary_hash
-        else:
-            packages_props = search_packages(self._client_cache, reference, query)
-            ordered_packages = OrderedDict(sorted(packages_props.items()))
-            try:
-                recipe_hash = self._client_cache.load_manifest(reference).summary_hash
-            except IOError:  # It could not exist in local
-                recipe_hash = None
+        if not remote_name:
+            return self._search_packages_in_local(reference, query, outdated)
+
+        if remote_name == 'all':
+            return self._search_packages_in_all(reference, query, outdated)
+
+        return self._search_packages_in(remote_name, reference, query, outdated)
+
+    def _search_packages_in_local(self, reference=None, query=None, outdated=False):
+        packages_props = search_packages(self._client_cache, reference, query)
+        ordered_packages = OrderedDict(sorted(packages_props.items()))
+        try:
+            recipe_hash = self._client_cache.load_manifest(reference).summary_hash
+        except IOError:  # It could not exist in local
+            recipe_hash = None
+
         if outdated and recipe_hash:
             ordered_packages = filter_outdated(ordered_packages, recipe_hash)
-        return ordered_packages, reference, recipe_hash
+
+        references = OrderedDict()
+        references[None] = self.remote_ref(ordered_packages, recipe_hash)
+        return references
+
+    def _search_packages_in_all(self, reference=None, query=None, outdated=False):
+        references = OrderedDict()
+        remotes = self._registry.remotes
+        # We have to check if there is a remote called "all"
+        # Deprecate: 2.0 can remove this check
+        if 'all' not in (r.name for r in remotes):
+            for remote in remotes:
+                try:
+                    packages_props = self._remote_manager.search_packages(remote, reference, query)
+                    if packages_props:
+                        ordered_packages = OrderedDict(sorted(packages_props.items()))
+                        manifest = self._remote_manager.get_conan_manifest(reference, remote)
+                        recipe_hash = manifest.summary_hash
+
+                        if outdated and recipe_hash:
+                            ordered_packages = filter_outdated(ordered_packages, recipe_hash)
+
+                        references[remote.name] = self.remote_ref(ordered_packages, recipe_hash)
+                except NotFoundException:
+                    continue
+            return references
+        
+        return self._search_packages_in(self, 'all', reference, query, outdated)
+
+    def _search_packages_in(self, remote_name, reference=None, query=None, outdated=False):
+        remote = self._registry.remote(remote_name)
+        packages_props = self._remote_manager.search_packages(remote, reference, query)
+        ordered_packages = OrderedDict(sorted(packages_props.items()))
+        manifest = self._remote_manager.get_conan_manifest(reference, remote)
+        recipe_hash = manifest.summary_hash
+
+        if outdated and recipe_hash:
+            ordered_packages = filter_outdated(ordered_packages, recipe_hash)
+
+        references = OrderedDict()
+        references[remote.name] = self.remote_ref(ordered_packages, recipe_hash)
+        return references
\ No newline at end of file
diff --git a/conans/client/cmd/user.py b/conans/client/cmd/user.py
index fcc20e1b3..1daf21c42 100644
--- a/conans/client/cmd/user.py
+++ b/conans/client/cmd/user.py
@@ -2,48 +2,36 @@ from conans.errors import ConanException
 from conans.client.store.localdb import LocalDB
 
 
-def users_list(client_cache, registry, remote_name=None):
-    # List all users from required remotes
-    if remote_name:
-        remotes = [registry.remote(remote_name)]
-    else:
-        remotes = registry.remotes
-
+def users_list(localdb_file, remotes):
     if not remotes:
         raise ConanException("No remotes defined")
 
-    localdb = LocalDB(client_cache.localdb)
-    result = []
+    localdb = LocalDB(localdb_file)
+    remotes_info = []
     for remote in remotes:
-        prev_user = localdb.get_username(remote.url)
-        username = prev_user or "None (anonymous)"
-        result.append((remote.name, username))
-    return result
+        user_info = {}
+        user, token = localdb.get_login(remote.url)
+        user_info["name"] = remote.name
+        user_info["user_name"] = user
+        user_info["authenticated"] = True if token else False
+        remotes_info.append(user_info)
+    return remotes_info
 
 
-def users_clean(client_cache):
-    localdb = LocalDB(client_cache.localdb)
+def users_clean(localdb_file):
+    localdb = LocalDB(localdb_file)
     localdb.init(clean=True)
 
 
-def user_set(client_cache, output, registry, user, remote_name=None):
-    localdb = LocalDB(client_cache.localdb)
-    if not remote_name:
-        remote = registry.default_remote
-    else:
-        remote = registry.remote(remote_name)
+def user_set(localdb_file, user, remote=None):
+    localdb = LocalDB(localdb_file)
 
     if user.lower() == "none":
         user = None
-    update_localdb(localdb, user, None, remote, output)
+    return update_localdb(localdb, user, None, remote)
 
 
-def update_localdb(localdb, user, token, remote, output):
+def update_localdb(localdb, user, token, remote):
     previous_user = localdb.get_username(remote.url)
     localdb.set_login((user, token), remote.url)
-    previous_user = previous_user or "None (anonymous)"
-    user = user or "None (anonymous)"
-    if previous_user == user:
-        output.info("Current '%s' user already: %s" % (remote.name, previous_user))
-    else:
-        output.info("Change '%s' user from %s to %s" % (remote.name, previous_user, user))
+    return remote.name, previous_user, user
diff --git a/conans/client/command.py b/conans/client/command.py
index 32d5a6934..0d1d62467 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -766,36 +766,66 @@ class Command(object):
         return self._conan.copy(reference=args.reference, user_channel=args.user_channel,
                                 force=args.force, packages=args.package or args.all)
 
-    def user(self, *parameters):
+    def user(self, *args):
         """Authenticates against a remote with user/pass, caching the auth token. Useful to avoid
         the user and password being requested later. e.g. while you're uploading a package.
         You can have one user for each remote. Changing the user, or introducing the
         password is only necessary to perform changes in remote packages.
         """
+        # FIXME: Difficult and confusing CLI. Better with:
+        # - conan user clean -> clean users
+        # - conan user list ('remote') -> list users (of a remote)
+        # - conan user auth 'remote' ('user') ('password') -> login a remote (w/o user or pass)
+        # - conan user set 'user' 'remote' -> set user for a remote (not login) necessary??
         parser = argparse.ArgumentParser(description=self.user.__doc__, prog="conan user")
         parser.add_argument("name", nargs='?', default=None,
                             help='Username you want to use. If no name is provided it will show the'
                             ' current user')
         parser.add_argument('-c', '--clean', default=False, action='store_true',
                             help='Remove user and tokens for all remotes')
-        parser.add_argument("-p", "--password", nargs='?', const="", type=str,
-                            action=OnceArgument,
+        parser.add_argument("-p", "--password", nargs='?', const="", type=str, action=OnceArgument,
                             help='User password. Use double quotes if password with spacing, '
                                  'and escape quotes if existing. If empty, the password is '
                                  'requested interactively (not exposed)')
         parser.add_argument("-r", "--remote", help='Use the specified remote server',
                             action=OnceArgument)
-        args = parser.parse_args(*parameters)  # To enable -h
+        parser.add_argument("-j", "--json", default=None, action=OnceArgument,
+                            help='json file path where the user list will be written to')
+        args = parser.parse_args(*args)
 
-        if args.clean:
-            return self._conan.users_clean()
-        if args.password is None:
-            if args.name is None:
-                return self._conan.users_list(args.remote)
-            else:
-                return self._conan.user_set(args.name, args.remote)
-        return self._conan.authenticate(name=args.name, remote=args.remote,
-                                        password=args.password)
+        if args.clean and any((args.name, args.remote, args.password, args.json)):
+            raise ConanException("'--clean' argument cannot be used together with 'name', "
+                                 "'--password', '--remote' or '--json'")
+        elif args.json and any((args.name, args.password)):
+            raise ConanException("'--json' cannot be used together with 'name' or '--password'")
+
+        cwd = os.getcwd()
+        info = None
+
+        try:
+            if args.clean:  # clean users
+                self._conan.users_clean()
+            elif not args.name and args.password is None:  # list users
+                info = self._conan.users_list(args.remote)
+                self._outputer.print_user_list(info)
+            elif args.password is None:  # set user for remote (no password indicated)
+                remote, prev_user, user = self._conan.user_set(args.name, args.remote)
+                self._outputer.print_user_set(remote, prev_user, user)
+            else:  # login a remote
+                remote = args.remote or self._conan.get_default_remote().name
+                name = args.name
+                password = args.password
+                if not password:
+                    name, password = self._user_io.request_login(remote_name=remote, username=name)
+                remote, prev_user, user = self._conan.authenticate(name, remote=remote,
+                                                                   password=password)
+                self._outputer.print_user_set(remote, prev_user, user)
+        except ConanException as exc:
+            info = exc.info
+            raise
+        finally:
+            if args.json and info:
+                self._outputer.json_output(info, args.json, cwd)
 
     def search(self, *args):
         """Searches package recipes and binaries in the local cache or in a remote.
@@ -845,7 +875,7 @@ class Command(object):
             if reference:
                 info = self._conan.search_packages(reference, query=args.query, remote=args.remote,
                                                    outdated=args.outdated)
-                # search is done for one reference and one remote
+                # search is done for one reference
                 self._outputer.print_search_packages(info["results"], reference, args.query,
                                                      args.table)
             else:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index 83696c31b..df7e73e45 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -660,29 +660,31 @@ class ConanAPIV1(object):
                  self._user_io, self._remote_manager, self._registry, force=force)
 
     @api_method
-    def authenticate(self, name, password, remote=None):
-        if not remote:
-            remote = self._registry.default_remote
-        else:
-            remote = self._registry.remote(remote)
-        if password == "":
-            name, password = self._user_io.request_login(remote_name=remote.name,
-                                                         username=name)
-        self._remote_manager.authenticate(remote, name, password)
+    def authenticate(self, name, password, remote):
+        remote = self.get_remote_by_name(remote)
+        _, remote, prev_user, user = self._remote_manager.authenticate(remote, name, password)
+        return remote, prev_user, user
 
     @api_method
-    def user_set(self, user, remote_name=None):
-        return user_set(self._client_cache, self._user_io.out, self._registry, user, remote_name)
+    def user_set(self, user, remote=None):
+        remote = self.get_default_remote() if not remote else self.get_remote_by_name(remote)
+        return user_set(self._client_cache.localdb, user, remote)
 
     @api_method
     def users_clean(self):
-        return users_clean(self._client_cache)
+        users_clean(self._client_cache.localdb)
 
     @api_method
     def users_list(self, remote=None):
-        users = users_list(self._client_cache, self._registry, remote)
-        for remote_name, username in users:
-            self._user_io.out.info("Current '%s' user: %s" % (remote_name, username))
+        info = {"error": False, "remotes": []}
+        remotes = [self.get_remote_by_name(remote)] if remote else self.remote_list()
+        try:
+            info["remotes"] = users_list(self._client_cache.localdb, remotes)
+            return info
+        except ConanException as exc:
+            info["error"] = True
+            exc.info = info
+            raise
 
     @api_method
     def search_recipes(self, pattern, remote=None, case_sensitive=False):
@@ -707,23 +709,24 @@ class ConanAPIV1(object):
         search = Search(self._client_cache, self._remote_manager, self._registry)
 
         try:
-            ordered_packages, ref, recipe_hash = search.search_packages(reference, remote,
-                                                                        query=query,
-                                                                        outdated=outdated)
+            references = search.search_packages(reference, remote,
+                                                query=query,
+                                                outdated=outdated)
         except ConanException as exc:
             recorder.error = True
             exc.info = recorder.get_info()
             raise
 
-        recorder.add_recipe(str(remote), str(ref))
-        if ordered_packages:
-            for package_id, properties in ordered_packages.items():
-                package_recipe_hash = properties.get("recipe_hash", None)
-                recorder.add_package(str(remote), str(reference), package_id,
+        for remote, remote_ref in references.items():
+            recorder.add_recipe(str(remote), str(reference))
+            if remote_ref.ordered_packages:
+                for package_id, properties in remote_ref.ordered_packages.items():
+                    package_recipe_hash = properties.get("recipe_hash", None)
+                    recorder.add_package(str(remote), str(reference), package_id,
                                      properties.get("options", []),
                                      properties.get("settings", []),
                                      properties.get("full_requires", []),
-                                     recipe_hash != package_recipe_hash)
+                                     remote_ref.recipe_hash != package_recipe_hash)
         return recorder.get_info()
 
     @api_method
@@ -824,7 +827,7 @@ class ConanAPIV1(object):
         if not remote:
             return get_path(self._client_cache, reference, package_id, path), path
         else:
-            remote = self._registry.remote(remote)
+            remote = self.get_remote_by_name(remote)
             return self._remote_manager.get_path(reference, package_id, path, remote), path
 
     @api_method
@@ -833,6 +836,14 @@ class ConanAPIV1(object):
         target_reference = ConanFileReference.loads(str(target_reference))
         return export_alias(reference, target_reference, self._client_cache)
 
+    @api_method
+    def get_default_remote(self):
+        return self._registry.default_remote
+
+    @api_method
+    def get_remote_by_name(self, remote):
+        return self._registry.remote(remote)
+
 
 Conan = ConanAPIV1
 
diff --git a/conans/client/conan_command_output.py b/conans/client/conan_command_output.py
index c2b20dca3..b2b652348 100644
--- a/conans/client/conan_command_output.py
+++ b/conans/client/conan_command_output.py
@@ -56,7 +56,8 @@ class CommandOutputer(object):
             return obj.isoformat() if hasattr(obj, 'isoformat') else obj
 
         save(json_output, json.dumps(info, default=date_handler))
-        self.user_io.out.info("json file created at '%s'" % json_output)
+        self.user_io.out.writeln("")
+        self.user_io.out.info("JSON file created at '%s'" % json_output)
 
     def _read_dates(self, deps_graph):
         ret = {}
@@ -98,7 +99,7 @@ class CommandOutputer(object):
     def print_search_packages(self, search_info, reference, packages_query, table):
         if table:
             from conans.client.graph.grapher import html_binary_graph
-            html_binary_graph(search_info, table)
+            html_binary_graph(search_info, reference, table)
         else:
             printer = Printer(self.user_io.out)
             printer.print_search_packages(search_info, reference, packages_query)
@@ -127,3 +128,25 @@ class CommandOutputer(object):
             lexer = TextLexer()
 
         self.user_io.out.write(highlight(contents, lexer, TerminalFormatter()))
+
+    def print_user_list(self, info):
+        for remote in info["remotes"]:
+            authenticated = " [Authenticated]" if remote["authenticated"] else ""
+            anonymous = " (anonymous)" if not remote["user_name"] else ""
+            self.user_io.out.info("Current user of remote '%s' set to: '%s'%s%s" %
+                                  (remote["name"], str(remote["user_name"]), anonymous,
+                                   authenticated))
+
+    def print_user_set(self, remote, prev_user, user):
+        previous_username = prev_user or "None"
+        previous_anonymous = " (anonymous)" if not prev_user else ""
+        username = user or "None"
+        anonymous = " (anonymous)" if not user else ""
+
+        if prev_user == user:
+            self.user_io.out.info("User of remote '%s' is already '%s'%s" %
+                                  (remote, previous_username, previous_anonymous))
+        else:
+            self.user_io.out.info("Changed user of remote '%s' from '%s'%s to '%s'%s" %
+                                  (remote, previous_username, previous_anonymous, username,
+                                   anonymous))
diff --git a/conans/client/graph/grapher.py b/conans/client/graph/grapher.py
index 852b6487b..5f0823400 100644
--- a/conans/client/graph/grapher.py
+++ b/conans/client/graph/grapher.py
@@ -3,45 +3,7 @@ from conans.client.installer import build_id
 from collections import defaultdict, namedtuple
 
 
-def html_binary_graph(search_info, table_filename):
-    reference = search_info[0]["items"][0]["recipe"]["id"]
-    ordered_packages = search_info[0]["items"][0]["packages"]
-    binary = namedtuple("Binary", "ID outdated")
-    columns = set()
-    table = defaultdict(dict)
-    for package in ordered_packages:
-        package_id = package["id"]
-        settings = package["settings"]
-        if settings:
-            row_name = "%s %s %s" % (settings.get("os", "None"), settings.get("compiler", "None"),
-                                     settings.get("compiler.version", "None"))
-            column_name = []
-            for setting, value in settings.items():
-                if setting.startswith("compiler."):
-                    if not setting.startswith("compiler.version"):
-                        row_name += " (%s)" % value
-                elif setting not in ("os", "compiler"):
-                    column_name.append(value)
-            column_name = " ".join(column_name)
-        else:
-            row_name = "NO settings"
-            column_name = ""
-
-        options = package["options"]
-        if options:
-            for k, v in options.items():
-                column_name += "<br>%s=%s" % (k, v)
-
-        column_name = column_name or "NO options"
-        columns.add(column_name)
-        # Always compare outdated with local recipe, simplification,
-        # if a remote check is needed install recipe first
-        if "outdated" in package:
-            outdated = package["outdated"]
-        else:
-            outdated = None
-        table[row_name][column_name] = binary(package_id, outdated)
-
+def html_binary_graph(search_info, reference, table_filename):
     result = ["""<style>
     table, th, td {
     border: 1px solid black;
@@ -64,25 +26,67 @@ def html_binary_graph(search_info, table_filename):
 </script>
 <h1>%s</h1>
     """ % str(reference)]
-    headers = sorted(columns)
-    result.append("<table>")
-    result.append("<tr>")
-    result.append("<th></th>")
-    for header in headers:
-        result.append("<th>%s</th>" % header)
-    result.append("</tr>")
-    for row, columns in sorted(table.items()):
+
+    for remote_info in search_info:
+        if remote_info["remote"] != 'None':
+            result.append("<h2>'%s':</h2>" % str(remote_info["remote"]))
+
+        ordered_packages = remote_info["items"][0]["packages"]
+        binary = namedtuple("Binary", "ID outdated")
+        columns = set()
+        table = defaultdict(dict)
+        for package in ordered_packages:
+            package_id = package["id"]
+            settings = package["settings"]
+            if settings:
+                row_name = "%s %s %s" % (settings.get("os", "None"), settings.get("compiler", "None"),
+                                        settings.get("compiler.version", "None"))
+                column_name = []
+                for setting, value in settings.items():
+                    if setting.startswith("compiler."):
+                        if not setting.startswith("compiler.version"):
+                            row_name += " (%s)" % value
+                    elif setting not in ("os", "compiler"):
+                        column_name.append(value)
+                column_name = " ".join(column_name)
+            else:
+                row_name = "NO settings"
+                column_name = ""
+
+            options = package["options"]
+            if options:
+                for k, v in options.items():
+                    column_name += "<br>%s=%s" % (k, v)
+
+            column_name = column_name or "NO options"
+            columns.add(column_name)
+            # Always compare outdated with local recipe, simplification,
+            # if a remote check is needed install recipe first
+            if "outdated" in package:
+                outdated = package["outdated"]
+            else:
+                outdated = None
+            table[row_name][column_name] = binary(package_id, outdated)
+
+        headers = sorted(columns)
+        result.append("<table>")
         result.append("<tr>")
-        result.append("<td>%s</td>" % row)
+        result.append("<th></th>")
         for header in headers:
-            col = columns.get(header, "")
-            if col:
-                color = "#ffff00" if col.outdated else "#00ff00"
-                result.append('<td bgcolor=%s id="%s" onclick=handleEvent("%s")></td>' % (color, col.ID, col.ID))
-            else:
-                result.append("<td></td>")
+            result.append("<th>%s</th>" % header)
         result.append("</tr>")
-    result.append("</table>")
+        for row, columns in sorted(table.items()):
+            result.append("<tr>")
+            result.append("<td>%s</td>" % row)
+            for header in headers:
+                col = columns.get(header, "")
+                if col:
+                    color = "#ffff00" if col.outdated else "#00ff00"
+                    result.append('<td bgcolor=%s id="%s" onclick=handleEvent("%s")></td>' % (color, col.ID, col.ID))
+                else:
+                    result.append("<td></td>")
+            result.append("</tr>")
+        result.append("</table>")
 
     result.append('<br>Selected: <div id="SelectedPackage"></div>')
 
diff --git a/conans/client/printer.py b/conans/client/printer.py
index 6aadedf15..c1504423c 100644
--- a/conans/client/printer.py
+++ b/conans/client/printer.py
@@ -171,40 +171,44 @@ class Printer(object):
                     self._out.writeln(conan_item["recipe"]["id"])
 
     def print_search_packages(self, search_info, reference, packages_query):
-        if not search_info[0]["items"][0]["packages"]:
-            if packages_query:
-                warn_msg = ("There are no packages for reference '%s' matching the query '%s'" %
-                            (str(reference), packages_query))
-            elif search_info[0]["items"][0]["recipe"]:
-                warn_msg = "There are no packages for reference '%s', but package recipe found." % \
-                           str(reference)
-            self._out.info(warn_msg)
-            return
-        # Only one repository is used, local cache (None) or a remote, so we use only first element
-        reference = search_info[0]["items"][0]["recipe"]["id"]
-        packages = search_info[0]["items"][0]["packages"]
-
         self._out.info("Existing packages for recipe %s:\n" % str(reference))
-        # Each package
-        for package in packages:
-            package_id = package["id"]
-            self._print_colored_line("Package_ID", package_id, 1)
-            for section in ("options", "settings", "requires"):
-                attr = package[section]
-                if attr:
-                    self._print_colored_line("[%s]" % section, indent=2)
-                    if isinstance(attr, dict):  # options, settings
-                        attr = OrderedDict(sorted(attr.items()))
-                        for key, value in attr.items():
-                            self._print_colored_line(key, value=value, indent=3)
-                    elif isinstance(attr, list):  # full requires
-                        for key in sorted(attr):
-                            self._print_colored_line(key, indent=3)
-            # Always compare outdated with local recipe, simplification,
-            # if a remote check is needed install recipe first
-            if "outdated" in package:
-                self._print_colored_line("Outdated from recipe: %s" % package["outdated"], indent=2)
-            self._out.writeln("")
+        for remote_info in search_info:
+            if remote_info["remote"] != 'None':
+                self._out.info("Existing recipe in remote '%s':\n" % str(remote_info["remote"]))
+
+            if not remote_info["items"][0]["packages"]:
+                if packages_query:
+                    warn_msg = ("There are no packages for reference '%s' matching the query '%s'" %
+                                (str(reference), packages_query))
+                elif remote_info["items"][0]["recipe"]:
+                    warn_msg = "There are no packages for reference '%s', but package recipe found." % \
+                            str(reference)
+                self._out.info(warn_msg)
+                continue
+                
+            reference = remote_info["items"][0]["recipe"]["id"]
+            packages = remote_info["items"][0]["packages"]
+
+            # Each package
+            for package in packages:
+                package_id = package["id"]
+                self._print_colored_line("Package_ID", package_id, 1)
+                for section in ("options", "settings", "requires"):
+                    attr = package[section]
+                    if attr:
+                        self._print_colored_line("[%s]" % section, indent=2)
+                        if isinstance(attr, dict):  # options, settings
+                            attr = OrderedDict(sorted(attr.items()))
+                            for key, value in attr.items():
+                                self._print_colored_line(key, value=value, indent=3)
+                        elif isinstance(attr, list):  # full requires
+                            for key in sorted(attr):
+                                self._print_colored_line(key, indent=3)
+                # Always compare outdated with local recipe, simplification,
+                # if a remote check is needed install recipe first
+                if "outdated" in package:
+                    self._print_colored_line("Outdated from recipe: %s" % package["outdated"], indent=2)
+                self._out.writeln("")
 
     def print_profile(self, name, profile):
         self._out.info("Configuration for profile %s:\n" % name)
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index f51c7afe7..8f46ca4e8 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -61,7 +61,7 @@ def input_credentials_if_unauthorized(func):
         for _ in range(LOGIN_RETRIES):
             user, password = self._user_io.request_login(self._remote.name, self.user)
             try:
-                token = self.authenticate(user, password)
+                token, _, _, _ = self.authenticate(user, password)
             except AuthenticationException:
                 if self.user is None:
                     self._user_io.out.error('Wrong user or password')
@@ -193,5 +193,5 @@ class ConanApiAuthManager(object):
             raise ConanException("Password contains not allowed symbols")
 
         # Store result in DB
-        update_localdb(self._localdb, user, token, self._remote, self._user_io.out)
-        return token
+        remote, prev_user, user = update_localdb(self._localdb, user, token, self._remote)
+        return token, remote, prev_user, user
diff --git a/conans/client/tools/files.py b/conans/client/tools/files.py
index e24f66d2a..5d6ddef97 100644
--- a/conans/client/tools/files.py
+++ b/conans/client/tools/files.py
@@ -4,6 +4,7 @@ import os
 import sys
 
 from contextlib import contextmanager
+from fnmatch import fnmatch
 from patch import fromfile, fromstring
 
 from conans.client.output import ConanOutput
@@ -50,20 +51,23 @@ def human_size(size_bytes):
     return "%s%s" % (formatted_size, suffix)
 
 
-def unzip(filename, destination=".", keep_permissions=False):
+def unzip(filename, destination=".", keep_permissions=False, pattern=None):
     """
     Unzip a zipped file
     :param filename: Path to the zip file
     :param destination: Destination folder
-    :param keep_permissions: Keep the zip permissions. WARNING: Can be dangerous if the zip was not created in a NIX
-    system, the bits could produce undefined permission schema. Use only this option if you are sure that the
-    zip was created correctly.
+    :param keep_permissions: Keep the zip permissions. WARNING: Can be
+    dangerous if the zip was not created in a NIX system, the bits could
+    produce undefined permission schema. Use this option only if you are sure
+    that the zip was created correctly.
+    :param pattern: Extract only paths matching the pattern. This should be a
+    Unix shell-style wildcard, see fnmatch documentation for more details.
     :return:
     """
     if (filename.endswith(".tar.gz") or filename.endswith(".tgz") or
             filename.endswith(".tbz2") or filename.endswith(".tar.bz2") or
             filename.endswith(".tar")):
-        return untargz(filename, destination)
+        return untargz(filename, destination, pattern)
     import zipfile
     full_path = os.path.normpath(os.path.join(get_cwd(), destination))
 
@@ -79,7 +83,11 @@ def unzip(filename, destination=".", keep_permissions=False):
             pass
 
     with zipfile.ZipFile(filename, "r") as z:
-        uncompress_size = sum((file_.file_size for file_ in z.infolist()))
+        if not pattern:
+            zip_info = z.infolist()
+        else:
+            zip_info = [zi for zi in z.infolist() if fnmatch(zi.filename, pattern)]
+        uncompress_size = sum((file_.file_size for file_ in zip_info))
         if uncompress_size > 100000:
             _global_output.info("Unzipping %s, this can take a while" % human_size(uncompress_size))
         else:
@@ -88,7 +96,7 @@ def unzip(filename, destination=".", keep_permissions=False):
 
         print_progress.last_size = -1
         if platform.system() == "Windows":
-            for file_ in z.infolist():
+            for file_ in zip_info:
                 extracted_size += file_.file_size
                 print_progress(extracted_size, uncompress_size)
                 try:
@@ -96,7 +104,7 @@ def unzip(filename, destination=".", keep_permissions=False):
                 except Exception as e:
                     _global_output.error("Error extract %s\n%s" % (file_.filename, str(e)))
         else:  # duplicated for, to avoid a platform check for each zipped file
-            for file_ in z.infolist():
+            for file_ in zip_info:
                 extracted_size += file_.file_size
                 print_progress(extracted_size, uncompress_size)
                 try:
@@ -110,10 +118,15 @@ def unzip(filename, destination=".", keep_permissions=False):
                     _global_output.error("Error extract %s\n%s" % (file_.filename, str(e)))
 
 
-def untargz(filename, destination="."):
+def untargz(filename, destination=".", pattern=None):
     import tarfile
     with tarfile.TarFile.open(filename, 'r:*') as tarredgzippedFile:
-        tarredgzippedFile.extractall(destination)
+        if not pattern:
+            tarredgzippedFile.extractall(destination)
+        else:
+            members = list(filter(lambda m: fnmatch(m.name, pattern),
+                                  tarredgzippedFile.getmembers()))
+            tarredgzippedFile.extractall(destination, members=members)
 
 
 def check_with_algorithm_sum(algorithm_name, file_path, signature):
diff --git a/conans/client/tools/oss.py b/conans/client/tools/oss.py
index 28ae4850e..85c06bd33 100644
--- a/conans/client/tools/oss.py
+++ b/conans/client/tools/oss.py
@@ -113,7 +113,7 @@ class OSInfo(object):
     @property
     def with_pacman(self):
         if self.is_linux:
-            return self.linux_distro == "arch"
+            return self.linux_distro in ["arch", "manjaro"]
         elif self.is_windows and which('uname.exe'):
             uname = subprocess.check_output(['uname.exe', '-s']).decode()
             return uname.startswith('MSYS_NT') and which('pacman.exe')
diff --git a/conans/client/tools/system_pm.py b/conans/client/tools/system_pm.py
index 259e2d4b6..0b58c6747 100644
--- a/conans/client/tools/system_pm.py
+++ b/conans/client/tools/system_pm.py
@@ -58,6 +58,10 @@ class SystemPackageTool(object):
         """
             Get the system package tool update command
         """
+        mode = self._get_sysrequire_mode()
+        if mode == "disabled":
+            _global_output.info("Not updating system_requirements. CONAN_SYSREQUIRES_MODE=Disabled")
+            return
         self._is_up_to_date = True
         self._tool.update()
 
diff --git a/conans/model/settings.py b/conans/model/settings.py
index 782c94fb8..7c4441144 100644
--- a/conans/model/settings.py
+++ b/conans/model/settings.py
@@ -345,6 +345,8 @@ class Settings(object):
             other_field_def = constraint_def[field]
             if other_field_def is None:  # Means leave it as is
                 continue
+            if isinstance(other_field_def, str):
+                other_field_def = [other_field_def]
 
             values_to_remove = []
             for value in config_item.values_range:  # value = "Visual Studio"


Test patch:
diff --git a/conans/test/command/search_test.py b/conans/test/command/search_test.py
index a364ceefc..69a7b3f38 100644
--- a/conans/test/command/search_test.py
+++ b/conans/test/command/search_test.py
@@ -147,7 +147,6 @@ class SearchTest(unittest.TestCase):
         shutil.copytree(self.client.paths.store, self.servers["local"].paths.store)
         os.rmdir(self.servers["search_able"].paths.store)
         shutil.copytree(self.client.paths.store, self.servers["search_able"].paths.store)
-        self.client.run("search Hello* -r=all")
 
         def check():
             for remote in ("local", "search_able"):
@@ -158,7 +157,9 @@ Hello/1.4.12@fenix/testing
 helloTest/1.4.10@fenix/stable""".format(remote)
                 self.assertIn(expected, self.client.out)
 
+        self.client.run("search Hello* -r=all")
         check()
+
         self.client.run("search Hello* -r=all --raw")
         check()
 
@@ -205,6 +206,22 @@ helloTest/1.4.10@fenix/stable""".format(remote)
         self.assertIn("<h1>Hello/1.4.10@fenix/testing</h1>", html)
         self.assertIn("<td>Linux gcc 4.5 (libstdc++11)</td>", html)
         self.assertIn("<td>Windows Visual Studio 8.1</td>", html)
+        
+    def search_html_table_all_test(self):
+        os.rmdir(self.servers["local"].paths.store)
+        shutil.copytree(self.client.paths.store, self.servers["local"].paths.store)
+        os.rmdir(self.servers["search_able"].paths.store)
+        shutil.copytree(self.client.paths.store, self.servers["search_able"].paths.store)
+
+        self.client.run("search Hello/1.4.10/fenix/testing -r=all --table=table.html")
+        html = load(os.path.join(self.client.current_folder, "table.html"))
+
+        self.assertIn("<h1>Hello/1.4.10@fenix/testing</h1>", html)
+        self.assertIn("<h2>'local':</h2>", html)
+        self.assertIn("<h2>'search_able':</h2>", html)
+
+        self.assertEqual(html.count("<td>Linux gcc 4.5 (libstdc++11)</td>"), 2)
+        self.assertEqual(html.count("<td>Windows Visual Studio 8.1</td>"), 2)
 
     def search_html_table_with_no_reference_test(self):
         self.client.run("search Hello* --table=table.html", ignore_error=True)
@@ -259,7 +276,7 @@ helloTest/1.4.10@fenix/stable""".format(remote)
         # Now search with a remote
         os.rmdir(self.servers["local"].paths.store)
         shutil.copytree(self.client.paths.store, self.servers["local"].paths.store)
-        self.client.run("remove Hello* -f")
+
         self.client.run('search Hello/1.4.10@fenix/testing -q "compiler=gcc AND compiler.libcxx=libstdc++11" -r local')
         self.assertIn("Outdated from recipe: False", self.client.user_io.out)
         self.assertIn("LinuxPackageSHA", self.client.user_io.out)
@@ -271,6 +288,21 @@ helloTest/1.4.10@fenix/stable""".format(remote)
         self.assertIn("PlatformIndependantSHA", self.client.user_io.out)
         self.assertNotIn("WindowsPackageSHA", self.client.user_io.out)
 
+        # Now search in all remotes
+        os.rmdir(self.servers["search_able"].paths.store)
+        shutil.copytree(self.client.paths.store, self.servers["search_able"].paths.store)
+
+        self.client.run('search Hello/1.4.10@fenix/testing -q "compiler=gcc AND compiler.libcxx=libstdc++11" -r all')
+        self.assertEqual(str(self.client.user_io.out).count("Outdated from recipe: False"), 2)
+        self.assertEqual(str(self.client.user_io.out).count("LinuxPackageSHA"), 2)
+        self.assertNotIn("PlatformIndependantSHA", self.client.user_io.out)
+        self.assertNotIn("WindowsPackageSHA", self.client.user_io.out)
+
+        self.client.run('search Hello/1.4.10@fenix/testing -q "compiler=gcc AND compiler.libcxx=libstdc++" -r all')
+        self.assertNotIn("LinuxPackageSHA", self.client.user_io.out)
+        self.assertEqual(str(self.client.user_io.out).count("PlatformIndependantSHA"), 2)
+        self.assertNotIn("WindowsPackageSHA", self.client.user_io.out)
+
     def _assert_pkg_q(self, query, packages_found, remote):
 
         command = 'search Hello/1.4.10@fenix/testing -q \'%s\'' % query
@@ -343,6 +375,20 @@ helloTest/1.4.10@fenix/stable""".format(remote)
         # test in remote with search capabilities
         test_cases(remote="search_able")
 
+    def package_search_all_remotes_test(self):
+        os.rmdir(self.servers["local"].paths.store)
+        shutil.copytree(self.client.paths.store, self.servers["local"].paths.store)
+        os.rmdir(self.servers["search_able"].paths.store)
+        shutil.copytree(self.client.paths.store, self.servers["search_able"].paths.store)
+
+        self.client.run("search Hello/1.4.10/fenix/testing -r=all")
+        self.assertIn("Existing recipe in remote 'local':", self.client.user_io.out)
+        self.assertIn("Existing recipe in remote 'search_able':", self.client.user_io.out)
+
+        self.assertEqual(str(self.client.user_io.out).count("WindowsPackageSHA"), 2)
+        self.assertEqual(str(self.client.user_io.out).count("PlatformIndependantSHA"), 2)
+        self.assertEqual(str(self.client.user_io.out).count("LinuxPackageSHA"), 2)
+
     def package_search_with_invalid_query_test(self):
         self.client.run("search Hello/1.4.10/fenix/testing -q 'invalid'", ignore_error=True)
         self.assertIn("Invalid package query: invalid", self.client.user_io.out)
@@ -729,6 +775,126 @@ helloTest/1.4.10@fenix/stable""".format(remote)
         }
         self.assertEqual(expected_output, output)
 
+        # Test search packages remote ALL
+        self.client.run("search Hello/1.4.10@fenix/testing -r all --json search.json")
+        self.assertTrue(os.path.exists(json_path))
+        json_content = load(json_path)
+        output = json.loads(json_content)
+        expected_output = {
+            'error': False,
+            'results': [
+                {
+                    'remote': 'local',
+                    'items': [
+                        {
+                            'recipe': {
+                                'id': 'Hello/1.4.10@fenix/testing'},
+                            'packages': [
+                                {
+                                    'id': 'LinuxPackageSHA',
+                                    'options': {
+                                        'use_Qt': 'False'},
+                                    'settings': {
+                                        'arch': 'x86',
+                                        'compiler': 'gcc',
+                                        'compiler.libcxx': 'libstdc++11',
+                                        'compiler.version': '4.5',
+                                        'os': 'Linux'},
+                                    'requires': [
+                                        'Hello2/0.1@lasote/stable:11111',
+                                        'HelloInfo1/0.45@fenix/testing:33333',
+                                        'OpenSSL/2.10@lasote/testing:2222'],
+                                    'outdated': False
+                                },
+                                {
+                                    'id': 'PlatformIndependantSHA',
+                                    'options': {
+                                        'use_Qt': 'True'},
+                                    'settings': {
+                                        'arch': 'x86',
+                                        'compiler': 'gcc',
+                                        'compiler.libcxx': 'libstdc++',
+                                        'compiler.version': '4.3'},
+                                    'requires': [],
+                                    'outdated': True
+                                },
+                                {
+                                    'id': 'WindowsPackageSHA',
+                                    'options': {
+                                        'use_Qt': 'True'},
+                                    'settings': {
+                                        'arch': 'x64',
+                                        'compiler': 'Visual Studio',
+                                        'compiler.version': '8.1',
+                                        'os': 'Windows'},
+                                    'requires': [
+                                        'Hello2/0.1@lasote/stable:11111',
+                                        'HelloInfo1/0.45@fenix/testing:33333',
+                                        'OpenSSL/2.10@lasote/testing:2222'],
+                                    'outdated': True
+                                }
+                            ]
+                        }
+                    ]
+                },
+                {
+                    'remote': 'search_able',
+                    'items': [
+                        {
+                            'recipe': {
+                                'id': 'Hello/1.4.10@fenix/testing'},
+                            'packages': [
+                                {
+                                    'id': 'LinuxPackageSHA',
+                                    'options': {
+                                        'use_Qt': 'False'},
+                                    'settings': {
+                                        'arch': 'x86',
+                                        'compiler': 'gcc',
+                                        'compiler.libcxx': 'libstdc++11',
+                                        'compiler.version': '4.5',
+                                        'os': 'Linux'},
+                                    'requires': [
+                                        'Hello2/0.1@lasote/stable:11111',
+                                        'HelloInfo1/0.45@fenix/testing:33333',
+                                        'OpenSSL/2.10@lasote/testing:2222'],
+                                    'outdated': False
+                                },
+                                {
+                                    'id': 'PlatformIndependantSHA',
+                                    'options': {
+                                        'use_Qt': 'True'},
+                                    'settings': {
+                                        'arch': 'x86',
+                                        'compiler': 'gcc',
+                                        'compiler.libcxx': 'libstdc++',
+                                        'compiler.version': '4.3'},
+                                    'requires': [],
+                                    'outdated': True
+                                },
+                                {
+                                    'id': 'WindowsPackageSHA',
+                                    'options': {
+                                        'use_Qt': 'True'},
+                                    'settings': {
+                                        'arch': 'x64',
+                                        'compiler': 'Visual Studio',
+                                        'compiler.version': '8.1',
+                                        'os': 'Windows'},
+                                    'requires': [
+                                        'Hello2/0.1@lasote/stable:11111',
+                                        'HelloInfo1/0.45@fenix/testing:33333',
+                                        'OpenSSL/2.10@lasote/testing:2222'],
+                                    'outdated': True
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        }
+        self.assertEqual(expected_output, output)
+
     def search_packages_with_reference_not_exported_test(self):
         error = self.client.run("search my_pkg/1.0@conan/stable", ignore_error=True)
         self.assertTrue(error)
diff --git a/conans/test/command/user_test.py b/conans/test/command/user_test.py
index c059b50e2..618eda91c 100644
--- a/conans/test/command/user_test.py
+++ b/conans/test/command/user_test.py
@@ -1,5 +1,10 @@
+import json
 import unittest
+
+import os
+
 from conans.test.utils.tools import TestClient, TestServer
+from conans.util.files import load
 
 
 class UserTest(unittest.TestCase):
@@ -32,13 +37,14 @@ class UserTest(unittest.TestCase):
 
         # Test user list for requested remote reported
         client.run("user -r test_remote_1")
-        self.assertIn("Current 'test_remote_1' user: None (anonymous)", client.out)
-        self.assertNotIn("Current 'default' user: None (anonymous)", client.out)
+        self.assertIn("Current user of remote 'test_remote_1' set to: 'None' (anonymous)",
+                      client.out)
+        self.assertNotIn("Current user of 'default' remote set to: 'None' (anonymous)", client.out)
 
         # Test user list for all remotes is reported
         client.run("user")
-        self.assertIn("Current 'test_remote_1' user: None (anonymous)", client.out)
-        self.assertIn("Current 'default' user: None (anonymous)", client.out)
+        self.assertIn("Current user of remote 'test_remote_1' set to: 'None' (anonymous)", client.out)
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", client.out)
 
     def test_with_no_user(self):
         test_server = TestServer()
@@ -52,22 +58,24 @@ class UserTest(unittest.TestCase):
         test_server = TestServer()
         client = TestClient(servers={"default": test_server})
         client.run('user')
-        self.assertIn("Current 'default' user: None (anonymous)", client.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", client.out)
 
         client.run('user john')
-        self.assertIn("Change 'default' user from None (anonymous) to john", client.user_io.out)
+        self.assertIn("Changed user of remote 'default' from 'None' (anonymous) to 'john'",
+                      client.out)
         self.assertEqual(('john', None), client.localdb.get_login(test_server.fake_url))
 
         client.run('user will')
-        self.assertIn("Change 'default' user from john to will", client.user_io.out)
+        self.assertIn("Changed user of remote 'default' from 'john' to 'will'", client.out)
         self.assertEqual(('will', None), client.localdb.get_login(test_server.fake_url))
 
         client.run('user None')
-        self.assertIn("Change 'default' user from will to None (anonymous)", client.user_io.out)
+        self.assertIn("Changed user of remote 'default' from 'will' to 'None' (anonymous)",
+                      client.out)
         self.assertEqual((None, None), client.localdb.get_login(test_server.fake_url))
 
         client.run('user')
-        self.assertIn("Current 'default' user: None (anonymous)", client.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", client.out)
 
     def test_command_user_with_password(self):
         """ Checks the -p option, that obtains a token from the password.
@@ -81,12 +89,14 @@ class UserTest(unittest.TestCase):
         self.assertIn("ERROR: Wrong user or password", conan.user_io.out)
         conan.run('user lasote -p mypass')
         self.assertNotIn("ERROR: Wrong user or password", conan.user_io.out)
-        self.assertIn("Change 'default' user from None (anonymous) to lasote", conan.user_io.out)
+        self.assertIn("Changed user of remote 'default' from 'None' (anonymous) to 'lasote'",
+                      conan.out)
         conan.run('user none')
-        self.assertIn("Change 'default' user from lasote to None (anonymous)", conan.user_io.out)
+        self.assertIn("Changed user of remote 'default' from 'lasote' to 'None' (anonymous)",
+                      conan.out)
         self.assertEqual((None, None), conan.localdb.get_login(test_server.fake_url))
         conan.run('user')
-        self.assertIn("Current 'default' user: None (anonymous)", conan.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", conan.out)
 
     def test_command_user_with_password_spaces(self):
         """ Checks the -p option, that obtains a token from the password.
@@ -97,12 +107,14 @@ class UserTest(unittest.TestCase):
         servers = {"default": test_server}
         conan = TestClient(servers=servers, users={"default": [("lasote", "mypass")]})
         conan.run(r'user lasote -p="my \"password"')
-        self.assertNotIn("ERROR: Wrong user or password", conan.user_io.out)
-        self.assertIn("Change 'default' user from None (anonymous) to lasote", conan.user_io.out)
+        self.assertNotIn("ERROR: Wrong user or password", conan.out)
+        self.assertIn("Changed user of remote 'default' from 'None' (anonymous) to 'lasote'",
+                      conan.out)
         conan.run('user none')
         conan.run(r'user lasote -p "my \"password"')
         self.assertNotIn("ERROR: Wrong user or password", conan.user_io.out)
-        self.assertIn("Change 'default' user from None (anonymous) to lasote", conan.user_io.out)
+        self.assertIn("Changed user of remote 'default' from 'None' (anonymous) to 'lasote'",
+                      conan.out)
 
     def test_clean(self):
         test_server = TestServer()
@@ -115,20 +127,19 @@ class ConanLib(ConanFile):
     name = "lib"
     version = "0.1"
 '''
-
         files = {"conanfile.py": base}
         client.save(files)
         client.run("export . lasote/stable")
         client.run("upload lib/0.1@lasote/stable")
         client.run("user")
-        self.assertIn("Current 'default' user: lasote", client.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'lasote'", client.out)
         client.run("user --clean")
         client.run("user")
-        self.assertNotIn("lasote", client.user_io.out)
-        self.assertEqual("Current 'default' user: None (anonymous)\n", client.user_io.out)
+        self.assertNotIn("lasote", client.out)
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", client.out)
         client.run("upload lib/0.1@lasote/stable")
         client.run("user")
-        self.assertIn("Current 'default' user: lasote", client.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'lasote'", client.user_io.out)
 
     def test_command_user_with_interactive_password(self):
         test_server = TestServer()
@@ -137,7 +148,7 @@ class ConanLib(ConanFile):
         conan.run('user -p -r default lasote')
         self.assertIn('Please enter a password for "lasote" account:', conan.user_io.out)
         conan.run("user")
-        self.assertIn("Current 'default' user: lasote", conan.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'lasote'", conan.user_io.out)
 
     def test_command_interactive_only(self):
         test_server = TestServer()
@@ -146,7 +157,7 @@ class ConanLib(ConanFile):
         conan.run('user -p')
         self.assertIn('Please enter a password for "lasote" account:', conan.user_io.out)
         conan.run("user")
-        self.assertIn("Current 'default' user: lasote", conan.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'lasote'", conan.user_io.out)
 
     def test_command_user_with_interactive_password_login_prompt_disabled(self):
         """ Interactive password should not work.
@@ -160,8 +171,173 @@ class ConanLib(ConanFile):
         self.assertIn('ERROR: Conan interactive mode disabled', conan.user_io.out)
         self.assertNotIn("Please enter a password for \"lasote\" account:", conan.out)
         conan.run("user")
-        self.assertIn("Current 'default' user: None", conan.user_io.out)
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", conan.out)
         error = conan.run("user -p", ignore_error=True)
         self.assertTrue(error)
         self.assertIn('ERROR: Conan interactive mode disabled', conan.out)
         self.assertNotIn("Remote 'default' username:", conan.out)
+
+    def authenticated_test(self):
+        test_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"})
+        servers = {"default": test_server, "other_server": None}
+        client = TestClient(servers=servers, users={"default": [("lasote", "mypass"),
+                                                                ("danimtb", "passpass")],
+                                                    "other_server": []})
+        client.run("user")
+        self.assertIn("Current user of remote 'default' set to: 'None' (anonymous)", client.out)
+        self.assertNotIn("[Authenticated]", client.out)
+        client.run('user bad_user')
+        client.run("user")
+        self.assertNotIn("[Authenticated]", client.out)
+        client.run("user lasote")
+        client.run("user")
+        self.assertNotIn("[Authenticated]", client.out)
+        client.run("user lasote -p mypass")
+        client.run("user")
+        self.assertIn("Current user of remote 'default' set to: 'lasote' [Authenticated]",
+                      client.out)
+        client.run("user danimtb -p passpass")
+        client.run("user")
+        self.assertIn("Current user of remote 'default' set to: 'danimtb' [Authenticated]",
+                      client.out)
+        client.run("user lasote -r other_server")
+        client.run("user")
+        self.assertIn("Current user of remote 'default' set to: 'danimtb' [Authenticated]",
+                      client.out)
+        self.assertIn("Current user of remote 'other_server' set to: 'lasote'", client.out)
+        client.run("user lasote -r default")
+        client.run("user")
+        self.assertIn("Current user of remote 'default' set to: 'lasote'", client.out)
+        self.assertIn("Current user of remote 'other_server' set to: 'lasote'", client.out)
+        self.assertNotIn("[Authenticated]", client.out)
+
+    def json_test(self):
+        test_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"})
+        servers = {"default": test_server, "other_server": None}
+        client = TestClient(servers=servers, users={"default": [("lasote", "mypass"),
+                                                                ("danimtb", "passpass")],
+                                                    "other_server": []})
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": False,
+                                  "user_name": None
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": None
+                              }
+                          ]}, info)
+
+        client.run('user bad_user')
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": False,
+                                  "user_name": "bad_user"
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": None
+                              }
+                          ]}, info)
+
+        client.run("user lasote")
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": False,
+                                  "user_name": "lasote"
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": None
+                              }
+                          ]}, info)
+
+        client.run("user lasote -p mypass")
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": True,
+                                  "user_name": "lasote"
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": None
+                              }
+                          ]}, info)
+
+        client.run("user danimtb -p passpass")
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": True,
+                                  "user_name": "danimtb"
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": None
+                              }
+                          ]}, info)
+
+        client.run("user lasote -r other_server")
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": True,
+                                  "user_name": "danimtb"
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": "lasote"
+                              }
+                          ]}, info)
+
+        client.run("user lasote -r default")
+        client.run("user --json user.json")
+        content = load(os.path.join(client.current_folder, "user.json"))
+        info = json.loads(content)
+        self.assertEqual({"error": False,
+                          "remotes": [
+                              {
+                                  "name": "default",
+                                  "authenticated": False,
+                                  "user_name": "lasote"
+                              },
+                              {
+                                  "name": "other_server",
+                                  "authenticated": False,
+                                  "user_name": "lasote"
+                              }
+                          ]}, info)
diff --git a/conans/test/functional/conan_trace_file_test.py b/conans/test/functional/conan_trace_file_test.py
index a533f5ae3..5a0e4c621 100644
--- a/conans/test/functional/conan_trace_file_test.py
+++ b/conans/test/functional/conan_trace_file_test.py
@@ -92,7 +92,7 @@ class HelloConan(ConanFile):
         self.assertIn('"Authorization": "**********"', traces)
         self.assertIn('"X-Client-Anonymous-Id": "**********"', traces)
         actions = traces.splitlines()
-        self.assertEquals(len(actions), 19)
+        self.assertEquals(len(actions), 20)
         for trace in actions:
             doc = json.loads(trace)
             self.assertIn("_action", doc)  # Valid jsons
@@ -100,13 +100,13 @@ class HelloConan(ConanFile):
         self.assertEquals(json.loads(actions[0])["_action"], "COMMAND")
         self.assertEquals(json.loads(actions[0])["name"], "authenticate")
 
-        self.assertEquals(json.loads(actions[2])["_action"], "COMMAND")
-        self.assertEquals(json.loads(actions[2])["name"], "export")
-
         self.assertEquals(json.loads(actions[3])["_action"], "COMMAND")
-        self.assertEquals(json.loads(actions[3])["name"], "install_reference")
+        self.assertEquals(json.loads(actions[3])["name"], "export")
+
+        self.assertEquals(json.loads(actions[4])["_action"], "COMMAND")
+        self.assertEquals(json.loads(actions[4])["name"], "install_reference")
 
-        self.assertEquals(json.loads(actions[4])["_action"], "GOT_RECIPE_FROM_LOCAL_CACHE")
-        self.assertEquals(json.loads(actions[4])["_id"], "Hello0/0.1@lasote/stable")
+        self.assertEquals(json.loads(actions[5])["_action"], "GOT_RECIPE_FROM_LOCAL_CACHE")
+        self.assertEquals(json.loads(actions[5])["_id"], "Hello0/0.1@lasote/stable")
 
         self.assertEquals(json.loads(actions[-1])["_action"], "UPLOADED_PACKAGE")
diff --git a/conans/test/integration/workspace_test.py b/conans/test/integration/workspace_test.py
index 6a3aba129..48c305c4e 100644
--- a/conans/test/integration/workspace_test.py
+++ b/conans/test/integration/workspace_test.py
@@ -98,7 +98,7 @@ target_link_libraries(hello{name} {dep})
 class WorkspaceTest(unittest.TestCase):
 
     @parameterized.expand([(True, ), (False, )])
-    @unittest.skipUnless(platform.system() in ("Windows", "Linux"), "Test doesn't work on OSX")
+    # @unittest.skipUnless(platform.system() in ("Windows", "Linux"), "Test doesn't work on OSX")
     def cmake_outsource_build_test(self, targets):
         client = TestClient()
 
@@ -153,11 +153,12 @@ name: MyProject
         self.assertIn("Hello World C Release!", client.out)
         self.assertIn("Hello World B Release!", client.out)
         self.assertIn("Hello World A Release!", client.out)
+        TIME_DELAY = 1
+        time.sleep(TIME_DELAY)
         tools.replace_in_file(os.path.join(client.current_folder, "C/src/hello.cpp"),
                               "Hello World", "Bye Moon")
         tools.replace_in_file(os.path.join(client.current_folder, "B/src/hello.cpp"),
                               "Hello World", "Bye Moon")
-        TIME_DELAY = 0.1
         time.sleep(TIME_DELAY)
         client.runner('cmake --build . --config Release', cwd=base_folder)
         time.sleep(TIME_DELAY)
diff --git a/conans/test/model/other_settings_test.py b/conans/test/model/other_settings_test.py
index 8b6d8e360..0dbba5d7d 100644
--- a/conans/test/model/other_settings_test.py
+++ b/conans/test/model/other_settings_test.py
@@ -131,6 +131,19 @@ compiler:
         self.assertIn("build_type", load(client.paths.settings_path))
         self.assertIn("390146894f59dda18c902ee25e649ef590140732", client.user_io.out)
 
+    def settings_constraint_error_type_test(self):
+        # https://github.com/conan-io/conan/issues/3022
+        conanfile = """from conans import ConanFile
+class Test(ConanFile):
+    settings = {"os": "Linux"}
+    def build(self):
+        self.output.info("OS!!: %s" % self.settings.os)
+    """
+        client = TestClient()
+        client.save({"conanfile.py": conanfile})
+        client.run("create . Pkg/0.1@user/testing -s os=Linux")
+        self.assertIn("Pkg/0.1@user/testing: OS!!: Linux", client.out)
+
     def settings_constraint_test(self):
         conanfile = """from conans import ConanFile
 class Test(ConanFile):
diff --git a/conans/test/util/files_extract_wildcard_test.py b/conans/test/util/files_extract_wildcard_test.py
new file mode 100644
index 000000000..143176818
--- /dev/null
+++ b/conans/test/util/files_extract_wildcard_test.py
@@ -0,0 +1,74 @@
+import os
+import tarfile
+import zipfile
+
+from unittest import TestCase
+
+from conans.test.utils.test_files import temp_folder
+from conans.tools import unzip
+from conans.util.files import save_files
+
+
+def create_archive(archive, root, relative_file_paths):
+    """ Create an archive with given file paths relative to given root."""
+    extension = os.path.basename(archive).split(os.extsep, 1)[1]
+    if extension == "zip":
+        with zipfile.ZipFile(archive, mode="w") as z:
+            for file_ in relative_file_paths:
+                z.write(os.path.join(root, file_), arcname=file_)
+    else:
+        # Treat any other extension except "tar.gz" as a normal "tar"
+        mode = "gz" if extension == "tar.gz" else ""
+        with tarfile.open(archive, mode="w:%s" % mode) as t:
+            for file_ in relative_file_paths:
+                t.add(os.path.join(root, file_), arcname=file_)
+    return archive
+
+
+class FilesExtractPatternTest(TestCase):
+
+    def test_patterns(self):
+        # Test setup
+        src_dir = temp_folder()
+        files = {
+            "foo/file.cpp": "code",
+            "foo/bar/file.txt": "text",
+            "foo/bar/file.cpp": "more code",
+            "foo/bar/baz/file.txt": "more text"
+        }
+        matches = {
+            "*.cpp": ["foo/file.cpp",
+                      "foo/bar/file.cpp"],
+            "*.txt": ["foo/bar/file.txt",
+                      "foo/bar/baz/file.txt"],
+            "foo/bar/*": ["foo/bar/file.txt",
+                          "foo/bar/file.cpp",
+                          "foo/bar/baz/file.txt"],
+            "foo/bar/baz/*": ["foo/bar/baz/file.txt"],
+            "*": ["foo/file.cpp",
+                  "foo/bar/file.txt",
+                  "foo/bar/file.cpp",
+                  "foo/bar/baz/file.txt"],
+            "nothing": []
+        }
+        save_files(src_dir, files)
+
+        for extension in ["zip", "tar", "tar.gz"]:
+            # GIVEN an archive with files inside
+            archive_dir = temp_folder()
+            archive = os.path.join(archive_dir, "archive.%s" % extension)
+            create_archive(archive, src_dir, files)
+
+            for (pattern, paths) in matches.items():
+                # WHEN a pattern is used for file extraction
+                dst_dir = temp_folder()
+                unzip(archive, dst_dir, pattern=pattern)
+
+                # THEN only and all files matching the pattern are extracted
+                actual = set()
+                expected = set(map(lambda x: os.path.join(dst_dir, *x.split("/")), paths))
+                for extracted_dir, _, extracted_files in os.walk(dst_dir):
+                    actual.update(map(lambda x: os.path.join(extracted_dir, x),
+                                      extracted_files))
+
+                self.assertSetEqual(expected, actual)