-
Notifications
You must be signed in to change notification settings - Fork 306
Expand file tree
/
Copy pathentitlements.ts
More file actions
211 lines (180 loc) · 6.71 KB
/
Copy pathentitlements.ts
File metadata and controls
211 lines (180 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { base64Decode } from "./utils.js";
import { z } from "zod";
import { createLogger } from "./logger.js";
import { env } from "./env.server.js";
import { verifySignature } from "./crypto.js";
import { License } from "@sourcebot/db";
import { LicenseStatus } from "./types.js";
const logger = createLogger('entitlements');
const offlineLicensePrefix = "sourcebot_ee_";
const offlineLicensePayloadSchema = z.object({
id: z.string(),
seats: z.number().optional(),
// Whether anonymous (unauthenticated) access is permitted.
anonymousAccess: z.boolean().optional(),
// ISO 8601 date string
expiryDate: z.string().datetime(),
sig: z.string(),
});
type getValidOfflineLicense = z.infer<typeof offlineLicensePayloadSchema>;
const ACTIVE_ONLINE_LICENSE_STATUSES: LicenseStatus[] = [
'active',
'trialing',
'past_due',
];
// @WARNING: when adding a new entitlement to this list, make sure
// lighthouse/lambda/entitlements.ts is also updated && deployed
// prior to rolling a new Sourcebot version.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ALL_ENTITLEMENTS = [
"search-contexts",
"sso",
"code-nav",
"audit",
"analytics",
"permission-syncing",
"github-app",
"org-management",
"oauth",
"ask",
"mcp",
"scim"
] as const;
export type Entitlement = (typeof ALL_ENTITLEMENTS)[number];
const decodeOfflineLicenseKeyPayload = (payload: string): getValidOfflineLicense | null => {
try {
const decodedPayload = base64Decode(payload);
const payloadJson = JSON.parse(decodedPayload);
const licenseData = offlineLicensePayloadSchema.parse(payloadJson);
// Keys are listed alphabetically to match the canonical JSON the
// signer produces (Python `json.dumps(..., sort_keys=True)`).
// `JSON.stringify` drops `undefined` values, so omitted optional
// fields (e.g. a legacy key without `anonymousAccess`) verify exactly
// as they were originally signed.
const dataToVerify = JSON.stringify({
anonymousAccess: licenseData.anonymousAccess,
expiryDate: licenseData.expiryDate,
id: licenseData.id,
seats: licenseData.seats
});
const isSignatureValid = verifySignature(dataToVerify, licenseData.sig, env.SOURCEBOT_PUBLIC_KEY_PATH);
if (!isSignatureValid) {
logger.error('License key signature verification failed');
return null;
}
return licenseData;
} catch (error) {
logger.error(`Failed to decode license key payload: ${error}`);
return null;
}
}
const getDecodedOfflineLicense = (): getValidOfflineLicense | null => {
const licenseKey = env.SOURCEBOT_EE_LICENSE_KEY;
if (!licenseKey || !licenseKey.startsWith(offlineLicensePrefix)) {
return null;
}
return decodeOfflineLicenseKeyPayload(licenseKey.substring(offlineLicensePrefix.length));
}
const getValidOfflineLicense = (): getValidOfflineLicense | null => {
const payload = getDecodedOfflineLicense();
if (!payload) {
return null;
}
const expiryDate = new Date(payload.expiryDate);
if (expiryDate.getTime() < new Date().getTime()) {
return null;
}
return payload;
}
// If the license hasn't successfully synced with Lighthouse for this long,
// the locally-cached state is no longer trusted. This guards against an
// operator blocking egress to prevent the license row from hearing about
// a canceled or past-due subscription. 7 days absorbs week-long transient
// outages (weekends, firewall rollouts) without punishing legitimate
// customers.
export const STALE_ONLINE_LICENSE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
// Surface a UI warning (banner + "refreshed" timestamp color) when the
// license hasn't synced for this long. Must be < the enforcement threshold
// so the warning has a chance to fire before entitlements are stripped.
export const STALE_ONLINE_LICENSE_WARNING_THRESHOLD_MS = 48 * 60 * 60 * 1000;
const getValidOnlineLicense = (_license: License | null): License | null => {
if (
_license &&
_license.status &&
ACTIVE_ONLINE_LICENSE_STATUSES.includes(_license.status as LicenseStatus) &&
_license.lastSyncAt &&
(Date.now() - _license.lastSyncAt.getTime()) <= STALE_ONLINE_LICENSE_THRESHOLD_MS &&
_license.lastSyncErrorCode !== 'ACTIVATION_CODE_BOUND_TO_DIFFERENT_INSTANCE'
) {
return _license;
}
return null;
}
export const isValidOfflineLicenseActive = (): boolean => {
return getValidOfflineLicense() !== null;
}
export const isValidOnlineLicenseActive = (_license: License | null): boolean => {
return getValidOnlineLicense(_license) !== null;
}
export const isValidLicenseActive = (_license: License | null): boolean => {
return (
isValidOfflineLicenseActive() ||
isValidOnlineLicenseActive(_license)
);
}
export const isAnonymousAccessAvailable = (_license: License | null): boolean => {
const offlineKey = getValidOfflineLicense();
if (offlineKey) {
return offlineKey.anonymousAccess === true;
}
const onlineLicense = getValidOnlineLicense(_license);
if (onlineLicense) {
return false;
}
return true;
}
export const getEntitlements = (_license: License | null): Entitlement[] => {
const offlineLicense = getValidOfflineLicense();
if (offlineLicense) {
return ALL_ENTITLEMENTS as unknown as Entitlement[];
}
const onlineLicense = getValidOnlineLicense(_license);
if (onlineLicense) {
return onlineLicense.entitlements as unknown as Entitlement[];
}
else {
return [];
}
}
export const hasEntitlement = (entitlement: Entitlement, _license: License | null) => {
const entitlements = getEntitlements(_license);
return entitlements.includes(entitlement);
}
export type OfflineLicenseMetadata = {
id: string;
seats?: number;
anonymousAccess?: boolean;
expiryDate: string;
}
// Returns the metadata of the offline license if one is configured, even
// if it has expired. Callers that only care about active entitlements
// should use `getEntitlements` / `getValidOfflineLicense` instead.
export const getOfflineLicenseMetadata = (): OfflineLicenseMetadata | null => {
const license = getDecodedOfflineLicense();
if (!license) {
return null;
}
return {
id: license.id,
seats: license.seats,
anonymousAccess: license.anonymousAccess,
expiryDate: license.expiryDate,
};
}
export const getSeatCap = (): number | undefined => {
const offlineLicense = getValidOfflineLicense();
if (offlineLicense?.seats && offlineLicense.seats > 0) {
return offlineLicense.seats;
}
return undefined;
}