Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deployment Fingerprint #30411

Merged
merged 18 commits into from
Oct 5, 2023
Merged
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
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 @@ -3652,6 +3658,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 @@ -5253,6 +5261,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
Loading