diff --git a/SyncKusto/Kusto/AuthenticationMode.cs b/SyncKusto/Kusto/AuthenticationMode.cs index 22b544d..5a53a23 100644 --- a/SyncKusto/Kusto/AuthenticationMode.cs +++ b/SyncKusto/Kusto/AuthenticationMode.cs @@ -4,11 +4,12 @@ namespace SyncKusto.Kusto { /// - /// When connecting to a Kusto cluster, this enum contains the multiple methods of authentication are supported. + /// When connecting to a Kusto cluster, this enum contains the multiple methods of authentication are supported. /// public enum AuthenticationMode { AadFederated, - AadApplication + AadApplication, + AadApplicationSni }; } \ No newline at end of file diff --git a/SyncKusto/Kusto/QueryEngine.cs b/SyncKusto/Kusto/QueryEngine.cs index a7d6ed8..8176b9f 100644 --- a/SyncKusto/Kusto/QueryEngine.cs +++ b/SyncKusto/Kusto/QueryEngine.cs @@ -10,6 +10,7 @@ using Kusto.Data.Common; using Kusto.Data.Net.Client; using Newtonsoft.Json; +using SyncKusto.Utilities; namespace SyncKusto.Kusto { @@ -222,33 +223,70 @@ public void Dispose() /// The name of the database to connect to /// Optionally connect with AAD client app /// Optional key for AAD client app + /// Optional thumbprint of a certificate to use for Subject Name Issuer authentication /// A connection string for accessing Kusto - public static KustoConnectionStringBuilder GetKustoConnectionStringBuilder(string cluster, string database, string aadClientId = null, string aadClientKey = null) + public static KustoConnectionStringBuilder GetKustoConnectionStringBuilder( + string cluster, + string database, + string aadClientId = null, + string aadClientKey = null, + string certificateThumbprint = null) { - if (string.IsNullOrEmpty(aadClientId) != string.IsNullOrEmpty(aadClientKey)) + if (string.IsNullOrEmpty(aadClientId) != string.IsNullOrEmpty(aadClientKey) && + string.IsNullOrEmpty(aadClientId) != string.IsNullOrEmpty(certificateThumbprint)) { throw new ArgumentException("If either aadClientId or aadClientKey are specified, they must both be specified."); } + if (string.IsNullOrWhiteSpace(SettingsWrapper.AADAuthority)) + { + throw new Exception("Authority value must be specified in the Settings dialog."); + } + cluster = NormalizeClusterName(cluster); - var kcsb = new KustoConnectionStringBuilder(cluster) + // User auth + if (string.IsNullOrWhiteSpace(aadClientId)) { - FederatedSecurity = true, - InitialCatalog = database, - Authority = SettingsWrapper.AADAuthority - }; + return new KustoConnectionStringBuilder(cluster) + { + FederatedSecurity = true, + InitialCatalog = database, + Authority = SettingsWrapper.AADAuthority + }; + } + + // App Key auth if (!string.IsNullOrWhiteSpace(aadClientId) && !string.IsNullOrWhiteSpace(aadClientKey)) { - kcsb.ApplicationKey = aadClientKey; - kcsb.ApplicationClientId = aadClientId; + return new KustoConnectionStringBuilder(cluster) + { + FederatedSecurity = true, + InitialCatalog = database, + Authority = SettingsWrapper.AADAuthority, + ApplicationKey = aadClientKey, + ApplicationClientId = aadClientId + }; + } + + // App SNI auth + if (!string.IsNullOrWhiteSpace(aadClientId) && !string.IsNullOrWhiteSpace(certificateThumbprint)) + { + return new KustoConnectionStringBuilder(cluster) + { + InitialCatalog = database, + }.WithAadApplicationCertificateAuthentication( + aadClientId, + CertificateStore.GetCertificate(certificateThumbprint), + SettingsWrapper.AADAuthority, + true); } - return kcsb; + throw new Exception("Could not determine how to create a connection string from provided parameters."); } /// - /// Allow users to specify cluster.eastus2, cluster.eastus2.kusto.windows.net, or https://cluster.eastus2.kusto.windows.net + /// Allow users to specify cluster.eastus2, cluster.eastus2.kusto.windows.net, or https://cluster.eastus2.kusto.windows.net /// /// Input cluster name /// Normalized cluster name e.g. https://cluster.eastus2.kusto.windows.net @@ -280,4 +318,4 @@ public static string NormalizeClusterName(string cluster) } } } -} +} \ No newline at end of file diff --git a/SyncKusto/MainForm.Designer.cs b/SyncKusto/MainForm.Designer.cs index 71d8c59..7bf062d 100644 --- a/SyncKusto/MainForm.Designer.cs +++ b/SyncKusto/MainForm.Designer.cs @@ -102,7 +102,7 @@ private void InitializeComponent() this.grpComparison.Controls.Add(this.rtbSourceText); this.grpComparison.Controls.Add(this.label1); this.grpComparison.Controls.Add(this.tvComparison); - this.grpComparison.Location = new System.Drawing.Point(12, 298); + this.grpComparison.Location = new System.Drawing.Point(12, 270); this.grpComparison.Name = "grpComparison"; this.grpComparison.Size = new System.Drawing.Size(615, 535); this.grpComparison.TabIndex = 3; @@ -150,13 +150,13 @@ private void InitializeComponent() this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(81, 13); this.label2.TabIndex = 6; - this.label2.Text = "Build 20240208"; + this.label2.Text = "Build 20240419"; // // spcTargetHolder // this.spcTargetHolder.Location = new System.Drawing.Point(326, 29); this.spcTargetHolder.Name = "spcTargetHolder"; - this.spcTargetHolder.Size = new System.Drawing.Size(306, 264); + this.spcTargetHolder.Size = new System.Drawing.Size(306, 235); this.spcTargetHolder.TabIndex = 5; this.spcTargetHolder.Title = "Target Schema"; this.spcTargetHolder.Visible = true; @@ -173,9 +173,9 @@ private void InitializeComponent() // // spcSourceHolder // - this.spcSourceHolder.Location = new System.Drawing.Point(18, 28); + this.spcSourceHolder.Location = new System.Drawing.Point(12, 28); this.spcSourceHolder.Name = "spcSourceHolder"; - this.spcSourceHolder.Size = new System.Drawing.Size(306, 264); + this.spcSourceHolder.Size = new System.Drawing.Size(306, 230); this.spcSourceHolder.TabIndex = 4; this.spcSourceHolder.Title = "Source Schema"; this.spcSourceHolder.Visible = true; @@ -194,7 +194,7 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(639, 845); + this.ClientSize = new System.Drawing.Size(639, 815); this.Controls.Add(this.label2); //this.Controls.Add(this.spcTargetHolder); //this.Controls.Add(this.spcSourceHolder); @@ -205,7 +205,7 @@ private void InitializeComponent() this.Controls.Add(this.grpComparison); this.Controls.Add(this.toolStrip1); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(655, 822); + this.MinimumSize = new System.Drawing.Size(655, 815); this.Name = "MainForm"; this.Text = "SyncKusto"; this.toolStrip1.ResumeLayout(false); diff --git a/SyncKusto/SchemaPickerControl.Designer.cs b/SyncKusto/SchemaPickerControl.Designer.cs index f11306b..9cf9254 100644 --- a/SyncKusto/SchemaPickerControl.Designer.cs +++ b/SyncKusto/SchemaPickerControl.Designer.cs @@ -37,13 +37,18 @@ private void InitializeComponent() this.txtOperationProgress = new System.Windows.Forms.Label(); this.pnlKusto = new System.Windows.Forms.Panel(); this.grpAuthentication = new System.Windows.Forms.GroupBox(); + this.pnlApplicationSniAuthentication = new System.Windows.Forms.Panel(); + this.btnCertificate = new System.Windows.Forms.Button(); + this.txtCertificate = new System.Windows.Forms.TextBox(); + this.lblCertificate = new System.Windows.Forms.Label(); + this.txtAppIdSni = new System.Windows.Forms.TextBox(); + this.lblAppIdSni = new System.Windows.Forms.Label(); + this.cmbAuthentication = new System.Windows.Forms.ComboBox(); this.pnlApplicationAuthentication = new System.Windows.Forms.Panel(); this.txtAppKey = new System.Windows.Forms.TextBox(); this.lblAppKey = new System.Windows.Forms.Label(); this.txtAppId = new System.Windows.Forms.TextBox(); this.lblAppId = new System.Windows.Forms.Label(); - this.rbApplication = new System.Windows.Forms.RadioButton(); - this.rbFederated = new System.Windows.Forms.RadioButton(); this.txtDatabase = new System.Windows.Forms.TextBox(); this.lblDatabase = new System.Windows.Forms.Label(); this.txtCluster = new System.Windows.Forms.TextBox(); @@ -57,6 +62,7 @@ private void InitializeComponent() this.grpSourceSchema.SuspendLayout(); this.pnlKusto.SuspendLayout(); this.grpAuthentication.SuspendLayout(); + this.pnlApplicationSniAuthentication.SuspendLayout(); this.pnlApplicationAuthentication.SuspendLayout(); this.pnlFilePath.SuspendLayout(); this.SuspendLayout(); @@ -68,9 +74,11 @@ private void InitializeComponent() this.grpSourceSchema.Controls.Add(this.pnlFilePath); this.grpSourceSchema.Controls.Add(this.rbKusto); this.grpSourceSchema.Controls.Add(this.rbFilePath); - this.grpSourceSchema.Location = new System.Drawing.Point(3, 3); + this.grpSourceSchema.Location = new System.Drawing.Point(4, 5); + this.grpSourceSchema.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.grpSourceSchema.Name = "grpSourceSchema"; - this.grpSourceSchema.Size = new System.Drawing.Size(297, 259); + this.grpSourceSchema.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grpSourceSchema.Size = new System.Drawing.Size(446, 348); this.grpSourceSchema.TabIndex = 1; this.grpSourceSchema.TabStop = false; this.grpSourceSchema.Text = "Source Schema"; @@ -78,10 +86,9 @@ private void InitializeComponent() // txtOperationProgress // this.txtOperationProgress.AutoSize = true; - this.txtOperationProgress.Location = new System.Drawing.Point(11, 239); - this.txtOperationProgress.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.txtOperationProgress.Location = new System.Drawing.Point(16, 368); this.txtOperationProgress.Name = "txtOperationProgress"; - this.txtOperationProgress.Size = new System.Drawing.Size(0, 13); + this.txtOperationProgress.Size = new System.Drawing.Size(0, 20); this.txtOperationProgress.TabIndex = 21; // // pnlKusto @@ -91,22 +98,95 @@ private void InitializeComponent() this.pnlKusto.Controls.Add(this.lblDatabase); this.pnlKusto.Controls.Add(this.txtCluster); this.pnlKusto.Controls.Add(this.lblCluster); - this.pnlKusto.Location = new System.Drawing.Point(6, 43); + this.pnlKusto.Location = new System.Drawing.Point(9, 66); + this.pnlKusto.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.pnlKusto.Name = "pnlKusto"; - this.pnlKusto.Size = new System.Drawing.Size(284, 201); + this.pnlKusto.Size = new System.Drawing.Size(426, 265); this.pnlKusto.TabIndex = 5; // // grpAuthentication // + this.grpAuthentication.Controls.Add(this.pnlApplicationSniAuthentication); + this.grpAuthentication.Controls.Add(this.cmbAuthentication); this.grpAuthentication.Controls.Add(this.pnlApplicationAuthentication); - this.grpAuthentication.Controls.Add(this.rbApplication); - this.grpAuthentication.Controls.Add(this.rbFederated); - this.grpAuthentication.Location = new System.Drawing.Point(7, 56); + this.grpAuthentication.Location = new System.Drawing.Point(10, 86); + this.grpAuthentication.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.grpAuthentication.Name = "grpAuthentication"; - this.grpAuthentication.Size = new System.Drawing.Size(270, 135); + this.grpAuthentication.Padding = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.grpAuthentication.Size = new System.Drawing.Size(405, 172); this.grpAuthentication.TabIndex = 4; this.grpAuthentication.TabStop = false; - this.grpAuthentication.Text = "Authentication"; + this.grpAuthentication.Text = "Authentication Mode"; + this.grpAuthentication.UseCompatibleTextRendering = true; + // + // pnlApplicationSniAuthentication + // + this.pnlApplicationSniAuthentication.Controls.Add(this.btnCertificate); + this.pnlApplicationSniAuthentication.Controls.Add(this.txtCertificate); + this.pnlApplicationSniAuthentication.Controls.Add(this.lblCertificate); + this.pnlApplicationSniAuthentication.Controls.Add(this.txtAppIdSni); + this.pnlApplicationSniAuthentication.Controls.Add(this.lblAppIdSni); + this.pnlApplicationSniAuthentication.Location = new System.Drawing.Point(9, 71); + this.pnlApplicationSniAuthentication.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.pnlApplicationSniAuthentication.Name = "pnlApplicationSniAuthentication"; + this.pnlApplicationSniAuthentication.Size = new System.Drawing.Size(388, 89); + this.pnlApplicationSniAuthentication.TabIndex = 4; + this.pnlApplicationSniAuthentication.Visible = false; + // + // btnCertificate + // + this.btnCertificate.Location = new System.Drawing.Point(349, 48); + this.btnCertificate.Name = "btnCertificate"; + this.btnCertificate.Size = new System.Drawing.Size(39, 31); + this.btnCertificate.TabIndex = 8; + this.btnCertificate.Text = "..."; + this.btnCertificate.UseVisualStyleBackColor = true; + this.btnCertificate.Click += new System.EventHandler(this.btnCertificate_Click); + // + // txtCertificate + // + this.txtCertificate.Location = new System.Drawing.Point(180, 48); + this.txtCertificate.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtCertificate.Name = "txtCertificate"; + this.txtCertificate.Size = new System.Drawing.Size(160, 26); + this.txtCertificate.TabIndex = 7; + // + // lblCertificate + // + this.lblCertificate.AutoSize = true; + this.lblCertificate.Location = new System.Drawing.Point(3, 51); + this.lblCertificate.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lblCertificate.Name = "lblCertificate"; + this.lblCertificate.Size = new System.Drawing.Size(169, 20); + this.lblCertificate.TabIndex = 6; + this.lblCertificate.Text = "Certificate Thumbprint:"; + // + // txtAppIdSni + // + this.txtAppIdSni.Location = new System.Drawing.Point(180, 8); + this.txtAppIdSni.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); + this.txtAppIdSni.Name = "txtAppIdSni"; + this.txtAppIdSni.Size = new System.Drawing.Size(204, 26); + this.txtAppIdSni.TabIndex = 5; + // + // lblAppIdSni + // + this.lblAppIdSni.AutoSize = true; + this.lblAppIdSni.Location = new System.Drawing.Point(3, 11); + this.lblAppIdSni.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lblAppIdSni.Name = "lblAppIdSni"; + this.lblAppIdSni.Size = new System.Drawing.Size(60, 20); + this.lblAppIdSni.TabIndex = 4; + this.lblAppIdSni.Text = "App Id:"; + // + // cmbAuthentication + // + this.cmbAuthentication.FormattingEnabled = true; + this.cmbAuthentication.Location = new System.Drawing.Point(7, 27); + this.cmbAuthentication.Name = "cmbAuthentication"; + this.cmbAuthentication.Size = new System.Drawing.Size(389, 28); + this.cmbAuthentication.TabIndex = 3; + this.cmbAuthentication.SelectedValueChanged += new System.EventHandler(this.cmbAuthentication_SelectedValueChanged); // // pnlApplicationAuthentication // @@ -114,97 +194,83 @@ private void InitializeComponent() this.pnlApplicationAuthentication.Controls.Add(this.lblAppKey); this.pnlApplicationAuthentication.Controls.Add(this.txtAppId); this.pnlApplicationAuthentication.Controls.Add(this.lblAppId); - this.pnlApplicationAuthentication.Location = new System.Drawing.Point(25, 66); + this.pnlApplicationAuthentication.Location = new System.Drawing.Point(9, 71); + this.pnlApplicationAuthentication.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.pnlApplicationAuthentication.Name = "pnlApplicationAuthentication"; - this.pnlApplicationAuthentication.Size = new System.Drawing.Size(239, 58); + this.pnlApplicationAuthentication.Size = new System.Drawing.Size(388, 89); this.pnlApplicationAuthentication.TabIndex = 2; this.pnlApplicationAuthentication.Visible = false; // // txtAppKey // - this.txtAppKey.Location = new System.Drawing.Point(64, 29); + this.txtAppKey.Location = new System.Drawing.Point(96, 48); + this.txtAppKey.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.txtAppKey.Name = "txtAppKey"; this.txtAppKey.PasswordChar = '*'; - this.txtAppKey.Size = new System.Drawing.Size(149, 20); + this.txtAppKey.Size = new System.Drawing.Size(288, 26); this.txtAppKey.TabIndex = 7; // // lblAppKey // this.lblAppKey.AutoSize = true; - this.lblAppKey.Location = new System.Drawing.Point(2, 33); + this.lblAppKey.Location = new System.Drawing.Point(3, 51); + this.lblAppKey.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.lblAppKey.Name = "lblAppKey"; - this.lblAppKey.Size = new System.Drawing.Size(50, 13); + this.lblAppKey.Size = new System.Drawing.Size(72, 20); this.lblAppKey.TabIndex = 6; this.lblAppKey.Text = "App Key:"; // // txtAppId // - this.txtAppId.Location = new System.Drawing.Point(64, 3); + this.txtAppId.Location = new System.Drawing.Point(96, 8); + this.txtAppId.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.txtAppId.Name = "txtAppId"; - this.txtAppId.Size = new System.Drawing.Size(149, 20); + this.txtAppId.Size = new System.Drawing.Size(288, 26); this.txtAppId.TabIndex = 5; // // lblAppId // this.lblAppId.AutoSize = true; - this.lblAppId.Location = new System.Drawing.Point(2, 7); + this.lblAppId.Location = new System.Drawing.Point(3, 11); + this.lblAppId.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.lblAppId.Name = "lblAppId"; - this.lblAppId.Size = new System.Drawing.Size(41, 13); + this.lblAppId.Size = new System.Drawing.Size(60, 20); this.lblAppId.TabIndex = 4; this.lblAppId.Text = "App Id:"; // - // rbApplication - // - this.rbApplication.AutoSize = true; - this.rbApplication.Location = new System.Drawing.Point(7, 44); - this.rbApplication.Name = "rbApplication"; - this.rbApplication.Size = new System.Drawing.Size(173, 17); - this.rbApplication.TabIndex = 1; - this.rbApplication.Text = "AAD Application Authentication"; - this.rbApplication.UseVisualStyleBackColor = true; - this.rbApplication.CheckedChanged += new System.EventHandler(this.rbApplication_CheckedChanged); - // - // rbFederated - // - this.rbFederated.AutoSize = true; - this.rbFederated.Checked = true; - this.rbFederated.Location = new System.Drawing.Point(7, 20); - this.rbFederated.Name = "rbFederated"; - this.rbFederated.Size = new System.Drawing.Size(169, 17); - this.rbFederated.TabIndex = 0; - this.rbFederated.TabStop = true; - this.rbFederated.Text = "AAD Federated Authentication"; - this.rbFederated.UseVisualStyleBackColor = true; - // // txtDatabase // - this.txtDatabase.Location = new System.Drawing.Point(66, 26); + this.txtDatabase.Location = new System.Drawing.Point(99, 44); + this.txtDatabase.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.txtDatabase.Name = "txtDatabase"; - this.txtDatabase.Size = new System.Drawing.Size(211, 20); + this.txtDatabase.Size = new System.Drawing.Size(314, 26); this.txtDatabase.TabIndex = 3; // // lblDatabase // this.lblDatabase.AutoSize = true; - this.lblDatabase.Location = new System.Drawing.Point(4, 30); + this.lblDatabase.Location = new System.Drawing.Point(6, 46); + this.lblDatabase.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.lblDatabase.Name = "lblDatabase"; - this.lblDatabase.Size = new System.Drawing.Size(56, 13); + this.lblDatabase.Size = new System.Drawing.Size(83, 20); this.lblDatabase.TabIndex = 2; this.lblDatabase.Text = "Database:"; // // txtCluster // - this.txtCluster.Location = new System.Drawing.Point(66, 0); + this.txtCluster.Location = new System.Drawing.Point(99, 4); + this.txtCluster.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.txtCluster.Name = "txtCluster"; - this.txtCluster.Size = new System.Drawing.Size(211, 20); + this.txtCluster.Size = new System.Drawing.Size(314, 26); this.txtCluster.TabIndex = 1; // // lblCluster // this.lblCluster.AutoSize = true; - this.lblCluster.Location = new System.Drawing.Point(4, 4); + this.lblCluster.Location = new System.Drawing.Point(6, 6); + this.lblCluster.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.lblCluster.Name = "lblCluster"; - this.lblCluster.Size = new System.Drawing.Size(42, 13); + this.lblCluster.Size = new System.Drawing.Size(63, 20); this.lblCluster.TabIndex = 0; this.lblCluster.Text = "Cluster:"; // @@ -213,34 +279,38 @@ private void InitializeComponent() this.pnlFilePath.Controls.Add(this.lblExample); this.pnlFilePath.Controls.Add(this.txtFilePath); this.pnlFilePath.Controls.Add(this.btnChooseDirectory); - this.pnlFilePath.Location = new System.Drawing.Point(7, 43); + this.pnlFilePath.Location = new System.Drawing.Point(10, 66); + this.pnlFilePath.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.pnlFilePath.Name = "pnlFilePath"; - this.pnlFilePath.Size = new System.Drawing.Size(284, 84); + this.pnlFilePath.Size = new System.Drawing.Size(426, 129); this.pnlFilePath.TabIndex = 4; // // lblExample // this.lblExample.AutoSize = true; this.lblExample.ForeColor = System.Drawing.SystemColors.ControlDarkDark; - this.lblExample.Location = new System.Drawing.Point(4, 30); + this.lblExample.Location = new System.Drawing.Point(6, 46); + this.lblExample.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); this.lblExample.Name = "lblExample"; - this.lblExample.Size = new System.Drawing.Size(278, 39); + this.lblExample.Size = new System.Drawing.Size(414, 60); this.lblExample.TabIndex = 4; this.lblExample.Text = "Choose the directory that is the parent of the \"Functions\" \r\ndirectory. \r\nExample" + ": c:\\git\\myrepo\\mycluster\\mydatabase"; // // txtFilePath // - this.txtFilePath.Location = new System.Drawing.Point(3, 3); + this.txtFilePath.Location = new System.Drawing.Point(4, 5); + this.txtFilePath.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.txtFilePath.Name = "txtFilePath"; - this.txtFilePath.Size = new System.Drawing.Size(242, 20); + this.txtFilePath.Size = new System.Drawing.Size(361, 26); this.txtFilePath.TabIndex = 2; // // btnChooseDirectory // - this.btnChooseDirectory.Location = new System.Drawing.Point(251, 3); + this.btnChooseDirectory.Location = new System.Drawing.Point(376, 5); + this.btnChooseDirectory.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.btnChooseDirectory.Name = "btnChooseDirectory"; - this.btnChooseDirectory.Size = new System.Drawing.Size(26, 20); + this.btnChooseDirectory.Size = new System.Drawing.Size(39, 31); this.btnChooseDirectory.TabIndex = 3; this.btnChooseDirectory.Text = "..."; this.btnChooseDirectory.UseVisualStyleBackColor = true; @@ -249,9 +319,10 @@ private void InitializeComponent() // rbKusto // this.rbKusto.AutoSize = true; - this.rbKusto.Location = new System.Drawing.Point(78, 19); + this.rbKusto.Location = new System.Drawing.Point(117, 29); + this.rbKusto.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.rbKusto.Name = "rbKusto"; - this.rbKusto.Size = new System.Drawing.Size(52, 17); + this.rbKusto.Size = new System.Drawing.Size(75, 24); this.rbKusto.TabIndex = 1; this.rbKusto.Text = "Kusto"; this.rbKusto.UseVisualStyleBackColor = true; @@ -261,9 +332,10 @@ private void InitializeComponent() // this.rbFilePath.AutoSize = true; this.rbFilePath.Checked = true; - this.rbFilePath.Location = new System.Drawing.Point(6, 19); + this.rbFilePath.Location = new System.Drawing.Point(9, 29); + this.rbFilePath.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.rbFilePath.Name = "rbFilePath"; - this.rbFilePath.Size = new System.Drawing.Size(66, 17); + this.rbFilePath.Size = new System.Drawing.Size(96, 24); this.rbFilePath.TabIndex = 0; this.rbFilePath.TabStop = true; this.rbFilePath.Text = "File Path"; @@ -272,17 +344,19 @@ private void InitializeComponent() // // SchemaPickerControl // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.grpSourceSchema); + this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); this.Name = "SchemaPickerControl"; - this.Size = new System.Drawing.Size(306, 261); + this.Size = new System.Drawing.Size(459, 362); this.grpSourceSchema.ResumeLayout(false); this.grpSourceSchema.PerformLayout(); this.pnlKusto.ResumeLayout(false); this.pnlKusto.PerformLayout(); this.grpAuthentication.ResumeLayout(false); - this.grpAuthentication.PerformLayout(); + this.pnlApplicationSniAuthentication.ResumeLayout(false); + this.pnlApplicationSniAuthentication.PerformLayout(); this.pnlApplicationAuthentication.ResumeLayout(false); this.pnlApplicationAuthentication.PerformLayout(); this.pnlFilePath.ResumeLayout(false); @@ -305,8 +379,6 @@ private void InitializeComponent() private System.Windows.Forms.TextBox txtCluster; private System.Windows.Forms.Label lblCluster; private System.Windows.Forms.GroupBox grpAuthentication; - private System.Windows.Forms.RadioButton rbApplication; - private System.Windows.Forms.RadioButton rbFederated; private System.Windows.Forms.Panel pnlApplicationAuthentication; private System.Windows.Forms.TextBox txtAppKey; private System.Windows.Forms.Label lblAppKey; @@ -314,5 +386,12 @@ private void InitializeComponent() private System.Windows.Forms.Label lblAppId; private System.Windows.Forms.Label lblExample; private System.Windows.Forms.Label txtOperationProgress; + private System.Windows.Forms.ComboBox cmbAuthentication; + private System.Windows.Forms.Panel pnlApplicationSniAuthentication; + private System.Windows.Forms.Button btnCertificate; + private System.Windows.Forms.TextBox txtCertificate; + private System.Windows.Forms.Label lblCertificate; + private System.Windows.Forms.TextBox txtAppIdSni; + private System.Windows.Forms.Label lblAppIdSni; } } diff --git a/SyncKusto/SchemaPickerControl.cs b/SyncKusto/SchemaPickerControl.cs index 91d41ff..d8a7a01 100644 --- a/SyncKusto/SchemaPickerControl.cs +++ b/SyncKusto/SchemaPickerControl.cs @@ -7,11 +7,13 @@ using SyncKusto.Kusto; using SyncKusto.Kusto.DatabaseSchemaBuilder; using SyncKusto.SyncSources; +using SyncKusto.Utilities; using SyncKusto.Validation.ErrorMessages; using SyncKusto.Validation.Infrastructure; using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.Windows.Forms; @@ -19,12 +21,24 @@ namespace SyncKusto { public partial class SchemaPickerControl : UserControl { + private const string ENTRA_ID_USER = "Microsoft Entra ID User"; + private const string ENTRA_ID_APP_KEY = "Microsoft Entra ID App (Key)"; + private const string ENTRA_ID_APP_SNI = "Microsoft Entra ID App (SubjectName/Issuer)"; + /// /// Default constructor to make the Windows Forms designer happy /// public SchemaPickerControl() { InitializeComponent(); + + this.cmbAuthentication.Items.AddRange( + new object[] { + ENTRA_ID_USER, + ENTRA_ID_APP_KEY, + ENTRA_ID_APP_SNI }); + + this.cmbAuthentication.SelectedIndex = 0; } /// @@ -62,8 +76,26 @@ public string Title set => grpSourceSchema.Text = value; } - private AuthenticationMode Authentication => - rbFederated.Checked ? AuthenticationMode.AadFederated : AuthenticationMode.AadApplication; + private AuthenticationMode Authentication + { + get + { + switch (cmbAuthentication.SelectedItem) + { + case ENTRA_ID_USER: + return AuthenticationMode.AadFederated; + + case ENTRA_ID_APP_KEY: + return AuthenticationMode.AadApplication; + + case ENTRA_ID_APP_SNI: + return AuthenticationMode.AadApplicationSni; + + default: + throw new Exception("Unknown authentication type"); + } + } + } public string SourceFilePath => txtFilePath.Text.HandleLongFileNames(); @@ -71,9 +103,28 @@ public KustoConnectionStringBuilder KustoConnection { get { - return rbApplication.Checked - ? QueryEngine.GetKustoConnectionStringBuilder(txtCluster.Text, txtDatabase.Text, txtAppId.Text, txtAppKey.Text) - : QueryEngine.GetKustoConnectionStringBuilder(txtCluster.Text, txtDatabase.Text); + switch (Authentication) + { + case AuthenticationMode.AadFederated: + return QueryEngine.GetKustoConnectionStringBuilder(txtCluster.Text, txtDatabase.Text); + + case AuthenticationMode.AadApplication: + return QueryEngine.GetKustoConnectionStringBuilder( + txtCluster.Text, + txtDatabase.Text, + aadClientId: txtAppId.Text, + aadClientKey: txtAppKey.Text); + + case AuthenticationMode.AadApplicationSni: + return QueryEngine.GetKustoConnectionStringBuilder( + txtCluster.Text, + txtDatabase.Text, + aadClientId: txtAppIdSni.Text, + certificateThumbprint: txtCertificate.Text); + + default: + throw new Exception("Unknown authentication type"); + } } } @@ -116,7 +167,11 @@ private bool KustoSourceSpecification() => .Or(Spec .IsTrue(s => s.Authentication == AuthenticationMode.AadApplication) .And(Spec.NonEmptyString(s => s.txtAppId.Text) - .And(Spec.NonEmptyString(s => s.txtAppKey.Text))))) + .And(Spec.NonEmptyString(s => s.txtAppKey.Text)))) + .Or(Spec + .IsTrue(s => s.Authentication == AuthenticationMode.AadApplicationSni) + .And(Spec.NonEmptyString(s => s.txtAppIdSni.Text) + .And(Spec.NonEmptyString(s => s.txtCertificate.Text))))) .IsSatisfiedBy(this); private void ToggleFilePathSourcePanel(bool predicate) => pnlFilePath.Visible = predicate; @@ -139,8 +194,11 @@ private void ToggleSourceSelections() } } - private void rbApplication_CheckedChanged(object sender, EventArgs e) => - pnlApplicationAuthentication.Visible = ((RadioButton)sender).Checked; + private void cmbAuthentication_SelectedValueChanged(object sender, EventArgs e) + { + pnlApplicationAuthentication.Visible = Authentication == AuthenticationMode.AadApplication; + pnlApplicationSniAuthentication.Visible = Authentication == AuthenticationMode.AadApplicationSni; + } private void rbKusto_CheckedChanged(object sender, EventArgs e) => SourceButtonCheckChange(sender, SourceSelection.Kusto()); @@ -234,5 +292,21 @@ public void ReportProgress(string message) if (Spec.NotNull(s => s).IsSatisfiedBy(message)) txtOperationProgress.Text = message; } + + private void btnCertificate_Click(object sender, EventArgs e) + { + // Show the certificate selection dialog + var selectedCertificateCollection = X509Certificate2UI.SelectFromCollection( + CertificateStore.GetAllCertificates(), + "Select a certificate", + "Choose a certificate for authentication", + X509SelectionFlag.SingleSelection); + + if (selectedCertificateCollection != null && + selectedCertificateCollection.Count == 1) + { + txtCertificate.Text = selectedCertificateCollection[0].Thumbprint; + } + } } } \ No newline at end of file diff --git a/SyncKusto/SettingsForm.cs b/SyncKusto/SettingsForm.cs index f347cca..ead4f3f 100644 --- a/SyncKusto/SettingsForm.cs +++ b/SyncKusto/SettingsForm.cs @@ -112,7 +112,7 @@ private void btnOk_Click(object sender, System.EventArgs e) } else if (ex.Message.Contains("Kusto client failed to perform authentication")) { - MessageBox.Show($"Could not authenticate with AAD. Please verify that the AAD Authority is specified correctly.", "Error Authenticating", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show($"Could not authenticate with Microsoft Entra ID. Please verify that the Microsoft Entra ID Authority is specified correctly.", "Error Authenticating", MessageBoxButtons.OK, MessageBoxIcon.Error); } else { diff --git a/SyncKusto/SettingsForm.resx b/SyncKusto/SettingsForm.resx index 086ded7..daef95a 100644 --- a/SyncKusto/SettingsForm.resx +++ b/SyncKusto/SettingsForm.resx @@ -121,7 +121,7 @@ When the local file system is selected as the source or the target, SyncKusto loads all of the files into a temporary database and then asks Kusto for the database schema. Specify a Kusto cluster and database to use for the comparison. You must be a database admin. - Specify the AAD authority to be used for authentication (e.g. contoso.com). Depending on your tenant configuration, this may be optional, unless you're connecting with an application id in which case it is required: + Specify the Entra ID authority to be used for authentication (e.g. contoso.com). Depending on your tenant configuration, this may be optional, unless you're connecting with an application id in which case it is required: These settings are only applied when a file is written. They do not affect the comparison operation. To apply this to all existing files, delete all the files locally, execute the comparison again and then update the local files. diff --git a/SyncKusto/SyncKusto.csproj b/SyncKusto/SyncKusto.csproj index cef955e..a77a8e8 100644 --- a/SyncKusto/SyncKusto.csproj +++ b/SyncKusto/SyncKusto.csproj @@ -99,6 +99,7 @@ + diff --git a/SyncKusto/Utilities/CertificateStore.cs b/SyncKusto/Utilities/CertificateStore.cs new file mode 100644 index 0000000..f364508 --- /dev/null +++ b/SyncKusto/Utilities/CertificateStore.cs @@ -0,0 +1,70 @@ +namespace SyncKusto.Utilities +{ + using System; + using System.Security.Cryptography.X509Certificates; + + internal class CertificateStore + { + /// + /// Find a certificate by thumbprint in the My store of either CurrentUser or LocalMachine. + /// + /// The thumbprint of the certificate to retrieve. + /// The requested certificate + public static X509Certificate2 GetCertificate(string thumbprint) + { + var certificate = GetCertificate(StoreLocation.CurrentUser, StoreName.My, thumbprint); + if (certificate == null) + { + certificate = GetCertificate(StoreLocation.LocalMachine, StoreName.My, thumbprint); + } + + if (certificate == null) + { + throw new Exception($"Cannot find certificate with thumbprint: {thumbprint}"); + } + + return certificate; + } + + /// + /// Get all the certificates in both the Current User and Local Machine locations. + /// + /// A collection of certificates + public static X509Certificate2Collection GetAllCertificates() + { + X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + X509Certificate2Collection certificates = store.Certificates; + store.Close(); + + store = new X509Store(StoreName.My, StoreLocation.LocalMachine); + store.Open(OpenFlags.ReadOnly); + certificates.AddRange(store.Certificates); + store.Close(); + + return certificates; + } + + /// + /// Find a certificate by thumbprint in the specified store and location. + /// + /// The location of the store to search for the certificate. + /// The name of the store to search for the certificate. + /// The thumbprint of the certificate to retrieve. + /// The requested certificate or null if it is not found. + private static X509Certificate2 GetCertificate(StoreLocation storeLocation, StoreName storeName, string thumbprint) + { + var certStore = new X509Store(storeName, storeLocation); + certStore.Open(OpenFlags.ReadOnly); + var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + certStore.Close(); + + if (certCollection.Count == 0) + { + return null; + } + + return certCollection[0]; + } + } +} \ No newline at end of file diff --git a/SyncKusto/Validation/ErrorMessages/Specifications/KustoOperationErrorSpecifications.cs b/SyncKusto/Validation/ErrorMessages/Specifications/KustoOperationErrorSpecifications.cs index 78606cf..729397f 100644 --- a/SyncKusto/Validation/ErrorMessages/Specifications/KustoOperationErrorSpecifications.cs +++ b/SyncKusto/Validation/ErrorMessages/Specifications/KustoOperationErrorSpecifications.cs @@ -23,7 +23,7 @@ public static IOperationErrorMessageSpecification DatabaseNotFound() => public static IOperationErrorMessageSpecification CannotAuthenticate() => new OperationErrorMessageSpecification(Spec .IsTrue(ex => ex is KustoClientAuthenticationException), - "Could not authenticate with AAD. Check the AAD authority in the Settings."); + "Could not authenticate with AAD. Check the Entra ID authority in the Settings."); public static IOperationErrorMessageSpecification NoPermissions() => new OperationErrorMessageSpecification(Spec diff --git a/screenshot.png b/screenshot.png index 1646362..96c6e1c 100644 Binary files a/screenshot.png and b/screenshot.png differ