-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathSqlConnectionConfig.ts
More file actions
178 lines (153 loc) · 8.01 KB
/
Copy pathSqlConnectionConfig.ts
File metadata and controls
178 lines (153 loc) · 8.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import * as core from '@actions/core';
import { parseSqlConnectionString } from '@tediousjs/connection-string';
import Constants from './Constants';
export default class SqlConnectionConfig {
private _parsedConnectionString: Record<string, string | number | boolean>;
private _rawConnectionString: string;
constructor(connectionString: string) {
this._validateConnectionString(connectionString);
this._rawConnectionString = connectionString;
this._parsedConnectionString = parseSqlConnectionString(connectionString, true, true);
this._maskSecrets();
this._validateconfig();
}
public get Server(): string {
let server = this._parsedConnectionString['data source'] as string;
// Remove port number
if (server?.includes(',')) {
server = server.split(',')[0].trim();
}
// Remove tcp protocol
if (server?.startsWith('tcp:')) {
server = server.slice(4).trim();
}
return server;
}
public get Port(): number | undefined {
const server = this._parsedConnectionString['data source'] as string;
if (server && server.includes(',')) {
return parseInt(server.split(',')[1].trim());
}
return undefined;
}
public get Database(): string {
return this._parsedConnectionString['initial catalog'] as string;
}
public get UserId(): string | undefined {
return this._parsedConnectionString['user id'] as string | undefined;
}
public get Password(): string | undefined {
return this._parsedConnectionString['password'] as string | undefined;
}
/**
* Returns the authentication type used in the connection string, with spaces removed and in lower case.
*/
public get FormattedAuthentication(): string | undefined {
const auth = this._parsedConnectionString['authentication'] as string | undefined;
return auth?.replace(/\s/g, '').toLowerCase();
}
/**
* Returns the connection string escaped by double quotes.
*/
public get EscapedConnectionString() : string {
let result = '';
// Isolate all the key value pairs from the raw connection string
// Using the raw connection string instead of the parsed one to keep it as close to the original as possible
const matches = Array.from(this._rawConnectionString.matchAll(Constants.connectionStringParserRegex));
for (const match of matches) {
if (match.groups) {
const key = match.groups.key.trim();
let val = match.groups.val.trim();
// If the value is enclosed in double quotes, escape the double quotes
if (val.startsWith('"') && val.endsWith('"')) {
val = '""' + val.slice(1, -1) + '""';
}
result += `${key}=${val};`;
}
}
return result;
}
/**
* Returns the database name enclosed by quotes for use in sqlcmd commands.
* Escapes any quotes in the database name with \"
*/
public get QuotedDatabaseName(): string {
let dbName = this.Database.replace(/"/g, '\\"');
return `"${dbName}"`;
}
/**
* The basic format of a connection string includes a series of keyword/value pairs separated by semicolons.
* The equal sign (=) connects each keyword and its value. (Ex: Key1=Val1;Key2=Val2)
*
* Following rules are to be followed while passing special characters in values:
1. To include values that contain a semicolon, single-quote character, or double-quote character, the value must be enclosed in double quotation marks.
2. If the value contains both a semicolon and a double-quote character, the value can be enclosed in single quotation marks.
3. The single quotation mark is also useful if the value starts with a double-quote character. Conversely, the double quotation mark can be used if the value starts with a single quotation mark.
4. If the value contains both single-quote and double-quote characters, the quotation mark character used to enclose the value must be doubled every time it occurs within the value.
Regex used by the parser(connectionStringParserRegex) to parse the VALUE:
('[^']*(''[^']*)*') -> value enclosed with single quotes and has consecutive single quotes
|("[^"]*(""[^"]*)*") -> value enclosed with double quotes and has consecutive double quotes
|((?!['"])[^;]*)) -> value does not start with quotes does not contain any special character. Here we do a positive lookahead to ensure that the value doesn't start with quotes which should have been handled in previous cases
Regex used to validate the entire connection string:
A connection string is considered valid if it is a series of key/value pairs separated by semicolons. Each key/value pair must satisy the connectionStringParserRegex to ensure it is a valid key/value pair.
^[;\s]*{KeyValueRegex}(;[;\s]*{KeyValueRegex})*[;\s]*$
where KeyValueRegex = ([\w\s]+=(?:('[^']*(''[^']*)*')|("[^"]*(""[^"]*)*")|((?!['"])[^;]*))))
*/
private _validateConnectionString(connectionString: string) {
if (!Constants.connectionStringTester.test(connectionString)) {
throw new Error('Invalid connection string. A valid connection string is a series of keyword/value pairs separated by semi-colons. If there are any special characters like quotes or semi-colons in the keyword value, enclose the value within quotes. Refer to this link for more info on connection string https://aka.ms/sqlconnectionstring');
}
}
/**
* Mask sensitive parts of the connection settings so they don't show up in the Github logs.
*/
private _maskSecrets(): void {
// User ID could be client ID in some authentication types
if (this.UserId) {
core.setSecret(this.UserId);
}
if (this.Password) {
core.setSecret(this.Password);
}
}
private _validateconfig(): void {
if (!this.Server) {
throw new Error(`Invalid connection string. Please ensure 'Server' or 'Data Source' is provided in the connection string.`);
}
if (!this.Database) {
throw new Error(`Invalid connection string. Please ensure 'Database' or 'Initial Catalog' is provided in the connection string.`);
}
switch (this.FormattedAuthentication) {
case undefined:
case 'sqlpassword': {
// SQL password
if (!this.UserId) {
throw new Error(`Invalid connection string. Please ensure 'User' or 'User ID' is provided in the connection string.`);
}
if (!this.Password) {
throw new Error(`Invalid connection string. Please ensure 'Password' is provided in the connection string.`);
}
break;
}
case 'activedirectorypassword': {
if (!this.UserId) {
throw new Error(`Invalid connection string. Please ensure 'User' or 'User ID' is provided in the connection string.`);
}
if (!this.Password) {
throw new Error(`Invalid connection string. Please ensure 'Password' is provided in the connection string.`);
}
break;
}
case 'activedirectoryserviceprincipal': {
// User ID is client ID and password is secret
if (!this.UserId) {
throw new Error(`Invalid connection string. Please ensure client ID is provided in the 'User' or 'User ID' field of the connection string.`);
}
if (!this.Password) {
throw new Error(`Invalid connection string. Please ensure client secret is provided in the 'Password' field of the connection string.`);
}
break;
}
}
}
}