Skip to content

Commit 5e40886

Browse files
Milton RucksMilton Rucks
authored andcommitted
Add server-to-server Exchange calendar presence
1 parent edbaeef commit 5e40886

44 files changed

Lines changed: 2990 additions & 3 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/meteor/app/metrics/server/lib/metrics.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,31 @@ export const metrics = {
325325
help: 'Histogram of time taken in seconds for an item to be processed for the first time by Omni queues',
326326
buckets: queueWaitBuckets,
327327
}),
328+
enterpriseCalendarSyncTotal: new client.Counter({
329+
name: 'rocketchat_enterprise_calendar_sync_total',
330+
labelNames: ['provider', 'result'],
331+
help: 'Enterprise calendar mailbox synchronization attempts by provider and sanitized result',
332+
}),
333+
enterpriseCalendarSyncDurationSeconds: new client.Histogram({
334+
name: 'rocketchat_enterprise_calendar_sync_duration_seconds',
335+
labelNames: ['provider'],
336+
help: 'Enterprise calendar mailbox synchronization duration',
337+
buckets: latencyBuckets,
338+
}),
339+
enterpriseCalendarNotificationsTotal: new client.Counter({
340+
name: 'rocketchat_enterprise_calendar_notifications_total',
341+
labelNames: ['result'],
342+
help: 'Microsoft Graph calendar notifications by validation result',
343+
}),
344+
enterpriseCalendarPresenceTransitionsTotal: new client.Counter({
345+
name: 'rocketchat_enterprise_calendar_presence_transitions_total',
346+
labelNames: ['status'],
347+
help: 'Enterprise calendar presence transitions by content-free status',
348+
}),
349+
enterpriseCalendarConfiguredUsers: new client.Gauge({
350+
name: 'rocketchat_enterprise_calendar_configured_users',
351+
help: 'Number of explicitly enabled enterprise calendar mappings',
352+
}),
328353
};
329354

330355
// Metrics
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { Settings } from '@rocket.chat/models';
2+
import {
3+
ajv,
4+
validateBadRequestErrorResponse,
5+
validateForbiddenErrorResponse,
6+
validateUnauthorizedErrorResponse,
7+
} from '@rocket.chat/rest-typings';
8+
9+
import { notifyOnSettingChangedById } from '../../../app/lib/server/lib/notifyListener';
10+
import { settings } from '../../../app/settings/server';
11+
import { API } from '../../../server/api/api';
12+
import { updateAuditedByUser } from '../../../server/settings/lib/auditedSettingUpdates';
13+
import {
14+
createGraphProvider,
15+
encryptCalendarSetting,
16+
generateWebhookClientState,
17+
getEnterpriseCalendarHealth,
18+
invalidateGraphSubscriptions,
19+
recordGraphConnectionTest,
20+
requestEnterpriseCalendarResync,
21+
} from '../enterprise-calendar/runtime';
22+
23+
type ConfigureBody =
24+
| { credentialType: 'client-secret'; clientSecret: string; webhookClientState?: string }
25+
| { credentialType: 'certificate'; certificate: string; privateKey: string; webhookClientState?: string };
26+
27+
const configureBody = ajv.compile<ConfigureBody>({
28+
oneOf: [
29+
{
30+
type: 'object',
31+
properties: {
32+
credentialType: { const: 'client-secret' },
33+
clientSecret: { type: 'string', minLength: 1, maxLength: 4096 },
34+
webhookClientState: { type: 'string', minLength: 32, maxLength: 255 },
35+
},
36+
required: ['credentialType', 'clientSecret'],
37+
additionalProperties: false,
38+
},
39+
{
40+
type: 'object',
41+
properties: {
42+
credentialType: { const: 'certificate' },
43+
certificate: { type: 'string', minLength: 1, maxLength: 16384 },
44+
privateKey: { type: 'string', minLength: 1, maxLength: 32768 },
45+
webhookClientState: { type: 'string', minLength: 32, maxLength: 255 },
46+
},
47+
required: ['credentialType', 'certificate', 'privateKey'],
48+
additionalProperties: false,
49+
},
50+
],
51+
});
52+
53+
const testBody = ajv.compile<{ mailbox?: string }>({
54+
type: 'object',
55+
properties: { mailbox: { type: 'string', format: 'email', maxLength: 320 } },
56+
additionalProperties: false,
57+
});
58+
59+
const resyncBody = ajv.compile<{ userId?: string }>({
60+
type: 'object',
61+
properties: { userId: { type: 'string', minLength: 1, maxLength: 64 } },
62+
additionalProperties: false,
63+
});
64+
65+
const genericResponse = ajv.compile({
66+
type: 'object',
67+
properties: { success: { type: 'boolean', enum: [true] }, result: { type: 'object' } },
68+
required: ['success', 'result'],
69+
additionalProperties: false,
70+
});
71+
72+
API.v1.post(
73+
'enterprise-calendar.configure-graph-credential',
74+
{
75+
authRequired: true,
76+
permissionsRequired: ['edit-privileged-setting'],
77+
license: ['outlook-calendar'],
78+
body: configureBody,
79+
rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60_000 },
80+
response: {
81+
200: genericResponse,
82+
400: validateBadRequestErrorResponse,
83+
401: validateUnauthorizedErrorResponse,
84+
403: validateForbiddenErrorResponse,
85+
},
86+
},
87+
async function action() {
88+
const updates: Array<[string, string]> = [['Enterprise_Calendar_Graph_Credential_Type', this.bodyParams.credentialType]];
89+
if (this.bodyParams.credentialType === 'client-secret') {
90+
updates.push([
91+
'Enterprise_Calendar_Graph_Client_Secret',
92+
encryptCalendarSetting('Enterprise_Calendar_Graph_Client_Secret', this.bodyParams.clientSecret),
93+
]);
94+
updates.push(['Enterprise_Calendar_Graph_Certificate', ''], ['Enterprise_Calendar_Graph_Private_Key', '']);
95+
} else {
96+
updates.push(
97+
[
98+
'Enterprise_Calendar_Graph_Certificate',
99+
encryptCalendarSetting('Enterprise_Calendar_Graph_Certificate', this.bodyParams.certificate),
100+
],
101+
[
102+
'Enterprise_Calendar_Graph_Private_Key',
103+
encryptCalendarSetting('Enterprise_Calendar_Graph_Private_Key', this.bodyParams.privateKey),
104+
],
105+
);
106+
updates.push(['Enterprise_Calendar_Graph_Client_Secret', '']);
107+
}
108+
if (this.bodyParams.webhookClientState || !settings.get<string>('Enterprise_Calendar_Graph_Webhook_Client_State')) {
109+
const clientState = this.bodyParams.webhookClientState ?? generateWebhookClientState();
110+
updates.push([
111+
'Enterprise_Calendar_Graph_Webhook_Client_State',
112+
encryptCalendarSetting('Enterprise_Calendar_Graph_Webhook_Client_State', clientState),
113+
]);
114+
}
115+
const audit = updateAuditedByUser({
116+
_id: this.userId,
117+
username: this.user.username ?? '',
118+
ip: this.requestIp ?? '',
119+
useragent: this.request.headers.get('user-agent') ?? '',
120+
});
121+
for (const [id, value] of updates) {
122+
await audit(Settings.updateValueById, id, value);
123+
void notifyOnSettingChangedById(id);
124+
}
125+
if (updates.some(([id]) => id === 'Enterprise_Calendar_Graph_Webhook_Client_State')) {
126+
await invalidateGraphSubscriptions();
127+
}
128+
return API.v1.success({ result: { credentialConfigured: true, webhookClientStateConfigured: true } });
129+
},
130+
);
131+
132+
API.v1.get(
133+
'enterprise-calendar.health',
134+
{
135+
authRequired: true,
136+
permissionsRequired: ['view-privileged-setting'],
137+
license: ['outlook-calendar'],
138+
response: {
139+
200: genericResponse,
140+
401: validateUnauthorizedErrorResponse,
141+
403: validateForbiddenErrorResponse,
142+
},
143+
},
144+
async function action() {
145+
return API.v1.success({ result: await getEnterpriseCalendarHealth() });
146+
},
147+
);
148+
149+
API.v1.post(
150+
'enterprise-calendar.resync',
151+
{
152+
authRequired: true,
153+
permissionsRequired: ['edit-privileged-setting'],
154+
license: ['outlook-calendar'],
155+
body: resyncBody,
156+
rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60_000 },
157+
response: {
158+
200: genericResponse,
159+
400: validateBadRequestErrorResponse,
160+
401: validateUnauthorizedErrorResponse,
161+
403: validateForbiddenErrorResponse,
162+
},
163+
},
164+
async function action() {
165+
return API.v1.success({ result: { queuedUsers: await requestEnterpriseCalendarResync(this.bodyParams.userId) } });
166+
},
167+
);
168+
169+
API.v1.post(
170+
'enterprise-calendar.test-graph',
171+
{
172+
authRequired: true,
173+
permissionsRequired: ['view-privileged-setting'],
174+
license: ['outlook-calendar'],
175+
body: testBody,
176+
rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60_000 },
177+
response: {
178+
200: genericResponse,
179+
400: validateBadRequestErrorResponse,
180+
401: validateUnauthorizedErrorResponse,
181+
403: validateForbiddenErrorResponse,
182+
},
183+
},
184+
async function action() {
185+
const validation = await createGraphProvider().validateConfiguration(
186+
this.bodyParams.mailbox ? { provider: 'microsoft-graph', address: this.bodyParams.mailbox } : undefined,
187+
);
188+
await recordGraphConnectionTest(validation);
189+
return API.v1.success({ result: validation });
190+
},
191+
);

apps/meteor/ee/server/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ import '../apps/communication/uikit';
88
import './engagementDashboard';
99
import './audit';
1010
import './abac';
11+
import './enterpriseCalendar';

apps/meteor/ee/server/configuration/outlookCalendar.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { Calendar } from '@rocket.chat/core-services';
22
import { License } from '@rocket.chat/license';
33
import { Meteor } from 'meteor/meteor';
44

5+
import { setupEnterpriseCalendar } from '../enterprise-calendar/runtime';
56
import { addSettings } from '../settings/outlookCalendar';
7+
import '../enterprise-calendar/webhook';
68

79
Meteor.startup(() =>
810
License.onLicense('outlook-calendar', async () => {
911
addSettings();
1012

1113
await Calendar.setupNextNotification();
1214
await Calendar.setupNextStatusChange();
15+
await setupEnterpriseCalendar();
1316
}),
1417
);

0 commit comments

Comments
 (0)