@oneuptime/common 12.0.21 → 12.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +564 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +38 -0
- package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
- package/Server/Services/UserService.ts +42 -0
- package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
- package/Server/Utils/TwoFactorBackupCode.ts +266 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
- package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
- package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
- package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
- package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
- package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
- package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
- package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
- package/Types/Permission.ts +47 -0
- package/Types/UserAuthenticationStatus.ts +16 -0
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
- package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
- package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +589 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +28 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js +277 -0
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
- package/build/dist/Server/Services/UserService.js +39 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/Permission.js +41 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
|
2
|
+
import User from "./User";
|
|
3
|
+
import Route from "../../Types/API/Route";
|
|
4
|
+
import AllowAccessIfSubscriptionIsUnpaid from "../../Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid";
|
|
5
|
+
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
|
6
|
+
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
|
7
|
+
import AllowUserQueryWithoutTenant from "../../Types/Database/AllowUserQueryWithoutTenant";
|
|
8
|
+
import ColumnLength from "../../Types/Database/ColumnLength";
|
|
9
|
+
import ColumnType from "../../Types/Database/ColumnType";
|
|
10
|
+
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
|
11
|
+
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
|
|
12
|
+
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
|
13
|
+
import TableColumn from "../../Types/Database/TableColumn";
|
|
14
|
+
import TableColumnType from "../../Types/Database/TableColumnType";
|
|
15
|
+
import TableMetadata from "../../Types/Database/TableMetadata";
|
|
16
|
+
import IconProp from "../../Types/Icon/IconProp";
|
|
17
|
+
import ObjectID from "../../Types/ObjectID";
|
|
18
|
+
import Permission from "../../Types/Permission";
|
|
19
|
+
import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* One single-use recovery code, for the sign-in a user cannot otherwise
|
|
23
|
+
* complete: the authenticator app went down with the phone, or the security
|
|
24
|
+
* key is gone.
|
|
25
|
+
*
|
|
26
|
+
* ONE ROW PER CODE, NOT ONE ROW PER USER
|
|
27
|
+
*
|
|
28
|
+
* The alternative -- a single row holding a JSON array of digests -- would
|
|
29
|
+
* make consuming a code a read-modify-write of the whole array, and two
|
|
30
|
+
* sign-in attempts arriving together would each write back a copy of the array
|
|
31
|
+
* missing only their own code, so one of the two consumed codes would come
|
|
32
|
+
* back to life. A row per code turns consumption into one conditional UPDATE
|
|
33
|
+
* that Postgres settles for us (see UserTwoFactorBackupCodeService.consumeCode).
|
|
34
|
+
*
|
|
35
|
+
* IT IS NOT A SECOND FACTOR, AND IS DELIBERATELY NOT COUNTED AS ONE
|
|
36
|
+
*
|
|
37
|
+
* UserService.countVerifiedTwoFactorAuthMethods sums TOTP apps and security
|
|
38
|
+
* keys and does NOT look here. That is load-bearing rather than an oversight:
|
|
39
|
+
* an account whose only remaining "method" was a backup code would read as
|
|
40
|
+
* fully configured, so login would send it to the two factor CHALLENGE screen
|
|
41
|
+
* instead of through enrolment -- and the user, having no authenticator left
|
|
42
|
+
* to enrol from, would burn recovery codes one per sign-in until they ran out
|
|
43
|
+
* and then be locked out for good. Backup codes are the way BACK IN to an
|
|
44
|
+
* account that has a factor it cannot currently reach; they are not the
|
|
45
|
+
* factor.
|
|
46
|
+
*
|
|
47
|
+
* NOTHING HERE IS WRITABLE THROUGH THE CRUD API
|
|
48
|
+
*
|
|
49
|
+
* Every table permission except read is empty, so codes can only be minted by
|
|
50
|
+
* the service (which writes as root) -- never by a caller choosing their own.
|
|
51
|
+
* A user who could POST their own code has turned a stolen password into a
|
|
52
|
+
* permanent second factor of their own devising.
|
|
53
|
+
*/
|
|
54
|
+
@EnableDocumentation({
|
|
55
|
+
isMasterAdminApiDocs: true,
|
|
56
|
+
})
|
|
57
|
+
@AllowAccessIfSubscriptionIsUnpaid()
|
|
58
|
+
@TableAccessControl({
|
|
59
|
+
/*
|
|
60
|
+
* Minting is POST /user-two-factor-backup-code/generate, which regenerates
|
|
61
|
+
* the whole set at once. There is no meaningful "create one code" operation
|
|
62
|
+
* and, more to the point, no safe one: the code is the credential, so a
|
|
63
|
+
* caller supplying it is choosing their own recovery secret.
|
|
64
|
+
*/
|
|
65
|
+
create: [],
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Read is open to the owner so the profile page can count what is left. It
|
|
69
|
+
* exposes nothing: `codeHash` denies read to everybody, so a row read here
|
|
70
|
+
* carries only its id, its `usedAt` and its owner.
|
|
71
|
+
*/
|
|
72
|
+
read: [Permission.CurrentUser],
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Consumption is a server-side write on the login path and regeneration
|
|
76
|
+
* replaces the set wholesale. A user-driven update or delete could only
|
|
77
|
+
* make a code stop working, which is the same thing regeneration does more
|
|
78
|
+
* clearly, and would give an attacker holding a session a way to quietly
|
|
79
|
+
* strip the account's recovery options.
|
|
80
|
+
*/
|
|
81
|
+
delete: [],
|
|
82
|
+
update: [],
|
|
83
|
+
})
|
|
84
|
+
@CrudApiEndpoint(new Route("/user-two-factor-backup-code"))
|
|
85
|
+
@Entity({
|
|
86
|
+
name: "UserTwoFactorBackupCode",
|
|
87
|
+
})
|
|
88
|
+
@TableMetadata({
|
|
89
|
+
tableName: "UserTwoFactorBackupCode",
|
|
90
|
+
singularName: "Two Factor Backup Code",
|
|
91
|
+
pluralName: "Two Factor Backup Codes",
|
|
92
|
+
icon: IconProp.Key,
|
|
93
|
+
tableDescription:
|
|
94
|
+
"Single-use backup codes that let a user sign in when their two factor authentication device is unavailable",
|
|
95
|
+
})
|
|
96
|
+
@AllowUserQueryWithoutTenant(true)
|
|
97
|
+
@CurrentUserCanAccessRecordBy("userId")
|
|
98
|
+
/*
|
|
99
|
+
* The entire hot path is `WHERE "userId" = ... AND "codeHash" = ...` -- the
|
|
100
|
+
* conditional UPDATE that consumes a code during a sign-in somebody is locked
|
|
101
|
+
* out of. `userId` alone leads the index so the profile page's "how many are
|
|
102
|
+
* left" count uses it too.
|
|
103
|
+
*/
|
|
104
|
+
@Index(["userId", "codeHash"])
|
|
105
|
+
class UserTwoFactorBackupCode extends BaseModel {
|
|
106
|
+
/*
|
|
107
|
+
* HMAC-SHA256 of the code, keyed by the instance's EncryptionSecret and
|
|
108
|
+
* domain separated by the owning user -- see
|
|
109
|
+
* Common/Server/Utils/TwoFactorBackupCode.ts for the construction.
|
|
110
|
+
*
|
|
111
|
+
* Read is denied to EVERYONE, including the owner and a master admin. There
|
|
112
|
+
* is no product question this column answers: the plaintext is shown once,
|
|
113
|
+
* at generation, and never again. A master admin bypasses column read
|
|
114
|
+
* permissions on some paths, which is exactly why nothing here is worth
|
|
115
|
+
* shipping to a browser even in digest form.
|
|
116
|
+
*
|
|
117
|
+
* Not UNIQUE. The digest is domain separated by user, so two users cannot
|
|
118
|
+
* collide even if they were issued the same code, and within one user
|
|
119
|
+
* `generateCodeSet` already rules duplicates out -- a unique constraint
|
|
120
|
+
* would add nothing and would turn an astronomically unlikely collision
|
|
121
|
+
* into a failed regeneration for somebody who is already having a bad day.
|
|
122
|
+
*/
|
|
123
|
+
@ColumnAccessControl({
|
|
124
|
+
create: [],
|
|
125
|
+
read: [],
|
|
126
|
+
update: [],
|
|
127
|
+
})
|
|
128
|
+
@TableColumn({
|
|
129
|
+
type: TableColumnType.ShortText,
|
|
130
|
+
canReadOnRelationQuery: false,
|
|
131
|
+
title: "Backup Code Hash",
|
|
132
|
+
description: "Keyed hash of this single-use two factor backup code",
|
|
133
|
+
hideColumnInDocumentation: true,
|
|
134
|
+
})
|
|
135
|
+
@Column({
|
|
136
|
+
type: ColumnType.ShortText,
|
|
137
|
+
length: ColumnLength.ShortText,
|
|
138
|
+
nullable: false,
|
|
139
|
+
unique: false,
|
|
140
|
+
})
|
|
141
|
+
public codeHash?: string = undefined;
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* When this code was spent, or null while it is still usable.
|
|
145
|
+
*
|
|
146
|
+
* The row is kept rather than deleted on use, for two reasons that both
|
|
147
|
+
* matter to somebody recovering an account: the profile page can say "3 of
|
|
148
|
+
* 10 remaining" instead of silently shrinking a list, and a user who is
|
|
149
|
+
* asking "did somebody else get into my account?" has a timestamp to look
|
|
150
|
+
* at rather than an absence.
|
|
151
|
+
*/
|
|
152
|
+
@ColumnAccessControl({
|
|
153
|
+
create: [],
|
|
154
|
+
read: [Permission.CurrentUser],
|
|
155
|
+
update: [],
|
|
156
|
+
})
|
|
157
|
+
@TableColumn({
|
|
158
|
+
type: TableColumnType.Date,
|
|
159
|
+
canReadOnRelationQuery: false,
|
|
160
|
+
title: "Used At",
|
|
161
|
+
description:
|
|
162
|
+
"When this backup code was used to sign in. Null while the code is still unused.",
|
|
163
|
+
})
|
|
164
|
+
@Column({
|
|
165
|
+
type: ColumnType.Date,
|
|
166
|
+
nullable: true,
|
|
167
|
+
unique: false,
|
|
168
|
+
})
|
|
169
|
+
public usedAt?: Date = undefined;
|
|
170
|
+
|
|
171
|
+
@ColumnAccessControl({
|
|
172
|
+
create: [],
|
|
173
|
+
read: [],
|
|
174
|
+
update: [],
|
|
175
|
+
})
|
|
176
|
+
@TableColumn({
|
|
177
|
+
manyToOneRelationColumn: "deletedByUserId",
|
|
178
|
+
type: TableColumnType.Entity,
|
|
179
|
+
title: "Deleted by User",
|
|
180
|
+
modelType: User,
|
|
181
|
+
description:
|
|
182
|
+
"Relation to User who deleted this object (if this object was deleted by a User)",
|
|
183
|
+
})
|
|
184
|
+
@ManyToOne(
|
|
185
|
+
() => {
|
|
186
|
+
return User;
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
cascade: false,
|
|
190
|
+
eager: false,
|
|
191
|
+
nullable: true,
|
|
192
|
+
onDelete: "SET NULL",
|
|
193
|
+
orphanedRowAction: "nullify",
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
@JoinColumn({ name: "deletedByUserId" })
|
|
197
|
+
public deletedByUser?: User = undefined;
|
|
198
|
+
|
|
199
|
+
@ColumnAccessControl({
|
|
200
|
+
create: [],
|
|
201
|
+
read: [],
|
|
202
|
+
update: [],
|
|
203
|
+
})
|
|
204
|
+
@TableColumn({
|
|
205
|
+
type: TableColumnType.ObjectID,
|
|
206
|
+
title: "Deleted by User ID",
|
|
207
|
+
description:
|
|
208
|
+
"User ID who deleted this object (if this object was deleted by a User)",
|
|
209
|
+
})
|
|
210
|
+
@Column({
|
|
211
|
+
type: ColumnType.ObjectID,
|
|
212
|
+
nullable: true,
|
|
213
|
+
transformer: ObjectID.getDatabaseTransformer(),
|
|
214
|
+
})
|
|
215
|
+
public deletedByUserId?: ObjectID = undefined;
|
|
216
|
+
|
|
217
|
+
@ColumnAccessControl({
|
|
218
|
+
create: [],
|
|
219
|
+
read: [Permission.CurrentUser],
|
|
220
|
+
update: [],
|
|
221
|
+
})
|
|
222
|
+
@TableColumn({
|
|
223
|
+
manyToOneRelationColumn: "userId",
|
|
224
|
+
type: TableColumnType.Entity,
|
|
225
|
+
title: "User",
|
|
226
|
+
modelType: User,
|
|
227
|
+
description: "Relation to User who owns this backup code",
|
|
228
|
+
})
|
|
229
|
+
@ManyToOne(
|
|
230
|
+
() => {
|
|
231
|
+
return User;
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
cascade: false,
|
|
235
|
+
eager: false,
|
|
236
|
+
nullable: true,
|
|
237
|
+
onDelete: "CASCADE",
|
|
238
|
+
orphanedRowAction: "nullify",
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
@JoinColumn({ name: "userId" })
|
|
242
|
+
public user?: User = undefined;
|
|
243
|
+
|
|
244
|
+
@ColumnAccessControl({
|
|
245
|
+
create: [],
|
|
246
|
+
read: [Permission.CurrentUser],
|
|
247
|
+
update: [],
|
|
248
|
+
})
|
|
249
|
+
@TableColumn({
|
|
250
|
+
type: TableColumnType.ObjectID,
|
|
251
|
+
title: "User ID",
|
|
252
|
+
description: "User ID who owns this backup code",
|
|
253
|
+
})
|
|
254
|
+
@Column({
|
|
255
|
+
type: ColumnType.ObjectID,
|
|
256
|
+
nullable: true,
|
|
257
|
+
transformer: ObjectID.getDatabaseTransformer(),
|
|
258
|
+
})
|
|
259
|
+
public userId?: ObjectID = undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export default UserTwoFactorBackupCode;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import ObjectID from "../../Types/ObjectID";
|
|
2
|
+
import Hostname from "../../Types/API/Hostname";
|
|
3
|
+
import Protocol from "../../Types/API/Protocol";
|
|
4
|
+
import Route from "../../Types/API/Route";
|
|
5
|
+
import URL from "../../Types/API/URL";
|
|
6
|
+
import OneUptimeDate from "../../Types/Date";
|
|
7
|
+
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
8
|
+
import { DashboardRoute } from "../../ServiceRoute";
|
|
9
|
+
import DatabaseConfig from "../DatabaseConfig";
|
|
10
|
+
import MailService from "../Services/MailService";
|
|
11
|
+
import UserService from "../Services/UserService";
|
|
12
|
+
import User from "../../Models/DatabaseModels/User";
|
|
13
|
+
import UserMiddleware from "../Middleware/UserAuthorization";
|
|
14
|
+
import UserTwoFactorBackupCodeService, {
|
|
15
|
+
Service as UserTwoFactorBackupCodeServiceType,
|
|
16
|
+
TwoFactorBackupCodeStatus,
|
|
17
|
+
} from "../Services/UserTwoFactorBackupCodeService";
|
|
18
|
+
import TwoFactorBackupCode from "../Utils/TwoFactorBackupCode";
|
|
19
|
+
import {
|
|
20
|
+
ExpressRequest,
|
|
21
|
+
ExpressResponse,
|
|
22
|
+
NextFunction,
|
|
23
|
+
OneUptimeRequest,
|
|
24
|
+
} from "../Utils/Express";
|
|
25
|
+
import BaseAPI from "./BaseAPI";
|
|
26
|
+
import UserTwoFactorBackupCode from "../../Models/DatabaseModels/UserTwoFactorBackupCode";
|
|
27
|
+
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
|
|
28
|
+
import Response from "../Utils/Response";
|
|
29
|
+
import logger from "../Utils/Logger";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The two routes a signed-in user needs to manage their recovery codes.
|
|
33
|
+
*
|
|
34
|
+
* Both act on the CALLER's own codes and nobody else's. There is deliberately
|
|
35
|
+
* no `:userId` parameter anywhere here: the owner is read from the validated
|
|
36
|
+
* access token, so there is no id for a caller to substitute and no ownership
|
|
37
|
+
* check to get wrong. `UserMiddleware.getUserMiddleware` is what puts it
|
|
38
|
+
* there.
|
|
39
|
+
*/
|
|
40
|
+
export default class UserTwoFactorBackupCodeAPI extends BaseAPI<
|
|
41
|
+
UserTwoFactorBackupCode,
|
|
42
|
+
UserTwoFactorBackupCodeServiceType
|
|
43
|
+
> {
|
|
44
|
+
public constructor() {
|
|
45
|
+
super(UserTwoFactorBackupCode, UserTwoFactorBackupCodeService);
|
|
46
|
+
|
|
47
|
+
const crudApiPath: string =
|
|
48
|
+
new this.entityType().getCrudApiPath()?.toString() || "";
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* Mint a fresh set and return the plaintext codes.
|
|
52
|
+
*
|
|
53
|
+
* THIS RESPONSE IS THE ONLY COPY. Nothing is stored but keyed digests, so
|
|
54
|
+
* if the user closes the tab the codes are gone -- not recoverable by
|
|
55
|
+
* them, by an operator, or by anybody with the database. The UI is
|
|
56
|
+
* expected to make that unmissable, and the count of a previous set is
|
|
57
|
+
* returned alongside so the page can say what was just invalidated.
|
|
58
|
+
*
|
|
59
|
+
* It is a POST because it is destructive: calling it throws away every
|
|
60
|
+
* code the user was previously holding. A GET that silently voided a
|
|
61
|
+
* printed list -- on a browser prefetch, say -- would be a lockout with no
|
|
62
|
+
* user action behind it.
|
|
63
|
+
*
|
|
64
|
+
* NOT gated on `enableTwoFactorAuth`. Generating codes before turning two
|
|
65
|
+
* factor auth on is the sensible order to do it in, and refusing here
|
|
66
|
+
* would push users into the exact sequence -- enable first, find recovery
|
|
67
|
+
* later -- that produces the lost-device support ticket this whole feature
|
|
68
|
+
* exists to prevent.
|
|
69
|
+
*/
|
|
70
|
+
this.router.post(
|
|
71
|
+
`${crudApiPath}/generate`,
|
|
72
|
+
UserMiddleware.getUserMiddleware,
|
|
73
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
74
|
+
try {
|
|
75
|
+
const userId: ObjectID =
|
|
76
|
+
UserTwoFactorBackupCodeAPI.getCurrentUserId(req);
|
|
77
|
+
|
|
78
|
+
const previousStatus: TwoFactorBackupCodeStatus =
|
|
79
|
+
await UserTwoFactorBackupCodeService.getStatusForUser({
|
|
80
|
+
userId: userId,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const codes: Array<string> =
|
|
84
|
+
await UserTwoFactorBackupCodeService.regenerateForUser({
|
|
85
|
+
userId: userId,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* The codes themselves are NEVER logged -- they are sign-in
|
|
90
|
+
* credentials, and logger output reaches stdout, the recent-log
|
|
91
|
+
* buffer and telemetry at once. The fact that a set was minted is
|
|
92
|
+
* worth recording: it is how an operator later reconstructs "who
|
|
93
|
+
* invalidated the codes this user says they had".
|
|
94
|
+
*/
|
|
95
|
+
logger.info(
|
|
96
|
+
`Two factor backup codes regenerated for user: ${userId.toString()}`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* Tell the account holder, at their address rather than in this
|
|
101
|
+
* browser.
|
|
102
|
+
*
|
|
103
|
+
* This route is the destructive one and it is the one with the least
|
|
104
|
+
* standing in front of it: a session alone -- no password, no second
|
|
105
|
+
* factor -- silently voids every recovery code the user is holding.
|
|
106
|
+
* Somebody who has stolen a session can therefore remove the owner's
|
|
107
|
+
* way back in and leave no trace the owner would ever look at, and
|
|
108
|
+
* the owner finds out at the next sign-in they cannot complete.
|
|
109
|
+
*
|
|
110
|
+
* The mail does not prevent that; it makes it visible, which is the
|
|
111
|
+
* same bargain `sendTwoFactorBackupCodeUsedEmail` strikes on the
|
|
112
|
+
* login path. Re-prompting for the password here would be stronger
|
|
113
|
+
* still, and is the obvious next step -- it needs a re-authentication
|
|
114
|
+
* mechanism this codebase does not have yet, so it is deliberately
|
|
115
|
+
* not invented on this route alone.
|
|
116
|
+
*
|
|
117
|
+
* Detached and never awaited: an unreachable mail server must not
|
|
118
|
+
* fail a regeneration that has already replaced the user's codes,
|
|
119
|
+
* because the response carries the only copy of the new ones.
|
|
120
|
+
*/
|
|
121
|
+
UserTwoFactorBackupCodeAPI.notifyCodesRegenerated({
|
|
122
|
+
userId: userId,
|
|
123
|
+
newCodeCount: codes.length,
|
|
124
|
+
}).catch((err: Error) => {
|
|
125
|
+
logger.error(err);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return Response.sendJsonObjectResponse(req, res, {
|
|
129
|
+
/*
|
|
130
|
+
* Hyphenated for the page to render as-is. The verify route
|
|
131
|
+
* normalizes whatever comes back, so a user may type them with
|
|
132
|
+
* the hyphen, without it, or with whatever spacing their password
|
|
133
|
+
* manager pasted in.
|
|
134
|
+
*/
|
|
135
|
+
codes: codes.map((code: string) => {
|
|
136
|
+
return TwoFactorBackupCode.formatForDisplay(code);
|
|
137
|
+
}),
|
|
138
|
+
replacedCodeCount: previousStatus.total,
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return next(err);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* How many codes the caller has left.
|
|
148
|
+
*
|
|
149
|
+
* Carries no code material of any kind -- three numbers and a date -- so
|
|
150
|
+
* it is safe for the profile page to poll and safe to render anywhere a
|
|
151
|
+
* session already exists.
|
|
152
|
+
*/
|
|
153
|
+
this.router.get(
|
|
154
|
+
`${crudApiPath}/status`,
|
|
155
|
+
UserMiddleware.getUserMiddleware,
|
|
156
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
157
|
+
try {
|
|
158
|
+
const userId: ObjectID =
|
|
159
|
+
UserTwoFactorBackupCodeAPI.getCurrentUserId(req);
|
|
160
|
+
|
|
161
|
+
const status: TwoFactorBackupCodeStatus =
|
|
162
|
+
await UserTwoFactorBackupCodeService.getStatusForUser({
|
|
163
|
+
userId: userId,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* A stale "you have 8 codes left" served out of a browser cache
|
|
168
|
+
* after the user has just regenerated is a user who thinks the
|
|
169
|
+
* button did nothing.
|
|
170
|
+
*/
|
|
171
|
+
Response.setNoCacheHeaders(res);
|
|
172
|
+
|
|
173
|
+
return Response.sendJsonObjectResponse(req, res, {
|
|
174
|
+
total: status.total,
|
|
175
|
+
unused: status.unused,
|
|
176
|
+
generatedAt: status.generatedAt
|
|
177
|
+
? status.generatedAt.toISOString()
|
|
178
|
+
: null,
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return next(err);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Mail the owner that their backup codes were replaced.
|
|
189
|
+
*
|
|
190
|
+
* Separate from the handler so the handler stays readable, and `private
|
|
191
|
+
* static` because nothing outside this route should be announcing a
|
|
192
|
+
* regeneration that did not happen here.
|
|
193
|
+
*
|
|
194
|
+
* Reads the address itself rather than taking it from the request: the only
|
|
195
|
+
* thing the caller proved is which user they are, and the address this goes
|
|
196
|
+
* to has to be the one on the account, not one they supplied.
|
|
197
|
+
*/
|
|
198
|
+
private static async notifyCodesRegenerated(data: {
|
|
199
|
+
userId: ObjectID;
|
|
200
|
+
newCodeCount: number;
|
|
201
|
+
}): Promise<void> {
|
|
202
|
+
const user: User | null = await UserService.findOneById({
|
|
203
|
+
id: data.userId,
|
|
204
|
+
select: {
|
|
205
|
+
email: true,
|
|
206
|
+
},
|
|
207
|
+
props: {
|
|
208
|
+
isRoot: true,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!user || !user.email) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const host: Hostname = await DatabaseConfig.getHost();
|
|
217
|
+
const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();
|
|
218
|
+
|
|
219
|
+
await MailService.sendMail({
|
|
220
|
+
toEmail: user.email,
|
|
221
|
+
subject: "Your OneUptime two factor backup codes were replaced",
|
|
222
|
+
templateType: EmailTemplateType.TwoFactorBackupCodesRegenerated,
|
|
223
|
+
vars: {
|
|
224
|
+
generatedAt: OneUptimeDate.getCurrentDateAsFormattedString(),
|
|
225
|
+
newCodeCount: data.newCodeCount.toString(),
|
|
226
|
+
twoFactorAuthUrl: new URL(
|
|
227
|
+
httpProtocol,
|
|
228
|
+
host,
|
|
229
|
+
new Route(DashboardRoute.toString()).addRoute(
|
|
230
|
+
"/user-profile/two-factor-auth",
|
|
231
|
+
),
|
|
232
|
+
).toString(),
|
|
233
|
+
homeUrl: new URL(httpProtocol, host).toString(),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The signed-in caller, from the token the middleware validated.
|
|
240
|
+
*
|
|
241
|
+
* Throws rather than returning null: every route in this file acts on the
|
|
242
|
+
* caller's own credentials, so "no caller" is not a case any of them can
|
|
243
|
+
* meaningfully proceed with. The middleware ahead of them should already
|
|
244
|
+
* have refused, which makes this a second lock rather than the first.
|
|
245
|
+
*/
|
|
246
|
+
private static getCurrentUserId(req: ExpressRequest): ObjectID {
|
|
247
|
+
const userId: ObjectID | undefined = (req as OneUptimeRequest)
|
|
248
|
+
.userAuthorization?.userId;
|
|
249
|
+
|
|
250
|
+
if (!userId) {
|
|
251
|
+
throw new NotAuthenticatedException(
|
|
252
|
+
"You are not authenticated. Please sign in.",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return userId;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Auto-import rules for network device discovery (issue #3378): a rule table
|
|
5
|
+
* whose conditions decide which discovered hosts become Network Devices with
|
|
6
|
+
* no manual review step, plus a processed-marker column on the scan so the
|
|
7
|
+
* worker that evaluates the rules knows which results it has already seen.
|
|
8
|
+
*/
|
|
9
|
+
export class AddNetworkDeviceAutoImportRule1789100000000
|
|
10
|
+
implements MigrationInterface
|
|
11
|
+
{
|
|
12
|
+
public name: string = "AddNetworkDeviceAutoImportRule1789100000000";
|
|
13
|
+
|
|
14
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
15
|
+
await queryRunner.query(
|
|
16
|
+
`CREATE TABLE "NetworkDeviceAutoImportRule" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "description" character varying(500), "isEnabled" boolean NOT NULL DEFAULT true, "ipMatchTarget" character varying(100), "sysNamePattern" character varying(500), "sysDescrPattern" character varying(500), "includePingOnlyHosts" boolean NOT NULL DEFAULT false, "isExclusion" boolean NOT NULL DEFAULT false, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_network_device_auto_import_rule_id" PRIMARY KEY ("_id"))`,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
for (const column of ["projectId", "name", "isEnabled", "isExclusion"]) {
|
|
20
|
+
await queryRunner.query(
|
|
21
|
+
`CREATE INDEX "IDX_network_device_auto_import_rule_${column}" ON "NetworkDeviceAutoImportRule" ("${column}")`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await queryRunner.query(
|
|
26
|
+
`ALTER TABLE "NetworkDeviceAutoImportRule" ADD CONSTRAINT "FK_nd_auto_import_rule_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
27
|
+
);
|
|
28
|
+
await queryRunner.query(
|
|
29
|
+
`ALTER TABLE "NetworkDeviceAutoImportRule" ADD CONSTRAINT "FK_nd_auto_import_rule_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
30
|
+
);
|
|
31
|
+
await queryRunner.query(
|
|
32
|
+
`ALTER TABLE "NetworkDeviceAutoImportRule" ADD CONSTRAINT "FK_nd_auto_import_rule_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await queryRunner.query(
|
|
36
|
+
`ALTER TABLE "NetworkDeviceDiscoveryScan" ADD "autoImportProcessedAt" TIMESTAMP WITH TIME ZONE`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* Backfill EVERY existing scan as already-processed, making the feature
|
|
41
|
+
* forward-only. One-shot scans stay "Completed" forever, so without this
|
|
42
|
+
* every scan ever run would sit unprocessed and the first rule a project
|
|
43
|
+
* creates would mass-import months-old results — the opposite of what an
|
|
44
|
+
* operator writing a rule about FUTURE discoveries intends. Old results
|
|
45
|
+
* remain reachable on purpose through the rule's explicit "Run Now".
|
|
46
|
+
*/
|
|
47
|
+
await queryRunner.query(
|
|
48
|
+
`UPDATE "NetworkDeviceDiscoveryScan" SET "autoImportProcessedAt" = now()`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
53
|
+
await queryRunner.query(
|
|
54
|
+
`ALTER TABLE "NetworkDeviceDiscoveryScan" DROP COLUMN "autoImportProcessedAt"`,
|
|
55
|
+
);
|
|
56
|
+
await queryRunner.query(
|
|
57
|
+
`DROP TABLE IF EXISTS "NetworkDeviceAutoImportRule"`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Single-use two factor backup codes, so losing an authenticator app or a
|
|
5
|
+
* security key stops being an account lockout that only a master admin can
|
|
6
|
+
* undo.
|
|
7
|
+
*
|
|
8
|
+
* One row per code rather than an array column on User: consuming a code is
|
|
9
|
+
* then one conditional UPDATE that Postgres serialises for us, instead of a
|
|
10
|
+
* read-modify-write of an array where two simultaneous sign-ins would each
|
|
11
|
+
* write back a copy missing only their own code.
|
|
12
|
+
*
|
|
13
|
+
* `codeHash` holds an HMAC-SHA256 keyed by the instance EncryptionSecret and
|
|
14
|
+
* domain separated by the owning user -- never a code in the clear. It is a
|
|
15
|
+
* plain varchar rather than a `hashed: true` column because the write path's
|
|
16
|
+
* hashing lanes do neither of those things: the salted lane is scrypt (right
|
|
17
|
+
* for a guessable password, pure cost for a 2^50 random code) and the unsalted
|
|
18
|
+
* lane is a bare SHA-256 with no per-user binding. See
|
|
19
|
+
* Common/Server/Utils/TwoFactorBackupCode.ts.
|
|
20
|
+
*
|
|
21
|
+
* The (userId, codeHash) index is the sign-in lookup, which happens while
|
|
22
|
+
* somebody is locked out and waiting. `usedAt` is deliberately not in it: a
|
|
23
|
+
* user holds ten rows, so Postgres filters those in memory for nothing.
|
|
24
|
+
*/
|
|
25
|
+
export class AddUserTwoFactorBackupCode1789100000000
|
|
26
|
+
implements MigrationInterface
|
|
27
|
+
{
|
|
28
|
+
public name: string = "AddUserTwoFactorBackupCode1789100000000";
|
|
29
|
+
|
|
30
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
31
|
+
await queryRunner.query(
|
|
32
|
+
`CREATE TABLE "UserTwoFactorBackupCode" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "codeHash" character varying(100) NOT NULL, "usedAt" TIMESTAMP WITH TIME ZONE, "deletedByUserId" uuid, "userId" uuid, CONSTRAINT "PK_8007aa681bcad47b88b906319ed" PRIMARY KEY ("_id"))`,
|
|
33
|
+
);
|
|
34
|
+
await queryRunner.query(
|
|
35
|
+
`CREATE INDEX "IDX_19a5937a4f6bd993383b31f3a2" ON "UserTwoFactorBackupCode" ("userId", "codeHash") `,
|
|
36
|
+
);
|
|
37
|
+
await queryRunner.query(
|
|
38
|
+
`ALTER TABLE "UserTwoFactorBackupCode" ADD CONSTRAINT "FK_b6f9bfb29ab554076a2d8dde717" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
|
39
|
+
);
|
|
40
|
+
/*
|
|
41
|
+
* CASCADE, not SET NULL. An orphaned row would be a live sign-in
|
|
42
|
+
* credential for an account that no longer exists -- and, with `userId`
|
|
43
|
+
* null, invisible to every query in the service, which all key on the
|
|
44
|
+
* owner.
|
|
45
|
+
*/
|
|
46
|
+
await queryRunner.query(
|
|
47
|
+
`ALTER TABLE "UserTwoFactorBackupCode" ADD CONSTRAINT "FK_dd1f93f94cd3a4f39435b56d944" FOREIGN KEY ("userId") REFERENCES "User"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
52
|
+
await queryRunner.query(
|
|
53
|
+
`ALTER TABLE "UserTwoFactorBackupCode" DROP CONSTRAINT "FK_dd1f93f94cd3a4f39435b56d944"`,
|
|
54
|
+
);
|
|
55
|
+
await queryRunner.query(
|
|
56
|
+
`ALTER TABLE "UserTwoFactorBackupCode" DROP CONSTRAINT "FK_b6f9bfb29ab554076a2d8dde717"`,
|
|
57
|
+
);
|
|
58
|
+
await queryRunner.query(
|
|
59
|
+
`DROP INDEX "public"."IDX_19a5937a4f6bd993383b31f3a2"`,
|
|
60
|
+
);
|
|
61
|
+
await queryRunner.query(`DROP TABLE "UserTwoFactorBackupCode"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Vendor-based auto-import conditions (issue #3378, phase 2): the probe now
|
|
5
|
+
* reports the full SNMP system group per discovered host (sysObjectId,
|
|
6
|
+
* sysLocation, sysContact, sysUpTimeSeconds ride inside the scan's existing
|
|
7
|
+
* discoveredDevices jsonb, so they need no column), and a rule can match on
|
|
8
|
+
* the sysObjectID — the vendor's registered enterprise OID.
|
|
9
|
+
*/
|
|
10
|
+
export class AddSysObjectIdPatternToAutoImportRule1789200000000
|
|
11
|
+
implements MigrationInterface
|
|
12
|
+
{
|
|
13
|
+
public name: string = "AddSysObjectIdPatternToAutoImportRule1789200000000";
|
|
14
|
+
|
|
15
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
16
|
+
await queryRunner.query(
|
|
17
|
+
`ALTER TABLE "NetworkDeviceAutoImportRule" ADD "sysObjectIdPattern" character varying(500)`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
22
|
+
await queryRunner.query(
|
|
23
|
+
`ALTER TABLE "NetworkDeviceAutoImportRule" DROP COLUMN "sysObjectIdPattern"`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|