unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#36052: 26.2.50; [PATCH] Improve auth-source-pass
@ 2019-06-02  9:11 Damien Cassou
  2019-06-07  0:43 ` Noam Postavsky
  0 siblings, 1 reply; 13+ messages in thread
From: Damien Cassou @ 2019-06-02  9:11 UTC (permalink / raw)
  To: 36052
  Cc: Magnus Henoch, Nicolas Petton, Ted Zlatanov, Iku Iwasa, galaunay,
	Keith Amidon

[-- Attachment #1: Type: text/plain, Size: 1678 bytes --]

Hi,

auth-source-pass users have worked hard to improve the package. Everyone
is on CC.

3 have signed the FSF paperwork:

- Keith Amidon:
  https://github.com/DamienCassou/auth-password-store/pull/80#issuecomment-480853577
- Iku Iwasa:
  https://github.com/DamienCassou/auth-password-store/pull/87#issuecomment-480816595
- galaunay:
  https://github.com/DamienCassou/auth-password-store/pull/78#issuecomment-453862089

Magnus Henoch hasn't signed but his contribution is rather small (4
lines of a new unit-test and 2 lines of code). See patch 0001.

Here is a summary of the attached patches.

Damien Cassou (5):
  * lisp/auth-source-pass.el: Version 4.0.2
  * lisp/auth-source-pass.el (auth-source-pass-get): Add autoload
  Refactoring of auth-source-pass
  * lisp/auth-source-pass.el: Version 5.0.0
  * etc/NEWS: Describe changes to auth-source-pass

Iku Iwasa (1):
  Add auth-source-pass-port-separator option

Keith Amidon (4):
  Fix auth-source-pass to search for hostname:port/username
  Split out the attribute retrieval form auth-source-pass-get
  Minimize entry parsing in auth-source-pass
  * lisp/auth-source-pass.el: Add Keith Amidon to authors

Magnus Henoch (1):
  Fix auth-source-pass to return nil if no entry found

galaunay (1):
  Add auth-source-pass-path option

 etc/NEWS                            |  19 ++
 lisp/auth-source-pass.el            | 224 ++++++++-----
 test/lisp/auth-source-pass-tests.el | 473 +++++++++++++++++++---------
 3 files changed, 491 insertions(+), 225 deletions(-)

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fix-auth-source-pass-to-return-nil-if-no-entry-found.patch --]
[-- Type: text/x-patch, Size: 1952 bytes --]

From 34961a0cb5220aefa3a432463d18c15a87bc61ba Mon Sep 17 00:00:00 2001
From: Magnus Henoch <magnus.henoch@gmail.com>
Date: Fri, 2 Nov 2018 21:51:59 +0000
Subject: [PATCH 01/12] Fix auth-source-pass to return nil if no entry found

* lisp/auth-source-pass.el (auth-source-pass-search): If there is no
matching entry, auth-source-pass-search should return nil, not (nil).
This lets auth-source fall back to other backends in the auth-sources
list.
* test/lisp/auth-source-pass-tests.el: Add corresponding test.
---
 lisp/auth-source-pass.el            | 3 ++-
 test/lisp/auth-source-pass-tests.el | 5 +++++
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4283ed0392..c82c90167a 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -56,7 +56,8 @@
          ;; Do not build a result, as none will match when HOST is nil
          nil)
         (t
-         (list (auth-source-pass--build-result host port user)))))
+         (when-let ((result (auth-source-pass--build-result host port user)))
+           (list result)))))
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index d1e486ad6b..ab9ef92c14 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -83,6 +83,11 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host nil))))
 
+(ert-deftest auth-source-pass-not-found ()
+  (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
+                                  ("bar"))
+    (should-not (auth-source-pass-search :host "baz"))))
+
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
   (auth-source-pass--with-store '(("foo"))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-lisp-auth-source-pass.el-Version-4.0.2.patch --]
[-- Type: text/x-patch, Size: 745 bytes --]

From a4b63f10e436614e2b2ac5449307fc8371853600 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sat, 3 Nov 2018 09:06:42 +0100
Subject: [PATCH 02/12] * lisp/auth-source-pass.el: Version 4.0.2

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index c82c90167a..d092292e51 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,7 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
-;; Version: 4.0.1
+;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-lisp-auth-source-pass.el-auth-source-pass-get-Add-au.patch --]
[-- Type: text/x-patch, Size: 758 bytes --]

From e65766da015376fc9cfef75ece0a8ae614e11e27 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 6 Nov 2018 14:45:20 +0100
Subject: [PATCH 03/12] * lisp/auth-source-pass.el (auth-source-pass-get): Add
 autoload

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index d092292e51..4fcb1015e7 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -98,6 +98,7 @@ auth-source-pass-backend-parse
   (advice-add 'auth-source-backend-parse :before-until #'auth-source-pass-backend-parse))
 
 \f
+;;;###autoload
 (defun auth-source-pass-get (key entry)
   "Return the value associated to KEY in the password-store entry ENTRY.
 
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-Add-auth-source-pass-path-option.patch --]
[-- Type: text/x-patch, Size: 2054 bytes --]

From d46e8dd1bbc3a1c7ac6506e3e6cfe3d87e57e99e Mon Sep 17 00:00:00 2001
From: galaunay <gaby.launay@tutanota.com>
Date: Sun, 13 Jan 2019 21:30:53 +0000
Subject: [PATCH 04/12] Add auth-source-pass-path option

* lisp/auth-source-pass.el (auth-source-pass):
(auth-source-pass-path): Add option to specify a customized
password-store path.
(auth-source-pass--read-entry):
(auth-source-pass-entries): Use the new option instead of hard-coded
`~/.password-store'.
---
 lisp/auth-source-pass.el | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 4fcb1015e7..a029946f6c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -38,6 +38,15 @@
 (require 'auth-source)
 (require 'url-parse)
 
+(defgroup auth-source-pass nil
+  "password-store integration within auth-source."
+  :prefix "auth-source-pass-"
+  :group 'auth-source)
+
+(defcustom auth-source-pass-path "~/.password-store"
+  "Path to the password-store folder."
+  :type 'directory)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -121,7 +130,7 @@ auth-source-pass--read-entry
   (with-temp-buffer
     (insert-file-contents (expand-file-name
                            (format "%s.gpg" entry)
-                           "~/.password-store"))
+                           auth-source-pass-path))
     (buffer-substring-no-properties (point-min) (point-max))))
 
 (defun auth-source-pass-parse-entry (entry)
@@ -188,7 +197,7 @@ auth-source-pass--entry-valid-p
 ;; in Emacs
 (defun auth-source-pass-entries ()
   "Return a list of all password store entries."
-  (let ((store-dir (expand-file-name "~/.password-store/")))
+  (let ((store-dir (expand-file-name auth-source-pass-path)))
     (mapcar
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-Add-auth-source-pass-port-separator-option.patch --]
[-- Type: text/x-patch, Size: 3393 bytes --]

From f2107a0c03b2aa0421d5ee4e7ccc74c7cfd4371c Mon Sep 17 00:00:00 2001
From: Iku Iwasa <iku.iwasa@gmail.com>
Date: Sun, 7 Apr 2019 17:59:59 +0900
Subject: [PATCH 05/12] Add auth-source-pass-port-separator option

* lisp/auth-source-pass.el (auth-source-pass-port-separator): New
option to specify separator between host and port, default to
colon (":").
(auth-source-pass--find-match-unambiguous): Adapt to make use of the
new variable.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            | 16 +++++++++++++---
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 24 insertions(+), 3 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index a029946f6c..e80149900b 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -47,6 +47,10 @@ auth-source-pass-path
   "Path to the password-store folder."
   :type 'directory)
 
+(defcustom auth-source-pass-port-separator ":"
+  "Separator string between host and port in entry filename."
+  :type 'string)
+
 (cl-defun auth-source-pass-search (&rest spec
                                          &key backend type host user port
                                          &allow-other-keys)
@@ -252,9 +256,15 @@ auth-source-pass--find-match-unambiguous
 
 HOSTNAME should not contain any username or port number."
   (or
-   (and user port (auth-source-pass--find-one-by-entry-name (format "%s@%s:%s" user hostname port) user))
-   (and user (auth-source-pass--find-one-by-entry-name (format "%s@%s" user hostname) user))
-   (and port (auth-source-pass--find-one-by-entry-name (format "%s:%s" hostname port) nil))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
+                   user))
+   (and user (auth-source-pass--find-one-by-entry-name
+              (format "%s@%s" user hostname)
+              user))
+   (and port (auth-source-pass--find-one-by-entry-name
+              (format "%s%s%s" hostname auth-source-pass-port-separator port)
+              nil))
    (auth-source-pass--find-one-by-entry-name hostname user)
    ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
    (let ((components (split-string hostname "\\.")))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ab9ef92c14..ae7a696bc6 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -186,6 +186,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
                    "host.com"))))
 
+(ert-deftest auth-source-pass-find-host-with-port ()
+  (auth-source-pass--with-store '(("host.com:443"))
+    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                   "host.com:443"))))
+
+(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("host.com#443"))
+      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
+                     "host.com#443")))))
+
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
   (declare (indent 1))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0006-Fix-auth-source-pass-to-search-for-hostname-port-use.patch --]
[-- Type: text/x-patch, Size: 2729 bytes --]

From 4012ad2d3572c5f9ecc29c22106aba137d2f244d Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Thu, 14 Feb 2019 10:27:31 -0800
Subject: [PATCH 06/12] Fix auth-source-pass to search for
 hostname:port/username

auth-source-pass supports entries with username either prefixed to the
hostname with an @ as separator or in a subdirectory under the
hostname.  This was true when there was no port or service included in
the name, but got broken with the introduction of
auth-source-pass-port-separator.

* lisp/auth-source-pass.el (auth-source-pass--find-match-unambiguous): Fix
to match hostname:port/username.
* test/lisp/auth-source-pass-tests.el: Add corresponding tests.
---
 lisp/auth-source-pass.el            |  3 +++
 test/lisp/auth-source-pass-tests.el | 11 +++++++++++
 2 files changed, 14 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index e80149900b..9ad371ea7c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -259,6 +259,9 @@ auth-source-pass--find-match-unambiguous
    (and user port (auth-source-pass--find-one-by-entry-name
                    (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
                    user))
+   (and user port (auth-source-pass--find-one-by-entry-name
+                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
+                   user))
    (and user (auth-source-pass--find-one-by-entry-name
               (format "%s@%s" user hostname)
               user))
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index ae7a696bc6..1539d9611f 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -144,6 +144,17 @@ auth-source-pass--with-store
     (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
                    nil))))
 
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+  (auth-source-pass--with-store '(("bar.com:443/someone"))
+    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                   "bar.com:443/someone"))))
+
+(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+  (let ((auth-source-pass-port-separator "#"))
+    (auth-source-pass--with-store '(("bar.com#443/someone"))
+      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
+                     "bar.com#443/someone")))))
+
 (ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
   (auth-source-pass--with-store '(("foo.com/bar"))
     (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0007-Split-out-the-attribute-retrieval-form-auth-source-p.patch --]
[-- Type: text/x-patch, Size: 3194 bytes --]

From 9e77cc4bf4131ef0fda205465cdf5ffdd6ef0a87 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Tue, 30 Apr 2019 07:52:14 -0700
Subject: [PATCH 07/12] Split out the attribute retrieval form
 auth-source-pass-get

Eliminate the need to repeatedly retrieve and parse the data for the
entry. This is generally a good thing since it eliminates repetitions
of the same crypto and parsing operations. It is especially valuable
when protecting an entry with a yubikey with touch required for crypto
operations as it eliminates the need to touch the yubikey sensor for
each attribute retrieved.

* lisp/auth-source-pass.el (auth-source-pass-get): Extract some code to
`auth-source-pass--get-attr'.
(auth-source-pass--get-attr): New function to get a field value from a
parsed entry.
(auth-source-pass--build-result): Make use of
`auth-source-pass--get-attr` to avoid repeated parsing.
---
 lisp/auth-source-pass.el | 31 +++++++++++++++++++++++--------
 1 file changed, 23 insertions(+), 8 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 9ad371ea7c..2203290d8c 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -76,11 +76,12 @@ auth-source-pass--build-result
   "Build auth-source-pass entry matching HOST, PORT and USER."
   (let ((entry (auth-source-pass--find-match host user port)))
     (when entry
-      (let ((retval (list
-                     :host host
-                     :port (or (auth-source-pass-get "port" entry) port)
-                     :user (or (auth-source-pass-get "user" entry) user)
-                     :secret (lambda () (auth-source-pass-get 'secret entry)))))
+      (let* ((entry-data (auth-source-pass-parse-entry entry))
+             (retval (list
+                      :host host
+                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -125,9 +126,23 @@ auth-source-pass-get
 key1: value1
 key2: value2"
   (let ((data (auth-source-pass-parse-entry entry)))
-    (or (cdr (assoc key data))
-        (and (string= key "user")
-             (cdr (assoc "username" data))))))
+    (auth-source-pass--get-attr key data)))
+
+(defun auth-source-pass--get-attr (key entry-data)
+  "Return the value associated to KEY in data from an already parsed entry.
+
+ENTRY-DATA is the data from a parsed password-store entry.
+The key used to retrieve the password is the symbol `secret'.
+
+The convention used as the format for a password-store file is
+the following (see http://www.passwordstore.org/#organization):
+
+secret
+key1: value1
+key2: value2"
+  (or (cdr (assoc key entry-data))
+      (and (string= key "user")
+           (cdr (assoc "username" entry-data)))))
 
 (defun auth-source-pass--read-entry (entry)
   "Return a string with the file content of ENTRY."
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0008-Minimize-entry-parsing-in-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 33646 bytes --]

From 997d3c0ef848983fb85ffa36f7a9301904171850 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sun, 5 May 2019 20:21:43 -0700
Subject: [PATCH 08/12] Minimize entry parsing in auth-source-pass

Prior to this commit, while searching for the most applicable entry
password-store entries were decrypted and parsed to ensure they were
valid. The entries were parsed in the order they were found on the
filesystem and all applicable entries would be decrypted and parsed,
which varied based on the contents of the password-store and the entry
to be found.

This is fine when the GPG key is cached and each entry can be
decrypted without user interaction. However, for security some people
have their GPG on a hardware token like a Yubikey setup so that they
have to touch a sensor on the toke for every cryptographic operation,
in which case it becomes inconvenient as each attempt to find an entry
requires a variable number of touches of the hardware token.

The implementation already assumes that names which contain more of
the information in the search key should be preferred so there is an
ordering of preference of applicable entries. If the decrypt and
parsing is removed from the initial identification of applicable
entries in the store then in most cases a single decrypt and parse of
the most preferred entry will suffice, improving the experience for
hardware token users that require interaction with the token.

This commit implements that strategy. It is in spirit a refactor of
the existing code. The core of the change is the function
auth-source-pass--applicable-entries, which generates an ordered list
of regular expression matchers for all possible names that could be in
the password-store for the entry to be found and then makes a pass
over the password-store entry names accumulating the matching entries
in a list after the regexp that matched. This implementation ensures
the password-store entry list still only has to be scanned once.

The existing auth-source-pass--find-match-unambiguous was modified to
use this new function to obtain candidate entries and then parse them
one by one until an entry containing the desired information is
located. When complete it now returns the parsed data of the entry
instead of the entry name so that the information can be used directly
to construct the auth-source response.

* lisp/auth-source-pass.el: Private functions were refactored to
reduce the number of decryption operations.
* test/lisp/auth-source-pass-tests.el: One test case was added to the
test suite to verify that only the minimal number of entries are
parsed in common cases. The
auth-source-pass-only-return-entries-that-can-be-open test case had to
be re-implemented because the function it used eliminated as the
functionality is provided elsewhere. All the other fairly substantial
changes to the test suite are the result of mechanical changes that
were required to adapt to auth-source-pass--find-match returning the
data from a parsed password-store entry instead of the entry name.
---
 lisp/auth-source-pass.el            | 210 ++++++++++++----------
 test/lisp/auth-source-pass-tests.el | 264 +++++++++++++++++-----------
 2 files changed, 282 insertions(+), 192 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 2203290d8c..bb090eae40 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -74,14 +74,13 @@ auth-source-pass-port-separator
 
 (defun auth-source-pass--build-result (host port user)
   "Build auth-source-pass entry matching HOST, PORT and USER."
-  (let ((entry (auth-source-pass--find-match host user port)))
-    (when entry
-      (let* ((entry-data (auth-source-pass-parse-entry entry))
-             (retval (list
-                      :host host
-                      :port (or (auth-source-pass--get-attr "port" entry-data) port)
-                      :user (or (auth-source-pass--get-attr "user" entry-data) user)
-                      :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
+  (let ((entry-data (auth-source-pass--find-match host user port)))
+    (when entry-data
+      (let ((retval (list
+                     :host host
+                     :port (or (auth-source-pass--get-attr "port" entry-data) port)
+                     :user (or (auth-source-pass--get-attr "user" entry-data) user)
+                     :secret (lambda () (auth-source-pass--get-attr 'secret entry-data)))))
         (auth-source-pass--do-debug "return %s as final result (plus hidden password)"
                                     (seq-subseq retval 0 -2)) ;; remove password
         retval))))
@@ -185,33 +184,6 @@ auth-source-pass--do-debug
          (cons (concat "auth-source-pass: " (car msg))
                (cdr msg))))
 
-(defun auth-source-pass--select-one-entry (entries user)
-  "Select one entry from ENTRIES by searching for a field matching USER."
-  (let ((number (length entries))
-        (entry-with-user
-         (and user
-              (seq-find (lambda (entry)
-                          (string-equal (auth-source-pass-get "user" entry) user))
-                        entries))))
-    (auth-source-pass--do-debug "found %s matches: %s" number
-                                (mapconcat #'identity entries ", "))
-    (if entry-with-user
-        (progn
-          (auth-source-pass--do-debug "return %s as it contains matching user field"
-                                      entry-with-user)
-          entry-with-user)
-      (auth-source-pass--do-debug "return %s as it is the first one" (car entries))
-      (car entries))))
-
-(defun auth-source-pass--entry-valid-p (entry)
-  "Return t iff ENTRY can be opened.
-Also displays a warning if not.  This function is slow, don't call it too
-often."
-  (if (auth-source-pass-parse-entry entry)
-      t
-    (auth-source-pass--do-debug "entry '%s' is not valid" entry)
-    nil))
-
 ;; TODO: add tests for that when `assess-with-filesystem' is included
 ;; in Emacs
 (defun auth-source-pass-entries ()
@@ -221,37 +193,8 @@ auth-source-pass-entries
      (lambda (file) (file-name-sans-extension (file-relative-name file store-dir)))
      (directory-files-recursively store-dir "\\.gpg$"))))
 
-(defun auth-source-pass--find-all-by-entry-name (entryname user)
-  "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME.
-Only return valid entries as of `auth-source-pass--entry-valid-p'."
-  (seq-filter (lambda (entry)
-                (and
-                 (or
-                  (let ((components-host-user
-                         (member entryname (split-string entry "/"))))
-                    (and (= (length components-host-user) 2)
-                         (string-equal user (cadr components-host-user))))
-                  (string-equal entryname (file-name-nondirectory entry)))
-                 (auth-source-pass--entry-valid-p entry)))
-              (auth-source-pass-entries)))
-
-(defun auth-source-pass--find-one-by-entry-name (entryname user)
-  "Search the store for an entry matching ENTRYNAME.
-If USER is non nil, give precedence to entries containing a user field
-matching USER."
-  (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)"
-                              entryname
-                              user)
-  (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user)))
-    (pcase (length matching-entries)
-      (0 (auth-source-pass--do-debug "no match found")
-         nil)
-      (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries))
-         (car matching-entries))
-      (_ (auth-source-pass--select-one-entry matching-entries user)))))
-
 (defun auth-source-pass--find-match (host user port)
-  "Return a password-store entry name matching HOST, USER and PORT.
+  "Return password-store entry data matching HOST, USER and PORT.
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
@@ -265,33 +208,124 @@ auth-source-pass--find-match
      (or port (number-to-string (url-port url))))))
 
 (defun auth-source-pass--find-match-unambiguous (hostname user port)
-  "Return a password-store entry name matching HOSTNAME, USER and PORT.
+  "Return password-store entry data matching HOSTNAME, USER and PORT.
 If many matches are found, return the first one.  If no match is found,
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (or
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port)
-                   user))
-   (and user port (auth-source-pass--find-one-by-entry-name
-                   (format "%s%s%s" hostname auth-source-pass-port-separator port)
-                   user))
-   (and user (auth-source-pass--find-one-by-entry-name
-              (format "%s@%s" user hostname)
-              user))
-   (and port (auth-source-pass--find-one-by-entry-name
-              (format "%s%s%s" hostname auth-source-pass-port-separator port)
-              nil))
-   (auth-source-pass--find-one-by-entry-name hostname user)
-   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
-   (let ((components (split-string hostname "\\.")))
-     (when (= (length components) 3)
-       ;; start from scratch
-       (auth-source-pass--find-match-unambiguous
-        (mapconcat 'identity (cdr components) ".")
-        user
-        port)))))
+  (cl-reduce
+   (lambda (result entries)
+     (or result
+         (pcase (length entries)
+           (0 nil)
+           (1 (auth-source-pass-parse-entry (car entries)))
+           (_ (auth-source-pass--select-from-entries entries user)))))
+   (auth-source-pass--matching-entries hostname user port)
+   :initial-value nil))
+
+(defun auth-source-pass--select-from-entries (entries user)
+  "Return best matching password-store entry data from ENTRIES.
+
+If USER is non nil, give precedence to entries containing a user field
+matching USER."
+  (cl-reduce
+   (lambda (result entry)
+     (let ((entry-data (auth-source-pass-parse-entry entry)))
+       (cond ((equal (auth-source-pass--get-attr "user" result) user)
+              result)
+             ((equal (auth-source-pass--get-attr "user" entry-data) user)
+              entry-data)
+             (t
+              result))))
+   entries
+   :initial-value (auth-source-pass-parse-entry (car entries))))
+
+(defun auth-source-pass--matching-entries (hostname user port)
+  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
+
+The result is a list of lists of password-store entries, where
+each sublist contains entries that actually exist in the
+password-store matching one of the entry name formats that
+auth-source-pass expects, most specific to least specific."
+  (let* ((entries-lists (mapcar
+                         #'cdr
+                         (auth-source-pass--accumulate-matches hostname user port)))
+         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
+    (if entries
+        (auth-source-pass--do-debug (format "found: %S" entries))
+      (auth-source-pass--do-debug "no matches found"))
+    entries-lists))
+
+(defun auth-source-pass--accumulate-matches (hostname user port)
+  "Accumulate matching password-store entries into sublists.
+
+Entries matching supported formats that combine HOSTNAME, USER, &
+PORT are accumulated into sublists where the car of each sublist
+is a regular expression for matching paths in the password-store
+and the remainder is the list of matching entries."
+  (let ((suffix-match-lists
+         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
+                 (auth-source-pass--generate-entry-suffixes hostname user port))))
+    (cl-reduce #'auth-source-pass--entry-reducer
+               (auth-source-pass-entries)
+               :initial-value suffix-match-lists)))
+
+(defun auth-source-pass--entry-reducer (match-lists entry)
+  "Match MATCH-LISTS sublists against ENTRY.
+
+The result is a copy of match-lists with the entry added to the
+end of any sublists for which the regular expression at the head
+of the list matches the entry name."
+  (mapcar (lambda (match-list)
+            (if (string-match (car match-list) entry)
+                (append match-list (list entry))
+              match-list))
+          match-lists))
+
+(defun auth-source-pass--generate-entry-suffixes (hostname user port)
+  "Return a list of possible entry path suffixes in the password-store.
+
+Based on the supported pathname patterns for HOSTNAME, USER, &
+PORT, return a list of possible suffixes for matching entries in
+the password-store."
+  (let ((domains (auth-source-pass--domains (split-string hostname "\\."))))
+    (seq-mapcat (lambda (n)
+                  (auth-source-pass--name-port-user-suffixes n user port))
+                domains)))
+
+(defun auth-source-pass--domains (name-components)
+  "Return a list of possible domain names matching the hostname.
+
+This function takes a list of NAME-COMPONENTS, the strings
+separated by periods in the hostname, and returns a list of full
+domain names containing the trailing sequences of those
+components, from longest to shortest."
+  (when (> (length name-components) 0)
+    (cons (mapconcat 'identity name-components ".")
+          (auth-source-pass--domains (cdr name-components)))))
+
+(defun auth-source-pass--name-port-user-suffixes (name user port)
+  "Return a list of possible path suffixes for NAME, USER, & PORT.
+
+The resulting list is ordered from most specifc to least
+specific, with paths matching all of NAME, USER, & PORT first,
+then NAME & USER, then NAME & PORT, then just NAME."
+  (seq-mapcat
+   #'identity
+   (list
+    (when (and user port)
+      (list
+       (format "%s@%s%s%s" user name auth-source-pass-port-separator port)
+       (format "%s%s%s/%s" name auth-source-pass-port-separator port user)))
+    (when user
+      (list
+       (format "%s@%s" user name)
+       (format "%s/%s" name user)))
+    (when port
+      (list
+       (format "%s%s%s" name auth-source-pass-port-separator port)))
+    (list
+     (format "%s" name)))))
 
 (provide 'auth-source-pass)
 ;;; auth-source-pass.el ends here
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 1539d9611f..2c28f79945 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -63,14 +63,19 @@ auth-source-pass--debug
 This function is intended to be set to `auth-source-debug`."
   (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
 
+(defvar auth-source-pass--parse-log nil)
+
 (defmacro auth-source-pass--with-store (store &rest body)
   "Use STORE as password-store while executing BODY."
   (declare (indent 1))
-  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry) (lambda (entry) (cdr (cl-find entry ,store :key #'car :test #'string=))) )
-             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store)))
-             ((symbol-function 'auth-source-pass--entry-valid-p) (lambda (_entry) t)))
+  `(cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+              (lambda (entry)
+                (add-to-list 'auth-source-pass--parse-log entry)
+                (cdr (cl-find entry ,store :key #'car :test #'string=))))
+             ((symbol-function 'auth-source-pass-entries) (lambda () (mapcar #'car ,store))))
      (let ((auth-source-debug #'auth-source-pass--debug)
-           (auth-source-pass--debug-log nil))
+           (auth-source-pass--debug-log nil)
+           (auth-source-pass--parse-log nil))
        ,@body)))
 
 (ert-deftest auth-source-pass-any-host ()
@@ -88,125 +93,184 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass-find-match-minimal-parsing ()
+  (let ((store-contents
+         '(("baz" ("secret" . "baz password"))
+           ("baz:123" ("secret" . "baz:123 password"))
+           ("baz/foo" ("secret" . "baz/foo password"))
+           ("foo@baz" ("secret" . "foo@baz password"))
+           ("baz:123/foo" ("secret" . "baz:123/foo password"))
+           ("foo@baz:123" ("secret" . "foo@baz:123 password"))
+           ("bar.baz" ("secret" . "bar.baz password"))
+           ("bar.baz:123" ("secret" . "bar.baz:123 password"))
+           ("bar.baz/foo" ("secret" . "bar.baz/foo password"))
+           ("foo@bar.baz" ("secret" . "foo@bar.baz password"))
+           ("bar.baz:123/foo" ("secret" . "bar.baz:123/foo password"))
+           ("foo@bar.baz:123" ("secret" . "foo@bar.baz:123 password")))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" "123")
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" "foo" nil)
+      (should (equal auth-source-pass--parse-log '("foo@bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil "123")
+      (should (equal auth-source-pass--parse-log '("bar.baz:123"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "bar.baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("bar.baz"))))
+    (auth-source-pass--with-store store-contents
+      (auth-source-pass--find-match "baz" nil nil)
+      (should (equal auth-source-pass--parse-log '("baz"))))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store '(("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "foo"))))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store '(("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store '(("SomeUser@foo") ("foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
+  (auth-source-pass--with-store
+      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
+        ("foo" ("secret" . "foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
 
 (ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store '(("foo") ("SomeUser@foo"))
-    (should (equal (auth-source-pass--find-match "https://SomeUser@foo" nil nil)
-                   "SomeUser@foo"))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
+    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "SomeUser@foo password")))))
+
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil ("bar.com") nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
   (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   "someone@bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
   (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" "someone" nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
+                   '(nil nil nil nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full ()
+(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
   (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   "foo.bar.com"))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(("foo.bar.com") ("bar.com") nil)))))
 
 (ert-deftest auth-source-pass-dont-match-at-folder-name ()
   (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--find-match "foo.bar.com" nil nil)
-                   nil))))
+    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
+                   '(nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
   (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                   "bar.com:443/someone"))))
+    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                   '(nil ("bar.com:443/someone") nil nil nil nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-match-matching-host-port-and-subdir-user-with-custom-separator ()
+(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--find-match "bar.com" "someone" "443")
-                     "bar.com#443/someone")))))
-
-(ert-deftest auth-source-pass-find-match-matching-extracting-user-from-host ()
-  (auth-source-pass--with-store '(("foo.com/bar"))
-    (should (equal (auth-source-pass--find-match "https://bar@foo.com" nil nil)
-                   "foo.com/bar"))))
-
-(ert-deftest auth-source-pass-search-with-user-first ()
+      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
+                     '(nil ("bar.com#443/someone") nil nil nil nil
+                           nil nil nil nil nil nil))))))
+
+(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
+  (auth-source-pass--with-store
+      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
+    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo.com/bar password")))))
+
+(ert-deftest auth-source-pass-matching-entries-with-user-first ()
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--find-match "foo" "user" nil)
-                   "user@foo"))
-    (auth-source-pass--should-have-message-containing "Found 1 match")))
+    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
+                   '(("user@foo") nil ("foo"))))
+    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo" ("user" . "someone")))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "subdir/foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "subdir/foo password"))
+      (should (equal (auth-source-pass--get-attr "user" result)
+                     "someone")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store '(("foo" ("user" . "someone")) ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" "someone" nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "matching user field")))
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password") ("user" . "someone"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
 
 (ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store '(("foo") ("subdir/foo"))
-    (should (equal (auth-source-pass--find-match "foo" nil nil)
-                   "foo"))
-    (auth-source-pass--should-have-message-containing "Found 2 matches")
-    (auth-source-pass--should-have-message-containing "the first one")))
-
-(ert-deftest auth-source-pass-make-divansantana-happy ()
+  (auth-source-pass--with-store
+      '(("foo" ("secret" . "foo password"))
+        ("subdir/foo" ("secret" . "subdir/foo password")))
+    (let ((result (auth-source-pass--find-match "foo" nil nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "foo password")))
+    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
+
+(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
   (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "smtp.host.com" "myusername@host.co.za" nil)
-                   "host.com"))))
+    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
+                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
 
 (ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--find-match "host.com:8888" "someuser" nil)
-                   "host.com"))))
+  (auth-source-pass--with-store
+      '(("host.com" ("secret" . "host.com password")))
+    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
+      (should (equal (auth-source-pass--get-attr "secret" result)
+                     "host.com password")))))
 
-(ert-deftest auth-source-pass-find-host-with-port ()
+(ert-deftest auth-source-pass-matching-entries-host-with-port ()
   (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                   "host.com:443"))))
+    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                   '(nil nil nil nil ("host.com:443") nil
+                         nil nil nil nil nil nil)))))
 
-(ert-deftest auth-source-pass-find-host-with-custom-port-separator ()
+(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--find-match "host.com" "someuser" "443")
-                     "host.com#443")))))
+      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
+                     '(nil nil nil nil ("host.com#443") nil
+                           nil nil nil nil nil nil))))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -218,7 +282,8 @@ auth-source-pass--with-store-find-foo
        ,@body)))
 
 (ert-deftest auth-source-pass-build-result-return-parameters ()
-  (auth-source-pass--with-store-find-foo '(("foo"))
+  (auth-source-pass--with-store-find-foo
+      '(("foo" ("secret" . "foo password")))
     (let ((result (auth-source-pass--build-result "foo" 512 "user")))
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "user")))))
@@ -238,7 +303,9 @@ auth-source-pass--with-store-find-foo
 (ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
   (let (passed-host)
     (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port) (setq passed-host host))))
+               (lambda (host _user _port)
+                 (setq passed-host host)
+                 nil)))
       (auth-source-pass--build-result "https://user@host.com:123" nil nil)
       (should (equal passed-host "https://user@host.com:123"))
       (auth-source-pass--build-result "https://user@host.com" nil nil)
@@ -249,27 +316,16 @@ auth-source-pass--with-store-find-foo
       (should (equal passed-host "user@host.com:443")))))
 
 (ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (cl-letf (((symbol-function 'auth-source-pass-entries)
-             (lambda () '("foo.site.com" "bar.site.com" "mail/baz.site.com/scott")))
-            ((symbol-function 'auth-source-pass--entry-valid-p)
-             ;; only foo.site.com and "mail/baz.site.com/scott" are valid
-             (lambda (entry) (member entry '("foo.site.com" "mail/baz.site.com/scott")))))
-    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com" "someuser")
-                   '("foo.site.com")))
-    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com" "someuser")
-                   '()))
-    (should (equal (auth-source-pass--find-all-by-entry-name "baz.site.com" "scott")
-                   '("mail/baz.site.com/scott")))))
-
-(ert-deftest auth-source-pass-entry-is-not-valid-when-unreadable ()
-  (cl-letf (((symbol-function 'auth-source-pass--read-entry)
-             (lambda (entry)
-               ;; only foo is a valid entry
-               (if (string-equal entry "foo")
-                   "password"
-                 nil))))
-    (should (auth-source-pass--entry-valid-p "foo"))
-    (should-not (auth-source-pass--entry-valid-p "bar"))))
+  (auth-source-pass--with-store
+      '(("foo.site.com" ("secret" . "foo.site.com password"))
+        ("bar.site.com") ; An entry name with no data is invalid
+        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
+    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
+                   '(("secret" . "foo.site.com password"))))
+    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
+                   nil))
+    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
+                   '(("secret" . "mail/baz.site.com/scott password"))))))
 
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #10: 0009-lisp-auth-source-pass.el-Add-Keith-Amidon-to-authors.patch --]
[-- Type: text/x-patch, Size: 755 bytes --]

From 90829466467c97f8eb350c13a8ad21ef566f7108 Mon Sep 17 00:00:00 2001
From: Keith Amidon <camalot@picnicpark.org>
Date: Sat, 11 May 2019 08:22:56 -0700
Subject: [PATCH 09/12] * lisp/auth-source-pass.el: Add Keith Amidon to authors

---
 lisp/auth-source-pass.el | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index bb090eae40..5f05a63c06 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -4,6 +4,7 @@
 
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
+;;         Keith Amidon <camalot@picnicpark.org>
 ;; Version: 4.0.2
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #11: 0010-Refactoring-of-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 34944 bytes --]

From 7d3db58d79d593314ba599855613305f39bf2bdb Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 14 May 2019 05:50:59 +0200
Subject: [PATCH 10/12] Refactoring of auth-source-pass

* lisp/auth-source-pass.el: Refactoring.
* test/lisp/auth-source-pass-tests.el: Refactoring and adding some
more tests.
---
 lisp/auth-source-pass.el            | 108 +++----
 test/lisp/auth-source-pass-tests.el | 464 +++++++++++++++++-----------
 2 files changed, 331 insertions(+), 241 deletions(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 5f05a63c06..1daafdb7e6 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -199,10 +199,17 @@ auth-source-pass--find-match
 
 Disambiguate between user provided inside HOST (e.g., user@server.com) and
 inside USER by giving priority to USER.  Same for PORT."
+  (apply #'auth-source-pass--find-match-unambiguous (auth-source-pass--disambiguate host user port)))
+
+(defun auth-source-pass--disambiguate (host &optional user port)
+  "Return (HOST USER PORT) after disambiguation.
+Disambiguate between having user provided inside HOST (e.g.,
+user@server.com) and inside USER by giving priority to USER.
+Same for PORT."
   (let* ((url (url-generic-parse-url (if (string-match-p ".*://" host)
                                          host
                                        (format "https://%s" host)))))
-    (auth-source-pass--find-match-unambiguous
+    (list
      (or (url-host url) host)
      (or user (url-user url))
      ;; url-port returns 443 (because of the https:// above) by default
@@ -214,74 +221,49 @@ auth-source-pass--find-match-unambiguous
 return nil.
 
 HOSTNAME should not contain any username or port number."
-  (cl-reduce
-   (lambda (result entries)
-     (or result
-         (pcase (length entries)
-           (0 nil)
-           (1 (auth-source-pass-parse-entry (car entries)))
-           (_ (auth-source-pass--select-from-entries entries user)))))
-   (auth-source-pass--matching-entries hostname user port)
-   :initial-value nil))
+  (let ((all-entries (auth-source-pass-entries))
+        (suffixes (auth-source-pass--generate-entry-suffixes hostname user port)))
+    (auth-source-pass--do-debug "searching for entries matching hostname=%S, user=%S, port=%S"
+                                hostname (or user "") (or port ""))
+    (auth-source-pass--do-debug "corresponding suffixes to search for: %S" suffixes)
+    (catch 'auth-source-pass-break
+      (dolist (suffix suffixes)
+        (let* ((matching-entries (auth-source-pass--entries-matching-suffix suffix all-entries))
+               (best-entry-data (auth-source-pass--select-from-entries matching-entries user)))
+          (pcase (length matching-entries)
+            (0 (auth-source-pass--do-debug "found no entries matching %S" suffix))
+            (1 (auth-source-pass--do-debug "found 1 entry matching %S: %S"
+                                           suffix
+                                           (car matching-entries)))
+            (_ (auth-source-pass--do-debug "found %s entries matching %S: %S"
+                                           (length matching-entries)
+                                           suffix
+                                           matching-entries)))
+          (when best-entry-data
+            (throw 'auth-source-pass-break best-entry-data)))))))
 
 (defun auth-source-pass--select-from-entries (entries user)
   "Return best matching password-store entry data from ENTRIES.
 
 If USER is non nil, give precedence to entries containing a user field
 matching USER."
-  (cl-reduce
-   (lambda (result entry)
-     (let ((entry-data (auth-source-pass-parse-entry entry)))
-       (cond ((equal (auth-source-pass--get-attr "user" result) user)
-              result)
-             ((equal (auth-source-pass--get-attr "user" entry-data) user)
-              entry-data)
-             (t
-              result))))
-   entries
-   :initial-value (auth-source-pass-parse-entry (car entries))))
-
-(defun auth-source-pass--matching-entries (hostname user port)
-  "Return all matching password-store entries for HOSTNAME, USER, & PORT.
-
-The result is a list of lists of password-store entries, where
-each sublist contains entries that actually exist in the
-password-store matching one of the entry name formats that
-auth-source-pass expects, most specific to least specific."
-  (let* ((entries-lists (mapcar
-                         #'cdr
-                         (auth-source-pass--accumulate-matches hostname user port)))
-         (entries (apply #'cl-concatenate (cons 'list entries-lists))))
-    (if entries
-        (auth-source-pass--do-debug (format "found: %S" entries))
-      (auth-source-pass--do-debug "no matches found"))
-    entries-lists))
-
-(defun auth-source-pass--accumulate-matches (hostname user port)
-  "Accumulate matching password-store entries into sublists.
-
-Entries matching supported formats that combine HOSTNAME, USER, &
-PORT are accumulated into sublists where the car of each sublist
-is a regular expression for matching paths in the password-store
-and the remainder is the list of matching entries."
-  (let ((suffix-match-lists
-         (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix)))
-                 (auth-source-pass--generate-entry-suffixes hostname user port))))
-    (cl-reduce #'auth-source-pass--entry-reducer
-               (auth-source-pass-entries)
-               :initial-value suffix-match-lists)))
-
-(defun auth-source-pass--entry-reducer (match-lists entry)
-  "Match MATCH-LISTS sublists against ENTRY.
-
-The result is a copy of match-lists with the entry added to the
-end of any sublists for which the regular expression at the head
-of the list matches the entry name."
-  (mapcar (lambda (match-list)
-            (if (string-match (car match-list) entry)
-                (append match-list (list entry))
-              match-list))
-          match-lists))
+  (let (fallback)
+    (catch 'auth-source-pass-break
+      (dolist (entry entries fallback)
+        (let ((entry-data (auth-source-pass-parse-entry entry)))
+          (when (and entry-data (not fallback))
+            (setq fallback entry-data)
+            (when (or (not user) (equal (auth-source-pass--get-attr "user" entry-data) user))
+              (throw 'auth-source-pass-break entry-data))))))))
+
+(defun auth-source-pass--entries-matching-suffix (suffix entries)
+  "Return entries matching SUFFIX.
+If ENTRIES is nil, use the result of calling `auth-source-pass-entries' instead."
+  (cl-remove-if-not
+   (lambda (entry) (string-match-p
+               (format "\\(^\\|/\\)%s$" (regexp-quote suffix))
+               entry))
+   (or entries (auth-source-pass-entries))))
 
 (defun auth-source-pass--generate-entry-suffixes (hostname user port)
   "Return a list of possible entry path suffixes in the password-store.
diff --git a/test/lisp/auth-source-pass-tests.el b/test/lisp/auth-source-pass-tests.el
index 2c28f79945..e9d4cde4d2 100644
--- a/test/lisp/auth-source-pass-tests.el
+++ b/test/lisp/auth-source-pass-tests.el
@@ -52,11 +52,21 @@
 (defvar auth-source-pass--debug-log nil
   "Contains a list of all messages passed to `auth-source-do-debug`.")
 
-(defun auth-source-pass--should-have-message-containing (regexp)
-  "Assert that at least one `auth-source-do-debug` matched REGEXP."
-  (should (seq-find (lambda (message)
-                      (string-match regexp message))
-                    auth-source-pass--debug-log)))
+(defun auth-source-pass--have-message-matching (regexp)
+  "Return non-nil iff at least one `auth-source-do-debug` match REGEXP."
+  (seq-find (lambda (message)
+              (string-match regexp message))
+            auth-source-pass--debug-log))
+
+(defun auth-source-pass--explain--have-message-matching (regexp)
+  "Explainer function for `auth-source-pass--have-message-matching'.
+REGEXP is the same as in `auth-source-pass--have-message-matching'."
+  `(regexp
+    ,regexp
+    messages
+    ,(mapconcat #'identity auth-source-pass--debug-log "\n- ")))
+
+(put #'auth-source-pass--have-message-matching 'ert-explainer #'auth-source-pass--explain--have-message-matching)
 
 (defun auth-source-pass--debug (&rest msg)
   "Format MSG and add that to `auth-source-pass--debug-log`.
@@ -78,6 +88,82 @@ auth-source-pass--with-store
            (auth-source-pass--parse-log nil))
        ,@body)))
 
+(defun auth-source-pass--explain-match-entry-p (entry hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-entry-p'.
+
+ENTRY, HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-entry-p'."
+  `(entry
+    ,entry
+    store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-entry-p 'ert-explainer #'auth-source-pass--explain-match-entry-p)
+
+(defun auth-source-pass--includes-sorted-entries (entries hostname &optional user port)
+  "Return non-nil iff ENTRIES matching the parameters are found in store.
+ENTRIES should be sorted from most specific to least specific.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (if (seq-empty-p entries)
+      t
+    (and
+     (auth-source-pass-match-entry-p (car entries) hostname user port)
+     (auth-source-pass--includes-sorted-entries (cdr entries) hostname user port))))
+
+(defun auth-source-pass--explain-includes-sorted-entries (entries hostname &optional user port)
+  "Explainer function for `auth-source-pass--includes-sorted-entries'.
+
+ENTRIES, HOSTNAME, USER and PORT are the same as in `auth-source-pass--includes-sorted-entries'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)
+    entries
+    ,entries))
+
+(put 'auth-source-pass--includes-sorted-entries 'ert-explainer #'auth-source-pass--explain-includes-sorted-entries)
+
+(defun auth-source-pass--explain-match-any-entry-p (hostname &optional user port)
+  "Explainer function for `auth-source-pass-match-any-entry-p'.
+
+HOSTNAME, USER and PORT are the same as in `auth-source-pass-match-any-entry-p'."
+  `(store
+    ,(auth-source-pass-entries)
+    matching-entries
+    ,(auth-source-pass--matching-entries hostname user port)))
+
+(put 'auth-source-pass-match-any-entry-p 'ert-explainer #'auth-source-pass--explain-match-any-entry-p)
+
+(defun auth-source-pass--matching-entries (hostname &optional user port)
+  "Return password-store entries matching HOSTNAME, USER, PORT.
+
+The result is a list of lists of password-store entries.  Each
+sublist contains the password-store entries whose names match a
+suffix in `auth-source-pass--generate-entry-suffixes'.  The
+result is ordered the same way as the suffixes."
+  (let ((entries (auth-source-pass-entries)))
+    (mapcar (lambda (suffix) (auth-source-pass--entries-matching-suffix suffix entries))
+            (auth-source-pass--generate-entry-suffixes hostname user port))))
+
+(defun auth-source-pass-match-entry-p (entry hostname &optional user port)
+  "Return non-nil iff an ENTRY matching the parameters is found in store.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if
+   (lambda (entries) (cl-find entry entries :test #'string=))
+   (auth-source-pass--matching-entries hostname user port)))
+
+(defun auth-source-pass-match-any-entry-p (hostname &optional user port)
+  "Return non-nil iff there is at least one entry matching the parameters.
+
+HOSTNAME, USER and PORT are passed unchanged to
+`auth-source-pass--matching-entries'."
+  (cl-find-if #'identity (auth-source-pass--matching-entries hostname user port)))
+
 (ert-deftest auth-source-pass-any-host ()
   (auth-source-pass--with-store '(("foo" ("port" . "foo-port") ("host" . "foo-user"))
                                   ("bar"))
@@ -93,6 +179,101 @@ auth-source-pass--with-store
                                   ("bar"))
     (should-not (auth-source-pass-search :host "baz"))))
 
+(ert-deftest auth-source-pass--disambiguate-extract-host-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo")) "foo"))
+  ;; only user
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo")) "foo"))
+  ;; only port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80")) "foo"))
+  ;; both user and port
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80")) "foo"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "foo:80/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "https://user@foo/path")) "foo"))
+  (should (equal (cl-first (auth-source-pass--disambiguate "user@foo:80/path")) "foo")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-user-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo")) nil))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo")) "user"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80")) nil))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80")) "user"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path")) nil))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path")) "user"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path")) "user")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-user-parameter ()
+  ;; no user or port
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo" "user2")) "user2"))
+  ;; only user
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo" "user2")) "user2"))
+  ;; only port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80" "user2")) "user2"))
+  ;; both user and port
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80" "user2")) "user2"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "foo:80/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "https://user@foo/path" "user2")) "user2"))
+  (should (equal (cl-second (auth-source-pass--disambiguate "user@foo:80/path" "user2")) "user2")))
+
+(ert-deftest auth-source-pass--disambiguate-extract-port-from-hostname ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo")) "443"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo")) "443"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80")) "80"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80")) "80"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path")) "80"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path")) "443"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path")) "80")))
+
+(ert-deftest auth-source-pass--disambiguate-prefer-port-parameter ()
+  ;; no user or port
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo" "user2" "8080")) "8080"))
+  ;; only user
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo" "user2" "8080")) "8080"))
+  ;; only port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80" "user2" "8080")) "8080"))
+  ;; both user and port
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80" "user2" "8080")) "8080"))
+  ;; all of the above with a trailing path
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "foo:80/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "https://user@foo/path" "user2" "8080")) "8080"))
+  (should (equal (cl-third (auth-source-pass--disambiguate "user@foo:80/path" "user2" "8080")) "8080")))
+
 (ert-deftest auth-source-pass-find-match-minimal-parsing ()
   (let ((store-contents
          '(("baz" ("secret" . "baz password"))
@@ -121,156 +302,92 @@ auth-source-pass--with-store
       (should (equal auth-source-pass--parse-log '("bar.baz"))))
     (auth-source-pass--with-store store-contents
       (auth-source-pass--find-match "baz" nil nil)
-      (should (equal auth-source-pass--parse-log '("baz"))))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-part ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-with-user ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
-  (auth-source-pass--with-store
-      '(("SomeUser@foo" ("secret" . "SomeUser@foo password"))
-        ("foo" ("secret" . "foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("SomeUser@foo" ("secret" . "SomeUser@foo password")))
-    (let ((result (auth-source-pass--find-match "https://SomeUser@foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "SomeUser@foo password")))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain ()
+      (should (equal auth-source-pass--parse-log '("baz"))))
+    (auth-source-pass--with-store
+        '(("dir1/bar.com" ("key" . "val"))
+          ("dir2/bar.com" ("key" . "val"))
+          ("dir3/bar.com" ("key" . "val")))
+      (auth-source-pass--find-match "bar.com" nil nil)
+      (should (= (length auth-source-pass--parse-log) 1)))))
+
+(ert-deftest auth-source-pass--find-match-return-parsed-data ()
+  (auth-source-pass--with-store '(("bar.com" ("key" . "val")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil))))
+  (auth-source-pass--with-store '(("dir1/bar.com" ("key1" . "val1")) ("dir2/bar.com" ("key2" . "val2")))
+    (should (consp (auth-source-pass--find-match "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries ()
   (auth-source-pass--with-store '(("bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-user ()
-  (auth-source-pass--with-store '(("someone@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil ("someone@bar.com") nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-with-bad-user ()
-  (auth-source-pass--with-store '(("someoneelse@bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" "someone" nil)
-                   '(nil nil nil nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-name-without-subdomain-prefer-full ()
-  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(("foo.bar.com") ("bar.com") nil)))))
-
-(ert-deftest auth-source-pass-dont-match-at-folder-name ()
-  (auth-source-pass--with-store '(("foo.bar.com/foo"))
-    (should (equal (auth-source-pass--matching-entries "foo.bar.com" nil nil)
-                   '(nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user ()
-  (auth-source-pass--with-store '(("bar.com:443/someone"))
-    (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                   '(nil ("bar.com:443/someone") nil nil nil nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-host-port-and-subdir-user-with-custom-separator ()
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com"))
+    ;; match even if sub-domain is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "foo.bar.com"))
+    ;; match even if a user is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user"))
+    ;; match even if user as an @ sign
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user@someplace"))
+    ;; match even if a port is asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" nil "8080"))
+    ;; match even if a user and a port are asked for
+    (should (auth-source-pass-match-entry-p "bar.com" "bar.com" "user" "8080"))
+    ;; don't match if a '.' is replaced with another character
+    (auth-source-pass--with-store '(("barXcom"))
+      (should-not (auth-source-pass-match-any-entry-p "bar.com" nil nil)))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-username ()
+  (auth-source-pass--with-store '(("user@foo"))
+    (should (auth-source-pass-match-entry-p "user@foo" "foo" "user")))
+  ;; match even if sub-domain is asked for
+  (auth-source-pass--with-store '(("user@bar.com"))
+    (should (auth-source-pass-match-entry-p "user@bar.com" "foo.bar.com" "user")))
+  ;; don't match if no user is asked for
+  (auth-source-pass--with-store '(("user@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo")))
+  ;; don't match if user is different
+  (auth-source-pass--with-store '(("user1@foo"))
+    (should-not (auth-source-pass-match-any-entry-p "foo" "user2")))
+  ;; don't match if sub-domain is asked for but user is different
+  (auth-source-pass--with-store '(("user1@bar.com"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.bar.com" "user2"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-a-port ()
+  (auth-source-pass--with-store '(("bar.com:8080"))
+    (should (auth-source-pass-match-entry-p "bar.com:8080" "bar.com" nil "8080"))))
+
+(ert-deftest auth-source-pass--matching-entries-find-entries-with-slash ()
+  ;; match if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com/user"))
+    (should (auth-source-pass-match-entry-p "foo.com/user" "foo.com" "user")))
+  ;; match with port if entry filename matches user
+  (auth-source-pass--with-store '(("foo.com:8080/user"))
+    (should (auth-source-pass-match-entry-p "foo.com:8080/user" "foo.com" "user" "8080")))
+  ;; don't match if entry filename doesn't match user
+  (auth-source-pass--with-store '(("foo.com/baz"))
+    (should-not (auth-source-pass-match-any-entry-p "foo.com" "user"))))
+
+(ert-deftest auth-source-pass-matching-entries-with-custom-separator ()
   (let ((auth-source-pass-port-separator "#"))
     (auth-source-pass--with-store '(("bar.com#443/someone"))
-      (should (equal (auth-source-pass--matching-entries "bar.com" "someone" "443")
-                     '(nil ("bar.com#443/someone") nil nil nil nil
-                           nil nil nil nil nil nil))))))
-
-(ert-deftest auth-source-pass-matching-entries-extracting-user-from-host ()
-  (auth-source-pass--with-store
-      '(("foo.com/bar" ("secret" . "foo.com/bar password")))
-    (let ((result (auth-source-pass--find-match "https://bar@foo.com" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo.com/bar password")))))
-
-(ert-deftest auth-source-pass-matching-entries-with-user-first ()
+      (should (auth-source-pass-match-entry-p "bar.com#443/someone" "bar.com" "someone" "443")))))
+
+(ert-deftest auth-source-pass--matching-entries-sort-results ()
+  (auth-source-pass--with-store '(("user@foo") ("foo"))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; same, but store is reversed
   (auth-source-pass--with-store '(("foo") ("user@foo"))
-    (should (equal (auth-source-pass--matching-entries "foo" "user" nil)
-                   '(("user@foo") nil ("foo"))))
-    (auth-source-pass--should-have-message-containing "found: (\"user@foo\" \"foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password") ("user" . "someone")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "subdir/foo password"))
-      (should (equal (auth-source-pass--get-attr "user" result)
-                     "someone")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-give-priority-to-desired-user-reversed ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password") ("user" . "someone"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" "someone" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-return-first-when-several-matches ()
-  (auth-source-pass--with-store
-      '(("foo" ("secret" . "foo password"))
-        ("subdir/foo" ("secret" . "subdir/foo password")))
-    (let ((result (auth-source-pass--find-match "foo" nil nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "foo password")))
-    (auth-source-pass--should-have-message-containing "found: (\"foo\" \"subdir/foo\"")))
-
-(ert-deftest auth-source-pass-matching-entries-make-divansantana-happy ()
-  (auth-source-pass--with-store '(("host.com"))
-    (should (equal (auth-source-pass--matching-entries "smtp.host.com" "myusername@host.co.za" nil)
-                   '(nil nil nil nil nil ("host.com") nil nil nil)))))
-
-(ert-deftest auth-source-pass-find-host-without-port ()
-  (auth-source-pass--with-store
-      '(("host.com" ("secret" . "host.com password")))
-    (let ((result (auth-source-pass--find-match "host.com:8888" "someuser" nil)))
-      (should (equal (auth-source-pass--get-attr "secret" result)
-                     "host.com password")))))
-
-(ert-deftest auth-source-pass-matching-entries-host-with-port ()
-  (auth-source-pass--with-store '(("host.com:443"))
-    (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                   '(nil nil nil nil ("host.com:443") nil
-                         nil nil nil nil nil nil)))))
-
-(ert-deftest auth-source-pass-matching-entries-with-custom-port-separator ()
-  (let ((auth-source-pass-port-separator "#"))
-    (auth-source-pass--with-store '(("host.com#443"))
-      (should (equal (auth-source-pass--matching-entries "host.com" "someuser" "443")
-                     '(nil nil nil nil ("host.com#443") nil
-                           nil nil nil nil nil nil))))))
+    (should (auth-source-pass--includes-sorted-entries '("user@foo" "foo") "foo" "user")))
+  ;; with sub-domain
+  (auth-source-pass--with-store '(("bar.com") ("foo.bar.com"))
+    (should (auth-source-pass--includes-sorted-entries '("foo.bar.com" "bar.com") "foo.bar.com")))
+  ;; matching user in the entry data takes priority
+  (auth-source-pass--with-store '(("dir1/bar.com") ("dir2/bar.com" ("user" . "user")))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user")))
+  ;; same, but store is reversed
+  (auth-source-pass--with-store '(("dir2/bar.com" ("user" . "user")) ("dir1/bar.com"))
+    (should (auth-source-pass--includes-sorted-entries
+             '("dir2/bar.com" "dir1/bar.com")
+             "bar.com" "user"))))
 
 (defmacro auth-source-pass--with-store-find-foo (store &rest body)
   "Use STORE while executing BODY.  \"foo\" is the matched entry."
@@ -300,33 +417,6 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :port) 512))
       (should (equal (plist-get result :user) "anuser")))))
 
-(ert-deftest auth-source-pass-build-result-passes-full-host-to-find-match ()
-  (let (passed-host)
-    (cl-letf (((symbol-function 'auth-source-pass--find-match)
-               (lambda (host _user _port)
-                 (setq passed-host host)
-                 nil)))
-      (auth-source-pass--build-result "https://user@host.com:123" nil nil)
-      (should (equal passed-host "https://user@host.com:123"))
-      (auth-source-pass--build-result "https://user@host.com" nil nil)
-      (should (equal passed-host "https://user@host.com"))
-      (auth-source-pass--build-result "user@host.com" nil nil)
-      (should (equal passed-host "user@host.com"))
-      (auth-source-pass--build-result "user@host.com:443" nil nil)
-      (should (equal passed-host "user@host.com:443")))))
-
-(ert-deftest auth-source-pass-only-return-entries-that-can-be-open ()
-  (auth-source-pass--with-store
-      '(("foo.site.com" ("secret" . "foo.site.com password"))
-        ("bar.site.com") ; An entry name with no data is invalid
-        ("mail/baz.site.com/scott" ("secret" . "mail/baz.site.com/scott password")))
-    (should (equal (auth-source-pass--find-match "foo.site.com" "someuser" nil)
-                   '(("secret" . "foo.site.com password"))))
-    (should (equal (auth-source-pass--find-match "bar.site.com" "someuser" nil)
-                   nil))
-    (should (equal (auth-source-pass--find-match "baz.site.com" "scott" nil)
-                   '(("secret" . "mail/baz.site.com/scott password"))))))
-
 (ert-deftest auth-source-pass-can-start-from-auth-source-search ()
   (auth-source-pass--with-store '(("gitlab.com" ("user" . "someone")))
     (auth-source-pass-enable)
@@ -334,6 +424,24 @@ auth-source-pass--with-store-find-foo
       (should (equal (plist-get result :user) "someone"))
       (should (equal (plist-get result :host) "gitlab.com")))))
 
+(ert-deftest auth-source-pass-prints-meaningful-debug-log ()
+  (auth-source-pass--with-store '()
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "entries matching hostname=\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "corresponding suffixes to search for: .*\"gitlab.com\""))
+    (should (auth-source-pass--have-message-matching
+             "found no entries matching \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 1 entry matching \"gitlab.com\": \"gitlab.com\"")))
+  (auth-source-pass--with-store '(("a/gitlab.com") ("b/gitlab.com"))
+    (auth-source-pass--find-match "gitlab.com" nil nil)
+    (should (auth-source-pass--have-message-matching
+             "found 2 entries matching \"gitlab.com\": (\"a/gitlab.com\" \"b/gitlab.com\")"))))
+
 (provide 'auth-source-pass-tests)
 
 ;;; auth-source-pass-tests.el ends here
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #12: 0011-lisp-auth-source-pass.el-Version-5.0.0.patch --]
[-- Type: text/x-patch, Size: 794 bytes --]

From be1914d3c6ceab3f0712088547545bdf5121d204 Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Tue, 28 May 2019 08:46:41 +0200
Subject: [PATCH 11/12] * lisp/auth-source-pass.el: Version 5.0.0

---
 lisp/auth-source-pass.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
index 1daafdb7e6..a5c1a26a88 100644
--- a/lisp/auth-source-pass.el
+++ b/lisp/auth-source-pass.el
@@ -5,7 +5,7 @@
 ;; Author: Damien Cassou <damien@cassou.me>,
 ;;         Nicolas Petton <nicolas@petton.fr>
 ;;         Keith Amidon <camalot@picnicpark.org>
-;; Version: 4.0.2
+;; Version: 5.0.0
 ;; Package-Requires: ((emacs "25"))
 ;; Url: https://github.com/DamienCassou/auth-password-store
 ;; Created: 07 Jun 2015
-- 
2.21.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #13: 0012-etc-NEWS-Describe-changes-to-auth-source-pass.patch --]
[-- Type: text/x-patch, Size: 1351 bytes --]

From 84f2050e78c0f19dacb67d50dbe4d1dc114f5e3e Mon Sep 17 00:00:00 2001
From: Damien Cassou <damien@cassou.me>
Date: Sun, 2 Jun 2019 11:08:40 +0200
Subject: [PATCH 12/12] * etc/NEWS: Describe changes to auth-source-pass

---
 etc/NEWS | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 975fab495a..5dcdac2668 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1485,6 +1485,25 @@ the new variable 'buffer-auto-revert-by-notification' to a non-nil
 value.  Auto Revert mode can use this information to avoid polling the
 buffer periodically when 'auto-revert-avoid-polling' is non-nil.
 
+** auth-source-pass
+
+*** New customizable variable 'auth-source-pass-path' for the path to
+the password-store.  This defaults to ~/.password-store.
+
+*** New customizable variable 'auth-source-pass-port-separator' to
+specify separator between host and port.  This defaults to colon
+":".
+
+*** auth-source-pass.el and auth-source-pass-tests.el have been
+massively rewritten to minimize parsing of password-store entries.
+This makes the package usable with physical tokens requiring touching
+a sensor for every decryption.
+
+*** 'auth-source-pass-get' has an autoload cookie now.
+
+*** 'auth-source-pass-search' now correctly returns nil if no entry
+found.
+
 \f
 * New Modes and Packages in Emacs 27.1
 
-- 
2.21.0


^ permalink raw reply related	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2019-06-24 14:33 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2019-06-02  9:11 bug#36052: 26.2.50; [PATCH] Improve auth-source-pass Damien Cassou
2019-06-07  0:43 ` Noam Postavsky
2019-06-08 15:47   ` Damien Cassou
2019-06-08 16:02     ` Eli Zaretskii
2019-06-08 22:38     ` Noam Postavsky
2019-06-13 19:59     ` Damien Cassou
2019-06-13 21:23       ` Noam Postavsky
2019-06-14  7:10       ` Damien Cassou
2019-06-14  7:47       ` Eli Zaretskii
2019-06-14 16:16         ` Damien Cassou
2019-06-22  9:02           ` Eli Zaretskii
2019-06-24  7:26             ` Damien Cassou
2019-06-24 14:33               ` Eli Zaretskii

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).