aboutsummaryrefslogtreecommitdiffstats
path: root/lisp/gnus/auth-source.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/gnus/auth-source.el')
-rw-r--r--lisp/gnus/auth-source.el314
1 files changed, 201 insertions, 113 deletions
diff --git a/lisp/gnus/auth-source.el b/lisp/gnus/auth-source.el
index 500de10b71c..e0bea324a25 100644
--- a/lisp/gnus/auth-source.el
+++ b/lisp/gnus/auth-source.el
@@ -54,6 +54,8 @@
54(autoload 'secrets-list-collections "secrets") 54(autoload 'secrets-list-collections "secrets")
55(autoload 'secrets-search-items "secrets") 55(autoload 'secrets-search-items "secrets")
56 56
57(autoload 'rfc2104-hash "rfc2104")
58
57(defvar secrets-enabled) 59(defvar secrets-enabled)
58 60
59(defgroup auth-source nil 61(defgroup auth-source nil
@@ -286,6 +288,28 @@ If the value is not a list, symmetric encryption will be used."
286 msg)) 288 msg))
287 289
288 290
291;;; (auth-source-read-char-choice "enter choice? " '(?a ?b ?q))
292(defun auth-source-read-char-choice (prompt choices)
293 "Read one of CHOICES by `read-char-choice', or `read-char'.
294`dropdown-list' support is disabled because it doesn't work reliably.
295Only one of CHOICES will be returned. The PROMPT is augmented
296with \"[a/b/c] \" if CHOICES is '\(?a ?b ?c\)."
297 (when choices
298 (let* ((prompt-choices
299 (apply 'concat (loop for c in choices
300 collect (format "%c/" c))))
301 (prompt-choices (concat "[" (substring prompt-choices 0 -1) "] "))
302 (full-prompt (concat prompt prompt-choices))
303 k)
304
305 (while (not (memq k choices))
306 (setq k (cond
307 ((fboundp 'read-char-choice)
308 (read-char-choice full-prompt choices))
309 (t (message "%s" full-prompt)
310 (setq k (read-char))))))
311 k)))
312
289;; (auth-source-pick nil :host "any" :port 'imap :user "joe") 313;; (auth-source-pick nil :host "any" :port 'imap :user "joe")
290;; (auth-source-pick t :host "any" :port 'imap :user "joe") 314;; (auth-source-pick t :host "any" :port 'imap :user "joe")
291;; (setq auth-sources '((:source (:secrets default) :host t :port t :user "joe") 315;; (setq auth-sources '((:source (:secrets default) :host t :port t :user "joe")
@@ -393,7 +417,7 @@ parameters."
393 417
394(defun* auth-source-search (&rest spec 418(defun* auth-source-search (&rest spec
395 &key type max host user port secret 419 &key type max host user port secret
396 create delete 420 require create delete
397 &allow-other-keys) 421 &allow-other-keys)
398 "Search or modify authentication backends according to SPEC. 422 "Search or modify authentication backends according to SPEC.
399 423
@@ -487,6 +511,11 @@ should `catch' the backend-specific error as usual. Some
487backends (netrc, at least) will prompt the user rather than throw 511backends (netrc, at least) will prompt the user rather than throw
488an error. 512an error.
489 513
514:require (A B C) means that only results that contain those
515tokens will be returned. Thus for instance requiring :secret
516will ensure that any results will actually have a :secret
517property.
518
490:delete t means to delete any found entries. nil by default. 519:delete t means to delete any found entries. nil by default.
491Use `auth-source-delete' in ELisp code instead of calling 520Use `auth-source-delete' in ELisp code instead of calling
492`auth-source-search' directly with this parameter. 521`auth-source-search' directly with this parameter.
@@ -516,11 +545,17 @@ is a plist with keys :backend :host :port :user, plus any other
516keys provided by the backend (notably :secret). But note the 545keys provided by the backend (notably :secret). But note the
517exception for :max 0, which see above. 546exception for :max 0, which see above.
518 547
548The token can hold a :save-function key. If you call that, the
549user will be prompted to save the data to the backend. You can't
550request that this should happen right after creation, because
551`auth-source-search' has no way of knowing if the token is
552actually useful. So the caller must arrange to call this function.
553
519The token's :secret key can hold a function. In that case you 554The token's :secret key can hold a function. In that case you
520must call it to obtain the actual value." 555must call it to obtain the actual value."
521 (let* ((backends (mapcar 'auth-source-backend-parse auth-sources)) 556 (let* ((backends (mapcar 'auth-source-backend-parse auth-sources))
522 (max (or max 1)) 557 (max (or max 1))
523 (ignored-keys '(:create :delete :max)) 558 (ignored-keys '(:require :create :delete :max))
524 (keys (loop for i below (length spec) by 2 559 (keys (loop for i below (length spec) by 2
525 unless (memq (nth i spec) ignored-keys) 560 unless (memq (nth i spec) ignored-keys)
526 collect (nth i spec))) 561 collect (nth i spec)))
@@ -539,6 +574,10 @@ must call it to obtain the actual value."
539 (or (eq t create) (listp create)) t 574 (or (eq t create) (listp create)) t
540 "Invalid auth-source :create parameter (must be t or a list): %s %s") 575 "Invalid auth-source :create parameter (must be t or a list): %s %s")
541 576
577 (assert
578 (listp require) t
579 "Invalid auth-source :require parameter (must be a list): %s")
580
542 (setq filtered-backends (copy-sequence backends)) 581 (setq filtered-backends (copy-sequence backends))
543 (dolist (backend backends) 582 (dolist (backend backends)
544 (dolist (key keys) 583 (dolist (key keys)
@@ -562,8 +601,9 @@ must call it to obtain the actual value."
562 spec 601 spec
563 ;; to exit early 602 ;; to exit early
564 max 603 max
565 ;; create and delete 604 ;; create is always nil here
566 nil delete)) 605 nil delete
606 require))
567 607
568 (auth-source-do-debug 608 (auth-source-do-debug
569 "auth-source-search: found %d results (max %d) matching %S" 609 "auth-source-search: found %d results (max %d) matching %S"
@@ -577,9 +617,9 @@ must call it to obtain the actual value."
577 spec 617 spec
578 ;; to exit early 618 ;; to exit early
579 max 619 max
580 ;; create and delete 620 create delete
581 create delete)) 621 require))
582 (auth-source-do-warn 622 (auth-source-do-debug
583 "auth-source-search: CREATED %d results (max %d) matching %S" 623 "auth-source-search: CREATED %d results (max %d) matching %S"
584 (length found) max spec)) 624 (length found) max spec))
585 625
@@ -589,18 +629,19 @@ must call it to obtain the actual value."
589 629
590 found)) 630 found))
591 631
592(defun auth-source-search-backends (backends spec max create delete) 632(defun auth-source-search-backends (backends spec max create delete require)
593 (let (matches) 633 (let (matches)
594 (dolist (backend backends) 634 (dolist (backend backends)
595 (when (> max (length matches)) ; when we need more matches... 635 (when (> max (length matches)) ; when we need more matches...
596 (let ((bmatches (apply 636 (let* ((bmatches (apply
597 (slot-value backend 'search-function) 637 (slot-value backend 'search-function)
598 :backend backend 638 :backend backend
599 ;; note we're overriding whatever the spec 639 ;; note we're overriding whatever the spec
600 ;; has for :create and :delete 640 ;; has for :require, :create, and :delete
601 :create create 641 :require require
602 :delete delete 642 :create create
603 spec))) 643 :delete delete
644 spec)))
604 (when bmatches 645 (when bmatches
605 (auth-source-do-trivia 646 (auth-source-do-trivia
606 "auth-source-search-backend: got %d (max %d) in %s:%s matching %S" 647 "auth-source-search-backend: got %d (max %d) in %s:%s matching %S"
@@ -713,7 +754,28 @@ while \(:host t) would find all host entries."
713 (return 'no))) 754 (return 'no)))
714 'no)))) 755 'no))))
715 756
716;;; Backend specific parsing: netrc/authinfo backend 757;;; (auth-source-pick-first-password :host "z.lifelogs.com")
758;;; (auth-source-pick-first-password :port "imap")
759(defun auth-source-pick-first-password (&rest spec)
760 "Pick the first secret found from applying SPEC to `auth-source-search'."
761 (let* ((result (nth 0 (apply 'auth-source-search (plist-put spec :max 1))))
762 (secret (plist-get result :secret)))
763
764 (if (functionp secret)
765 (funcall secret)
766 secret)))
767
768;; (auth-source-format-prompt "test %u %h %p" '((?u "user") (?h "host")))
769(defun auth-source-format-prompt (prompt alist)
770 "Format PROMPT using %x (for any character x) specifiers in ALIST."
771 (dolist (cell alist)
772 (let ((c (nth 0 cell))
773 (v (nth 1 cell)))
774 (when (and c v)
775 (setq prompt (replace-regexp-in-string (format "%%%c" c)
776 (format "%s" v)
777 prompt)))))
778 prompt)
717 779
718(defun auth-source-ensure-strings (values) 780(defun auth-source-ensure-strings (values)
719 (unless (listp values) 781 (unless (listp values)
@@ -724,12 +786,14 @@ while \(:host t) would find all host entries."
724 value)) 786 value))
725 values)) 787 values))
726 788
789;;; Backend specific parsing: netrc/authinfo backend
790
727(defvar auth-source-netrc-cache nil) 791(defvar auth-source-netrc-cache nil)
728 792
729;;; (auth-source-netrc-parse "~/.authinfo.gpg") 793;;; (auth-source-netrc-parse "~/.authinfo.gpg")
730(defun* auth-source-netrc-parse (&rest 794(defun* auth-source-netrc-parse (&rest
731 spec 795 spec
732 &key file max host user port delete 796 &key file max host user port delete require
733 &allow-other-keys) 797 &allow-other-keys)
734 "Parse FILE and return a list of all entries in the file. 798 "Parse FILE and return a list of all entries in the file.
735Note that the MAX parameter is used so we can exit the parse early." 799Note that the MAX parameter is used so we can exit the parse early."
@@ -828,7 +892,15 @@ Note that the MAX parameter is used so we can exit the parse early."
828 (or 892 (or
829 (aget alist "port") 893 (aget alist "port")
830 (aget alist "protocol") 894 (aget alist "protocol")
831 t))) 895 t))
896 (or
897 ;; the required list of keys is nil, or
898 (null require)
899 ;; every element of require is in the normalized list
900 (let ((normalized (nth 0 (auth-source-netrc-normalize
901 (list alist)))))
902 (loop for req in require
903 always (plist-get normalized req)))))
832 (decf max) 904 (decf max)
833 (push (nreverse alist) result) 905 (push (nreverse alist) result)
834 ;; to delete a line, we just comment it out 906 ;; to delete a line, we just comment it out
@@ -853,7 +925,7 @@ Note that the MAX parameter is used so we can exit the parse early."
853 (setq epa-file-encrypt-to auth-source-gpg-encrypt-to))) 925 (setq epa-file-encrypt-to auth-source-gpg-encrypt-to)))
854 926
855 ;; ask AFTER we've successfully opened the file 927 ;; ask AFTER we've successfully opened the file
856 (when (y-or-n-p (format "Save file %s? (%d modifications)" 928 (when (y-or-n-p (format "Save file %s? (%d deletions)"
857 file modified)) 929 file modified))
858 (write-region (point-min) (point-max) file nil 'silent) 930 (write-region (point-min) (point-max) file nil 'silent)
859 (auth-source-do-debug 931 (auth-source-do-debug
@@ -893,7 +965,7 @@ Note that the MAX parameter is used so we can exit the parse early."
893 965
894(defun* auth-source-netrc-search (&rest 966(defun* auth-source-netrc-search (&rest
895 spec 967 spec
896 &key backend create delete 968 &key backend require create delete
897 type max host user port 969 type max host user port
898 &allow-other-keys) 970 &allow-other-keys)
899"Given a property list SPEC, return search matches from the :backend. 971"Given a property list SPEC, return search matches from the :backend.
@@ -905,6 +977,7 @@ See `auth-source-search' for details on SPEC."
905 (let ((results (auth-source-netrc-normalize 977 (let ((results (auth-source-netrc-normalize
906 (auth-source-netrc-parse 978 (auth-source-netrc-parse
907 :max max 979 :max max
980 :require require
908 :delete delete 981 :delete delete
909 :file (oref backend source) 982 :file (oref backend source)
910 :host (or host t) 983 :host (or host t)
@@ -933,17 +1006,6 @@ See `auth-source-search' for details on SPEC."
933 (nth 0 v) 1006 (nth 0 v)
934 v)) 1007 v))
935 1008
936;; (auth-source-format-prompt "test %u %h %p" '((?u "user") (?h "host")))
937
938(defun auth-source-format-prompt (prompt alist)
939 "Format PROMPT using %x (for any character x) specifiers in ALIST."
940 (dolist (cell alist)
941 (let ((c (nth 0 cell))
942 (v (nth 1 cell)))
943 (when (and c v)
944 (setq prompt (replace-regexp-in-string (format "%%%c" c) v prompt)))))
945 prompt)
946
947;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t) 1009;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t)
948;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t :create-extra-keys '((A "default A") (B))) 1010;;; (auth-source-search :host "nonesuch" :type 'netrc :max 1 :create t :create-extra-keys '((A "default A") (B)))
949 1011
@@ -992,12 +1054,12 @@ See `auth-source-search' for details on SPEC."
992 (data (auth-source-netrc-element-or-first data)) 1054 (data (auth-source-netrc-element-or-first data))
993 ;; this is the default to be offered 1055 ;; this is the default to be offered
994 (given-default (aget auth-source-creation-defaults r)) 1056 (given-default (aget auth-source-creation-defaults r))
995 ;; the default supplementals are simple: for the user, 1057 ;; the default supplementals are simple:
996 ;; try (user-login-name), otherwise take given-default 1058 ;; for the user, try `given-default' and then (user-login-name);
1059 ;; otherwise take `given-default'
997 (default (cond 1060 (default (cond
998 ;; don't default the user name 1061 ((and (not given-default) (eq r 'user))
999 ;; ((and (not given-default) (eq r 'user)) 1062 (user-login-name))
1000 ;; (user-login-name))
1001 (t given-default))) 1063 (t given-default)))
1002 (printable-defaults (list 1064 (printable-defaults (list
1003 (cons 'user 1065 (cons 'user
@@ -1020,10 +1082,10 @@ See `auth-source-search' for details on SPEC."
1020 "[any port]")))) 1082 "[any port]"))))
1021 (prompt (or (aget auth-source-creation-prompts r) 1083 (prompt (or (aget auth-source-creation-prompts r)
1022 (case r 1084 (case r
1023 ('secret "%p password for user %u, host %h: ") 1085 (secret "%p password for %u@%h: ")
1024 ('user "%p user name: ") 1086 (user "%p user name for %h: ")
1025 ('host "%p host name for user %u: ") 1087 (host "%p host name for user %u: ")
1026 ('port "%p port for user %u and host %h: ")) 1088 (port "%p port for %u@%h: "))
1027 (format "Enter %s (%%u@%%h:%%p): " r))) 1089 (format "Enter %s (%%u@%%h:%%p): " r)))
1028 (prompt (auth-source-format-prompt 1090 (prompt (auth-source-format-prompt
1029 prompt 1091 prompt
@@ -1031,14 +1093,20 @@ See `auth-source-search' for details on SPEC."
1031 (?h ,(aget printable-defaults 'host)) 1093 (?h ,(aget printable-defaults 'host))
1032 (?p ,(aget printable-defaults 'port)))))) 1094 (?p ,(aget printable-defaults 'port))))))
1033 1095
1034 ;; store the data, prompting for the password if needed 1096 ;; Store the data, prompting for the password if needed.
1035 (setq data 1097 (setq data
1036 (cond 1098 (cond
1037 ((and (null data) (eq r 'secret)) 1099 ((and (null data) (eq r 'secret))
1038 ;; special case prompt for passwords 1100 ;; Special case prompt for passwords.
1039 (read-passwd prompt)) 1101 (read-passwd prompt))
1040 ((null data) 1102 ((null data)
1041 (read-string prompt default)) 1103 (when default
1104 (setq prompt
1105 (if (string-match ": *\\'" prompt)
1106 (concat (substring prompt 0 (match-beginning 0))
1107 " (default " default "): ")
1108 (concat prompt "(default " default ") "))))
1109 (read-string prompt nil nil default))
1042 (t (or data default)))) 1110 (t (or data default))))
1043 1111
1044 (when data 1112 (when data
@@ -1049,7 +1117,7 @@ See `auth-source-search' for details on SPEC."
1049 (lambda () data)) 1117 (lambda () data))
1050 data)))) 1118 data))))
1051 1119
1052 ;; when r is not an empty string... 1120 ;; When r is not an empty string...
1053 (when (and (stringp data) 1121 (when (and (stringp data)
1054 (< 0 (length data))) 1122 (< 0 (length data)))
1055 ;; this function is not strictly necessary but I think it 1123 ;; this function is not strictly necessary but I think it
@@ -1062,79 +1130,99 @@ See `auth-source-search' for details on SPEC."
1062 (if (zerop (length add)) "" " ") 1130 (if (zerop (length add)) "" " ")
1063 ;; remap auth-source tokens to netrc 1131 ;; remap auth-source tokens to netrc
1064 (case r 1132 (case r
1065 ('user "login") 1133 (user "login")
1066 ('host "machine") 1134 (host "machine")
1067 ('secret "password") 1135 (secret "password")
1068 ('port "port") ; redundant but clearer 1136 (port "port") ; redundant but clearer
1069 (t (symbol-name r))) 1137 (t (symbol-name r)))
1070 ;; the value will be printed in %S format 1138 ;; the value will be printed in %S format
1071 data)))) 1139 data))))
1072 (setq add (concat add (funcall printer))))))) 1140 (setq add (concat add (funcall printer)))))))
1073 1141
1074 (with-temp-buffer 1142 (plist-put
1075 (when (file-exists-p file) 1143 artificial
1076 (insert-file-contents file)) 1144 :save-function
1077 (when auth-source-gpg-encrypt-to 1145 (lexical-let ((file file)
1078 ;; (see bug#7487) making `epa-file-encrypt-to' local to 1146 (add add))
1079 ;; this buffer lets epa-file skip the key selection query 1147 (lambda () (auth-source-netrc-saver file add))))
1080 ;; (see the `local-variable-p' check in 1148
1081 ;; `epa-file-write-region'). 1149 (list artificial)))
1082 (unless (local-variable-p 'epa-file-encrypt-to (current-buffer)) 1150
1083 (make-local-variable 'epa-file-encrypt-to)) 1151;;(funcall (plist-get (nth 0 (auth-source-search :host '("nonesuch2") :user "tzz" :port "imap" :create t :max 1)) :save-function))
1084 (if (listp auth-source-gpg-encrypt-to) 1152(defun auth-source-netrc-saver (file add)
1085 (setq epa-file-encrypt-to auth-source-gpg-encrypt-to))) 1153 "Save a line ADD in FILE, prompting along the way.
1086 (goto-char (point-max)) 1154Respects `auth-source-save-behavior'. Uses
1087 1155`auth-source-netrc-cache' to avoid prompting more than once."
1088 ;; ask AFTER we've successfully opened the file 1156 (let* ((key (format "%s %s" file (rfc2104-hash 'md5 64 16 file add)))
1089 (let ((prompt (format "Save auth info to file %s? %s: " 1157 (cached (assoc key auth-source-netrc-cache)))
1090 file 1158
1091 "y/n/N/e/?")) 1159 (if cached
1092 (done (not (eq auth-source-save-behavior 'ask))) 1160 (auth-source-do-trivia
1093 (bufname "*auth-source Help*") 1161 "auth-source-netrc-saver: found previous run for key %s, returning"
1094 k) 1162 key)
1095 (while (not done) 1163 (with-temp-buffer
1096 (message "%s" prompt) 1164 (when (file-exists-p file)
1097 (setq k (read-char)) 1165 (insert-file-contents file))
1098 (case k 1166 (when auth-source-gpg-encrypt-to
1099 (?y (setq done t)) 1167 ;; (see bug#7487) making `epa-file-encrypt-to' local to
1100 (?? (save-excursion 1168 ;; this buffer lets epa-file skip the key selection query
1101 (with-output-to-temp-buffer bufname 1169 ;; (see the `local-variable-p' check in
1102 (princ 1170 ;; `epa-file-write-region').
1103 (concat "(y)es, save\n" 1171 (unless (local-variable-p 'epa-file-encrypt-to (current-buffer))
1104 "(n)o but use the info\n" 1172 (make-local-variable 'epa-file-encrypt-to))
1105 "(N)o and don't ask to save again\n" 1173 (if (listp auth-source-gpg-encrypt-to)
1106 "(e)dit the line\n" 1174 (setq epa-file-encrypt-to auth-source-gpg-encrypt-to)))
1107 "(?) for help as you can see.\n")) 1175 ;; we want the new data to be found first, so insert at beginning
1108 (set-buffer standard-output) 1176 (goto-char (point-min))
1109 (help-mode)))) 1177
1110 (?n (setq add "" 1178 ;; Ask AFTER we've successfully opened the file.
1111 done t)) 1179 (let ((prompt (format "Save auth info to file %s? " file))
1112 (?N (setq add "" 1180 (done (not (eq auth-source-save-behavior 'ask)))
1113 done t 1181 (bufname "*auth-source Help*")
1114 auth-source-save-behavior nil)) 1182 k)
1115 (?e (setq add (read-string "Line to add: " add))) 1183 (while (not done)
1116 (t nil))) 1184 (setq k (auth-source-read-char-choice prompt '(?y ?n ?N ?e ??)))
1117 1185 (case k
1118 (when (get-buffer-window bufname) 1186 (?y (setq done t))
1119 (delete-window (get-buffer-window bufname))) 1187 (?? (save-excursion
1120 1188 (with-output-to-temp-buffer bufname
1121 ;; make sure the info is not saved 1189 (princ
1122 (when (null auth-source-save-behavior) 1190 (concat "(y)es, save\n"
1123 (setq add "")) 1191 "(n)o but use the info\n"
1124 1192 "(N)o and don't ask to save again\n"
1125 (when (< 0 (length add)) 1193 "(e)dit the line\n"
1126 (progn 1194 "(?) for help as you can see.\n"))
1127 (unless (bolp) 1195 ;; Why? Doesn't with-output-to-temp-buffer already do
1128 (insert "\n")) 1196 ;; the exact same thing anyway? --Stef
1129 (insert add "\n") 1197 (set-buffer standard-output)
1130 (write-region (point-min) (point-max) file nil 'silent) 1198 (help-mode))))
1131 (auth-source-do-warn 1199 (?n (setq add ""
1132 "auth-source-netrc-create: wrote 1 new line to %s" 1200 done t))
1133 file) 1201 (?N (setq add ""
1134 nil)) 1202 done t
1135 1203 auth-source-save-behavior nil))
1136 (when (eq done t) 1204 (?e (setq add (read-string "Line to add: " add)))
1137 (list artificial)))))) 1205 (t nil)))
1206
1207 (when (get-buffer-window bufname)
1208 (delete-window (get-buffer-window bufname)))
1209
1210 ;; Make sure the info is not saved.
1211 (when (null auth-source-save-behavior)
1212 (setq add ""))
1213
1214 (when (< 0 (length add))
1215 (progn
1216 (unless (bolp)
1217 (insert "\n"))
1218 (insert add "\n")
1219 (write-region (point-min) (point-max) file nil 'silent)
1220 (auth-source-do-debug
1221 "auth-source-netrc-create: wrote 1 new line to %s"
1222 file)
1223 (message "Saved new authentication information to %s" file)
1224 nil))))
1225 (aput 'auth-source-netrc-cache key "ran"))))
1138 1226
1139;;; Backend specific parsing: Secrets API backend 1227;;; Backend specific parsing: Secrets API backend
1140 1228