@oneuptime/common 12.0.14 → 12.0.15
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/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,18 +4,138 @@ import Team from "../../Models/DatabaseModels/Team";
|
|
|
4
4
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
5
5
|
import ObjectID from "../../Types/ObjectID";
|
|
6
6
|
import CreateBy from "../Types/Database/CreateBy";
|
|
7
|
-
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
7
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
8
8
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
9
9
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
10
10
|
import validateGlobalProviderProjectTeams, {
|
|
11
11
|
resolveAttachmentProjectId,
|
|
12
12
|
} from "../Utils/ValidateGlobalProviderProjectTeams";
|
|
13
|
+
import {
|
|
14
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
15
|
+
GlobalProviderAttachments,
|
|
16
|
+
clearGlobalSsoAuthorizationCaches,
|
|
17
|
+
doAttachmentsGovernProject,
|
|
18
|
+
globalProviderCacheKey,
|
|
19
|
+
globalSsoAttachmentsCache,
|
|
20
|
+
loadAttachmentsOnce,
|
|
21
|
+
} from "../Utils/GlobalSsoAuthorization";
|
|
22
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
13
23
|
|
|
14
24
|
export class Service extends DatabaseService<Model> {
|
|
15
25
|
public constructor() {
|
|
16
26
|
super(Model);
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Whether this provider's attachments cover `projectId`.
|
|
31
|
+
*
|
|
32
|
+
* Only consulted when the provider has `restrictToAttachedProjects` on -
|
|
33
|
+
* attachments are the PROVISIONING allow-list by default, not an access
|
|
34
|
+
* boundary, and reading them as one without the admin asking would lock
|
|
35
|
+
* existing users out of projects they legitimately reach.
|
|
36
|
+
*
|
|
37
|
+
* "No attachment rows at all" means instance-wide, matching the login
|
|
38
|
+
* router's default-all mode. "Rows exist but none are enabled" is a
|
|
39
|
+
* different answer and denies - otherwise an admin disabling the last
|
|
40
|
+
* attachment would WIDEN the provider to every project.
|
|
41
|
+
*
|
|
42
|
+
* Cached for 60s, with concurrent misses sharing one query.
|
|
43
|
+
*/
|
|
44
|
+
@CaptureSpan()
|
|
45
|
+
public async doesProviderGovernProject(data: {
|
|
46
|
+
globalSsoId: ObjectID;
|
|
47
|
+
projectId: ObjectID;
|
|
48
|
+
}): Promise<boolean> {
|
|
49
|
+
const key: string = globalProviderCacheKey("sso", data.globalSsoId);
|
|
50
|
+
|
|
51
|
+
const cached: GlobalProviderAttachments | undefined =
|
|
52
|
+
globalSsoAttachmentsCache.get(key);
|
|
53
|
+
|
|
54
|
+
if (cached !== undefined) {
|
|
55
|
+
return doAttachmentsGovernProject(cached, data.projectId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const attachments: GlobalProviderAttachments = await loadAttachmentsOnce(
|
|
59
|
+
key,
|
|
60
|
+
async (): Promise<GlobalProviderAttachments> => {
|
|
61
|
+
/*
|
|
62
|
+
* Fetched WITHOUT the isEnabled filter so a disabled row still counts
|
|
63
|
+
* as "this provider has attachments"; the enabled ones are selected
|
|
64
|
+
* out below.
|
|
65
|
+
*/
|
|
66
|
+
const rows: Array<Model> = await this.findBy({
|
|
67
|
+
query: { globalSsoId: data.globalSsoId },
|
|
68
|
+
select: { _id: true, projectId: true, isEnabled: true },
|
|
69
|
+
limit: LIMIT_PER_PROJECT,
|
|
70
|
+
skip: 0,
|
|
71
|
+
props: { isRoot: true },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const loaded: GlobalProviderAttachments = {
|
|
75
|
+
hasAnyAttachmentRows: rows.length > 0,
|
|
76
|
+
enabledProjectIds: rows
|
|
77
|
+
.filter((row: Model) => {
|
|
78
|
+
return Boolean(row.isEnabled) && Boolean(row.projectId);
|
|
79
|
+
})
|
|
80
|
+
.map((row: Model) => {
|
|
81
|
+
return row.projectId!.toString();
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
globalSsoAttachmentsCache.set(
|
|
86
|
+
key,
|
|
87
|
+
loaded,
|
|
88
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return loaded;
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return doAttachmentsGovernProject(attachments, data.projectId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@CaptureSpan()
|
|
99
|
+
protected override async onBeforeDelete(
|
|
100
|
+
deleteBy: DeleteBy<Model>,
|
|
101
|
+
): Promise<OnDelete<Model>> {
|
|
102
|
+
// Detaching a project has to take effect now, not in 60s, on this node.
|
|
103
|
+
clearGlobalSsoAuthorizationCaches();
|
|
104
|
+
return { deleteBy, carryForward: null };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Cleared again AFTER the write commits. A clear that runs before the row
|
|
109
|
+
* lands can be immediately re-filled with the pre-change answer by a
|
|
110
|
+
* concurrent request, handing the old attachment set another full TTL.
|
|
111
|
+
*/
|
|
112
|
+
@CaptureSpan()
|
|
113
|
+
protected override async onDeleteSuccess(
|
|
114
|
+
onDelete: OnDelete<Model>,
|
|
115
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
116
|
+
): Promise<OnDelete<Model>> {
|
|
117
|
+
clearGlobalSsoAuthorizationCaches();
|
|
118
|
+
return onDelete;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@CaptureSpan()
|
|
122
|
+
protected override async onCreateSuccess(
|
|
123
|
+
_onCreate: OnCreate<Model>,
|
|
124
|
+
createdItem: Model,
|
|
125
|
+
): Promise<Model> {
|
|
126
|
+
clearGlobalSsoAuthorizationCaches();
|
|
127
|
+
return createdItem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@CaptureSpan()
|
|
131
|
+
protected override async onUpdateSuccess(
|
|
132
|
+
onUpdate: OnUpdate<Model>,
|
|
133
|
+
_updatedItemIds: Array<ObjectID>,
|
|
134
|
+
): Promise<OnUpdate<Model>> {
|
|
135
|
+
clearGlobalSsoAuthorizationCaches();
|
|
136
|
+
return onUpdate;
|
|
137
|
+
}
|
|
138
|
+
|
|
19
139
|
@CaptureSpan()
|
|
20
140
|
protected override async onBeforeCreate(
|
|
21
141
|
createBy: CreateBy<Model>,
|
|
@@ -38,6 +158,9 @@ export class Service extends DatabaseService<Model> {
|
|
|
38
158
|
projectId,
|
|
39
159
|
});
|
|
40
160
|
|
|
161
|
+
// A new attachment narrows (or widens) which projects the provider governs.
|
|
162
|
+
clearGlobalSsoAuthorizationCaches();
|
|
163
|
+
|
|
41
164
|
return { createBy, carryForward: null };
|
|
42
165
|
}
|
|
43
166
|
|
|
@@ -78,6 +201,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
78
201
|
}
|
|
79
202
|
}
|
|
80
203
|
|
|
204
|
+
clearGlobalSsoAuthorizationCaches();
|
|
205
|
+
|
|
81
206
|
return { updateBy, carryForward: null };
|
|
82
207
|
}
|
|
83
208
|
}
|
|
@@ -1,10 +1,142 @@
|
|
|
1
1
|
import DatabaseService from "./DatabaseService";
|
|
2
2
|
import Model from "../../Models/DatabaseModels/GlobalSso";
|
|
3
|
+
import ObjectID from "../../Types/ObjectID";
|
|
4
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
5
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
6
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
7
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
8
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
9
|
+
import {
|
|
10
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
11
|
+
GlobalProviderTrust,
|
|
12
|
+
clearGlobalSsoAuthorizationCaches,
|
|
13
|
+
globalProviderCacheKey,
|
|
14
|
+
globalSsoProviderTrustCache,
|
|
15
|
+
loadTrustOnce,
|
|
16
|
+
} from "../Utils/GlobalSsoAuthorization";
|
|
3
17
|
|
|
4
18
|
export class Service extends DatabaseService<Model> {
|
|
5
19
|
public constructor() {
|
|
6
20
|
super(Model);
|
|
7
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What the SSO-enforcement middleware needs to know about this provider:
|
|
25
|
+
* whether it is still usable at all, and whether the admin opted it into
|
|
26
|
+
* attachment-scoped access.
|
|
27
|
+
*
|
|
28
|
+
* Called on every authenticated request against an SSO-enforced project, so
|
|
29
|
+
* the answer is cached for 60s and concurrent misses share one query.
|
|
30
|
+
* Without this lookup, turning a provider off leaves every token it ever
|
|
31
|
+
* issued working until each one expires - up to thirty days.
|
|
32
|
+
*/
|
|
33
|
+
@CaptureSpan()
|
|
34
|
+
public async getProviderTrust(
|
|
35
|
+
providerId: ObjectID,
|
|
36
|
+
): Promise<GlobalProviderTrust> {
|
|
37
|
+
const key: string = globalProviderCacheKey("sso", providerId);
|
|
38
|
+
|
|
39
|
+
const cached: GlobalProviderTrust | undefined =
|
|
40
|
+
globalSsoProviderTrustCache.get(key);
|
|
41
|
+
|
|
42
|
+
if (cached !== undefined) {
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return loadTrustOnce(key, async (): Promise<GlobalProviderTrust> => {
|
|
47
|
+
const provider: Model | null = await this.findOneBy({
|
|
48
|
+
query: { _id: providerId.toString() },
|
|
49
|
+
select: {
|
|
50
|
+
_id: true,
|
|
51
|
+
isEnabled: true,
|
|
52
|
+
restrictToAttachedProjects: true,
|
|
53
|
+
},
|
|
54
|
+
props: { isRoot: true },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* A deleted provider and a disabled one are the same answer: no. Both
|
|
59
|
+
* are cached, so a revoked provider does not cost a query per request.
|
|
60
|
+
*/
|
|
61
|
+
const trust: GlobalProviderTrust = {
|
|
62
|
+
isUsable: Boolean(provider && provider.isEnabled),
|
|
63
|
+
restrictToAttachedProjects: Boolean(
|
|
64
|
+
provider && provider.restrictToAttachedProjects,
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
globalSsoProviderTrustCache.set(
|
|
69
|
+
key,
|
|
70
|
+
trust,
|
|
71
|
+
GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return trust;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* Cache invalidation runs in the SUCCESS hooks, not the before-hooks: a
|
|
80
|
+
* clear that happens before the row is written can be immediately re-filled
|
|
81
|
+
* with the pre-change answer by a concurrent request, which would hand a
|
|
82
|
+
* disabled provider another full TTL of life on the very node that served
|
|
83
|
+
* the disable. Clearing in both is deliberate - the before-hook narrows the
|
|
84
|
+
* window, the success hook closes it.
|
|
85
|
+
*
|
|
86
|
+
* These caches are per-process, so the hooks make a change immediate on the
|
|
87
|
+
* node that served it and the TTL bounds every other node.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
@CaptureSpan()
|
|
91
|
+
protected override async onBeforeUpdate(
|
|
92
|
+
updateBy: UpdateBy<Model>,
|
|
93
|
+
): Promise<OnUpdate<Model>> {
|
|
94
|
+
clearGlobalSsoAuthorizationCaches();
|
|
95
|
+
return { updateBy, carryForward: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@CaptureSpan()
|
|
99
|
+
protected override async onUpdateSuccess(
|
|
100
|
+
onUpdate: OnUpdate<Model>,
|
|
101
|
+
_updatedItemIds: Array<ObjectID>,
|
|
102
|
+
): Promise<OnUpdate<Model>> {
|
|
103
|
+
clearGlobalSsoAuthorizationCaches();
|
|
104
|
+
return onUpdate;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@CaptureSpan()
|
|
108
|
+
protected override async onBeforeDelete(
|
|
109
|
+
deleteBy: DeleteBy<Model>,
|
|
110
|
+
): Promise<OnDelete<Model>> {
|
|
111
|
+
clearGlobalSsoAuthorizationCaches();
|
|
112
|
+
return { deleteBy, carryForward: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@CaptureSpan()
|
|
116
|
+
protected override async onDeleteSuccess(
|
|
117
|
+
onDelete: OnDelete<Model>,
|
|
118
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
119
|
+
): Promise<OnDelete<Model>> {
|
|
120
|
+
clearGlobalSsoAuthorizationCaches();
|
|
121
|
+
return onDelete;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@CaptureSpan()
|
|
125
|
+
protected override async onBeforeCreate(
|
|
126
|
+
createBy: CreateBy<Model>,
|
|
127
|
+
): Promise<OnCreate<Model>> {
|
|
128
|
+
clearGlobalSsoAuthorizationCaches();
|
|
129
|
+
return { createBy, carryForward: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@CaptureSpan()
|
|
133
|
+
protected override async onCreateSuccess(
|
|
134
|
+
_onCreate: OnCreate<Model>,
|
|
135
|
+
createdItem: Model,
|
|
136
|
+
): Promise<Model> {
|
|
137
|
+
clearGlobalSsoAuthorizationCaches();
|
|
138
|
+
return createdItem;
|
|
139
|
+
}
|
|
8
140
|
}
|
|
9
141
|
|
|
10
142
|
export default new Service();
|
|
@@ -117,10 +117,10 @@ export class Service extends DatabaseService<Model> {
|
|
|
117
117
|
* create response for it to confirm the address the way a user would.
|
|
118
118
|
*
|
|
119
119
|
* It is gated on an environment variable that is unset in every shipped
|
|
120
|
-
* config and set true ONLY in the CI e2e
|
|
121
|
-
* through
|
|
122
|
-
* In production the flag is absent,
|
|
123
|
-
* never leaves the mail path.
|
|
120
|
+
* config and set true ONLY in the CI e2e jobs (docker-compose passes it
|
|
121
|
+
* through; both SaaS e2e jobs set it — test-release.yaml for master and
|
|
122
|
+
* release.yml for the release branch). In production the flag is absent,
|
|
123
|
+
* this branch never runs, and the code never leaves the mail path.
|
|
124
124
|
*/
|
|
125
125
|
if (
|
|
126
126
|
process.env["EXPOSE_VERIFICATION_CODE_IN_API_RESPONSE_FOR_E2E"] ===
|
package/Server/Utils/CronTab.ts
CHANGED
|
@@ -15,4 +15,77 @@ export default class CronTab {
|
|
|
15
15
|
throw new BadDataException(`Invalid cron expression: ${crontab}`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Longest gap (in seconds) between two consecutive runs of this cron.
|
|
21
|
+
*
|
|
22
|
+
* Used to reason about how much data an evaluation window can possibly
|
|
23
|
+
* contain: a monitor polled once every five minutes produces at most one
|
|
24
|
+
* sample per five minutes, so "all values over the last 5 minutes" can
|
|
25
|
+
* never be backed by more than one sample. Callers use this to decide
|
|
26
|
+
* whether a window is actually covered by data or is merely still
|
|
27
|
+
* filling up.
|
|
28
|
+
*
|
|
29
|
+
* The *longest* gap is deliberate. Irregular expressions (say
|
|
30
|
+
* `0,1,30 * * * *`) have both very short and very long gaps; taking the
|
|
31
|
+
* shortest would make callers demand data at a cadence the cron does not
|
|
32
|
+
* actually deliver, and they would wait forever.
|
|
33
|
+
*
|
|
34
|
+
* Returns null when the expression is missing or unparseable so callers
|
|
35
|
+
* can fall back to inferring the cadence from the data itself.
|
|
36
|
+
*/
|
|
37
|
+
@CaptureSpan()
|
|
38
|
+
public static getIntervalInSeconds(
|
|
39
|
+
crontab: string | undefined | null,
|
|
40
|
+
referenceDate?: Date | undefined,
|
|
41
|
+
): number | null {
|
|
42
|
+
if (!crontab) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const interval: CronExpression = CronParser.parseExpression(crontab, {
|
|
48
|
+
currentDate: referenceDate || new Date(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* Six executions -> five gaps. Enough to cover a full hour-wrap for
|
|
53
|
+
* every interval offered in the UI without walking the schedule far
|
|
54
|
+
* into the future.
|
|
55
|
+
*/
|
|
56
|
+
const executionTimes: Array<Date> = [];
|
|
57
|
+
|
|
58
|
+
for (let i: number = 0; i < 6; i++) {
|
|
59
|
+
executionTimes.push(interval.next().toDate());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let longestGapInSeconds: number = 0;
|
|
63
|
+
|
|
64
|
+
for (let i: number = 1; i < executionTimes.length; i++) {
|
|
65
|
+
const gapInSeconds: number =
|
|
66
|
+
(executionTimes[i]!.getTime() - executionTimes[i - 1]!.getTime()) /
|
|
67
|
+
1000;
|
|
68
|
+
|
|
69
|
+
if (gapInSeconds > longestGapInSeconds) {
|
|
70
|
+
longestGapInSeconds = gapInSeconds;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (longestGapInSeconds <= 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return longestGapInSeconds;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
/*
|
|
81
|
+
* Debug rather than error: this runs on the criteria-evaluation hot
|
|
82
|
+
* path, and monitors created before the interval was a cron (or
|
|
83
|
+
* through a Terraform example using the "Every 5 minutes" label) would
|
|
84
|
+
* otherwise log on every single check. Callers treat null as "cadence
|
|
85
|
+
* unknown" and fall back to inferring it from the data.
|
|
86
|
+
*/
|
|
87
|
+
logger.debug(error);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
18
91
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import ObjectID from "../../Types/ObjectID";
|
|
2
|
+
import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* The stateful half of Global SSO enforcement.
|
|
6
|
+
*
|
|
7
|
+
* A Global SSO/OIDC token is a 30-day, self-contained JWT with no project
|
|
8
|
+
* binding, so on its own it answers only "this person authenticated against
|
|
9
|
+
* SOME instance-wide identity provider at some point in the last month". Two
|
|
10
|
+
* questions it cannot answer:
|
|
11
|
+
*
|
|
12
|
+
* 1. IS THE PROVIDER STILL TRUSTED? Turning a provider off, or deleting it,
|
|
13
|
+
* has to actually cut off access. Without a check at request time the
|
|
14
|
+
* "Enabled" toggle does nothing to anyone already signed in, for up to
|
|
15
|
+
* thirty days. This check is UNCONDITIONAL - "disabled" has only one
|
|
16
|
+
* possible meaning.
|
|
17
|
+
*
|
|
18
|
+
* 2. DOES THE PROVIDER GOVERN THIS PROJECT? Deliberately OPT-IN, per
|
|
19
|
+
* provider, via `restrictToAttachedProjects`. The attachment rows
|
|
20
|
+
* (GlobalSsoProject / GlobalOidcProject) were introduced as the
|
|
21
|
+
* PROVISIONING allow-list - "a SAML assertion can only ever provision a
|
|
22
|
+
* user into projects that a master admin has explicitly attached here",
|
|
23
|
+
* per the model's own docstring - and the login routers gate a session on
|
|
24
|
+
* membership of ANY project, not of an attached one. Reading attachments
|
|
25
|
+
* as an access boundary by default would therefore lock existing users
|
|
26
|
+
* out of projects they legitimately reach today, immediately on upgrade,
|
|
27
|
+
* with no way to recover from inside the product. So it stays off unless
|
|
28
|
+
* an admin asks for it.
|
|
29
|
+
*
|
|
30
|
+
* Both answers come from Postgres and run on every authenticated request
|
|
31
|
+
* against an SSO-enforced project, so both are cached in-process for 60
|
|
32
|
+
* seconds, and concurrent misses share one query.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const GLOBAL_SSO_AUTHORIZATION_CACHE_TTL_MS: number = 60_000;
|
|
36
|
+
|
|
37
|
+
/** What the enforcement path needs to know about a global provider. */
|
|
38
|
+
export interface GlobalProviderTrust {
|
|
39
|
+
// The provider row exists and its "Enabled" toggle is on.
|
|
40
|
+
isUsable: boolean;
|
|
41
|
+
// The admin opted this provider into attachment-scoped access.
|
|
42
|
+
restrictToAttachedProjects: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* Absence of an entry means "not yet looked up", which is why these are read
|
|
47
|
+
* with an explicit `undefined` check rather than a truthiness test: a cached
|
|
48
|
+
* "this provider is disabled" must not be mistaken for a cache miss and
|
|
49
|
+
* re-queried on every single request.
|
|
50
|
+
*/
|
|
51
|
+
export const globalSsoProviderTrustCache: InMemoryTTLCache<GlobalProviderTrust> =
|
|
52
|
+
new InMemoryTTLCache(10_000);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A provider's attachment rows.
|
|
56
|
+
*
|
|
57
|
+
* `hasAnyAttachmentRows` distinguishes "this provider has no attachments at
|
|
58
|
+
* all" from "it has attachments but every one of them is disabled". Collapsing
|
|
59
|
+
* those two would mean an admin disabling the last attachment WIDENS the
|
|
60
|
+
* provider to every project - the exact opposite of the intent.
|
|
61
|
+
*/
|
|
62
|
+
export interface GlobalProviderAttachments {
|
|
63
|
+
hasAnyAttachmentRows: boolean;
|
|
64
|
+
enabledProjectIds: Array<string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const globalSsoAttachmentsCache: InMemoryTTLCache<GlobalProviderAttachments> =
|
|
68
|
+
new InMemoryTTLCache(10_000);
|
|
69
|
+
|
|
70
|
+
export type GlobalProviderKind = "sso" | "oidc";
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* Namespaced by kind. A Global SSO provider and a Global OIDC provider are
|
|
74
|
+
* different rows in different tables and could in principle share an id;
|
|
75
|
+
* collapsing the keys would let one vouch for the other.
|
|
76
|
+
*/
|
|
77
|
+
export function globalProviderCacheKey(
|
|
78
|
+
providerKind: GlobalProviderKind,
|
|
79
|
+
providerId: ObjectID,
|
|
80
|
+
): string {
|
|
81
|
+
return `${providerKind}:${providerId.toString()}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decides whether a provider's attachments cover `projectId`, for a provider
|
|
86
|
+
* that has opted into attachment-scoped access.
|
|
87
|
+
*
|
|
88
|
+
* A provider with no attachment rows at all is instance-wide, matching the
|
|
89
|
+
* "default-all" reading the login routers use. A provider that HAS attachment
|
|
90
|
+
* rows governs exactly the enabled ones - so disabling them all denies rather
|
|
91
|
+
* than widens.
|
|
92
|
+
*/
|
|
93
|
+
export function doAttachmentsGovernProject(
|
|
94
|
+
attachments: GlobalProviderAttachments,
|
|
95
|
+
projectId: ObjectID,
|
|
96
|
+
): boolean {
|
|
97
|
+
if (!attachments.hasAnyAttachmentRows) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return attachments.enabledProjectIds.includes(projectId.toString());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Drops every cached answer. Used by the write hooks and by tests. */
|
|
105
|
+
export function clearGlobalSsoAuthorizationCaches(): void {
|
|
106
|
+
globalSsoProviderTrustCache.clear();
|
|
107
|
+
globalSsoAttachmentsCache.clear();
|
|
108
|
+
inFlightTrustLookups.clear();
|
|
109
|
+
inFlightAttachmentLookups.clear();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* In-flight de-duplication.
|
|
114
|
+
*
|
|
115
|
+
* The multi-tenant permission path fans out over every project the user
|
|
116
|
+
* belongs to CONCURRENTLY. Without this, a cold cache means N simultaneous
|
|
117
|
+
* copies of the same two queries - for a user in fifty projects, a hundred
|
|
118
|
+
* queries where two would do. Worse, failures are never cached, so a database
|
|
119
|
+
* that is already struggling gets the full fan-out again on every request.
|
|
120
|
+
*
|
|
121
|
+
* Sharing the promise collapses all of that to one query per provider, and a
|
|
122
|
+
* rejection is shared too rather than being retried N times in the same tick.
|
|
123
|
+
*/
|
|
124
|
+
const inFlightTrustLookups: Map<
|
|
125
|
+
string,
|
|
126
|
+
Promise<GlobalProviderTrust>
|
|
127
|
+
> = new Map();
|
|
128
|
+
const inFlightAttachmentLookups: Map<
|
|
129
|
+
string,
|
|
130
|
+
Promise<GlobalProviderAttachments>
|
|
131
|
+
> = new Map();
|
|
132
|
+
|
|
133
|
+
async function loadOnce<T>(
|
|
134
|
+
inFlight: Map<string, Promise<T>>,
|
|
135
|
+
key: string,
|
|
136
|
+
load: () => Promise<T>,
|
|
137
|
+
): Promise<T> {
|
|
138
|
+
const existing: Promise<T> | undefined = inFlight.get(key);
|
|
139
|
+
|
|
140
|
+
if (existing) {
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pending: Promise<T> = load().finally((): void => {
|
|
145
|
+
/*
|
|
146
|
+
* Only clear the slot if it is still OURS. A cache clear between the two
|
|
147
|
+
* (every write to a Global SSO/OIDC service does exactly that, twice) lets
|
|
148
|
+
* a NEWER lookup take the key while this one is still on the wire; a blind
|
|
149
|
+
* delete would evict that newer entry when this one settles, and the fan-
|
|
150
|
+
* out that follows would stop de-duplicating - the one situation this
|
|
151
|
+
* exists for.
|
|
152
|
+
*/
|
|
153
|
+
if (inFlight.get(key) === pending) {
|
|
154
|
+
inFlight.delete(key);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
inFlight.set(key, pending);
|
|
159
|
+
|
|
160
|
+
return pending;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function loadTrustOnce(
|
|
164
|
+
key: string,
|
|
165
|
+
load: () => Promise<GlobalProviderTrust>,
|
|
166
|
+
): Promise<GlobalProviderTrust> {
|
|
167
|
+
return loadOnce(inFlightTrustLookups, key, load);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function loadAttachmentsOnce(
|
|
171
|
+
key: string,
|
|
172
|
+
load: () => Promise<GlobalProviderAttachments>,
|
|
173
|
+
): Promise<GlobalProviderAttachments> {
|
|
174
|
+
return loadOnce(inFlightAttachmentLookups, key, load);
|
|
175
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { AxiosError } from "axios";
|
|
2
2
|
import { JSONObject } from "../../../Types/JSON";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AdUploadableMarketingConversionTypes,
|
|
5
|
+
MarketingConversionType,
|
|
6
|
+
} from "../../../Types/Marketing/MarketingConversion";
|
|
4
7
|
import MarketingConversion from "../../../Models/DatabaseModels/MarketingConversion";
|
|
5
8
|
|
|
6
9
|
/*
|
|
@@ -11,9 +14,12 @@ import MarketingConversion from "../../../Models/DatabaseModels/MarketingConvers
|
|
|
11
14
|
*
|
|
12
15
|
* Contract:
|
|
13
16
|
* - getSkipReason returns a human-readable reason when this conversion can
|
|
14
|
-
* never be uploaded to this platform (
|
|
15
|
-
* platform's upload window,
|
|
16
|
-
* type). Null means uploadable.
|
|
17
|
+
* never be uploaded to this platform (conversion type with no ad-platform
|
|
18
|
+
* mapping, wrong/missing click id, outside the platform's upload window,
|
|
19
|
+
* provider not configured for this conversion type). Null means uploadable.
|
|
20
|
+
* It is implemented here and must not be overridden — providers implement
|
|
21
|
+
* getProviderSkipReason instead, which only runs for conversion types that
|
|
22
|
+
* are ad-uploadable at all.
|
|
17
23
|
* - upload() sends one batch. Per-conversion PERMANENT rejections are
|
|
18
24
|
* returned in the result keyed by array index; they will not be retried.
|
|
19
25
|
* Transport/auth-level failures must THROW — the whole batch stays pending
|
|
@@ -52,7 +58,31 @@ export default abstract class ConversionUploadProvider {
|
|
|
52
58
|
|
|
53
59
|
public abstract isConfigured(): boolean;
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
/*
|
|
62
|
+
* Every provider maps a conversion onto a platform conversion action by
|
|
63
|
+
* branching on isSignUp() — so a conversion type the ad platforms have no
|
|
64
|
+
* mapping for (a booked meeting) would otherwise be silently uploaded as a
|
|
65
|
+
* purchase. Screen those out here, before any provider sees them, so no
|
|
66
|
+
* provider or call site can skip the check.
|
|
67
|
+
*/
|
|
68
|
+
public getSkipReason(conversion: MarketingConversion): ConversionSkip | null {
|
|
69
|
+
if (
|
|
70
|
+
!AdUploadableMarketingConversionTypes.includes(
|
|
71
|
+
conversion.conversionType as MarketingConversionType,
|
|
72
|
+
)
|
|
73
|
+
) {
|
|
74
|
+
return {
|
|
75
|
+
reason: `Conversion type ${
|
|
76
|
+
conversion.conversionType || "unknown"
|
|
77
|
+
} has no ad platform conversion mapping`,
|
|
78
|
+
isPermanent: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return this.getProviderSkipReason(conversion);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected abstract getProviderSkipReason(
|
|
56
86
|
conversion: MarketingConversion,
|
|
57
87
|
): ConversionSkip | null;
|
|
58
88
|
|
|
@@ -46,7 +46,7 @@ export default class GoogleAdsProvider extends ConversionUploadProvider {
|
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
protected override getProviderSkipReason(
|
|
50
50
|
conversion: MarketingConversion,
|
|
51
51
|
): ConversionSkip | null {
|
|
52
52
|
if (!this.getGoogleClickId(conversion)) {
|
|
@@ -41,7 +41,7 @@ export default class LinkedInProvider extends ConversionUploadProvider {
|
|
|
41
41
|
return Boolean(LinkedInConversionsAccessToken);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
protected override getProviderSkipReason(
|
|
45
45
|
conversion: MarketingConversion,
|
|
46
46
|
): ConversionSkip | null {
|
|
47
47
|
if (!this.getClickId(conversion, "li_fat_id")) {
|
|
@@ -32,7 +32,7 @@ export default class MetaProvider extends ConversionUploadProvider {
|
|
|
32
32
|
return Boolean(MetaConversionsPixelId && MetaConversionsAccessToken);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
protected override getProviderSkipReason(
|
|
36
36
|
conversion: MarketingConversion,
|
|
37
37
|
): ConversionSkip | null {
|
|
38
38
|
if (!this.getClickId(conversion, "fbclid")) {
|
|
@@ -45,7 +45,7 @@ export default class MicrosoftAdsProvider extends ConversionUploadProvider {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
protected override getProviderSkipReason(
|
|
49
49
|
conversion: MarketingConversion,
|
|
50
50
|
): ConversionSkip | null {
|
|
51
51
|
if (!this.getClickId(conversion, "msclkid")) {
|
|
@@ -41,7 +41,7 @@ export default class RedditProvider extends ConversionUploadProvider {
|
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
protected override getProviderSkipReason(
|
|
45
45
|
conversion: MarketingConversion,
|
|
46
46
|
): ConversionSkip | null {
|
|
47
47
|
if (!this.getClickId(conversion, "rdt_cid")) {
|