@oneuptime/common 8.0.5587 → 9.0.5595

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.
Files changed (28) hide show
  1. package/Models/DatabaseModels/StatusPage.ts +73 -0
  2. package/Server/API/StatusPageAPI.ts +79 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1763643080445-MigrationName.ts +23 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/StatusPageService.ts +63 -0
  6. package/Server/Utils/Cookie.ts +48 -0
  7. package/Types/CookieName.ts +1 -0
  8. package/Types/Exception/MasterPasswordRequiredException.ts +7 -0
  9. package/Types/StatusPage/MasterPassword.ts +10 -0
  10. package/build/dist/Models/DatabaseModels/StatusPage.js +74 -0
  11. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  12. package/build/dist/Server/API/StatusPageAPI.js +80 -28
  13. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  14. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763643080445-MigrationName.js +14 -0
  15. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1763643080445-MigrationName.js.map +1 -0
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  18. package/build/dist/Server/Services/StatusPageService.js +38 -0
  19. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  20. package/build/dist/Server/Utils/Cookie.js +36 -0
  21. package/build/dist/Server/Utils/Cookie.js.map +1 -1
  22. package/build/dist/Types/CookieName.js +1 -0
  23. package/build/dist/Types/CookieName.js.map +1 -1
  24. package/build/dist/Types/Exception/MasterPasswordRequiredException.js +7 -0
  25. package/build/dist/Types/Exception/MasterPasswordRequiredException.js.map +1 -0
  26. package/build/dist/Types/StatusPage/MasterPassword.js +5 -0
  27. package/build/dist/Types/StatusPage/MasterPassword.js.map +1 -0
  28. package/package.json +1 -1
@@ -30,6 +30,7 @@ import { JSONObject } from "../../Types/JSON";
30
30
  import ObjectID from "../../Types/ObjectID";
31
31
  import Permission from "../../Types/Permission";
32
32
  import Timezone from "../../Types/Timezone";
33
+ import HashedString from "../../Types/HashedString";
33
34
  import {
34
35
  Column,
35
36
  Entity,
@@ -883,6 +884,78 @@ export default class StatusPage extends BaseModel {
883
884
  })
884
885
  public isPublicStatusPage?: boolean = undefined;
885
886
 
887
+ @ColumnAccessControl({
888
+ create: [
889
+ Permission.ProjectOwner,
890
+ Permission.ProjectAdmin,
891
+ Permission.ProjectMember,
892
+ Permission.CreateProjectStatusPage,
893
+ ],
894
+ read: [
895
+ Permission.ProjectOwner,
896
+ Permission.ProjectAdmin,
897
+ Permission.ProjectMember,
898
+ Permission.ReadProjectStatusPage,
899
+ ],
900
+ update: [
901
+ Permission.ProjectOwner,
902
+ Permission.ProjectAdmin,
903
+ Permission.ProjectMember,
904
+ Permission.EditProjectStatusPage,
905
+ ],
906
+ })
907
+ @TableColumn({
908
+ isDefaultValueColumn: true,
909
+ type: TableColumnType.Boolean,
910
+ title: "Enable Master Password",
911
+ description:
912
+ "Require visitors to enter a master password before viewing a private status page.",
913
+ defaultValue: false,
914
+ })
915
+ @Column({
916
+ type: ColumnType.Boolean,
917
+ default: false,
918
+ })
919
+ public enableMasterPassword?: boolean = undefined;
920
+
921
+ @ColumnAccessControl({
922
+ create: [
923
+ Permission.ProjectOwner,
924
+ Permission.ProjectAdmin,
925
+ Permission.ProjectMember,
926
+ Permission.CreateProjectStatusPage,
927
+ ],
928
+
929
+ // This is a hashed column. So, reading the value is does not affect anything.
930
+ read: [
931
+ Permission.ProjectOwner,
932
+ Permission.ProjectAdmin,
933
+ Permission.ProjectMember,
934
+ Permission.ReadProjectStatusPage,
935
+ ],
936
+ update: [
937
+ Permission.ProjectOwner,
938
+ Permission.ProjectAdmin,
939
+ Permission.ProjectMember,
940
+ Permission.EditProjectStatusPage,
941
+ ],
942
+ })
943
+ @TableColumn({
944
+ title: "Master Password",
945
+ description:
946
+ "Password required to unlock a private status page. This value is stored as a secure hash.",
947
+ hashed: true,
948
+ type: TableColumnType.HashedString,
949
+ placeholder: "Enter a new master password",
950
+ })
951
+ @Column({
952
+ type: ColumnType.HashedString,
953
+ length: ColumnLength.HashedString,
954
+ nullable: true,
955
+ transformer: HashedString.getDatabaseTransformer(),
956
+ })
957
+ public masterPassword?: HashedString = undefined;
958
+
886
959
  @ColumnAccessControl({
887
960
  create: [
888
961
  Permission.ProjectOwner,
@@ -49,6 +49,7 @@ import JSONFunctions from "../../Types/JSONFunctions";
49
49
  import ObjectID from "../../Types/ObjectID";
50
50
  import Phone from "../../Types/Phone";
51
51
  import PositiveNumber from "../../Types/PositiveNumber";
52
+ import HashedString from "../../Types/HashedString";
52
53
  import AcmeChallenge from "../../Models/DatabaseModels/AcmeChallenge";
53
54
  import Incident from "../../Models/DatabaseModels/Incident";
54
55
  import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
@@ -87,10 +88,13 @@ import EmailTemplateType from "../../Types/Email/EmailTemplateType";
87
88
  import Hostname from "../../Types/API/Hostname";
88
89
  import Protocol from "../../Types/API/Protocol";
89
90
  import DatabaseConfig from "../DatabaseConfig";
91
+ import CookieUtil from "../Utils/Cookie";
92
+ import { EncryptionSecret } from "../EnvironmentConfig";
90
93
  import { StatusPageApiRoute } from "../../ServiceRoute";
91
94
  import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
92
95
  import ForbiddenException from "../../Types/Exception/ForbiddenException";
93
96
  import SlackUtil from "../Utils/Workspace/Slack/Slack";
97
+ import { MASTER_PASSWORD_INVALID_MESSAGE } from "../../Types/StatusPage/MasterPassword";
94
98
 
95
99
  type ResolveStatusPageIdOrThrowFunction = (
96
100
  statusPageIdOrDomain: string,
@@ -798,6 +802,7 @@ export default class StatusPageAPI extends BaseAPI<
798
802
  enableMicrosoftTeamsSubscribers: true,
799
803
  enableSmsSubscribers: true,
800
804
  isPublicStatusPage: true,
805
+ enableMasterPassword: true,
801
806
  allowSubscribersToChooseResources: true,
802
807
  allowSubscribersToChooseEventTypes: true,
803
808
  requireSsoForLogin: true,
@@ -910,6 +915,80 @@ export default class StatusPageAPI extends BaseAPI<
910
915
  },
911
916
  );
912
917
 
918
+ this.router.post(
919
+ `${new this.entityType()
920
+ .getCrudApiPath()
921
+ ?.toString()}/master-password/:statusPageId`,
922
+ UserMiddleware.getUserMiddleware,
923
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
924
+ try {
925
+ if (!req.params["statusPageId"]) {
926
+ throw new BadDataException("Status Page ID not found");
927
+ }
928
+
929
+ const statusPageId: ObjectID = new ObjectID(
930
+ req.params["statusPageId"] as string,
931
+ );
932
+
933
+ const password: string | undefined =
934
+ req.body && (req.body["password"] as string);
935
+
936
+ if (!password) {
937
+ throw new BadDataException("Master password is required.");
938
+ }
939
+
940
+ const statusPage: StatusPage | null =
941
+ await StatusPageService.findOneById({
942
+ id: statusPageId,
943
+ select: {
944
+ _id: true,
945
+ projectId: true,
946
+ enableMasterPassword: true,
947
+ masterPassword: true,
948
+ isPublicStatusPage: true,
949
+ },
950
+ props: {
951
+ isRoot: true,
952
+ },
953
+ });
954
+
955
+ if (!statusPage) {
956
+ throw new NotFoundException("Status Page not found");
957
+ }
958
+
959
+ if (statusPage.isPublicStatusPage) {
960
+ throw new BadDataException(
961
+ "This status page is already visible to everyone.",
962
+ );
963
+ }
964
+
965
+ if (!statusPage.enableMasterPassword || !statusPage.masterPassword) {
966
+ throw new BadDataException(
967
+ "Master password has not been configured for this status page.",
968
+ );
969
+ }
970
+
971
+ const hashedInput: string = await HashedString.hashValue(
972
+ password,
973
+ EncryptionSecret,
974
+ );
975
+
976
+ if (hashedInput !== statusPage.masterPassword.toString()) {
977
+ throw new BadDataException(MASTER_PASSWORD_INVALID_MESSAGE);
978
+ }
979
+
980
+ CookieUtil.setStatusPageMasterPasswordCookie({
981
+ expressResponse: res,
982
+ statusPageId,
983
+ });
984
+
985
+ return Response.sendEmptySuccessResponse(req, res);
986
+ } catch (err) {
987
+ next(err);
988
+ }
989
+ },
990
+ );
991
+
913
992
  this.router.post(
914
993
  `${new this.entityType().getCrudApiPath()?.toString()}/sso/:statusPageId`,
915
994
  UserMiddleware.getUserMiddleware,
@@ -0,0 +1,23 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1763643080445 implements MigrationInterface {
4
+ public name = "MigrationName1763643080445";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "StatusPage" ADD "enableMasterPassword" boolean NOT NULL DEFAULT false`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "StatusPage" ADD "masterPassword" character varying(64)`,
12
+ );
13
+ }
14
+
15
+ public async down(queryRunner: QueryRunner): Promise<void> {
16
+ await queryRunner.query(
17
+ `ALTER TABLE "StatusPage" DROP COLUMN "masterPassword"`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "StatusPage" DROP COLUMN "enableMasterPassword"`,
21
+ );
22
+ }
23
+ }
@@ -185,6 +185,7 @@ import { MigrationName1762890441920 } from "./1762890441920-MigrationName";
185
185
  import { MigrationName1763471659817 } from "./1763471659817-MigrationName";
186
186
  import { MigrationName1763477560906 } from "./1763477560906-MigrationName";
187
187
  import { MigrationName1763480947474 } from "./1763480947474-MigrationName";
188
+ import { MigrationName1763643080445 } from "./1763643080445-MigrationName";
188
189
 
189
190
  export default [
190
191
  InitialMigration,
@@ -374,4 +375,5 @@ export default [
374
375
  MigrationName1763471659817,
375
376
  MigrationName1763477560906,
376
377
  MigrationName1763480947474,
378
+ MigrationName1763643080445,
377
379
  ];
@@ -47,6 +47,7 @@ import ProjectSMTPConfigService from "./ProjectSmtpConfigService";
47
47
  import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
48
48
  import StatusPageResourceService from "./StatusPageResourceService";
49
49
  import Dictionary from "../../Types/Dictionary";
50
+ import { JSONObject } from "../../Types/JSON";
50
51
  import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
51
52
  import MonitorGroupResourceService from "./MonitorGroupResourceService";
52
53
  import QueryHelper from "../Types/Database/QueryHelper";
@@ -61,6 +62,11 @@ import IP from "../../Types/IP/IP";
61
62
  import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
62
63
  import ForbiddenException from "../../Types/Exception/ForbiddenException";
63
64
  import CommonAPI from "../API/CommonAPI";
65
+ import MasterPasswordRequiredException from "../../Types/Exception/MasterPasswordRequiredException";
66
+ import {
67
+ MASTER_PASSWORD_COOKIE_IDENTIFIER,
68
+ MASTER_PASSWORD_REQUIRED_MESSAGE,
69
+ } from "../../Types/StatusPage/MasterPassword";
64
70
 
65
71
  export interface StatusPageReportItem {
66
72
  resourceName: string;
@@ -389,6 +395,8 @@ export class Service extends DatabaseService<StatusPage> {
389
395
  _id: true,
390
396
  isPublicStatusPage: true,
391
397
  ipWhitelist: true,
398
+ enableMasterPassword: true,
399
+ masterPassword: true,
392
400
  },
393
401
  });
394
402
 
@@ -462,6 +470,34 @@ export class Service extends DatabaseService<StatusPage> {
462
470
  }
463
471
  }
464
472
 
473
+ const shouldEnforceMasterPassword: boolean = Boolean(
474
+ statusPage &&
475
+ statusPage.enableMasterPassword &&
476
+ statusPage.masterPassword &&
477
+ !statusPage.isPublicStatusPage,
478
+ );
479
+
480
+ if (shouldEnforceMasterPassword) {
481
+ const hasValidMasterPassword: boolean =
482
+ this.hasValidMasterPasswordCookie({
483
+ req,
484
+ statusPageId,
485
+ });
486
+
487
+ if (hasValidMasterPassword) {
488
+ return {
489
+ hasReadAccess: true,
490
+ };
491
+ }
492
+
493
+ return {
494
+ hasReadAccess: false,
495
+ error: new MasterPasswordRequiredException(
496
+ MASTER_PASSWORD_REQUIRED_MESSAGE,
497
+ ),
498
+ };
499
+ }
500
+
465
501
  // if it does not have public access, check if this user has access.
466
502
 
467
503
  const items: Array<StatusPage> = await this.findBy({
@@ -493,6 +529,33 @@ export class Service extends DatabaseService<StatusPage> {
493
529
  };
494
530
  }
495
531
 
532
+ private hasValidMasterPasswordCookie(data: {
533
+ req: ExpressRequest;
534
+ statusPageId: ObjectID;
535
+ }): boolean {
536
+ const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
537
+ data.req,
538
+ CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
539
+ );
540
+
541
+ if (!token) {
542
+ return false;
543
+ }
544
+
545
+ try {
546
+ const payload: JSONObject = JSONWebToken.decodeJsonPayload(token);
547
+
548
+ return (
549
+ payload["statusPageId"] === data.statusPageId.toString() &&
550
+ payload["type"] === MASTER_PASSWORD_COOKIE_IDENTIFIER
551
+ );
552
+ } catch (err) {
553
+ logger.error(err);
554
+ }
555
+
556
+ return false;
557
+ }
558
+
496
559
  @CaptureSpan()
497
560
  public async getMonitorStatusTimelineForStatusPage(data: {
498
561
  monitorIds: Array<ObjectID>;
@@ -8,6 +8,10 @@ import StatusPagePrivateUser from "../../Models/DatabaseModels/StatusPagePrivate
8
8
  import OneUptimeDate from "../../Types/Date";
9
9
  import PositiveNumber from "../../Types/PositiveNumber";
10
10
  import CookieName from "../../Types/CookieName";
11
+ import {
12
+ MASTER_PASSWORD_COOKIE_IDENTIFIER,
13
+ MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
14
+ } from "../../Types/StatusPage/MasterPassword";
11
15
  import CaptureSpan from "./Telemetry/CaptureSpan";
12
16
 
13
17
  export default class CookieUtil {
@@ -233,6 +237,34 @@ export default class CookieUtil {
233
237
  return token;
234
238
  }
235
239
 
240
+ @CaptureSpan()
241
+ public static setStatusPageMasterPasswordCookie(data: {
242
+ expressResponse: ExpressResponse;
243
+ statusPageId: ObjectID;
244
+ }): void {
245
+ const expiresInDays: PositiveNumber = new PositiveNumber(
246
+ MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS,
247
+ );
248
+
249
+ const token: string = JSONWebToken.signJsonPayload(
250
+ {
251
+ statusPageId: data.statusPageId.toString(),
252
+ type: MASTER_PASSWORD_COOKIE_IDENTIFIER,
253
+ },
254
+ OneUptimeDate.getSecondsInDays(expiresInDays),
255
+ );
256
+
257
+ CookieUtil.setCookie(
258
+ data.expressResponse,
259
+ CookieUtil.getStatusPageMasterPasswordKey(data.statusPageId),
260
+ token,
261
+ {
262
+ maxAge: OneUptimeDate.getMillisecondsInDays(expiresInDays),
263
+ httpOnly: true,
264
+ },
265
+ );
266
+ }
267
+
236
268
  @CaptureSpan()
237
269
  public static setCookie(
238
270
  res: ExpressResponse,
@@ -280,6 +312,17 @@ export default class CookieUtil {
280
312
  });
281
313
  }
282
314
 
315
+ @CaptureSpan()
316
+ public static removeStatusPageMasterPasswordCookie(
317
+ res: ExpressResponse,
318
+ statusPageId: ObjectID,
319
+ ): void {
320
+ CookieUtil.removeCookie(
321
+ res,
322
+ CookieUtil.getStatusPageMasterPasswordKey(statusPageId),
323
+ );
324
+ }
325
+
283
326
  // get all cookies with express request
284
327
  @CaptureSpan()
285
328
  public static getAllCookies(req: ExpressRequest): Dictionary<string> {
@@ -304,6 +347,11 @@ export default class CookieUtil {
304
347
  return `${CookieName.RefreshToken}-${id.toString()}`;
305
348
  }
306
349
 
350
+ @CaptureSpan()
351
+ public static getStatusPageMasterPasswordKey(id: ObjectID): string {
352
+ return `${CookieName.StatusPageMasterPassword}-${id.toString()}`;
353
+ }
354
+
307
355
  @CaptureSpan()
308
356
  public static getUserSSOKey(id: ObjectID): string {
309
357
  return `${this.getSSOKey()}${id.toString()}`;
@@ -7,6 +7,7 @@ enum CookieName {
7
7
  Timezone = "user-timezone",
8
8
  IsMasterAdmin = "user-is-master-admin",
9
9
  ProfilePicID = "user-profile-pic-id",
10
+ StatusPageMasterPassword = "status-page-master-password",
10
11
  }
11
12
 
12
13
  export default CookieName;
@@ -0,0 +1,7 @@
1
+ import NotAuthenticatedException from "./NotAuthenticatedException";
2
+
3
+ export default class MasterPasswordRequiredException extends NotAuthenticatedException {
4
+ public constructor(message: string) {
5
+ super(message);
6
+ }
7
+ }
@@ -0,0 +1,10 @@
1
+ export const MASTER_PASSWORD_REQUIRED_MESSAGE: string =
2
+ "Master password required";
3
+
4
+ export const MASTER_PASSWORD_INVALID_MESSAGE: string =
5
+ "Invalid master password. Please try again.";
6
+
7
+ export const MASTER_PASSWORD_COOKIE_IDENTIFIER: string =
8
+ "status-page-master-password";
9
+
10
+ export const MASTER_PASSWORD_COOKIE_MAX_AGE_IN_DAYS: number = 7;
@@ -37,6 +37,7 @@ import Recurring from "../../Types/Events/Recurring";
37
37
  import IconProp from "../../Types/Icon/IconProp";
38
38
  import ObjectID from "../../Types/ObjectID";
39
39
  import Permission from "../../Types/Permission";
40
+ import HashedString from "../../Types/HashedString";
40
41
  import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, } from "typeorm";
41
42
  import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
42
43
  let StatusPage = class StatusPage extends BaseModel {
@@ -66,6 +67,8 @@ let StatusPage = class StatusPage extends BaseModel {
66
67
  this.customCSS = undefined;
67
68
  this.customJavaScript = undefined;
68
69
  this.isPublicStatusPage = undefined;
70
+ this.enableMasterPassword = undefined;
71
+ this.masterPassword = undefined;
69
72
  this.showIncidentLabelsOnStatusPage = undefined;
70
73
  this.showScheduledEventLabelsOnStatusPage = undefined;
71
74
  // This column is Deprectaed.
@@ -899,6 +902,77 @@ __decorate([
899
902
  }),
900
903
  __metadata("design:type", Boolean)
901
904
  ], StatusPage.prototype, "isPublicStatusPage", void 0);
905
+ __decorate([
906
+ ColumnAccessControl({
907
+ create: [
908
+ Permission.ProjectOwner,
909
+ Permission.ProjectAdmin,
910
+ Permission.ProjectMember,
911
+ Permission.CreateProjectStatusPage,
912
+ ],
913
+ read: [
914
+ Permission.ProjectOwner,
915
+ Permission.ProjectAdmin,
916
+ Permission.ProjectMember,
917
+ Permission.ReadProjectStatusPage,
918
+ ],
919
+ update: [
920
+ Permission.ProjectOwner,
921
+ Permission.ProjectAdmin,
922
+ Permission.ProjectMember,
923
+ Permission.EditProjectStatusPage,
924
+ ],
925
+ }),
926
+ TableColumn({
927
+ isDefaultValueColumn: true,
928
+ type: TableColumnType.Boolean,
929
+ title: "Enable Master Password",
930
+ description: "Require visitors to enter a master password before viewing a private status page.",
931
+ defaultValue: false,
932
+ }),
933
+ Column({
934
+ type: ColumnType.Boolean,
935
+ default: false,
936
+ }),
937
+ __metadata("design:type", Boolean)
938
+ ], StatusPage.prototype, "enableMasterPassword", void 0);
939
+ __decorate([
940
+ ColumnAccessControl({
941
+ create: [
942
+ Permission.ProjectOwner,
943
+ Permission.ProjectAdmin,
944
+ Permission.ProjectMember,
945
+ Permission.CreateProjectStatusPage,
946
+ ],
947
+ // This is a hashed column. So, reading the value is does not affect anything.
948
+ read: [
949
+ Permission.ProjectOwner,
950
+ Permission.ProjectAdmin,
951
+ Permission.ProjectMember,
952
+ Permission.ReadProjectStatusPage,
953
+ ],
954
+ update: [
955
+ Permission.ProjectOwner,
956
+ Permission.ProjectAdmin,
957
+ Permission.ProjectMember,
958
+ Permission.EditProjectStatusPage,
959
+ ],
960
+ }),
961
+ TableColumn({
962
+ title: "Master Password",
963
+ description: "Password required to unlock a private status page. This value is stored as a secure hash.",
964
+ hashed: true,
965
+ type: TableColumnType.HashedString,
966
+ placeholder: "Enter a new master password",
967
+ }),
968
+ Column({
969
+ type: ColumnType.HashedString,
970
+ length: ColumnLength.HashedString,
971
+ nullable: true,
972
+ transformer: HashedString.getDatabaseTransformer(),
973
+ }),
974
+ __metadata("design:type", HashedString)
975
+ ], StatusPage.prototype, "masterPassword", void 0);
902
976
  __decorate([
903
977
  ColumnAccessControl({
904
978
  create: [