* auth-source.el (top): Autoload `secrets-list-collections',

`secrets-create-item', `secrets-delete-item'.
(auth-sources): Fix tag string.
(auth-get-source, auth-source-retrieve, auth-source-create)
(auth-source-delete): New defuns.
(auth-source-pick): Rewrite in order to avoid 2 passes.
(auth-source-forget-user-or-password): New parameter USERNAME.
(auth-source-user-or-password): New parameters CREATE-MISSING and
DELETE-EXISTING.  Retrieve password interactively, if needed.
This commit is contained in:
Michael Albinus 2010-06-08 15:05:11 +02:00
parent d7c5d87df6
commit 0e4966fb65
2 changed files with 212 additions and 133 deletions

View file

@ -1,3 +1,15 @@
2010-06-08 Michael Albinus <michael.albinus@gmx.de>
* auth-source.el (top): Autoload `secrets-list-collections',
`secrets-create-item', `secrets-delete-item'.
(auth-sources): Fix tag string.
(auth-get-source, auth-source-retrieve, auth-source-create)
(auth-source-delete): New defuns.
(auth-source-pick): Rewrite in order to avoid 2 passes.
(auth-source-forget-user-or-password): New parameter USERNAME.
(auth-source-user-or-password): New parameters CREATE-MISSING and
DELETE-EXISTING. Retrieve password interactively, if needed.
2010-06-07 Teemu Likonen <tlikonen@iki.fi> (tiny change) 2010-06-07 Teemu Likonen <tlikonen@iki.fi> (tiny change)
* gnus-agent.el (gnus-agent-expire-unagentized-dirs): Don't ask about * gnus-agent.el (gnus-agent-expire-unagentized-dirs): Don't ask about

View file

@ -35,10 +35,13 @@
(eval-when-compile (require 'cl)) (eval-when-compile (require 'cl))
(autoload 'netrc-machine-user-or-password "netrc") (autoload 'netrc-machine-user-or-password "netrc")
(autoload 'secrets-search-items "secrets") (autoload 'secrets-create-item "secrets")
(autoload 'secrets-delete-item "secrets")
(autoload 'secrets-get-alias "secrets") (autoload 'secrets-get-alias "secrets")
(autoload 'secrets-get-attribute "secrets") (autoload 'secrets-get-attribute "secrets")
(autoload 'secrets-get-secret "secrets") (autoload 'secrets-get-secret "secrets")
(autoload 'secrets-list-collections "secrets")
(autoload 'secrets-search-items "secrets")
(defgroup auth-source nil (defgroup auth-source nil
"Authentication sources." "Authentication sources."
@ -122,7 +125,7 @@ can get pretty complex."
(const :format "" :value :source) (const :format "" :value :source)
(choice :tag "Authentication backend choice" (choice :tag "Authentication backend choice"
(string :tag "Authentication Source (file)") (string :tag "Authentication Source (file)")
(list :tag "secrets.el (Secret Service API/KWallet/GNOME KeyRing)" (list :tag "secrets.el (Secret Service API/KWallet/GNOME Keyring)"
(const :format "" :value :secrets) (const :format "" :value :secrets)
(choice :tag "Collection to use" (choice :tag "Collection to use"
(string :tag "Collection name") (string :tag "Collection name")
@ -178,123 +181,182 @@ can get pretty complex."
;; (auth-source-pick nil :host "any" :protocol 'imap :user "joe") ;; (auth-source-pick nil :host "any" :protocol 'imap :user "joe")
;; (auth-source-pick t :host "any" :protocol 'imap :user "joe") ;; (auth-source-pick t :host "any" :protocol 'imap :user "joe")
;; (setq auth-sources '((:source (:secrets default) :host t :protocol t :user "joe") ;; (setq auth-sources '((:source (:secrets default) :host t :protocol t :user "joe")
;; (:source (:secrets "session") :host t :protocol t :user "joe") ;; (:source (:secrets "session") :host t :protocol t :user "joe")
;; (:source (:secrets "login") :host t :protocol t) ;; (:source (:secrets "login") :host t :protocol t)
;; (:source "~/.authinfo.gpg" :host t :protocol t))) ;; (:source "~/.authinfo.gpg" :host t :protocol t)))
;; (setq auth-sources '((:source (:secrets default) :host t :protocol t :user "joe") ;; (setq auth-sources '((:source (:secrets default) :host t :protocol t :user "joe")
;; (:source (:secrets "session") :host t :protocol t :user "joe") ;; (:source (:secrets "session") :host t :protocol t :user "joe")
;; (:source (:secrets "login") :host t :protocol t) ;; (:source (:secrets "login") :host t :protocol t)
;; )) ;; ))
;; (setq auth-sources '((:source "~/.authinfo.gpg" :host t :protocol t))) ;; (setq auth-sources '((:source "~/.authinfo.gpg" :host t :protocol t)))
(defun auth-get-source (entry)
"Return the source string of ENTRY, which is one entry in `auth-sources'.
If it is a Secret Service API, return the collection name, otherwise
the file name."
(let ((source (plist-get entry :source)))
(if (stringp source)
source
;; Secret Service API.
(setq source (plist-get source :secrets))
(when (eq source 'default)
(setq source (or (secrets-get-alias "default") "login")))
(or source "session"))))
(defun auth-source-pick (&rest spec) (defun auth-source-pick (&rest spec)
"Parse `auth-sources' for matches of the SPEC plist. "Parse `auth-sources' for matches of the SPEC plist.
Common keys are :host, :protocol, and :user. A value of t in Common keys are :host, :protocol, and :user. A value of t in
SPEC means to always succeed in the match. A string value is SPEC means to always succeed in the match. A string value is
matched as a regex. matched as a regex."
(let ((keys (loop for i below (length spec) by 2 collect (nth i spec)))
choices)
(dolist (choice (copy-tree auth-sources) choices)
(let ((source (plist-get choice :source))
(match t))
(when
(and
;; Check existence of source.
(if (consp source)
;; Secret Service API.
(member (auth-get-source choice) (secrets-list-collections))
;; authinfo file.
(file-exists-p source))
The first pass skips fallback choices. If no choices are found ;; Check keywords.
on the first pass, a second pass is made including the fallback (dolist (k keys match)
choices. (let* ((v (plist-get spec k))
(choicev (plist-get choice k)))
(setq match
(and match
(or
;; source always matches spec key
(eq t choicev)
;; source key gives regex to match against spec
(and (stringp choicev) (string-match choicev v))
;; source key gives symbol to match against spec
(and (symbolp choicev) (eq choicev v))))))))
For string (filename) sources, fallback choices are those where (add-to-list 'choices choice 'append))))))
PROTOCOL or HOST are nil.
For secrets.el collections, the :host and :protocol keys are not (defun auth-source-retrieve (mode entry &rest spec)
checked for fallback choices." "Retrieve MODE credentials according to SPEC from ENTRY."
(let (choices) (catch 'no-password
(dolist (fallback '(nil t)) (let ((host (plist-get spec :host))
(let ((keys (loop for i below (length spec) by 2 (user (plist-get spec :user))
collect (nth i spec))) (prot (plist-get spec :protocol))
(default-session-fallback "login")) (source (plist-get entry :source))
(dolist (choice auth-sources) result)
(let* ((s (plist-get choice :source)) (cond
;; this is only set for Secret Service API specs (see secrets.el) ;; Secret Service API.
(coll (and (consp s) (plist-get s :secrets))) ((consp source)
(score 0)) (let ((coll (auth-get-source entry))
(cond item)
(coll ; use secrets.el here ;; Loop over candidates with a matching host attribute.
(when (eq coll 'default) (dolist (elt (secrets-search-items coll :host host) item)
(setq coll (secrets-get-alias "default")) (when (and (or (not user)
(unless coll (string-equal
(auth-source-do-debug user (secrets-get-attribute coll elt :user)))
"No 'default' alias. Trying collection '%s'." (or (not prot)
default-session-fallback) (string-equal
(setq coll default-session-fallback))) prot (secrets-get-attribute coll elt :protocol))))
(let* ((coll-search (cond (setq item elt)
((stringp coll) coll) (return elt)))
;; Compose result.
;; when the collection is nil: (when item
;; in fallback mode, accept it as any (setq result
;; otherwise, hope to fail (mapcar (lambda (m)
((null coll) (if fallback (if (string-equal "password" m)
nil (or (secrets-get-secret coll item)
" *fallback-fail*")))) ;; When we do not find a password,
;; assemble a search query for secrets-search-items ;; we return nil anyway.
;; in fallback mode, host and protocol are not checked (throw 'no-password nil))
(other-search (loop for k (or (secrets-get-attribute coll item :user)
in (if fallback user)))
(remove :host (if (consp mode) mode (list mode)))))
(remove :protocol keys)) (if (consp mode) result (car result))))
keys) ;; Anything else is netrc.
append (list (t
k (let ((search (list source (list host) (list (format "%s" prot))
;; convert symbols to a string (auth-source-protocol-defaults prot))))
(let ((v (plist-get spec k))) (setq result
(if (stringp v) (mapcar (lambda (m)
v (if (string-equal "password" m)
(prin1-to-string v)))))) (or (apply
;; the score is based on how exact the search was, 'netrc-machine-user-or-password m search)
;; plus base score = 1 for any match ;; When we do not find a password, we
(score (1+ (length other-search))) ;; return nil anyway.
(results (apply 'secrets-search-items (throw 'no-password nil))
coll-search (or (apply
other-search))) 'netrc-machine-user-or-password m search)
(auth-source-do-debug user)))
"auth-source-pick: got items %s in collection '%s' + %s" (if (consp mode) mode (list mode)))))
results coll-search other-search) (if (consp mode) result (car result)))))))
;; put the results in the choices variable
(dolist (result results)
(setq choices (cons (list score
`(:source secrets
:item ,result
:collection ,coll
:search ,coll-search
,@other-search))
choices)))))
;; this is any non-secrets spec (currently means a string filename)
(t
(let ((match t))
(dolist (k keys)
(let* ((v (plist-get spec k))
(choicev (plist-get choice k)))
(setq match
(and match
(or (eq t choicev) ; source always matches spec key
;; source key gives regex to match against spec
(and (stringp choicev) (string-match choicev v))
;; source key gives symbol to match against spec
(and (symbolp choicev) (eq choicev v))
;; in fallback mode, missing source key is OK
fallback)))
(when match (incf score)))) ; increment the score for each match
;; now if the whole iteration resulted in a match: (defun auth-source-create (mode entry &rest spec)
(when match "Create interactively credentials according to SPEC in ENTRY.
(setq choices (cons (list score choice) choices)))))))) Return structure as specified by MODE."
;; when there were matches, skip the second pass (let* ((host (plist-get spec :host))
(when choices (return choices)))) (user (plist-get spec :user))
(prot (plist-get spec :protocol))
(source (plist-get entry :source))
(name (concat (if user (format "%s@" user))
host
(if prot (format ":%s" prot))))
result)
(setq result
(mapcar
(lambda (m)
(if (equal "password" m)
(let ((passwd (read-passwd "Password: ")))
(cond
;; Secret Service API.
((consp source)
(apply
'secrets-create-item
(auth-get-source entry) name passwd spec))
(t)) ;; netrc not implemented yes.
passwd)
(or
;; the originally requested :user
user
"unknown-user")))
(if (consp mode) mode (list mode))))
(if (consp mode) result (car result))))
;; return the results sorted by score (defun auth-source-delete (entry &rest spec)
(mapcar 'cadr (sort choices (lambda (x y) (> (car x) (car y))))))) "Delete credentials according to SPEC in ENTRY."
(let ((host (plist-get spec :host))
(user (plist-get spec :user))
(prot (plist-get spec :protocol))
(source (plist-get entry :source)))
(cond
;; Secret Service API.
((consp source)
(let ((coll (auth-get-source entry)))
;; Loop over candidates with a matching host attribute.
(dolist (elt (secrets-search-items coll :host host))
(when (and (or (not user)
(string-equal
user (secrets-get-attribute coll elt :user)))
(or (not prot)
(string-equal
prot (secrets-get-attribute coll elt :protocol))))
(secrets-delete-item coll elt)))))
(t)))) ;; netrc not implemented yes.
(defun auth-source-forget-user-or-password (mode host protocol) (defun auth-source-forget-user-or-password
(mode host protocol &optional username)
"Remove cached authentication token."
(interactive "slogin/password: \nsHost: \nsProtocol: \n") ;for testing (interactive "slogin/password: \nsHost: \nsProtocol: \n") ;for testing
(remhash (format "%s %s:%s" mode host protocol) auth-source-cache)) (remhash
(if username
(format "%s %s:%s %s" mode host protocol username)
(format "%s %s:%s" mode host protocol))
auth-source-cache))
(defun auth-source-forget-all-cached () (defun auth-source-forget-all-cached ()
"Forget all cached auth-source authentication tokens." "Forget all cached auth-source authentication tokens."
@ -308,7 +370,8 @@ checked for fallback choices."
;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" "other" "tzz") ;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" "other" "tzz")
;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" "other" "joe"))) ;; (auth-source-user-or-password '("login" "password") "imap.myhost.com" "other" "joe")))
(defun auth-source-user-or-password (mode host protocol &optional username) (defun auth-source-user-or-password
(mode host protocol &optional username create-missing delete-existing)
"Find MODE (string or list of strings) matching HOST and PROTOCOL. "Find MODE (string or list of strings) matching HOST and PROTOCOL.
USERNAME is optional and will be used as \"login\" in a search USERNAME is optional and will be used as \"login\" in a search
@ -317,17 +380,31 @@ items don't have a username. This means that if you search for
username \"joe\" and it matches an item but the item doesn't have username \"joe\" and it matches an item but the item doesn't have
a :user attribute, the username \"joe\" will be returned. a :user attribute, the username \"joe\" will be returned.
MODE can be \"login\" or \"password\" for example." A non nil DELETE-EXISTING means deleting any matching password
entry in the respective sources. This is useful only when
CREATE-MISSING is non nil as well; the intended use case is to
remove wrong password entries.
If no matching entry is found, and CREATE-MISSING is non nil,
the password will be retrieved interactively, and it will be
stored in the password database which matches best (see
`auth-sources').
MODE can be \"login\" or \"password\"."
(auth-source-do-debug (auth-source-do-debug
"auth-source-user-or-password: get %s for %s (%s) + user=%s" "auth-source-user-or-password: get %s for %s (%s) + user=%s"
mode host protocol username) mode host protocol username)
(let* ((listy (listp mode)) (let* ((listy (listp mode))
(mode (if listy mode (list mode))) (mode (if listy mode (list mode)))
(extras (when username `(:user ,username))) (cname (if username
(cname (format "%s %s:%s %s" mode host protocol extras)) (format "%s %s:%s %s" mode host protocol username)
(format "%s %s:%s" mode host protocol)))
(search (list :host host :protocol protocol)) (search (list :host host :protocol protocol))
(search (if username (append search (list :user username)) search)) (search (if username (append search (list :user username)) search))
(found (gethash cname auth-source-cache))) (found (if (not delete-existing)
(gethash cname auth-source-cache)
(remhash cname auth-source-cache)
nil)))
(if found (if found
(progn (progn
(auth-source-do-debug (auth-source-do-debug
@ -337,45 +414,35 @@ MODE can be \"login\" or \"password\" for example."
(if (and (member "password" mode) auth-source-hide-passwords) (if (and (member "password" mode) auth-source-hide-passwords)
"SECRET" "SECRET"
found) found)
host protocol extras) host protocol username)
found) ; return the found data found) ; return the found data
;; else, if not found ;; else, if not found
(dolist (choice (apply 'auth-source-pick search)) (let ((choices (apply 'auth-source-pick search)))
(setq found (cond (dolist (choice choices)
;; the secrets.el spec (if delete-existing
((eq (plist-get choice :source) 'secrets) (apply 'auth-source-delete choice search)
(let ((coll (plist-get choice :search)) (setq found (apply 'auth-source-retrieve mode choice search)))
(item (plist-get choice :item))) (and found (return found)))
(mapcar (lambda (m)
(if (equal "password" m) ;; We haven't found something, so we will create it interactively.
(secrets-get-secret coll item) (when (and (not found) choices create-missing)
;; the user name is either (setq found (apply 'auth-source-create mode (car choices) search)))
(or
;; the secret's attribute :user, or ;; Cache the result.
(secrets-get-attribute coll item :user)
;; the originally requested :user
username
"unknown-user")))
mode)))
(t ; anything else is netrc
(netrc-machine-user-or-password
mode
(plist-get choice :source)
(list host)
(list (format "%s" protocol))
(auth-source-protocol-defaults protocol)))))
(when found (when found
(auth-source-do-debug (auth-source-do-debug
"auth-source-user-or-password: found %s=%s for %s (%s) + %s" "auth-source-user-or-password: found %s=%s for %s (%s) + %s"
mode mode
;; don't show the password ;; don't show the password
(if (and (member "password" mode) auth-source-hide-passwords) "SECRET" found) (if (and (member "password" mode) auth-source-hide-passwords)
host protocol extras) "SECRET" found)
host protocol username)
(setq found (if listy found (car-safe found))) (setq found (if listy found (car-safe found)))
(when auth-source-do-cache (when auth-source-do-cache
(puthash cname found auth-source-cache))) (puthash cname found auth-source-cache)))
(return found)))))
found))))
(defun auth-source-protocol-defaults (protocol) (defun auth-source-protocol-defaults (protocol)
"Return a list of default ports and names for PROTOCOL." "Return a list of default ports and names for PROTOCOL."
(cdr-safe (assoc protocol auth-source-protocols))) (cdr-safe (assoc protocol auth-source-protocols)))