diff options
| author | Michael R. Mauger | 2019-02-18 23:15:54 -0500 |
|---|---|---|
| committer | Michael R. Mauger | 2019-02-18 23:15:54 -0500 |
| commit | 1a6bcc91e3e468e5a6d3e0b121bb675b576d3362 (patch) | |
| tree | ca325907ed02edcd6a463b5f5e5482bbc6f81ab9 | |
| parent | ed1e805af7d4892e7354e8c9e2246d5017d4ff52 (diff) | |
| download | emacs-wallet.tar.gz emacs-wallet.zip | |
* lisp/progmodes/sql.el: Added password wallet usingwallet
`auth-source' package.
(sql-auth-source-search-wallet): New function.
(sql-password-wallet): New variable.
(sql-password-search-wallet-function): New variable.
(sql-get-login): Handle password wallet search.
(sql-product-interactive): Handle password function.
* test/lisp/progmodes/sql-test.el: Test wallet changes.
(sql-test-login-params): New test variable.
(with-sql-test-connect-harness): New macro to wrap test
configuration around calls to `sql-connect'.
(sql-test-connect, sql-test-connect-password-func)
(sql-test-connect-wallet-server-database)
(sql-test-connect-wallet-database)
(sql-test-connect-wallet-server): New ERT tests.
* etc/NEWS: Updated SQL Mode descriptions.
| -rw-r--r-- | etc/NEWS | 48 | ||||
| -rw-r--r-- | lisp/progmodes/sql.el | 151 | ||||
| -rw-r--r-- | test/lisp/progmodes/sql-tests.el | 101 |
3 files changed, 284 insertions, 16 deletions
| @@ -515,27 +515,45 @@ end. | |||
| 515 | 515 | ||
| 516 | ** SQL | 516 | ** SQL |
| 517 | 517 | ||
| 518 | *** Installation of 'sql-indent' from ELPA is strongly encouraged. | 518 | *** SQL Indent Minor Mode |
| 519 | This package support sophisticated rules for properly indenting SQL | 519 | |
| 520 | statements. SQL is not like other programming languages like C, Java, | 520 | SQL Mode now supports the ELPA 'sql-indent' package for assisting |
| 521 | or Python where code is sparse and rules for formatting are fairly | 521 | sophisticated SQL indenting rules. Note, however, that SQL is not |
| 522 | well established. Instead SQL is more like COBOL (from which it came) | 522 | like other programming languages like C, Java, or Python where code is |
| 523 | and code tends to be very dense and line ending decisions driven by | 523 | sparse and rules for formatting are fairly well established. Instead |
| 524 | syntax and line length considerations to make readable code. | 524 | SQL is more like COBOL (from which it came) and code tends to be very |
| 525 | Experienced SQL developers may prefer to rely upon existing Emacs | 525 | dense and line ending decisions driven by syntax and line length |
| 526 | facilities for formatting code but the 'sql-indent' package provides | 526 | considerations to make readable code. Experienced SQL developers may |
| 527 | facilities to aid more casual SQL developers layout queries and | 527 | prefer to rely upon existing Emacs facilities for formatting code but |
| 528 | complex expressions. | 528 | the 'sql-indent' package provides facilities to aid more casual SQL |
| 529 | 529 | developers layout queries and complex expressions. | |
| 530 | *** 'sql-use-indent-support' (default t) enables SQL indention support. | 530 | |
| 531 | **** 'sql-use-indent-support' (default t) enables SQL indention support. | ||
| 531 | The 'sql-indent' package from ELPA must be installed to get the | 532 | The 'sql-indent' package from ELPA must be installed to get the |
| 532 | indentation support in 'sql-mode' and 'sql-interactive-mode'. | 533 | indentation support in 'sql-mode' and 'sql-interactive-mode'. |
| 533 | 534 | ||
| 534 | *** 'sql-mode-hook' and 'sql-interactive-mode-hook' changed. | 535 | **** 'sql-mode-hook' and 'sql-interactive-mode-hook' changed. |
| 535 | Both hook variables have had 'sql-indent-enable' added to their | 536 | Both hook variables have had 'sql-indent-enable' added to their |
| 536 | default values. If youhave existing customizations to these variables, | 537 | default values. If you have existing customizations to these variables, |
| 537 | you should make sure that the new default entry is included. | 538 | you should make sure that the new default entry is included. |
| 538 | 539 | ||
| 540 | *** Connection Wallet | ||
| 541 | |||
| 542 | Database passwords can now by stored in NETRC or JSON data files that | ||
| 543 | may optionally be encrypted. When establishing an interactive session | ||
| 544 | with the database via 'sql-connect' or a product specific function, | ||
| 545 | like 'sql-mysql' or 'my-postgres', the password wallet will be | ||
| 546 | searched for the password. The 'sql-product', 'sql-server', | ||
| 547 | 'sql-database', and the 'sql-username' will be used to identify the | ||
| 548 | appropriate authorization. This eliminates the discouraged practice of | ||
| 549 | embedding database passwords in your Emacs initialization. | ||
| 550 | |||
| 551 | See the `auth-source' module for complete documentation on the file | ||
| 552 | formats. By default, the wallet file is expected to be in the | ||
| 553 | `user-emacs-directory', named 'sql-wallet' or '.sql-wallet', with | ||
| 554 | '.json' (JSON) or no (NETRC) suffix. Both file formats can optionally | ||
| 555 | be encrypted with GPG by adding an additional '.gpg' suffix. | ||
| 556 | |||
| 539 | ** Term | 557 | ** Term |
| 540 | 558 | ||
| 541 | --- | 559 | --- |
diff --git a/lisp/progmodes/sql.el b/lisp/progmodes/sql.el index 2df62585a0d..c72070b8923 100644 --- a/lisp/progmodes/sql.el +++ b/lisp/progmodes/sql.el | |||
| @@ -748,6 +748,126 @@ The package must be available to be loaded and activated." | |||
| 748 | (when (sql-is-indent-available) | 748 | (when (sql-is-indent-available) |
| 749 | (sqlind-minor-mode (if sql-use-indent-support +1 -1)))) | 749 | (sqlind-minor-mode (if sql-use-indent-support +1 -1)))) |
| 750 | 750 | ||
| 751 | ;; Secure Password wallet | ||
| 752 | |||
| 753 | (require 'auth-source) | ||
| 754 | |||
| 755 | (defun sql-auth-source-search-wallet (wallet product user server database port) | ||
| 756 | "Read auth source WALLET to locate the USER secret. | ||
| 757 | Sets `auth-sources' to WALLET and uses `auth-source-search' to locate the entry. | ||
| 758 | The DATABASE and SERVER are concatenated with a slash between them as the | ||
| 759 | host key." | ||
| 760 | (let* ((auth-sources wallet) | ||
| 761 | host | ||
| 762 | secret h-secret sd-secret) | ||
| 763 | |||
| 764 | ;; product | ||
| 765 | (setq product (symbol-name product)) | ||
| 766 | |||
| 767 | ;; user | ||
| 768 | (setq user (unless (string-empty-p user) user)) | ||
| 769 | |||
| 770 | ;; port | ||
| 771 | (setq port | ||
| 772 | (when (and port (numberp port) (not (zerop port))) | ||
| 773 | (number-to-string port))) | ||
| 774 | |||
| 775 | ;; server | ||
| 776 | (setq server (unless (string-empty-p server) server)) | ||
| 777 | |||
| 778 | ;; database | ||
| 779 | (setq database (unless (string-empty-p database) database)) | ||
| 780 | |||
| 781 | ;; host | ||
| 782 | (setq host (if server | ||
| 783 | (if database | ||
| 784 | (concat server "/" database) | ||
| 785 | server) | ||
| 786 | database)) | ||
| 787 | |||
| 788 | ;; Perform search | ||
| 789 | (dolist (s (auth-source-search :max 1000)) | ||
| 790 | (when (and | ||
| 791 | ;; Is PRODUCT specified, in the enty, and they are equal | ||
| 792 | (if product | ||
| 793 | (if (plist-member s :product) | ||
| 794 | (equal (plist-get s :product) product) | ||
| 795 | t) | ||
| 796 | t) | ||
| 797 | ;; Is USER specified, in the entry, and they are equal | ||
| 798 | (if user | ||
| 799 | (if (plist-member s :user) | ||
| 800 | (equal (plist-get s :user) user) | ||
| 801 | t) | ||
| 802 | t) | ||
| 803 | ;; Is PORT specified, in the entry, and they are equal | ||
| 804 | (if port | ||
| 805 | (if (plist-member s :port) | ||
| 806 | (equal (plist-get s :port) port) | ||
| 807 | t) | ||
| 808 | t)) | ||
| 809 | ;; Is HOST specified, in the entry, and they are equal | ||
| 810 | ;; then the H-SECRET list | ||
| 811 | (if (and host | ||
| 812 | (plist-member s :host) | ||
| 813 | (equal (plist-get s :host) host)) | ||
| 814 | (push s h-secret) | ||
| 815 | ;; Are SERVER and DATABASE specified, present, and equal | ||
| 816 | ;; then the SD-SECRET list | ||
| 817 | (if (and server | ||
| 818 | (plist-member s :server) | ||
| 819 | database | ||
| 820 | (plist-member s :database) | ||
| 821 | (equal (plist-get s :server) server) | ||
| 822 | (equal (plist-get s :database) database)) | ||
| 823 | (push s sd-secret) | ||
| 824 | ;; Is SERVER specified, in the entry, and they are equal | ||
| 825 | ;; then the base SECRET list | ||
| 826 | (if (and server | ||
| 827 | (plist-member s :server) | ||
| 828 | (equal (plist-get s :server) server)) | ||
| 829 | (push s secret) | ||
| 830 | ;; Is DATABASE specified, in the entry, and they are equal | ||
| 831 | ;; then the base SECRET list | ||
| 832 | (if (and database | ||
| 833 | (plist-member s :database) | ||
| 834 | (equal (plist-get s :database) database)) | ||
| 835 | (push s secret))))))) | ||
| 836 | (setq secret (or h-secret sd-secret secret)) | ||
| 837 | |||
| 838 | ;; If we found a single secret, return the password | ||
| 839 | (when (= 1 (length secret)) | ||
| 840 | (setq secret (car secret)) | ||
| 841 | (if (plist-member secret :secret) | ||
| 842 | (plist-get secret :secret) | ||
| 843 | nil)))) | ||
| 844 | |||
| 845 | (defcustom sql-password-wallet | ||
| 846 | (let (wallet w) | ||
| 847 | (dolist (ext '(".json.gpg" ".gpg" ".json" "") wallet) | ||
| 848 | (unless wallet | ||
| 849 | (setq w (locate-user-emacs-file (concat "sql-wallet" ext) | ||
| 850 | (concat ".sql-wallet" ext))) | ||
| 851 | (when (file-exists-p w) | ||
| 852 | (setq wallet w))))) | ||
| 853 | "Identification of the password wallet. | ||
| 854 | See `sql-password-search-wallet-function' to understand how this value | ||
| 855 | is used to locate the password wallet." | ||
| 856 | :type `(plist-get (symbol-plist 'auth-sources) 'custom-type) | ||
| 857 | :group 'SQL | ||
| 858 | :version "27.1") | ||
| 859 | |||
| 860 | (defvar sql-password-search-wallet-function #'sql-auth-source-search-wallet | ||
| 861 | "Function to handle the lookup of the database password. | ||
| 862 | The specified function will be called as: | ||
| 863 | (wallet-func WALLET PRODUCT USER SERVER DATABASE PORT) | ||
| 864 | |||
| 865 | It is expected to return either a string containing the password, | ||
| 866 | a function returning the password, or nil, If you want to support | ||
| 867 | another format of password file, then implement a different | ||
| 868 | search wallet function and identify the location of the password | ||
| 869 | store with `sql-password-wallet'.") | ||
| 870 | |||
| 751 | ;; misc customization of sql.el behavior | 871 | ;; misc customization of sql.el behavior |
| 752 | 872 | ||
| 753 | (defcustom sql-electric-stuff nil | 873 | (defcustom sql-electric-stuff nil |
| @@ -3199,6 +3319,10 @@ symbol `password', for the server if it contains the symbol | |||
| 3199 | `database'. The members of WHAT are processed in the order in | 3319 | `database'. The members of WHAT are processed in the order in |
| 3200 | which they are provided. | 3320 | which they are provided. |
| 3201 | 3321 | ||
| 3322 | If the `sql-password-wallet' is non-nil and WHAT contains the | ||
| 3323 | `password' token, then the `password' token will be pushed to the | ||
| 3324 | end to be sure that all of the values can be fed to the wallet. | ||
| 3325 | |||
| 3202 | Each token may also be a list with the token in the car and a | 3326 | Each token may also be a list with the token in the car and a |
| 3203 | plist of options as the cdr. The following properties are | 3327 | plist of options as the cdr. The following properties are |
| 3204 | supported: | 3328 | supported: |
| @@ -3210,6 +3334,15 @@ supported: | |||
| 3210 | 3334 | ||
| 3211 | In order to ask the user for username, password and database, call the | 3335 | In order to ask the user for username, password and database, call the |
| 3212 | function like this: (sql-get-login \\='user \\='password \\='database)." | 3336 | function like this: (sql-get-login \\='user \\='password \\='database)." |
| 3337 | |||
| 3338 | ;; Push the password to the end if we have a wallet | ||
| 3339 | (when (and sql-password-wallet | ||
| 3340 | (fboundp sql-password-search-wallet-function) | ||
| 3341 | (member 'password what)) | ||
| 3342 | (setq what (append (cl-delete 'password what) | ||
| 3343 | '(password)))) | ||
| 3344 | |||
| 3345 | ;; Prompt for each parameter | ||
| 3213 | (dolist (w what) | 3346 | (dolist (w what) |
| 3214 | (let ((plist (cdr-safe w))) | 3347 | (let ((plist (cdr-safe w))) |
| 3215 | (pcase (or (car-safe w) w) | 3348 | (pcase (or (car-safe w) w) |
| @@ -3218,7 +3351,19 @@ function like this: (sql-get-login \\='user \\='password \\='database)." | |||
| 3218 | 3351 | ||
| 3219 | ('password | 3352 | ('password |
| 3220 | (setq-default sql-password | 3353 | (setq-default sql-password |
| 3221 | (read-passwd "Password: " nil (sql-default-value 'sql-password)))) | 3354 | (if (and sql-password-wallet |
| 3355 | (fboundp sql-password-search-wallet-function)) | ||
| 3356 | (let ((password (funcall sql-password-search-wallet-function | ||
| 3357 | sql-password-wallet | ||
| 3358 | sql-product | ||
| 3359 | sql-user | ||
| 3360 | sql-server | ||
| 3361 | sql-database | ||
| 3362 | sql-port))) | ||
| 3363 | (if password | ||
| 3364 | password | ||
| 3365 | (read-passwd "Password: " nil (sql-default-value 'sql-password)))) | ||
| 3366 | (read-passwd "Password: " nil (sql-default-value 'sql-password))))) | ||
| 3222 | 3367 | ||
| 3223 | ('server | 3368 | ('server |
| 3224 | (sql-get-login-ext 'sql-server "Server: " 'sql-server-history plist)) | 3369 | (sql-get-login-ext 'sql-server "Server: " 'sql-server-history plist)) |
| @@ -4481,6 +4626,10 @@ the call to \\[sql-product-interactive] with | |||
| 4481 | (or sql-default-directory | 4626 | (or sql-default-directory |
| 4482 | default-directory))) | 4627 | default-directory))) |
| 4483 | 4628 | ||
| 4629 | ;; The password wallet returns a function which supplies the password. | ||
| 4630 | (when (functionp sql-password) | ||
| 4631 | (setq sql-password (funcall sql-password))) | ||
| 4632 | |||
| 4484 | ;; Call the COMINT service | 4633 | ;; Call the COMINT service |
| 4485 | (funcall (sql-get-product-feature product :sqli-comint-func) | 4634 | (funcall (sql-get-product-feature product :sqli-comint-func) |
| 4486 | product | 4635 | product |
diff --git a/test/lisp/progmodes/sql-tests.el b/test/lisp/progmodes/sql-tests.el index 604c02172ea..a68f9319c2f 100644 --- a/test/lisp/progmodes/sql-tests.el +++ b/test/lisp/progmodes/sql-tests.el | |||
| @@ -53,5 +53,106 @@ | |||
| 53 | (error "some error")))) | 53 | (error "some error")))) |
| 54 | (should-not (sql-postgres-list-databases)))) | 54 | (should-not (sql-postgres-list-databases)))) |
| 55 | 55 | ||
| 56 | (defvar sql-test-login-params nil) | ||
| 57 | (defmacro with-sql-test-connect-harness (id login-params connection expected) | ||
| 58 | "Set-up and tear-down SQL connect related test. | ||
| 59 | |||
| 60 | Identify tests by ID. Set :sql-login dialect attribute to | ||
| 61 | LOGIN-PARAMS. Provide the CONNECTION parameters and the EXPECTED | ||
| 62 | string of values passed to the comint function for validation." | ||
| 63 | (declare (indent 2)) | ||
| 64 | `(cl-letf | ||
| 65 | ((sql-test-login-params ' ,login-params) | ||
| 66 | ((symbol-function 'sql-comint-test) | ||
| 67 | (lambda (product options &optional buf-name) | ||
| 68 | (with-current-buffer (get-buffer-create buf-name) | ||
| 69 | (insert (pp-to-string (list product options sql-user sql-password sql-server sql-database)))))) | ||
| 70 | ((symbol-function 'sql-run-test) | ||
| 71 | (lambda (&optional buffer) | ||
| 72 | (interactive "P") | ||
| 73 | (sql-product-interactive 'sqltest buffer))) | ||
| 74 | (sql-user nil) | ||
| 75 | (sql-server nil) | ||
| 76 | (sql-database nil) | ||
| 77 | (sql-product-alist | ||
| 78 | '((ansi) | ||
| 79 | (sqltest | ||
| 80 | :name "SqlTest" | ||
| 81 | :sqli-login sql-test-login-params | ||
| 82 | :sqli-comint-func sql-comint-test))) | ||
| 83 | (sql-connection-alist | ||
| 84 | '((,(format "test-%s" id) | ||
| 85 | ,@connection))) | ||
| 86 | (sql-password-wallet | ||
| 87 | (list | ||
| 88 | (make-temp-file | ||
| 89 | "sql-test-netrc" nil nil | ||
| 90 | (mapconcat #'identity | ||
| 91 | '("machine aMachine user aUserName password \"netrc-A aPassword\"" | ||
| 92 | "machine aServer user aUserName password \"netrc-B aPassword\"" | ||
| 93 | "machine aMachine server aServer user aUserName password \"netrc-C aPassword\"" | ||
| 94 | "machine aMachine database aDatabase user aUserName password \"netrc-D aPassword\"" | ||
| 95 | "machine aDatabase user aUserName password \"netrc-E aPassword\"" | ||
| 96 | "machine aMachine server aServer database aDatabase user aUserName password \"netrc-F aPassword\"" | ||
| 97 | "machine \"aServer/aDatabase\" user aUserName password \"netrc-G aPassword\"" | ||
| 98 | ) "\n"))))) | ||
| 99 | |||
| 100 | (let* ((connection ,(format "test-%s" id)) | ||
| 101 | (buffername (format "*SQL: ERT TEST <%s>*" connection))) | ||
| 102 | (when (get-buffer buffername) | ||
| 103 | (kill-buffer buffername)) | ||
| 104 | (sql-connect connection buffername) | ||
| 105 | (should (get-buffer buffername)) | ||
| 106 | (should (string-equal (with-current-buffer buffername (buffer-string)) ,expected)) | ||
| 107 | (when (get-buffer buffername) | ||
| 108 | (kill-buffer buffername)) | ||
| 109 | (delete-file (car sql-password-wallet))))) | ||
| 110 | |||
| 111 | (ert-deftest sql-test-connect () | ||
| 112 | "Test of basic `sql-connect'." | ||
| 113 | (with-sql-test-connect-harness 1 (user password server database) | ||
| 114 | ((sql-product 'sqltest) | ||
| 115 | (sql-user "aUserName") | ||
| 116 | (sql-password "test-1 aPassword") | ||
| 117 | (sql-server "aServer") | ||
| 118 | (sql-database "aDatabase")) | ||
| 119 | "(sqltest nil \"aUserName\" \"test-1 aPassword\" \"aServer\" \"aDatabase\")\n")) | ||
| 120 | |||
| 121 | (ert-deftest sql-test-connect-password-func () | ||
| 122 | "Test of password function." | ||
| 123 | (with-sql-test-connect-harness 2 (user password server database) | ||
| 124 | ((sql-product 'sqltest) | ||
| 125 | (sql-user "aUserName") | ||
| 126 | (sql-password (lambda () (concat [?t ?e ?s ?t ?- ?2 ?\s | ||
| 127 | ?a ?P ?a ?s ?s ?w ?o ?r ?d]))) | ||
| 128 | (sql-server "aServer") | ||
| 129 | (sql-database "aDatabase")) | ||
| 130 | "(sqltest nil \"aUserName\" \"test-2 aPassword\" \"aServer\" \"aDatabase\")\n")) | ||
| 131 | |||
| 132 | (ert-deftest sql-test-connect-wallet-server-database () | ||
| 133 | "Test of password function." | ||
| 134 | (with-sql-test-connect-harness 3 (user password server database) | ||
| 135 | ((sql-product 'sqltest) | ||
| 136 | (sql-user "aUserName") | ||
| 137 | (sql-server "aServer") | ||
| 138 | (sql-database "aDatabase")) | ||
| 139 | "(sqltest nil \"aUserName\" \"netrc-G aPassword\" \"aServer\" \"aDatabase\")\n")) | ||
| 140 | |||
| 141 | (ert-deftest sql-test-connect-wallet-database () | ||
| 142 | "Test of password function." | ||
| 143 | (with-sql-test-connect-harness 4 (user password database) | ||
| 144 | ((sql-product 'sqltest) | ||
| 145 | (sql-user "aUserName") | ||
| 146 | (sql-database "aDatabase")) | ||
| 147 | "(sqltest nil \"aUserName\" \"netrc-E aPassword\" nil \"aDatabase\")\n")) | ||
| 148 | |||
| 149 | (ert-deftest sql-test-connect-wallet-server () | ||
| 150 | "Test of password function." | ||
| 151 | (with-sql-test-connect-harness 5 (user password server) | ||
| 152 | ((sql-product 'sqltest) | ||
| 153 | (sql-user "aUserName") | ||
| 154 | (sql-server "aServer")) | ||
| 155 | "(sqltest nil \"aUserName\" \"netrc-B aPassword\" \"aServer\" nil)\n")) | ||
| 156 | |||
| 56 | (provide 'sql-tests) | 157 | (provide 'sql-tests) |
| 57 | ;;; sql-tests.el ends here | 158 | ;;; sql-tests.el ends here |