emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Emacs-diffs] feature/auth-source-pass 6f6e7c2 3/5: Integrate auth-sourc


From: Teodor Zlatanov
Subject: [Emacs-diffs] feature/auth-source-pass 6f6e7c2 3/5: Integrate auth-source with password-store
Date: Wed, 26 Apr 2017 15:16:41 -0400 (EDT)

branch: feature/auth-source-pass
commit 6f6e7c2add8e5eb4a1dd00a812f3cf35eb2eae37
Author: Damien Cassou <address@hidden>
Commit: Ted Zlatanov <address@hidden>

    Integrate auth-source with password-store
    
    * lisp/auth-source-pass.el: auth-source backend for password-store.
    * test/lisp/auth-source-pass-tests.el: Tests for auth-source-pass
      behavior.
---
 lisp/auth-source-pass.el            | 255 ++++++++++++++++++++++++++++++++++++
 test/lisp/auth-source-pass-tests.el | 234 +++++++++++++++++++++++++++++++++
 2 files changed, 489 insertions(+)

diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el
new file mode 100644
index 0000000..a9d61cf
--- /dev/null
+++ b/lisp/auth-source-pass.el
@@ -0,0 +1,255 @@
+;;; auth-source-pass.el --- Integrate auth-source with password-store -*- 
lexical-binding: t -*-
+
+;; Copyright (C) 2015 Damien Cassou & Nicolas Petton
+
+;; Author: Damien Cassou <address@hidden>,
+;;         Nicolas Petton <address@hidden>
+;; Version: 2.0.0
+;; Package-Requires: ((emacs "24.4")
+;; Created: 07 Jun 2015
+;; Keywords: pass password-store auth-source username password login
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Integrates password-store (http://passwordstore.org/) within
+;; auth-source.
+
+;;; Code:
+
+(require 'seq)
+(require 'subr-x)
+(eval-when-compile
+  (require 'cl-lib))
+(require 'auth-source)
+(require 'url-parse)
+
+(cl-defun auth-source-pass-search (&rest spec
+                         &key backend type host user port
+                         &allow-other-keys)
+  "Given a property list SPEC, return search matches from the :backend.
+See `auth-source-search' for details on SPEC."
+  (cl-assert (or (null type) (eq type (oref backend type)))
+             t "Invalid password-store search: %s %s")
+  (when (listp host)
+    ;; Take the first non-nil item of the list of hosts
+    (setq host (seq-find #'identity host)))
+  (list (auth-source-pass--build-result host port user)))
+
+(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)))
+    (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)))))
+        (auth-source-pass--do-debug "return %s as final result (plus hidden 
password)"
+                    (seq-subseq retval 0 -2)) ;; remove password
+        retval))))
+
+;;;###autoload
+(defun auth-source-pass-enable ()
+  "Enable auth-source-password-store."
+  ;; To add password-store to the list of sources, evaluate the following:
+  (add-to-list 'auth-sources 'password-store)
+  ;; clear the cache (required after each change to #'auth-source-pass-search)
+  (auth-source-forget-all-cached))
+
+(defvar auth-source-pass-backend
+  (auth-source-backend
+   (format "Password store")
+   :source "." ;; not used
+   :type 'password-store
+   :search-function #'auth-source-pass-search)
+  "Auth-source backend for password-store.")
+
+(defun auth-source-pass-backend-parse (entry)
+  "Create a password-store auth-source backend from ENTRY."
+  (when (eq entry 'password-store)
+    (auth-source-backend-parse-parameters entry auth-source-pass-backend)))
+
+(add-hook 'auth-source-backend-parser-functions 
#'auth-source-pass-backend-parse)
+
+
+(defun auth-source-pass-get (key entry)
+  "Return the value associated to KEY in the password-store entry ENTRY.
+
+ENTRY is the name of a 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"
+  (let ((data (auth-source-pass-parse-entry entry)))
+    (or (cdr (assoc key data))
+        (and (string= key "user")
+             (cdr (assoc "username" data))))))
+
+(defun auth-source-pass--read-entry (entry)
+  "Return a string with the file content of ENTRY."
+  (with-temp-buffer
+    (insert-file-contents (expand-file-name
+                           (format "%s.gpg" entry)
+                           "~/.password-store"))
+    (buffer-substring-no-properties (point-min) (point-max))))
+
+(defun auth-source-pass-parse-entry (entry)
+  "Return an alist of the data associated with ENTRY.
+
+ENTRY is the name of a password-store entry."
+  (let ((file-contents (ignore-errors (auth-source-pass--read-entry entry))))
+    (and file-contents
+         (cons `(secret . ,(auth-source-pass--parse-secret file-contents))
+               (auth-source-pass--parse-data file-contents)))))
+
+(defun auth-source-pass--parse-secret (contents)
+  "Parse the password-store data in the string CONTENTS and return its secret.
+The secret is the first line of CONTENTS."
+  (car (split-string contents "\\\n" t)))
+
+(defun auth-source-pass--parse-data (contents)
+  "Parse the password-store data in the string CONTENTS and return an alist.
+CONTENTS is the contents of a password-store formatted file."
+  (let ((lines (split-string contents "\\\n" t "\\\s")))
+    (seq-remove #'null
+                (mapcar (lambda (line)
+                          (let ((pair (mapcar #'string-trim
+                                              (split-string line ":"))))
+                            (when (> (length pair) 1)
+                              (cons (car pair)
+                                    (mapconcat #'identity (cdr pair) ":")))))
+                        (cdr lines)))))
+
+(defun auth-source-pass--user-match-p (entry user)
+  "Return true iff ENTRY match USER."
+  (or (null user)
+      (string= user (auth-source-pass-get "user" entry))))
+
+(defun auth-source-pass--hostname (host)
+  "Extract hostname from HOST."
+  (let ((url (url-generic-parse-url host)))
+    (or (url-host url) host)))
+
+(defun auth-source-pass--hostname-with-user (host)
+  "Extract hostname and user from HOST."
+  (let* ((url (url-generic-parse-url host))
+         (user (url-user url))
+         (hostname (url-host url)))
+    (cond
+     ((and user hostname) (format "address@hidden" user hostname))
+     (hostname hostname)
+     (t host))))
+
+(defun auth-source-pass--remove-directory-name (name)
+  "Remove directories from NAME.
+E.g., if NAME is \"foo/bar\", return \"bar\"."
+  (replace-regexp-in-string ".*/" "" name))
+
+(defun auth-source-pass--do-debug (&rest msg)
+  "Call `auth-source-do-debug` with MSG and a prefix."
+  (apply #'auth-source-do-debug
+         (cons (concat "auth-source-password-store: " (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 ()
+  "Return a list of all password store entries."
+  (let ((store-dir (expand-file-name "~/.password-store/")))
+    (mapcar
+     (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 (name)
+  "Search the store for all entries matching NAME.
+Only return valid entries as of `auth-source-pass--entry-valid-p'."
+  (seq-filter (lambda (entry)
+                (and
+                 (string-equal
+                  name
+                  (auth-source-pass--remove-directory-name entry))
+                 (auth-source-pass--entry-valid-p entry)))
+              (auth-source-pass-entries)))
+
+(defun auth-source-pass--find-one-by-entry-name (name user)
+  "Search the store for an entry matching NAME.
+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)"
+              name
+              user)
+  (let ((matching-entries (auth-source-pass--find-all-by-entry-name name)))
+    (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)
+  "Return a password-store entry name matching HOST and USER.
+If many matches are found, return the first one.  If no match is
+found, return nil."
+  (or
+   (if (url-user (url-generic-parse-url host))
+       ;; if HOST contains a user (e.g., "address@hidden"), <HOST>
+       (auth-source-pass--find-one-by-entry-name 
(auth-source-pass--hostname-with-user host) user)
+     ;; otherwise, if USER is provided, search for <USER>@<HOST>
+     (when (stringp user)
+       (auth-source-pass--find-one-by-entry-name (concat user "@" 
(auth-source-pass--hostname host)) user)))
+   ;; if that didn't work, search for HOST without it's user component if any
+   (auth-source-pass--find-one-by-entry-name (auth-source-pass--hostname host) 
user)
+   ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com
+   (let ((components (split-string host "\\.")))
+     (when (= (length components) 3)
+       ;; start from scratch
+       (auth-source-pass--find-match (mapconcat 'identity (cdr components) 
".") user)))))
+
+(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
new file mode 100644
index 0000000..c3586d8
--- /dev/null
+++ b/test/lisp/auth-source-pass-tests.el
@@ -0,0 +1,234 @@
+;;; auth-source-pass-tests.el --- Tests for auth-source-pass.el  -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2013 Damien Cassou
+
+;; Author: Damien Cassou <address@hidden>
+
+;; This file is not part of GNU Emacs.
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for auth-source-pass.el
+
+;;; Code:
+
+(require 'ert)
+
+(require 'auth-source-pass)
+
+(eval-when-compile (require 'cl-macs))
+
+(ert-deftest auth-source-pass-parse-simple ()
+  (let ((content "pass\nkey1:val1\nkey2:val2\n"))
+    (should (equal (auth-source-pass--parse-data content)
+                   '(("key1" . "val1")
+                     ("key2" . "val2"))))))
+
+(ert-deftest auth-source-pass-parse-with-dash-line ()
+  (let ((content "pass\n--\nkey1:val1\nkey2:val2\n"))
+    (should (equal (auth-source-pass--parse-data content)
+                   '(("key1" . "val1")
+                     ("key2" . "val2"))))))
+
+(ert-deftest auth-source-pass-parse-with-trailing-spaces ()
+  (let ((content "pass\n--\nkey1 :val1   \nkey2:   val2\n\n"))
+    (should (equal (auth-source-pass--parse-data content)
+                   '(("key1" . "val1")
+                     ("key2" . "val2"))))))
+
+(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--debug (&rest msg)
+  "Format MSG and add that to `auth-source-pass--debug-log`.
+This function is intended to be set to `auth-source-debug`."
+  (add-to-list 'auth-source-pass--debug-log (apply #'format msg) t))
+
+(defmacro auth-source-pass--deftest (name arglist store &rest body)
+  "Define a new ert-test NAME with ARGLIST using STORE as password-store.
+BODY is a sequence of instructions that will be evaluated.
+
+This macro overrides `auth-source-pass-parse-entry' and 
`auth-source-pass-entries' to
+test code without touching the file system."
+  (declare (indent 3))
+  `(ert-deftest ,name ,arglist
+     (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)))
+       (let ((auth-source-debug #'auth-source-pass--debug)
+             (auth-source-pass--debug-log nil))
+         ,@body))))
+
+(auth-source-pass--deftest auth-source-pass-find-match-matching-at-entry-name 
()
+           '(("foo"))
+  (should (equal (auth-source-pass--find-match "foo" nil)
+                 "foo")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-part ()
+                   '(("foo"))
+  (should (equal (auth-source-pass--find-match "https://foo"; nil)
+                 "foo")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-ignoring-user ()
+                   '(("foo"))
+  (should (equal (auth-source-pass--find-match "https://address@hidden"; nil)
+                 "foo")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-with-user ()
+                   '(("address@hidden"))
+                   (should (equal (auth-source-pass--find-match 
"https://address@hidden"; nil)
+                                  "address@hidden")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-prefer-full ()
+                   '(("address@hidden") ("foo"))
+                   (should (equal (auth-source-pass--find-match 
"https://address@hidden"; nil)
+                                  "address@hidden")))
+
+;; same as previous one except the store is in another order
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-prefer-full-reversed ()
+                   '(("foo") ("address@hidden"))
+                   (should (equal (auth-source-pass--find-match 
"https://address@hidden"; nil)
+                                  "address@hidden")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-without-subdomain ()
+                   '(("bar.com"))
+                   (should (equal (auth-source-pass--find-match "foo.bar.com" 
nil)
+                                  "bar.com")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-user 
()
+                   '(("address@hidden"))
+                   (should (equal (auth-source-pass--find-match "foo.bar.com" 
"someone")
+                                  "address@hidden")))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-without-subdomain-with-bad-user
 ()
+                   '(("address@hidden"))
+                   (should (equal (auth-source-pass--find-match "foo.bar.com" 
"someone")
+                                  nil)))
+
+(auth-source-pass--deftest 
auth-source-pass-find-match-matching-at-entry-name-without-subdomain-prefer-full
 ()
+                   '(("bar.com") ("foo.bar.com"))
+                   (should (equal (auth-source-pass--find-match "foo.bar.com" 
nil)
+                                  "foo.bar.com")))
+
+(auth-source-pass--deftest auth-source-pass-dont-match-at-folder-name ()
+           '(("foo.bar.com/foo"))
+  (should (equal (auth-source-pass--find-match "foo.bar.com" nil)
+                 nil)))
+
+(auth-source-pass--deftest auth-source-pass-search-with-user-first ()
+           '(("foo") ("address@hidden"))
+  (should (equal (auth-source-pass--find-match "foo" "user")
+                 "address@hidden"))
+  (auth-source-pass--should-have-message-containing "Found 1 match"))
+
+(auth-source-pass--deftest auth-source-pass-give-priority-to-desired-user ()
+           '(("foo") ("subdir/foo" ("user" . "someone")))
+  (should (equal (auth-source-pass--find-match "foo" "someone")
+                 "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--deftest 
auth-source-pass-give-priority-to-desired-user-reversed ()
+           '(("foo" ("user" . "someone")) ("subdir/foo"))
+  (should (equal (auth-source-pass--find-match "foo" "someone")
+                 "foo"))
+  (auth-source-pass--should-have-message-containing "Found 2 matches")
+  (auth-source-pass--should-have-message-containing "matching user field"))
+
+(auth-source-pass--deftest auth-source-pass-return-first-when-several-matches 
()
+           '(("foo") ("subdir/foo"))
+  (should (equal (auth-source-pass--find-match "foo" nil)
+                 "foo"))
+  (auth-source-pass--should-have-message-containing "Found 2 matches")
+  (auth-source-pass--should-have-message-containing "the first one"))
+
+(auth-source-pass--deftest auth-source-pass-make-divansantana-happy ()
+           '(("host.com"))
+  (should (equal (auth-source-pass--find-match "smtp.host.com" 
"address@hidden")
+                 "host.com")))
+
+(ert-deftest auth-source-pass-hostname ()
+  (should (equal (auth-source-pass--hostname "https://foo.bar";) "foo.bar"))
+  (should (equal (auth-source-pass--hostname "http://foo.bar";) "foo.bar"))
+  (should (equal (auth-source-pass--hostname "https://address@hidden";) 
"foo.bar")))
+
+(ert-deftest auth-source-pass-hostname-with-user ()
+  (should (equal (auth-source-pass--hostname-with-user "https://foo.bar";) 
"foo.bar"))
+  (should (equal (auth-source-pass--hostname-with-user "http://foo.bar";) 
"foo.bar"))
+  (should (equal (auth-source-pass--hostname-with-user 
"https://address@hidden";) "address@hidden")))
+
+(defmacro auth-source-pass--deftest-build-result (name arglist store &rest 
body)
+  "Define a new ert-test NAME with ARGLIST using STORE as password-store.
+BODY is a sequence of instructions that will be evaluated.
+
+This macro overrides `auth-source-pass-parse-entry',
+`auth-source-pass-entries', and `auth-source-pass--find-match' to
+ease testing."
+  (declare (indent 3))
+  `(auth-source-pass--deftest ,name ,arglist ,store
+     (cl-letf (((symbol-function 'auth-source-pass-find-match)
+                (lambda (_host _user)
+                  "foo")))
+       ,@body)))
+
+(auth-source-pass--deftest-build-result 
auth-source-pass-build-result-return-parameters ()
+                        '(("foo"))
+  (let ((result (auth-source-pass--build-result "foo" 512 "user")))
+    (should (equal (plist-get result :port) 512))
+    (should (equal (plist-get result :user) "user"))))
+
+(auth-source-pass--deftest-build-result 
auth-source-pass-build-result-return-entry-values ()
+                        '(("foo" ("port" . 512) ("user" . "anuser")))
+  (let ((result (auth-source-pass--build-result "foo" nil nil)))
+    (should (equal (plist-get result :port) 512))
+    (should (equal (plist-get result :user) "anuser"))))
+
+(auth-source-pass--deftest-build-result 
auth-source-pass-build-result-entry-takes-precedence ()
+                        '(("foo" ("port" . 512) ("user" . "anuser")))
+  (let ((result (auth-source-pass--build-result "foo" 1024 "anotheruser")))
+    (should (equal (plist-get result :port) 512))
+    (should (equal (plist-get result :user) "anuser"))))
+
+(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")))
+            ((symbol-function 'auth-source-pass--entry-valid-p)
+             ;; only foo.site.com is valid
+             (lambda (entry) (string-equal entry "foo.site.com"))))
+    (should (equal (auth-source-pass--find-all-by-entry-name "foo.site.com")
+                   '("foo.site.com")))
+    (should (equal (auth-source-pass--find-all-by-entry-name "bar.site.com")
+                   '()))))
+
+(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"))))
+
+(provide 'auth-source-pass-tests)
+
+;;; auth-source-pass-tests.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]