Skip to content

Commit

Permalink
feat: Deployment Fingerprint (#30411)
Browse files Browse the repository at this point in the history
  • Loading branch information
rodrigok authored and debdutdeb committed Oct 26, 2023
1 parent 6b71005 commit e380155
Show file tree
Hide file tree
Showing 21 changed files with 395 additions and 7 deletions.
10 changes: 10 additions & 0 deletions .changeset/tidy-bears-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/rest-typings": minor
---

Create a deployment fingerprint to identify possible deployment changes caused by database cloning. A question to the admin will confirm if it's a regular deployment change or an intent of a new deployment and correct identification values as needed.
The fingerprint is composed by `${siteUrl}${dbConnectionString}` and hashed via `sha256` in `base64`.
An environment variable named `AUTO_ACCEPT_FINGERPRINT`, when set to `true`, can be used to auto-accept an expected fingerprint change as a regular deployment update.
76 changes: 75 additions & 1 deletion apps/meteor/app/api/server/v1/misc.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import crypto from 'crypto';

import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Settings, Users } from '@rocket.chat/models';
import {
isShieldSvgProps,
isSpotlightProps,
isDirectoryProps,
isMethodCallProps,
isMethodCallAnonProps,
isFingerprintProps,
isMeteorCall,
validateParamsPwGetPolicyRest,
} from '@rocket.chat/rest-typings';
Expand All @@ -16,6 +17,7 @@ import EJSON from 'ejson';
import { check } from 'meteor/check';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { Meteor } from 'meteor/meteor';
import { v4 as uuidv4 } from 'uuid';

import { i18n } from '../../../../server/lib/i18n';
import { SystemLogger } from '../../../../server/lib/logger/system';
Expand Down Expand Up @@ -643,3 +645,75 @@ API.v1.addRoute(
},
},
);

/**
* @openapi
* /api/v1/fingerprint:
* post:
* description: Update Fingerprint definition as a new workspace or update of configuration
* security:
* $ref: '#/security/authenticated'
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* setDeploymentAs:
* type: string
* example: |
* {
* "setDeploymentAs": "new-workspace"
* }
* responses:
* 200:
* description: Workspace successfully configured
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiSuccessV1'
* default:
* description: Unexpected error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiFailureV1'
*/
API.v1.addRoute(
'fingerprint',
{
authRequired: true,
validateParams: isFingerprintProps,
},
{
async post() {
check(this.bodyParams, {
setDeploymentAs: String,
});

if (this.bodyParams.setDeploymentAs === 'new-workspace') {
await Promise.all([
Settings.resetValueById('uniqueID', process.env.DEPLOYMENT_ID || uuidv4()),
// Settings.resetValueById('Cloud_Url'),
Settings.resetValueById('Cloud_Service_Agree_PrivacyTerms'),
Settings.resetValueById('Cloud_Workspace_Id'),
Settings.resetValueById('Cloud_Workspace_Name'),
Settings.resetValueById('Cloud_Workspace_Client_Id'),
Settings.resetValueById('Cloud_Workspace_Client_Secret'),
Settings.resetValueById('Cloud_Workspace_Client_Secret_Expires_At'),
Settings.resetValueById('Cloud_Workspace_Registration_Client_Uri'),
Settings.resetValueById('Cloud_Workspace_PublicKey'),
Settings.resetValueById('Cloud_Workspace_License'),
Settings.resetValueById('Cloud_Workspace_Had_Trial'),
Settings.resetValueById('Cloud_Workspace_Access_Token'),
Settings.resetValueById('Cloud_Workspace_Access_Token_Expires_At', new Date(0)),
Settings.resetValueById('Cloud_Workspace_Registration_State'),
]);
}

await Settings.updateValueById('Deployment_FingerPrint_Verified', true);

return API.v1.success({});
},
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { LICENSE_VERSION } from '../license';

export type WorkspaceRegistrationData<T> = {
uniqueId: string;
deploymentFingerprintHash: string;
deploymentFingerprintVerified: boolean;
workspaceId: string;
address: string;
contactName: string;
Expand Down Expand Up @@ -50,6 +52,8 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine
const npsEnabled = settings.get<string>('NPS_survey_enabled');
const agreePrivacyTerms = settings.get<string>('Cloud_Service_Agree_PrivacyTerms');
const setupWizardState = settings.get<string>('Show_Setup_Wizard');
const deploymentFingerprintHash = settings.get<string>('Deployment_FingerPrint_Hash');
const deploymentFingerprintVerified = settings.get<boolean>('Deployment_FingerPrint_Verified');

const firstUser = await Users.getOldest({ projection: { name: 1, emails: 1 } });
const contactName = firstUser?.name || '';
Expand All @@ -59,6 +63,8 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine

return {
uniqueId: stats.uniqueId,
deploymentFingerprintHash,
deploymentFingerprintVerified,
workspaceId,
address,
contactName,
Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/app/statistics/server/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export const statistics = {
statistics.installedAt = uniqueID.createdAt.toISOString();
}

statistics.deploymentFingerprintHash = settings.get('Deployment_FingerPrint_Hash');
statistics.deploymentFingerprintVerified = settings.get('Deployment_FingerPrint_Verified');

if (Info) {
statistics.version = Info.version;
statistics.tag = Info.tag;
Expand Down
44 changes: 44 additions & 0 deletions apps/meteor/client/components/FingerprintChangeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Box } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';

import GenericModal from './GenericModal';

type FingerprintChangeModalProps = {
onConfirm: () => void;
onCancel: () => void;
onClose: () => void;
};

const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => {
const t = useTranslation();
return (
<GenericModal
variant='warning'
title={t('Unique_ID_change_detected')}
onConfirm={onConfirm}
onClose={onClose}
onCancel={onCancel}
confirmText={t('New_workspace')}
cancelText={t('Configuration_update')}
>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: t('Unique_ID_change_detected_description'),
}}
/>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: t('Unique_ID_change_detected_learn_more_link'),
}}
/>
</GenericModal>
);
};

export default FingerprintChangeModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';

import GenericModal from './GenericModal';

type FingerprintChangeModalConfirmationProps = {
onConfirm: () => void;
onCancel: () => void;
newWorkspace: boolean;
};

const FingerprintChangeModalConfirmation = ({
onConfirm,
onCancel,
newWorkspace,
}: FingerprintChangeModalConfirmationProps): ReactElement => {
const t = useTranslation();
return (
<GenericModal
variant='warning'
title={newWorkspace ? t('Confirm_new_workspace') : t('Confirm_configuration_update')}
onConfirm={onConfirm}
onCancel={onCancel}
cancelText={t('Back')}
confirmText={newWorkspace ? t('Confirm_new_workspace') : t('Confirm_configuration_update')}
>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: newWorkspace ? t('Confirm_new_workspace_description') : t('Confirm_configuration_update_description'),
}}
/>
<Box
is='p'
mbe={16}
dangerouslySetInnerHTML={{
__html: t('Unique_ID_change_detected_learn_more_link'),
}}
/>
</GenericModal>
);
};

export default FingerprintChangeModalConfirmation;
71 changes: 71 additions & 0 deletions apps/meteor/client/startup/rootUrlChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Roles } from '../../app/models/client';
import { settings } from '../../app/settings/client';
import { sdk } from '../../app/utils/client/lib/SDKClient';
import { t } from '../../app/utils/lib/i18n';
import FingerprintChangeModal from '../components/FingerprintChangeModal';
import FingerprintChangeModalConfirmation from '../components/FingerprintChangeModalConfirmation';
import UrlChangeModal from '../components/UrlChangeModal';
import { imperativeModal } from '../lib/imperativeModal';
import { dispatchToastMessage } from '../lib/toast';
Expand Down Expand Up @@ -58,3 +60,72 @@ Meteor.startup(() => {
return c.stop();
});
});

Meteor.startup(() => {
Tracker.autorun((c) => {
const userId = Meteor.userId();
if (!userId) {
return;
}

if (!Roles.ready.get() || !isSyncReady.get()) {
return;
}

if (hasRole(userId, 'admin') === false) {
return c.stop();
}

const deploymentFingerPrintVerified = settings.get('Deployment_FingerPrint_Verified');
if (deploymentFingerPrintVerified == null || deploymentFingerPrintVerified === true) {
return;
}

const updateWorkspace = (): void => {
imperativeModal.close();
void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'updated-configuration' }).then(() => {
dispatchToastMessage({ type: 'success', message: t('Configuration_update_confirmed') });
});
};

const setNewWorkspace = (): void => {
imperativeModal.close();
void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'new-workspace' }).then(() => {
dispatchToastMessage({ type: 'success', message: t('New_workspace_confirmed') });
});
};

const openModal = (): void => {
imperativeModal.open({
component: FingerprintChangeModal,
props: {
onConfirm: () => {
imperativeModal.open({
component: FingerprintChangeModalConfirmation,
props: {
onConfirm: setNewWorkspace,
onCancel: openModal,
newWorkspace: true,
},
});
},
onCancel: () => {
imperativeModal.open({
component: FingerprintChangeModalConfirmation,
props: {
onConfirm: updateWorkspace,
onCancel: openModal,
newWorkspace: false,
},
});
},
onClose: imperativeModal.close,
},
});
};

openModal();

return c.stop();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export default {
_id: '',
wizard: {},
uniqueId: '',
deploymentFingerprintHash: '',
deploymentFingerprintVerified: true,
installedAt: '',
version: '1.0.0',
tag: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export default {
_id: '',
wizard: {},
uniqueId: '',
deploymentFingerprintHash: '',
deploymentFingerprintVerified: true,
installedAt: '',
version: '',
tag: '',
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/client/views/admin/info/UsageCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export default {
_id: '',
wizard: {},
uniqueId: '',
deploymentFingerprintHash: '',
deploymentFingerprintVerified: true,
installedAt: '',
version: '',
tag: '',
Expand Down
11 changes: 11 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1106,8 +1106,14 @@
"Confirm_New_Password_Placeholder": "Please re-enter new password...",
"Confirm_password": "Confirm password",
"Confirm_your_password": "Confirm your password",
"Confirm_configuration_update_description": "Identification data and cloud connection data will be retained.<br/><br/><strong>Warning</strong>: If this is actually a new workspace, please go back and select new workspace option to avoid communication conflicts.",
"Confirm_configuration_update": "Confirm configuration update",
"Confirm_new_workspace_description": "Identification data and cloud connection data will be reset.<br/><br/><strong>Warning</strong>: License can be affected if changing workspace URL.",
"Confirm_new_workspace": "Confirm new workspace",
"Confirmation": "Confirmation",
"Configure_video_conference": "Configure conference call",
"Configuration_update_confirmed": "Configuration update confirmed",
"Configuration_update": "Configuration update",
"Connect": "Connect",
"Connected": "Connected",
"Connect_SSL_TLS": "Connect with SSL/TLS",
Expand Down Expand Up @@ -3658,6 +3664,8 @@
"New_version_available_(s)": "New version available (%s)",
"New_videocall_request": "New Video Call Request",
"New_visitor_navigation": "New Navigation: {{history}}",
"New_workspace_confirmed": "New workspace confirmed",
"New_workspace": "New workspace",
"Newer_than": "Newer than",
"Newer_than_may_not_exceed_Older_than": "\"Newer than\" may not exceed \"Older than\"",
"Nickname": "Nickname",
Expand Down Expand Up @@ -5259,6 +5267,9 @@
"Uninstall": "Uninstall",
"Units": "Units",
"Unit_removed": "Unit Removed",
"Unique_ID_change_detected_description": "Information that identifies this workspace has changed. This can happen when the site URL or database connection string are changed or when a new workspace is created from a copy of an existing database.<br/><br/>Would you like to proceed with a configuration update to the existing workspace or create a new workspace and unique ID?",
"Unique_ID_change_detected_learn_more_link": "<a href=\"https://go.rocket.chat/i/fingerprint-changed-faq\" target=\"_blank\">Learn more</a>",
"Unique_ID_change_detected": "Unique ID change detected",
"Unknown_Import_State": "Unknown Import State",
"Unknown_User": "Unknown User",
"Unlimited": "Unlimited",
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/server/configureLogLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { LogLevelSetting } from '@rocket.chat/logger';
import { logLevel } from '@rocket.chat/logger';
import { Settings } from '@rocket.chat/models';

const LogLevel = await Settings.getValueById('Log_Level');
if (LogLevel) {
logLevel.emit('changed', LogLevel as LogLevelSetting);
}
1 change: 1 addition & 0 deletions apps/meteor/server/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './models/startup';
import './configureLogLevel';
import './settings/index';
import '../ee/server/models/startup';
import './services/startup';
Expand Down
Loading

0 comments on commit e380155

Please sign in to comment.