diff --git a/clojure/deps.edn b/clojure/deps.edn index 496cd572..45c18179 100644 --- a/clojure/deps.edn +++ b/clojure/deps.edn @@ -16,6 +16,7 @@ org.postgresql/postgresql {:mvn/version "42.7.3"} com.mysql/mysql-connector-j {:mvn/version "9.0.0"} com.microsoft.sqlserver/mssql-jdbc {:mvn/version "12.6.1.jre11"} + com.microsoft.azure/msal4j {:mvn/version "1.14.1"} org.xerial/sqlite-jdbc {:mvn/version "3.46.0.0"} com.opencsv/opencsv {:mvn/version "5.9"} diff --git a/clojure/src/pgloader/load_file/ast.clj b/clojure/src/pgloader/load_file/ast.clj index 832fc41b..4f9b1ef7 100644 --- a/clojure/src/pgloader/load_file/ast.clj +++ b/clojure/src/pgloader/load_file/ast.clj @@ -118,7 +118,13 @@ :raw uri-str :jdbc-url (str "jdbc:sqlserver://" host ":" (if (pos? (.getPort uri)) (.getPort uri) 1433) - ";databaseName=" db ";encrypt=false" + ";databaseName=" db + ;; Only default encrypt=false when the caller hasn't + ;; already set it via query params (e.g. encrypt=true + ;; is required for Azure SQL). + (when-not (and raw-query + (str/includes? raw-query "encrypt=")) + ";encrypt=false") (when raw-query (str ";" (str/replace raw-query #"&" ";"))))} table-from-query) diff --git a/clojure/test/pgloader/load_file/ast_test.clj b/clojure/test/pgloader/load_file/ast_test.clj index 92888390..51e6ac38 100644 --- a/clojure/test/pgloader/load_file/ast_test.clj +++ b/clojure/test/pgloader/load_file/ast_test.clj @@ -1,5 +1,6 @@ (ns pgloader.load-file.ast-test (:require [clojure.test :refer [deftest is testing]] + [clojure.string :as str] [pgloader.load-file.ast :refer [parse-uri]] [pgloader.load-file.parser :as parser]) (:import [java.sql DriverManager])) @@ -64,12 +65,34 @@ (is (nil? (:user m)))))) (deftest test-parse-mssql-native-uri - (testing "mssql:// synthesises jdbc-url" + (testing "mssql:// synthesises jdbc-url with encrypt=false default" (let [m (parse-uri "mssql://sa:secret@mssql:1433/mydb")] (is (= :mssql (:type m))) (is (= "jdbc:sqlserver://mssql:1433;databaseName=mydb;encrypt=false" (:jdbc-url m)) - "mssql:// gets jdbc-url with databaseName and encrypt=false")))) + "mssql:// gets jdbc-url with databaseName and encrypt=false"))) + + (testing "mssql:// with encrypt=true in query string — no duplicate encrypt=false" + (let [m (parse-uri "mssql://sa:secret@mssql:1433/mydb?encrypt=true")] + (is (= "jdbc:sqlserver://mssql:1433;databaseName=mydb;encrypt=true" + (:jdbc-url m)) + "encrypt= in query string suppresses the default encrypt=false"))) + + (testing "mssql:// with authentication= for Azure AD (non-interactive)" + (let [m (parse-uri "mssql://user@server.database.windows.net:1433/mydb?authentication=ActiveDirectoryPassword&encrypt=true")] + (is (= :mssql (:type m))) + (is (str/includes? (:jdbc-url m) "authentication=ActiveDirectoryPassword")) + (is (str/includes? (:jdbc-url m) "encrypt=true")) + (is (not (str/includes? (:jdbc-url m) "encrypt=false")) + "no duplicate encrypt=false when encrypt= already in query string")))) + +(deftest test-msal4j-on-classpath + (testing "MSAL4J is bundled — Azure AD non-interactive auth modes are available" + (is (try (Class/forName "com.microsoft.aad.msal4j.PublicClientApplication") + true + (catch ClassNotFoundException _ + false)) + "com.microsoft.aad.msal4j must be on the classpath for ActiveDirectoryPassword / ActiveDirectoryServicePrincipal auth"))) (deftest test-parse-postgresql-synthesises-jdbc-url (testing "postgresql:// synthesises jdbc-url" diff --git a/docs/ref/mssql.rst b/docs/ref/mssql.rst index fc61646c..bbcab04d 100644 --- a/docs/ref/mssql.rst +++ b/docs/ref/mssql.rst @@ -345,6 +345,48 @@ The matching is done in pgloader itself, with a Common Lisp regular expression lib, so doesn't depend on the *LIKE* implementation of MS SQL, nor on the lack of support for regular expressions in the engine. +Azure SQL / Azure Active Directory Authentication +------------------------------------------------- + +pgloader v4 uses the Microsoft JDBC driver (``mssql-jdbc``) and bundles +MSAL4J, so the following non-interactive Azure AD authentication modes work +out of the box. Always use a ``jdbc:sqlserver://`` URL for Azure SQL so that +``encrypt=true`` and ``authentication=`` reach the driver exactly as written: + +``ActiveDirectoryPassword`` + Authenticate with an Azure AD username and password:: + + FROM "jdbc:sqlserver://myserver.database.windows.net:1433;databaseName=mydb;\ +authentication=ActiveDirectoryPassword;user=user@tenant.onmicrosoft.com;\ +password=secret;encrypt=true;trustServerCertificate=false" + +``ActiveDirectoryServicePrincipal`` + Authenticate as an Azure AD application (client ID + client secret), + suitable for fully automated / CI pipelines:: + + FROM "jdbc:sqlserver://myserver.database.windows.net:1433;databaseName=mydb;\ +authentication=ActiveDirectoryServicePrincipal;\ +AADSecurePrincipalId=;AADSecurePrincipalSecret=;\ +encrypt=true" + +``ActiveDirectoryManagedIdentity`` + Authenticate using the managed identity of the Azure VM, App Service, + or container that pgloader runs in — no credentials in the URL:: + + FROM "jdbc:sqlserver://myserver.database.windows.net:1433;databaseName=mydb;\ +authentication=ActiveDirectoryManagedIdentity;encrypt=true" + +.. note:: + + ``ActiveDirectoryInteractive`` opens a browser window for an OAuth2 + interactive login and **cannot** be used with pgloader, which runs + non-interactively. + + The native ``mssql://`` scheme defaults to ``encrypt=false`` to match + on-premises SQL Server behaviour. For Azure SQL use the + ``jdbc:sqlserver://`` form shown above so that ``encrypt=true`` is set + explicitly. + MS SQL Driver setup and encoding --------------------------------