* 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)
* gnus-agent.el (gnus-agent-expire-unagentized-dirs): Don't ask about

View file

@ -35,10 +35,13 @@
(eval-when-compile (require 'cl))
(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-attribute "secrets")
(autoload 'secrets-get-secret "secrets")
(autoload 'secrets-list-collections "secrets")
(autoload 'secrets-search-items "secrets")
(defgroup auth-source nil
"Authentication sources."
@ -122,7 +125,7 @@ can get pretty complex."
(const :format "" :value :source)
(choice :tag "Authentication backend choice"
(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)
(choice :tag "Collection to use"
(string :tag "Collection name")
@ -190,111 +193,170 @@ can get pretty complex."
;; (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)
"Parse `auth-sources' for matches of the SPEC plist.
Common keys are :host, :protocol, and :user. A value of t in
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
on the first pass, a second pass is made including the fallback
choices.
;; Check keywords.
(dolist (k keys match)
(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
PROTOCOL or HOST are nil.
(add-to-list 'choices choice 'append))))))
For secrets.el collections, the :host and :protocol keys are not
checked for fallback choices."
(let (choices)
(dolist (fallback '(nil t))
(let ((keys (loop for i below (length spec) by 2
collect (nth i spec)))
(default-session-fallback "login"))
(dolist (choice auth-sources)
(let* ((s (plist-get choice :source))
;; this is only set for Secret Service API specs (see secrets.el)
(coll (and (consp s) (plist-get s :secrets)))
(score 0))
(cond
(coll ; use secrets.el here
(when (eq coll 'default)
(setq coll (secrets-get-alias "default"))
(unless coll
(auth-source-do-debug
"No 'default' alias. Trying collection '%s'."
default-session-fallback)
(setq coll default-session-fallback)))
(let* ((coll-search (cond
((stringp coll) coll)
(defun auth-source-retrieve (mode entry &rest spec)
"Retrieve MODE credentials according to SPEC from ENTRY."
(catch 'no-password
(let ((host (plist-get spec :host))
(user (plist-get spec :user))
(prot (plist-get spec :protocol))
(source (plist-get entry :source))
result)
(cond
;; Secret Service API.
((consp source)
(let ((coll (auth-get-source entry))
item)
;; Loop over candidates with a matching host attribute.
(dolist (elt (secrets-search-items coll :host host) item)
(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))))
(setq item elt)
(return elt)))
;; Compose result.
(when item
(setq result
(mapcar (lambda (m)
(if (string-equal "password" m)
(or (secrets-get-secret coll item)
;; When we do not find a password,
;; we return nil anyway.
(throw 'no-password nil))
(or (secrets-get-attribute coll item :user)
user)))
(if (consp mode) mode (list mode)))))
(if (consp mode) result (car result))))
;; Anything else is netrc.
(t
(let ((search (list source (list host) (list (format "%s" prot))
(auth-source-protocol-defaults prot))))
(setq result
(mapcar (lambda (m)
(if (string-equal "password" m)
(or (apply
'netrc-machine-user-or-password m search)
;; When we do not find a password, we
;; return nil anyway.
(throw 'no-password nil))
(or (apply
'netrc-machine-user-or-password m search)
user)))
(if (consp mode) mode (list mode)))))
(if (consp mode) result (car result)))))))
;; when the collection is nil:
;; in fallback mode, accept it as any
;; otherwise, hope to fail
((null coll) (if fallback
nil
" *fallback-fail*"))))
;; assemble a search query for secrets-search-items
;; in fallback mode, host and protocol are not checked
(other-search (loop for k
in (if fallback
(remove :host
(remove :protocol keys))
keys)
append (list
k
;; convert symbols to a string
(let ((v (plist-get spec k)))
(if (stringp v)
v
(prin1-to-string v))))))
;; the score is based on how exact the search was,
;; plus base score = 1 for any match
(score (1+ (length other-search)))
(results (apply 'secrets-search-items
coll-search
other-search)))
(auth-source-do-debug
"auth-source-pick: got items %s in collection '%s' + %s"
results coll-search other-search)
;; 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
(defun auth-source-create (mode entry &rest spec)
"Create interactively credentials according to SPEC in ENTRY.
Return structure as specified by MODE."
(let* ((host (plist-get spec :host))
(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))))
;; now if the whole iteration resulted in a match:
(when match
(setq choices (cons (list score choice) choices))))))))
;; when there were matches, skip the second pass
(when choices (return choices))))
(defun auth-source-delete (entry &rest spec)
"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.
;; return the results sorted by score
(mapcar 'cadr (sort choices (lambda (x y) (> (car x) (car y)))))))
(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
(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 ()
"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" "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.
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
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-user-or-password: get %s for %s (%s) + user=%s"
mode host protocol username)
(let* ((listy (listp mode))
(mode (if listy mode (list mode)))
(extras (when username `(:user ,username)))
(cname (format "%s %s:%s %s" mode host protocol extras))
(cname (if username
(format "%s %s:%s %s" mode host protocol username)
(format "%s %s:%s" mode host protocol)))
(search (list :host host :protocol protocol))
(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
(progn
(auth-source-do-debug
@ -337,44 +414,34 @@ MODE can be \"login\" or \"password\" for example."
(if (and (member "password" mode) auth-source-hide-passwords)
"SECRET"
found)
host protocol extras)
host protocol username)
found) ; return the found data
;; else, if not found
(dolist (choice (apply 'auth-source-pick search))
(setq found (cond
;; the secrets.el spec
((eq (plist-get choice :source) 'secrets)
(let ((coll (plist-get choice :search))
(item (plist-get choice :item)))
(mapcar (lambda (m)
(if (equal "password" m)
(secrets-get-secret coll item)
;; the user name is either
(or
;; the secret's attribute :user, or
(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)))))
(let ((choices (apply 'auth-source-pick search)))
(dolist (choice choices)
(if delete-existing
(apply 'auth-source-delete choice search)
(setq found (apply 'auth-source-retrieve mode choice search)))
(and found (return found)))
;; We haven't found something, so we will create it interactively.
(when (and (not found) choices create-missing)
(setq found (apply 'auth-source-create mode (car choices) search)))
;; Cache the result.
(when found
(auth-source-do-debug
"auth-source-user-or-password: found %s=%s for %s (%s) + %s"
mode
;; don't show the password
(if (and (member "password" mode) auth-source-hide-passwords) "SECRET" found)
host protocol extras)
(if (and (member "password" mode) auth-source-hide-passwords)
"SECRET" found)
host protocol username)
(setq found (if listy found (car-safe found)))
(when auth-source-do-cache
(puthash cname found auth-source-cache)))
(return found)))))
found))))
(defun auth-source-protocol-defaults (protocol)
"Return a list of default ports and names for PROTOCOL."