Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/meteor/app/metrics/server/lib/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,31 @@ export const metrics = {
help: 'Histogram of time taken in seconds for an item to be processed for the first time by Omni queues',
buckets: queueWaitBuckets,
}),
enterpriseCalendarSyncTotal: new client.Counter({
name: 'rocketchat_enterprise_calendar_sync_total',
labelNames: ['provider', 'result'],
help: 'Enterprise calendar mailbox synchronization attempts by provider and sanitized result',
}),
enterpriseCalendarSyncDurationSeconds: new client.Histogram({
name: 'rocketchat_enterprise_calendar_sync_duration_seconds',
labelNames: ['provider'],
help: 'Enterprise calendar mailbox synchronization duration',
buckets: latencyBuckets,
}),
enterpriseCalendarNotificationsTotal: new client.Counter({
name: 'rocketchat_enterprise_calendar_notifications_total',
labelNames: ['result'],
help: 'Microsoft Graph calendar notifications by validation result',
}),
enterpriseCalendarPresenceTransitionsTotal: new client.Counter({
name: 'rocketchat_enterprise_calendar_presence_transitions_total',
labelNames: ['status'],
help: 'Enterprise calendar presence transitions by content-free status',
}),
enterpriseCalendarConfiguredUsers: new client.Gauge({
name: 'rocketchat_enterprise_calendar_configured_users',
help: 'Number of explicitly enabled enterprise calendar mappings',
}),
};

// Metrics
Expand Down
191 changes: 191 additions & 0 deletions apps/meteor/ee/server/api/enterpriseCalendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { Settings } from '@rocket.chat/models';
import {
ajv,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';

import { notifyOnSettingChangedById } from '../../../app/lib/server/lib/notifyListener';
import { settings } from '../../../app/settings/server';
import { API } from '../../../server/api/api';
import { updateAuditedByUser } from '../../../server/settings/lib/auditedSettingUpdates';
import {
createGraphProvider,
encryptCalendarSetting,
generateWebhookClientState,
getEnterpriseCalendarHealth,
invalidateGraphSubscriptions,
recordGraphConnectionTest,
requestEnterpriseCalendarResync,
} from '../enterprise-calendar/runtime';

type ConfigureBody =
| { credentialType: 'client-secret'; clientSecret: string; webhookClientState?: string }
| { credentialType: 'certificate'; certificate: string; privateKey: string; webhookClientState?: string };

const configureBody = ajv.compile<ConfigureBody>({
oneOf: [
{
type: 'object',
properties: {
credentialType: { const: 'client-secret' },
clientSecret: { type: 'string', minLength: 1, maxLength: 4096 },
webhookClientState: { type: 'string', minLength: 32, maxLength: 255 },
},
required: ['credentialType', 'clientSecret'],
additionalProperties: false,
},
{
type: 'object',
properties: {
credentialType: { const: 'certificate' },
certificate: { type: 'string', minLength: 1, maxLength: 16384 },
privateKey: { type: 'string', minLength: 1, maxLength: 32768 },
webhookClientState: { type: 'string', minLength: 32, maxLength: 255 },
},
required: ['credentialType', 'certificate', 'privateKey'],
additionalProperties: false,
},
],
});

const testBody = ajv.compile<{ mailbox?: string }>({
type: 'object',
properties: { mailbox: { type: 'string', format: 'email', maxLength: 320 } },
additionalProperties: false,
});

const resyncBody = ajv.compile<{ userId?: string }>({
type: 'object',
properties: { userId: { type: 'string', minLength: 1, maxLength: 64 } },
additionalProperties: false,
});

const genericResponse = ajv.compile({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] }, result: { type: 'object' } },
required: ['success', 'result'],
additionalProperties: false,
});

API.v1.post(
'enterprise-calendar.configure-graph-credential',
{
authRequired: true,
permissionsRequired: ['edit-privileged-setting'],
license: ['outlook-calendar'],
body: configureBody,
rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60_000 },
response: {
200: genericResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const updates: Array<[string, string]> = [['Enterprise_Calendar_Graph_Credential_Type', this.bodyParams.credentialType]];
if (this.bodyParams.credentialType === 'client-secret') {
updates.push([
'Enterprise_Calendar_Graph_Client_Secret',
encryptCalendarSetting('Enterprise_Calendar_Graph_Client_Secret', this.bodyParams.clientSecret),
]);
updates.push(['Enterprise_Calendar_Graph_Certificate', ''], ['Enterprise_Calendar_Graph_Private_Key', '']);
} else {
updates.push(
[
'Enterprise_Calendar_Graph_Certificate',
encryptCalendarSetting('Enterprise_Calendar_Graph_Certificate', this.bodyParams.certificate),
],
[
'Enterprise_Calendar_Graph_Private_Key',
encryptCalendarSetting('Enterprise_Calendar_Graph_Private_Key', this.bodyParams.privateKey),
],
);
updates.push(['Enterprise_Calendar_Graph_Client_Secret', '']);
}
if (this.bodyParams.webhookClientState || !settings.get<string>('Enterprise_Calendar_Graph_Webhook_Client_State')) {
const clientState = this.bodyParams.webhookClientState ?? generateWebhookClientState();
updates.push([
'Enterprise_Calendar_Graph_Webhook_Client_State',
encryptCalendarSetting('Enterprise_Calendar_Graph_Webhook_Client_State', clientState),
]);
}
const audit = updateAuditedByUser({
_id: this.userId,
username: this.user.username ?? '',
ip: this.requestIp ?? '',
useragent: this.request.headers.get('user-agent') ?? '',
});
for (const [id, value] of updates) {
await audit(Settings.updateValueById, id, value);
void notifyOnSettingChangedById(id);
}
if (updates.some(([id]) => id === 'Enterprise_Calendar_Graph_Webhook_Client_State')) {
await invalidateGraphSubscriptions();
}
return API.v1.success({ result: { credentialConfigured: true, webhookClientStateConfigured: true } });
},
);

API.v1.get(
'enterprise-calendar.health',
{
authRequired: true,
permissionsRequired: ['view-privileged-setting'],
license: ['outlook-calendar'],
response: {
200: genericResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
return API.v1.success({ result: await getEnterpriseCalendarHealth() });
},
);

API.v1.post(
'enterprise-calendar.resync',
{
authRequired: true,
permissionsRequired: ['edit-privileged-setting'],
license: ['outlook-calendar'],
body: resyncBody,
rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60_000 },
response: {
200: genericResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
return API.v1.success({ result: { queuedUsers: await requestEnterpriseCalendarResync(this.bodyParams.userId) } });
},
);

API.v1.post(
'enterprise-calendar.test-graph',
{
authRequired: true,
permissionsRequired: ['view-privileged-setting'],
license: ['outlook-calendar'],
body: testBody,
rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60_000 },
response: {
200: genericResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const validation = await createGraphProvider().validateConfiguration(
this.bodyParams.mailbox ? { provider: 'microsoft-graph', address: this.bodyParams.mailbox } : undefined,
);
await recordGraphConnectionTest(validation);
return API.v1.success({ result: validation });
},
);
1 change: 1 addition & 0 deletions apps/meteor/ee/server/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ import '../apps/communication/uikit';
import './engagementDashboard';
import './audit';
import './abac';
import './enterpriseCalendar';
3 changes: 3 additions & 0 deletions apps/meteor/ee/server/configuration/outlookCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { Calendar } from '@rocket.chat/core-services';
import { License } from '@rocket.chat/license';
import { Meteor } from 'meteor/meteor';

import { setupEnterpriseCalendar } from '../enterprise-calendar/runtime';
import { addSettings } from '../settings/outlookCalendar';
import '../enterprise-calendar/webhook';

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

await Calendar.setupNextNotification();
await Calendar.setupNextStatusChange();
await setupEnterpriseCalendar();
}),
);
Loading
Loading