diff options
Diffstat (limited to 'lisp')
| -rw-r--r-- | lisp/auth-source-pass.el | 209 |
1 files changed, 121 insertions, 88 deletions
diff --git a/lisp/auth-source-pass.el b/lisp/auth-source-pass.el index a0b0841e1f6..bcb215a6ace 100644 --- a/lisp/auth-source-pass.el +++ b/lisp/auth-source-pass.el | |||
| @@ -77,14 +77,13 @@ See `auth-source-search' for details on SPEC." | |||
| 77 | 77 | ||
| 78 | (defun auth-source-pass--build-result (host port user) | 78 | (defun auth-source-pass--build-result (host port user) |
| 79 | "Build auth-source-pass entry matching HOST, PORT and USER." | 79 | "Build auth-source-pass entry matching HOST, PORT and USER." |
| 80 | (let ((entry (auth-source-pass--find-match host user port))) | 80 | (let ((entry-data (auth-source-pass--find-match host user port))) |
| 81 | (when entry | 81 | (when entry-data |
| 82 | (let* ((entry-data (auth-source-pass-parse-entry entry)) | 82 | (let ((retval (list |
| 83 | (retval (list | 83 | :host host |
| 84 | :host host | 84 | :port (or (auth-source-pass--get-attr "port" entry-data) port) |
| 85 | :port (or (auth-source-pass--get-attr "port" entry-data) port) | 85 | :user (or (auth-source-pass--get-attr "user" entry-data) user) |
| 86 | :user (or (auth-source-pass--get-attr "user" entry-data) user) | 86 | :secret (lambda () (auth-source-pass--get-attr 'secret entry-data))))) |
| 87 | :secret (lambda () (auth-source-pass--get-attr 'secret entry-data))))) | ||
| 88 | (auth-source-pass--do-debug "return %s as final result (plus hidden password)" | 87 | (auth-source-pass--do-debug "return %s as final result (plus hidden password)" |
| 89 | (seq-subseq retval 0 -2)) ;; remove password | 88 | (seq-subseq retval 0 -2)) ;; remove password |
| 90 | retval)))) | 89 | retval)))) |
| @@ -183,33 +182,6 @@ CONTENTS is the contents of a password-store formatted file." | |||
| 183 | (cons (concat "auth-source-pass: " (car msg)) | 182 | (cons (concat "auth-source-pass: " (car msg)) |
| 184 | (cdr msg)))) | 183 | (cdr msg)))) |
| 185 | 184 | ||
| 186 | (defun auth-source-pass--select-one-entry (entries user) | ||
| 187 | "Select one entry from ENTRIES by searching for a field matching USER." | ||
| 188 | (let ((number (length entries)) | ||
| 189 | (entry-with-user | ||
| 190 | (and user | ||
| 191 | (seq-find (lambda (entry) | ||
| 192 | (string-equal (auth-source-pass-get "user" entry) user)) | ||
| 193 | entries)))) | ||
| 194 | (auth-source-pass--do-debug "found %s matches: %s" number | ||
| 195 | (mapconcat #'identity entries ", ")) | ||
| 196 | (if entry-with-user | ||
| 197 | (progn | ||
| 198 | (auth-source-pass--do-debug "return %s as it contains matching user field" | ||
| 199 | entry-with-user) | ||
| 200 | entry-with-user) | ||
| 201 | (auth-source-pass--do-debug "return %s as it is the first one" (car entries)) | ||
| 202 | (car entries)))) | ||
| 203 | |||
| 204 | (defun auth-source-pass--entry-valid-p (entry) | ||
| 205 | "Return t iff ENTRY can be opened. | ||
| 206 | Also displays a warning if not. This function is slow, don't call it too | ||
| 207 | often." | ||
| 208 | (if (auth-source-pass-parse-entry entry) | ||
| 209 | t | ||
| 210 | (auth-source-pass--do-debug "entry '%s' is not valid" entry) | ||
| 211 | nil)) | ||
| 212 | |||
| 213 | ;; TODO: add tests for that when `assess-with-filesystem' is included | 185 | ;; TODO: add tests for that when `assess-with-filesystem' is included |
| 214 | ;; in Emacs | 186 | ;; in Emacs |
| 215 | (defun auth-source-pass-entries () | 187 | (defun auth-source-pass-entries () |
| @@ -219,37 +191,8 @@ often." | |||
| 219 | (lambda (file) (file-name-sans-extension (file-relative-name file store-dir))) | 191 | (lambda (file) (file-name-sans-extension (file-relative-name file store-dir))) |
| 220 | (directory-files-recursively store-dir "\\.gpg$")))) | 192 | (directory-files-recursively store-dir "\\.gpg$")))) |
| 221 | 193 | ||
| 222 | (defun auth-source-pass--find-all-by-entry-name (entryname user) | ||
| 223 | "Search the store for all entries either matching ENTRYNAME/USER or ENTRYNAME. | ||
| 224 | Only return valid entries as of `auth-source-pass--entry-valid-p'." | ||
| 225 | (seq-filter (lambda (entry) | ||
| 226 | (and | ||
| 227 | (or | ||
| 228 | (let ((components-host-user | ||
| 229 | (member entryname (split-string entry "/")))) | ||
| 230 | (and (= (length components-host-user) 2) | ||
| 231 | (string-equal user (cadr components-host-user)))) | ||
| 232 | (string-equal entryname (file-name-nondirectory entry))) | ||
| 233 | (auth-source-pass--entry-valid-p entry))) | ||
| 234 | (auth-source-pass-entries))) | ||
| 235 | |||
| 236 | (defun auth-source-pass--find-one-by-entry-name (entryname user) | ||
| 237 | "Search the store for an entry matching ENTRYNAME. | ||
| 238 | If USER is non nil, give precedence to entries containing a user field | ||
| 239 | matching USER." | ||
| 240 | (auth-source-pass--do-debug "searching for '%s' in entry names (user: %s)" | ||
| 241 | entryname | ||
| 242 | user) | ||
| 243 | (let ((matching-entries (auth-source-pass--find-all-by-entry-name entryname user))) | ||
| 244 | (pcase (length matching-entries) | ||
| 245 | (0 (auth-source-pass--do-debug "no match found") | ||
| 246 | nil) | ||
| 247 | (1 (auth-source-pass--do-debug "found 1 match: %s" (car matching-entries)) | ||
| 248 | (car matching-entries)) | ||
| 249 | (_ (auth-source-pass--select-one-entry matching-entries user))))) | ||
| 250 | |||
| 251 | (defun auth-source-pass--find-match (host user port) | 194 | (defun auth-source-pass--find-match (host user port) |
| 252 | "Return a password-store entry name matching HOST, USER and PORT. | 195 | "Return password-store entry data matching HOST, USER and PORT. |
| 253 | 196 | ||
| 254 | Disambiguate between user provided inside HOST (e.g., user@server.com) and | 197 | Disambiguate between user provided inside HOST (e.g., user@server.com) and |
| 255 | inside USER by giving priority to USER. Same for PORT." | 198 | inside USER by giving priority to USER. Same for PORT." |
| @@ -263,33 +206,123 @@ inside USER by giving priority to USER. Same for PORT." | |||
| 263 | (or port (number-to-string (url-port url)))))) | 206 | (or port (number-to-string (url-port url)))))) |
| 264 | 207 | ||
| 265 | (defun auth-source-pass--find-match-unambiguous (hostname user port) | 208 | (defun auth-source-pass--find-match-unambiguous (hostname user port) |
| 266 | "Return a password-store entry name matching HOSTNAME, USER and PORT. | 209 | "Return password-store entry data matching HOSTNAME, USER and PORT. |
| 267 | If many matches are found, return the first one. If no match is found, | 210 | If many matches are found, return the first one. If no match is found, |
| 268 | return nil. | 211 | return nil. |
| 269 | 212 | ||
| 270 | HOSTNAME should not contain any username or port number." | 213 | HOSTNAME should not contain any username or port number." |
| 271 | (or | 214 | (cl-reduce |
| 272 | (and user port (auth-source-pass--find-one-by-entry-name | 215 | (lambda (result entries) |
| 273 | (format "%s@%s%s%s" user hostname auth-source-pass-port-separator port) | 216 | (or result |
| 274 | user)) | 217 | (pcase (length entries) |
| 275 | (and user port (auth-source-pass--find-one-by-entry-name | 218 | (0 nil) |
| 276 | (format "%s%s%s" hostname auth-source-pass-port-separator port) | 219 | (1 (auth-source-pass-parse-entry (car entries))) |
| 277 | user)) | 220 | (_ (auth-source-pass--select-from-entries entries user))))) |
| 278 | (and user (auth-source-pass--find-one-by-entry-name | 221 | (auth-source-pass--matching-entries hostname user port) |
| 279 | (format "%s@%s" user hostname) | 222 | :initial-value nil)) |
| 280 | user)) | 223 | |
| 281 | (and port (auth-source-pass--find-one-by-entry-name | 224 | (defun auth-source-pass--select-from-entries (entries user) |
| 282 | (format "%s%s%s" hostname auth-source-pass-port-separator port) | 225 | "Return best matching password-store entry data from ENTRIES. |
| 283 | nil)) | 226 | |
| 284 | (auth-source-pass--find-one-by-entry-name hostname user) | 227 | If USER is non nil, give precedence to entries containing a user field |
| 285 | ;; if that didn't work, remove subdomain: foo.bar.com -> bar.com | 228 | matching USER." |
| 286 | (let ((components (split-string hostname "\\."))) | 229 | (cl-reduce |
| 287 | (when (= (length components) 3) | 230 | (lambda (result entry) |
| 288 | ;; start from scratch | 231 | (let ((entry-data (auth-source-pass-parse-entry entry))) |
| 289 | (auth-source-pass--find-match-unambiguous | 232 | (cond ((equal (auth-source-pass--get-attr "user" result) user) |
| 290 | (mapconcat 'identity (cdr components) ".") | 233 | result) |
| 291 | user | 234 | ((equal (auth-source-pass--get-attr "user" entry-data) user) |
| 292 | port))))) | 235 | entry-data) |
| 236 | (t | ||
| 237 | result)))) | ||
| 238 | entries | ||
| 239 | :initial-value (auth-source-pass-parse-entry (car entries)))) | ||
| 240 | |||
| 241 | (defun auth-source-pass--matching-entries (hostname user port) | ||
| 242 | "Return all matching password-store entries for HOSTNAME, USER, & PORT. | ||
| 243 | |||
| 244 | The result is a list of lists of password-store entries, where | ||
| 245 | each sublist contains entries that actually exist in the | ||
| 246 | password-store matching one of the entry name formats that | ||
| 247 | auth-source-pass expects, most specific to least specific." | ||
| 248 | (let* ((entries-lists (mapcar | ||
| 249 | #'cdr | ||
| 250 | (auth-source-pass--accumulate-matches hostname user port))) | ||
| 251 | (entries (apply #'cl-concatenate (cons 'list entries-lists)))) | ||
| 252 | (if entries | ||
| 253 | (auth-source-pass--do-debug (format "found: %S" entries)) | ||
| 254 | (auth-source-pass--do-debug "no matches found")) | ||
| 255 | entries-lists)) | ||
| 256 | |||
| 257 | (defun auth-source-pass--accumulate-matches (hostname user port) | ||
| 258 | "Accumulate matching password-store entries into sublists. | ||
| 259 | |||
| 260 | Entries matching supported formats that combine HOSTNAME, USER, & | ||
| 261 | PORT are accumulated into sublists where the car of each sublist | ||
| 262 | is a regular expression for matching paths in the password-store | ||
| 263 | and the remainder is the list of matching entries." | ||
| 264 | (let ((suffix-match-lists | ||
| 265 | (mapcar (lambda (suffix) (list (format "\\(^\\|/\\)%s$" suffix))) | ||
| 266 | (auth-source-pass--generate-entry-suffixes hostname user port)))) | ||
| 267 | (cl-reduce #'auth-source-pass--entry-reducer | ||
| 268 | (auth-source-pass-entries) | ||
| 269 | :initial-value suffix-match-lists))) | ||
| 270 | |||
| 271 | (defun auth-source-pass--entry-reducer (match-lists entry) | ||
| 272 | "Match MATCH-LISTS sublists against ENTRY. | ||
| 273 | |||
| 274 | The result is a copy of match-lists with the entry added to the | ||
| 275 | end of any sublists for which the regular expression at the head | ||
| 276 | of the list matches the entry name." | ||
| 277 | (mapcar (lambda (match-list) | ||
| 278 | (if (string-match (car match-list) entry) | ||
| 279 | (append match-list (list entry)) | ||
| 280 | match-list)) | ||
| 281 | match-lists)) | ||
| 282 | |||
| 283 | (defun auth-source-pass--generate-entry-suffixes (hostname user port) | ||
| 284 | "Return a list of possible entry path suffixes in the password-store. | ||
| 285 | |||
| 286 | Based on the supported pathname patterns for HOSTNAME, USER, & | ||
| 287 | PORT, return a list of possible suffixes for matching entries in | ||
| 288 | the password-store." | ||
| 289 | (let ((domains (auth-source-pass--domains (split-string hostname "\\.")))) | ||
| 290 | (seq-mapcat (lambda (n) | ||
| 291 | (auth-source-pass--name-port-user-suffixes n user port)) | ||
| 292 | domains))) | ||
| 293 | |||
| 294 | (defun auth-source-pass--domains (name-components) | ||
| 295 | "Return a list of possible domain names matching the hostname. | ||
| 296 | |||
| 297 | This function takes a list of NAME-COMPONENTS, the strings | ||
| 298 | separated by periods in the hostname, and returns a list of full | ||
| 299 | domain names containing the trailing sequences of those | ||
| 300 | components, from longest to shortest." | ||
| 301 | (cl-maplist (lambda (components) (mapconcat #'identity components ".")) | ||
| 302 | name-components)) | ||
| 303 | |||
| 304 | (defun auth-source-pass--name-port-user-suffixes (name user port) | ||
| 305 | "Return a list of possible path suffixes for NAME, USER, & PORT. | ||
| 306 | |||
| 307 | The resulting list is ordered from most specifc to least | ||
| 308 | specific, with paths matching all of NAME, USER, & PORT first, | ||
| 309 | then NAME & USER, then NAME & PORT, then just NAME." | ||
| 310 | (seq-mapcat | ||
| 311 | #'identity | ||
| 312 | (list | ||
| 313 | (when (and user port) | ||
| 314 | (list | ||
| 315 | (format "%s@%s%s%s" user name auth-source-pass-port-separator port) | ||
| 316 | (format "%s%s%s/%s" name auth-source-pass-port-separator port user))) | ||
| 317 | (when user | ||
| 318 | (list | ||
| 319 | (format "%s@%s" user name) | ||
| 320 | (format "%s/%s" name user))) | ||
| 321 | (when port | ||
| 322 | (list | ||
| 323 | (format "%s%s%s" name auth-source-pass-port-separator port))) | ||
| 324 | (list | ||
| 325 | (format "%s" name))))) | ||
| 293 | 326 | ||
| 294 | (provide 'auth-source-pass) | 327 | (provide 'auth-source-pass) |
| 295 | ;;; auth-source-pass.el ends here | 328 | ;;; auth-source-pass.el ends here |