Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/mssql/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@
},
"Default": "Default",
"Delete saved connection": "Delete saved connection",
"Remove recent connection": "Remove recent connection",
"Clear from recent connections list": "Clear from recent connections list",
"Copy connection string to clipboard": "Copy connection string to clipboard",
"Paste connection string from clipboard": "Paste connection string from clipboard",
"Paste": "Paste",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import { generateConnectionComponents, groupAdvancedOptions } from "./formCompon
import { FormWebviewController } from "../forms/formWebviewController";
import { ConnectionCredentials } from "../models/connectionCredentials";
import { Deferred } from "../protocol";
import { configSelectedAzureSubscriptions } from "../constants/constants";
import { configSelectedAzureSubscriptions, defaultDatabase } from "../constants/constants";
import * as AzureConstants from "../azure/constants";
import { AddFirewallRuleState } from "../sharedInterfaces/addFirewallRule";
import * as Utils from "../models/utils";
Expand Down Expand Up @@ -1061,17 +1061,21 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
savedConnections: IConnectionDialogProfile[];
recentConnections: IConnectionDialogProfile[];
}> {
const recentConnectionsLimit =
this._mainController.connectionManager.connectionStore.getMaxRecentConnectionsCount();
const unsortedConnections: IConnectionProfileWithSource[] =
await this._mainController.connectionManager.connectionStore.readAllConnections(
true /* includeRecentConnections */,
recentConnectionsLimit,
);

const savedConnections = unsortedConnections.filter(
(c) => c.profileSource === CredentialsQuickPickItemType.Profile,
);

const recentConnections = unsortedConnections.filter(
(c) => c.profileSource === CredentialsQuickPickItemType.Mru,
const recentConnections = this.normalizeRecentConnectionsForDisplay(
unsortedConnections.filter((c) => c.profileSource === CredentialsQuickPickItemType.Mru),
savedConnections,
);

sendActionEvent(
Expand Down Expand Up @@ -1131,6 +1135,84 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
state.savedConnections = loadedConnections.savedConnections;
}

private normalizeRecentConnectionsForDisplay(
recentConnections: IConnectionProfileWithSource[],
savedConnections: IConnectionProfileWithSource[],
): IConnectionProfileWithSource[] {
return recentConnections.map((recentConnection) => {
const matchingSavedConnection = savedConnections.find((savedConnection) =>
this.isOriginalSavedProfile(savedConnection, recentConnection),
);

if (
matchingSavedConnection &&
!this.isSameDatabaseName(
matchingSavedConnection.database,
recentConnection.database,
)
) {
return {
...recentConnection,
profileName: undefined,
};
}

return recentConnection;
});
}
Comment thread
aasimkhan30 marked this conversation as resolved.

private isOriginalSavedProfile(
savedConnection: IConnectionProfileWithSource,
recentConnection: IConnectionProfileWithSource,
): boolean {
if (savedConnection.id && recentConnection.id) {
return savedConnection.id === recentConnection.id;
}

if (
!savedConnection.profileName ||
!recentConnection.profileName ||
savedConnection.profileName !== recentConnection.profileName
) {
return false;
}

if (savedConnection.connectionString || recentConnection.connectionString) {
return savedConnection.connectionString === recentConnection.connectionString;
}

if (savedConnection.server !== recentConnection.server) {
return false;
}

const savedAuthType = savedConnection.authenticationType || AuthenticationType.SqlLogin;
const recentAuthType = recentConnection.authenticationType || AuthenticationType.SqlLogin;

if (savedAuthType !== recentAuthType) {
return false;
}

if ((savedConnection.user ?? "") !== (recentConnection.user ?? "")) {
return false;
}

if (savedConnection.accountId || recentConnection.accountId) {
return areCompatibleEntraAccountIds(
savedConnection.accountId,
recentConnection.accountId,
);
}

return true;
}

private isSameDatabaseName(currentDatabase?: string, expectedDatabase?: string): boolean {
const normalizedCurrentDatabase = currentDatabase?.trim() || defaultDatabase;
const normalizedExpectedDatabase = expectedDatabase?.trim() || defaultDatabase;

return normalizedCurrentDatabase === normalizedExpectedDatabase;
}

private async validateProfile(connectionProfile?: IConnectionDialogProfile): Promise<string[]> {
if (!connectionProfile) {
connectionProfile = this.state.connectionProfile;
Expand Down
76 changes: 61 additions & 15 deletions extensions/mssql/src/models/connectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,13 +393,18 @@ export class ConnectionStore {
*
* @returns the array of connections, empty if none are found
*/
public getRecentlyUsedConnections(): IConnectionInfo[] {
public getRecentlyUsedConnections(limit?: number): IConnectionInfo[] {
let configValues = this._context.globalState.get<IConnectionInfo[]>(
Constants.configRecentConnections,
);
if (!configValues) {
configValues = [];
}

if (limit && limit > 0) {
Comment thread
aasimkhan30 marked this conversation as resolved.
return configValues.slice(0, limit);
}

return configValues;
}

Expand All @@ -420,7 +425,10 @@ export class ConnectionStore {
// Remove the connection from the list if it already exists
configValues = configValues.filter(
(value) =>
!Utils.isSameProfile(<IConnectionProfile>value, <IConnectionProfile>conn),
!self.isSameRecentConnectionEntry(
value as IConnectionProfile,
conn as IConnectionProfile,
),
);

// Add the connection to the front of the list, taking care to clear out the password field
Expand Down Expand Up @@ -493,7 +501,7 @@ export class ConnectionStore {

// Remove the connection from the list if it already exists
configValues = configValues.filter(
(value) => !Utils.isSameProfile(<IConnectionProfile>value, conn),
(value) => !self.isSameRecentConnectionEntry(value as IConnectionProfile, conn),
);

// Remove any saved password
Expand Down Expand Up @@ -665,6 +673,7 @@ export class ConnectionStore {

public async readAllConnections(
includeRecentConnections: boolean = false,
recentConnectionsLimit?: number,
): Promise<IConnectionProfileWithSource[]> {
let connResults: IConnectionProfileWithSource[] = [];

Expand All @@ -680,22 +689,33 @@ export class ConnectionStore {

// Include recent connections, if specified
if (includeRecentConnections) {
const recentConnections = this.getRecentlyUsedConnections().map((c) => {
const conn = c as IConnectionProfileWithSource;
conn.profileSource = CredentialsQuickPickItemType.Mru;
return conn;
});
const recentConnections = this.getRecentlyUsedConnections(recentConnectionsLimit).map(
(c) => {
const conn = c as IConnectionProfileWithSource;
conn.profileSource = CredentialsQuickPickItemType.Mru;
return conn;
},
);

connResults = connResults.concat(recentConnections);
}

// Deduplicate connections by ID
const uniqueConnections = new Map<string, IConnectionProfileWithSource>();
// Deduplicate connections within the same source while allowing the same connection
// to appear in both the saved and MRU lists.
const uniqueConnections: IConnectionProfileWithSource[] = [];
let dupeCount = 0;

for (const conn of connResults) {
if (!uniqueConnections.has(conn.id)) {
uniqueConnections.set(conn.id, conn);
const isDuplicate = uniqueConnections.some(
(existingConn) =>
existingConn.profileSource === conn.profileSource &&
(conn.profileSource === CredentialsQuickPickItemType.Mru
? this.isSameRecentConnectionEntry(existingConn, conn)
: Utils.isSameProfile(existingConn, conn)),
);
Comment thread
aasimkhan30 marked this conversation as resolved.

if (!isDuplicate) {
uniqueConnections.push(conn);
} else {
Comment thread
aasimkhan30 marked this conversation as resolved.
dupeCount++;
this._logger.verbose(
Expand All @@ -704,12 +724,16 @@ export class ConnectionStore {
}
Comment thread
aasimkhan30 marked this conversation as resolved.
}

connResults = Array.from(uniqueConnections.values());
connResults = uniqueConnections;

let logMessage = `readAllConnections(): ${connResults.length} connections found`;

if (includeRecentConnections) {
logMessage += ` (${configConnections.length} from config, ${connResults.length - configConnections.length} from recent)`;

if (typeof recentConnectionsLimit === "number" && recentConnectionsLimit > 0) {
logMessage += `; recent limit ${recentConnectionsLimit}`;
}
} else {
logMessage += "; excluded recent";
}
Expand Down Expand Up @@ -738,9 +762,13 @@ export class ConnectionStore {

public async getConnectionQuickpickItems(
includeRecentConnections: boolean = false,
recentConnectionsLimit?: number,
): Promise<IConnectionCredentialsQuickPickItem[]> {
let output: IConnectionCredentialsQuickPickItem[] = [];
const connections = await this.readAllConnections(includeRecentConnections);
const connections = await this.readAllConnections(
includeRecentConnections,
recentConnectionsLimit,
);

output = connections.map((c) => {
return this.createQuickPickItem(c, c.profileSource);
Expand All @@ -757,7 +785,7 @@ export class ConnectionStore {
return quickPickItems;
}

private getMaxRecentConnectionsCount(): number {
public getMaxRecentConnectionsCount(): number {
let config = this._vscodeWrapper.getConfiguration(Constants.extensionConfigSectionName);

let maxConnections: number = config[Constants.configMaxRecentConnections];
Expand All @@ -766,4 +794,22 @@ export class ConnectionStore {
}
return maxConnections;
}

private isSameRecentConnectionEntry(
currentProfile: IConnectionProfile,
expectedProfile: IConnectionProfile,
): boolean {
const currentRecentProfile = {
...currentProfile,
id: undefined,
profileName: undefined,
} as IConnectionProfile;
const expectedRecentProfile = {
...expectedProfile,
id: undefined,
profileName: undefined,
} as IConnectionProfile;

return Utils.isSameProfile(currentRecentProfile, expectedRecentProfile);
}
}
2 changes: 1 addition & 1 deletion extensions/mssql/src/webviews/common/locConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export class LocConstants {
}),
default: l10n.t("Default"),
deleteSavedConnection: l10n.t("Delete saved connection"),
removeRecentConnection: l10n.t("Remove recent connection"),
removeRecentConnection: l10n.t("Clear from recent connections list"),
copyConnectionString: l10n.t("Copy connection string to clipboard"),
pasteConnectionString: l10n.t("Paste connection string from clipboard"),
copy: l10n.t("Copy"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IConnectionDialogProfile } from "../../../sharedInterfaces/connectionDialog";

const redactConnectionStringSecrets = (connectionString?: string): string => {
if (!connectionString) {
return "";
}

return connectionString.replace(
/((?:password|pwd|access token)\s*=\s*)(?:'[^']*'|"[^"]*"|\{[^}]*\}|[^;]*)/gi,
"$1<redacted>",
);
};

export const getConnectionCardKey = (connection: IConnectionDialogProfile): string => {
return [
connection.id ?? "",
connection.server ?? "",
connection.database ?? "",
connection.authenticationType ?? "",
connection.profileName ?? "",
connection.user ?? "",
connection.accountId ?? "",
redactConnectionStringSecrets(connection.connectionString),
].join("|");
};

export const getConnectionsListKey = (connections: IConnectionDialogProfile[]): string => {
return connections.map(getConnectionCardKey).join("::");
};
Loading
Loading