Skip to content

Commit

Permalink
Removing references to old license structure
Browse files Browse the repository at this point in the history
  • Loading branch information
pierre-lehnen-rc committed Sep 15, 2023
1 parent 7253f90 commit b7f36ca
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 76 deletions.
39 changes: 26 additions & 13 deletions apps/meteor/ee/app/authorization/server/validateUserRoles.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { isEnterprise, getMaxGuestUsers } from '../../license/server';
import { i18n } from '../../../../server/lib/i18n';
import { isEnterprise, canAddNewGuestUser, canAddNewUser } from '../../license/server/license';

export const validateUserRoles = async function (userId, userData) {
if (!isEnterprise()) {
return;
}

if (!userData.roles.includes('guest')) {
const isGuest = Boolean(userData.roles?.includes('guest') && userData.roles.length === 1);
const currentUserData = userData._id ? await Users.findOneById(userData._id) : null;
const wasGuest = Boolean(currentUserData.roles?.includes('guest') && currentUserData.roles.length === 1);

if (currentUserData?.type === 'app') {
return;
}

if (userData.roles.length >= 2) {
throw new Meteor.Error('error-guests-cant-have-other-roles', "Guest users can't receive any other role", {
method: 'insertOrUpdateUser',
field: 'Assign_role',
});
if (isGuest) {
if (wasGuest) {
return;
}

if (!(await canAddNewGuestUser())) {
throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', {
method: 'insertOrUpdateUser',
field: 'Assign_role',
});
}

return;
}

if (!wasGuest && userData._id) {
return;
}

const guestCount = await Users.getActiveLocalGuestCount(userData._id);
if (guestCount >= getMaxGuestUsers()) {
throw new Meteor.Error('error-max-guests-number-reached', 'Maximum number of guests reached.', {
method: 'insertOrUpdateUser',
field: 'Assign_role',
});
if (!(await canAddNewUser())) {
throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached'));
}
};
2 changes: 1 addition & 1 deletion apps/meteor/ee/app/license/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import './settings';
import './methods';
import './startup';

export { onLicense, overwriteClassOnLicense, isEnterprise, getMaxGuestUsers } from './license';
export { onLicense, overwriteClassOnLicense, isEnterprise } from './license';

export { getStatistics } from './getStatistics';
21 changes: 21 additions & 0 deletions apps/meteor/ee/app/license/server/lib/getAppCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Apps } from '@rocket.chat/core-services';
import type { LicenseAppSources } from '@rocket.chat/core-typings';

import { getInstallationSourceFromAppStorageItem } from '../../../../../lib/apps/getInstallationSourceFromAppStorageItem';

export async function getAppCount(source: LicenseAppSources): Promise<number> {
if (!(await Apps.isInitialized())) {
return 0;
}

const apps = await Apps.getApps({ enabled: true });

if (!apps || !Array.isArray(apps)) {
return 0;
}

const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id)));
const activeAppsFromSameSource = storageItems.filter((item) => item && getInstallationSourceFromAppStorageItem(item) === source);

return activeAppsFromSameSource.length;
}
110 changes: 79 additions & 31 deletions apps/meteor/ee/app/license/server/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ import { EventEmitter } from 'events';

import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage';
import { Apps } from '@rocket.chat/core-services';
import type { ILicenseV2, ILicenseTag, ILicenseV3, Timestamp, LicenseBehavior } from '@rocket.chat/core-typings';
import type { ILicenseV2, ILicenseTag, ILicenseV3, Timestamp, LicenseBehavior, IUser, LicenseLimitKind } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { Users } from '@rocket.chat/models';
import { Users, Subscriptions } from '@rocket.chat/models';

import { getInstallationSourceFromAppStorageItem } from '../../../../lib/apps/getInstallationSourceFromAppStorageItem';
import type { BundleFeature } from './bundles';
import { getBundleModules, isBundle } from './bundles';
import decrypt from './decrypt';
import { fromV2toV3 } from './fromV2toV3';
import { isUnderAppLimits } from './lib/isUnderAppLimits';
import { getAppCount } from './lib/getAppCount';

const EnterpriseLicenses = new EventEmitter();

const logger = new Logger('License');

type LimitContext<T extends LicenseLimitKind> = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record<string, never>;

class LicenseClass {
private url: string | null = null;

Expand Down Expand Up @@ -248,12 +250,16 @@ class LicenseClass {
).reduce((prev, curr) => [...new Set([...prev, ...curr])], []);
}

private async shouldPreventAction(action: keyof ILicenseV3['limits'], newCount = 1): Promise<boolean> {
private async shouldPreventAction<T extends LicenseLimitKind>(
action: T,
context?: Partial<LimitContext<T>>,
newCount = 1,
): Promise<boolean> {
if (!this.valid) {
return false;
}

const currentValue = (await this.getCurrentValueForLicenseLimit(action)) + newCount;
const currentValue = (await this.getCurrentValueForLicenseLimit(action, context)) + newCount;
return Boolean(
this.license?.limits[action]
?.filter(({ behavior, max }) => behavior === 'prevent_action' && max >= 0)
Expand Down Expand Up @@ -303,7 +309,10 @@ class LicenseClass {
this.showLicense();
}

private async getCurrentValueForLicenseLimit(limitKey: keyof ILicenseV3['limits']): Promise<number> {
private async getCurrentValueForLicenseLimit<T extends LicenseLimitKind>(
limitKey: T,
context?: Partial<LimitContext<T>>,
): Promise<number> {
switch (limitKey) {
case 'activeUsers':
return this.getCurrentActiveUsers();
Expand All @@ -313,6 +322,11 @@ class LicenseClass {
return this.getCurrentPrivateAppsCount();
case 'marketplaceApps':
return this.getCurrentMarketplaceAppsCount();
case 'roomsPerGuest':
if (context?.userId) {
return Subscriptions.countByUserId(context.userId);
}
return 0;
default:
return 0;
}
Expand All @@ -323,22 +337,35 @@ class LicenseClass {
}

private async getCurrentGuestUsers(): Promise<number> {
// #TODO: Load current count
return 0;
return Users.getActiveLocalGuestCount();
}

private async getCurrentPrivateAppsCount(): Promise<number> {
// #TODO: Load current count
return 0;
return getAppCount('private');
}

private async getCurrentMarketplaceAppsCount(): Promise<number> {
// #TODO: Load current count
return 0;
return getAppCount('marketplace');
}

public async canAddNewUser(userCount = 1): Promise<boolean> {
return !(await this.shouldPreventAction('activeUsers', userCount));
return !(await this.shouldPreventAction('activeUsers', {}, userCount));
}

public async canAddNewGuestUser(guestCount = 1): Promise<boolean> {
return !(await this.shouldPreventAction('guestUsers', {}, guestCount));
}

public async canAddNewPrivateApp(appCount = 1): Promise<boolean> {
return !(await this.shouldPreventAction('privateApps', {}, appCount));
}

public async canAddNewMarketplaceApp(appCount = 1): Promise<boolean> {
return !(await this.shouldPreventAction('marketplaceApps', {}, appCount));
}

public async canAddNewGuestSubscription(guest: IUser['_id'], roomCount = 1): Promise<boolean> {
return !(await this.shouldPreventAction('roomsPerGuest', { userId: guest }, roomCount));
}

public async canEnableApp(app: IAppStorageItem): Promise<boolean> {
Expand All @@ -352,7 +379,13 @@ class LicenseClass {
return true;
}

return isUnderAppLimits(getAppsConfig(), getInstallationSourceFromAppStorageItem(app));
const source = getInstallationSourceFromAppStorageItem(app);
switch (source) {
case 'private':
return this.canAddNewPrivateApp();
default:
return this.canAddNewMarketplaceApp();
}
}

private showLicense(): void {
Expand All @@ -378,13 +411,22 @@ class LicenseClass {
console.log('-------------------------');
}

public getMaxActiveUsers(): number {
return (this.valid && this.license?.limits.activeUsers?.find(({ behavior }) => behavior === 'prevent_action')?.max) || 0;
}

public startedFairPolicy(): boolean {
return Boolean(this.valid && this.inFairPolicy);
}

public getLicenseLimit(kind: LicenseLimitKind): number | undefined {
if (!this.valid || !this.license) {
return;
}

const limitList = this.license.limits[kind];
if (!limitList?.length) {
return;
}

return Math.min(...limitList.map(({ max }) => max));
}
}

const License = new LicenseClass();
Expand Down Expand Up @@ -445,19 +487,9 @@ export function isEnterprise(): boolean {
return License.hasValidLicense();
}

export function getMaxGuestUsers(): number {
// #TODO: Adjust any place currently using this function to stop doing so.
return 0;
}

export function getMaxRoomsPerGuest(): number {
// #TODO: Adjust any place currently using this function to stop doing so.
return 0;
}

export function getMaxActiveUsers(): number {
// #TODO: Adjust any place currently using this function to stop doing so.
return License.getMaxActiveUsers();
return License.getLicenseLimit('activeUsers') ?? 0;
}

export function getUnmodifiedLicense(): ILicenseV3 | ILicenseV2 | undefined {
Expand All @@ -475,15 +507,31 @@ export function getTags(): ILicenseTag[] {
export function getAppsConfig(): NonNullable<ILicenseV2['apps']> {
// #TODO: Adjust any place currently using this function to stop doing so.
return {
maxPrivateApps: -1,
maxMarketplaceApps: -1,
maxPrivateApps: License.getLicenseLimit('privateApps') ?? -1,
maxMarketplaceApps: License.getLicenseLimit('marketplaceApps') ?? -1,
};
}

export async function canAddNewUser(userCount = 1): Promise<boolean> {
return License.canAddNewUser(userCount);
}

export async function canAddNewGuestUser(guestCount = 1): Promise<boolean> {
return License.canAddNewGuestUser(guestCount);
}

export async function canAddNewGuestSubscription(guest: IUser['_id'], roomCount = 1): Promise<boolean> {
return License.canAddNewGuestSubscription(guest, roomCount);
}

export async function canAddNewPrivateApp(appCount = 1): Promise<boolean> {
return License.canAddNewPrivateApp(appCount);
}

export async function canAddNewMarketplaceApp(appCount = 1): Promise<boolean> {
return License.canAddNewMarketplaceApp(appCount);
}

export async function canEnableApp(app: IAppStorageItem): Promise<boolean> {
return License.canEnableApp(app);
}
Expand Down
7 changes: 2 additions & 5 deletions apps/meteor/ee/server/startup/maxRoomsPerGuest.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Subscriptions } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { callbacks } from '../../../lib/callbacks';
import { i18n } from '../../../server/lib/i18n';
import { getMaxRoomsPerGuest } from '../../app/license/server/license';
import { canAddNewGuestSubscription } from '../../app/license/server/license';

callbacks.add(
'beforeAddedToRoom',
async ({ user }) => {
if (user.roles?.includes('guest')) {
const totalSubscriptions = await Subscriptions.countByUserId(user._id);

if (totalSubscriptions >= getMaxRoomsPerGuest()) {
if (!(await canAddNewGuestSubscription(user._id))) {
throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached'));
}
}
Expand Down
26 changes: 1 addition & 25 deletions apps/meteor/ee/server/startup/seatsCap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,31 +62,7 @@ callbacks.add(

callbacks.add(
'validateUserRoles',
async (userData: Partial<IUser>) => {
const isGuest = userData.roles?.includes('guest');
if (isGuest) {
await validateUserRoles(Meteor.userId(), userData);
return;
}

if (!userData._id) {
return;
}

const currentUserData = await Users.findOneById(userData._id);
if (currentUserData?.type === 'app') {
return;
}

const wasGuest = currentUserData?.roles?.length === 1 && currentUserData.roles.includes('guest');
if (!wasGuest) {
return;
}

if (!(await canAddNewUser())) {
throw new Meteor.Error('error-license-user-limit-reached', i18n.t('error-license-user-limit-reached'));
}
},
async (userData: Partial<IUser>) => validateUserRoles(Meteor.userId(), userData),
callbacks.priority.MEDIUM,
'check-max-user-seats',
);
Expand Down
2 changes: 2 additions & 0 deletions packages/core-typings/src/ee/ILicense/ILicenseV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,5 @@ export interface ILicenseV3 {
};
cloudMeta?: Record<string, any>;
}

export type LicenseLimitKind = keyof ILicenseV3['limits'];
2 changes: 1 addition & 1 deletion packages/model-typings/src/models/IUsersModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ export interface IUsersModel extends IBaseModel<IUser> {
getUsersToSendOfflineEmail(userIds: string[]): FindCursor<Pick<IUser, 'name' | 'username' | 'emails' | 'settings' | 'language'>>;
countActiveUsersByService(service: string, options?: FindOptions<IUser>): Promise<number>;
getActiveLocalUserCount(): Promise<number>;
getActiveLocalGuestCount(): Promise<number>;
getActiveLocalGuestCount(exceptions?: IUser['_id'] | IUser['_id'][]): Promise<number>;
removeOlderResumeTokensByUserId(userId: string, fromDate: Date): Promise<UpdateResult>;
findAllUsersWithPendingAvatar(): FindCursor<IUser>;
updateCustomFieldsById(userId: string, customFields: Record<string, unknown>): Promise<UpdateResult>;
Expand Down

0 comments on commit b7f36ca

Please sign in to comment.