Skip to content
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
7 changes: 7 additions & 0 deletions .changeset/clean-dryers-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/model-typings': patch
'@rocket.chat/models': patch
'@rocket.chat/meteor': patch
---

Enforces app limitations on license downgrade by disabling premium marketplace apps, limiting marketplace apps to the oldest 5, and disabling private apps unless grandfathered based on historical statistics.
109 changes: 106 additions & 3 deletions apps/meteor/ee/server/apps/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { registerOrchestrator } from '@rocket.chat/apps';
import { EssentialAppDisabledException } from '@rocket.chat/apps-engine/definition/exceptions';
import { AppManager } from '@rocket.chat/apps-engine/server/AppManager';
import { Logger } from '@rocket.chat/logger';
import { AppLogs, Apps as AppsModel, AppsPersistence } from '@rocket.chat/models';
import { AppLogs, Apps as AppsModel, AppsPersistence, Statistics } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication';
Expand Down Expand Up @@ -51,6 +51,7 @@ export class AppServerOrchestrator {
this._model = AppsModel;
this._logModel = AppLogs;
this._persistModel = AppsPersistence;
this._statisticsModel = Statistics;
this._storage = new AppRealStorage(this._model);
this._logStorage = new AppRealLogStorage(this._logModel);
this._appSourceStorage = new ConfigurableAppSourceStorage(
Expand Down Expand Up @@ -99,6 +100,10 @@ export class AppServerOrchestrator {
return this._persistModel;
}

getStatisticsModel() {
return this._statisticsModel;
}

getStorage() {
return this._storage;
}
Expand Down Expand Up @@ -201,10 +206,108 @@ export class AppServerOrchestrator {
await Promise.all(apps.map((app) => this.getNotifier().appUpdated(app.getID())));
}

async findMajorVersionUpgradeDate(targetVersion = 7) {
let upgradeToV7Date = null;
let hadPreTargetVersion = false;

try {
const statistics = await this.getStatisticsModel().findInstallationDates();
if (!statistics || statistics.length === 0) {
this._rocketchatLogger.info('No statistics found');
return upgradeToV7Date;
}

const statsAscendingByInstallDate = statistics.sort((a, b) => new Date(a.installedAt) - new Date(b.installedAt));
for (const stat of statsAscendingByInstallDate) {
const version = stat.version || '';

if (!version) {
continue;
}

const majorVersion = parseInt(version.split('.')[0], 10);
if (isNaN(majorVersion)) {
continue;
}

if (majorVersion < targetVersion) {
hadPreTargetVersion = true;
}

if (hadPreTargetVersion && majorVersion >= targetVersion) {
upgradeToV7Date = new Date(stat.installedAt);
this._rocketchatLogger.info(`Found upgrade to v${targetVersion} date: ${upgradeToV7Date.toISOString()}`);
break;
}
}
} catch (error) {
this._rocketchatLogger.error('Error checking statistics for version history:', error.message);
}

return upgradeToV7Date;
}

async disableMarketplaceApps() {
const apps = await this.getManager().get({ installationSource: 'marketplace' });
return this.disableApps('marketplace', false, 5);
}

async disablePrivateApps() {
return this.disableApps('private', true, 0);
}

await Promise.all(apps.map((app) => this.getManager().disable(app.getID())));
async disableApps(installationSource, grandfatherApps, maxApps) {
const upgradeToV7Date = await this.findMajorVersionUpgradeDate();
const apps = await this.getManager().get({ installationSource });

const grandfathered = [];
const toKeep = [];
const toDisable = [];

for (const app of apps) {
const storageItem = app.getStorageItem();
const isEnabled = ['enabled', 'manually_enabled', 'auto_enabled'].includes(storageItem.status);
const marketplaceInfo = storageItem.marketplaceInfo && storageItem.marketplaceInfo[0];

const wasInstalledBeforeV7 = upgradeToV7Date && storageItem.createdAt && new Date(storageItem.createdAt) < upgradeToV7Date;

if (wasInstalledBeforeV7 && isEnabled && grandfatherApps) {
grandfathered.push(app);
continue;
}

if (marketplaceInfo?.isEnterpriseOnly === true && installationSource === 'marketplace') {
toDisable.push(app);
continue;
}

if (isEnabled) {
toKeep.push(app);
}
}

toKeep.sort((a, b) => new Date(a.getStorageItem().createdAt || 0) - new Date(b.getStorageItem().createdAt || 0));

if (toKeep.length > maxApps) {
toDisable.push(...toKeep.splice(maxApps));
}

if (toDisable.length === 0) {
return;
}

const disablePromises = toDisable.map((app) => {
const appId = app.getID();
return this.getManager().disable(appId);
});

try {
await Promise.all(disablePromises);
this._rocketchatLogger.info(
`${installationSource} apps processing complete - kept ${grandfathered.length + toKeep.length}, disabled ${toDisable.length}`,
);
} catch (error) {
this._rocketchatLogger.error('Error disabling apps:', error.message);
}
}

async unload() {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/ee/server/apps/startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const startupApp = async function startupApp() {

async function migratePrivateAppsCallback() {
void Apps.migratePrivateApps();
void Apps.disablePrivateApps();
void Apps.disableMarketplaceApps();
}

Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/models/IStatisticsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface IStatisticsModel extends IBaseModel<IStats> {
findLast(): Promise<IStats>;
findMonthlyPeakConnections(): Promise<Pick<IStats, 'dailyPeakConnections' | 'createdAt'> | null>;
findLastStatsToken(): Promise<IStats['statsToken']>;
findInstallationDates(): Promise<Pick<IStats, 'version' | 'installedAt'>[]>;
}
23 changes: 23 additions & 0 deletions packages/models/src/models/Statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,27 @@ export class StatisticsRaw extends BaseRaw<IStats> implements IStatisticsModel {
},
);
}

async findInstallationDates() {
return this.col
.aggregate<Pick<IStats, 'version' | 'installedAt'>>([
{
$group: {
_id: '$version',
installedAt: { $min: '$installedAt' },
},
},
{
$project: {
_id: 0,
version: '$_id',
installedAt: 1,
},
},
{
$sort: { installedAt: 1 },
},
])
.toArray();
}
}
Loading