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

chore: License v3 - behavior and limited reached triggers #30561

Merged
merged 11 commits into from
Oct 6, 2023
1 change: 0 additions & 1 deletion ee/packages/license/__tests__/MockedLicenseBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,6 @@ export class MockedLicenseBuilder {
export const getReadyLicenseManager = async () => {
const license = new LicenseImp();
await license.setWorkspaceUrl('http://localhost:3000');
await license.setWorkspaceUrl('http://localhost:3000');

license.setLicenseLimitCounter('activeUsers', () => 0);
license.setLicenseLimitCounter('guestUsers', () => 0);
Expand Down
53 changes: 53 additions & 0 deletions ee/packages/license/__tests__/emitter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,57 @@ describe('Event License behaviors', () => {
await expect(license.hasValidLicense()).toBe(true);
await expect(fn).toBeCalledTimes(1);
});

describe('behavior:prevent_action event', () => {
it('should emit `behavior:prevent_action` event when the limit is reached', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();

licenseManager.onBehaviorTriggered('prevent_action', fn);

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
]);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 10);

await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);

await expect(fn).toBeCalledTimes(1);

await expect(fn).toBeCalledWith({
reason: 'limit',
limit: 'activeUsers',
});
});

it('should emit `limitReached:activeUsers` event when the limit is reached', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();

licenseManager.onLimitReached('activeUsers', fn);

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
]);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 10);

await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);

await expect(fn).toBeCalledTimes(1);

await expect(fn).toBeCalledWith(undefined);
});
});
});
4 changes: 3 additions & 1 deletion ee/packages/license/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@swc/jest": "^0.2.26",
"@types/babel__core": "^7",
"@types/babel__preset-env": "^7",
"@types/bcrypt": "^5.0.0",
"@types/jest": "~29.5.3",
"@types/ws": "^8.5.5",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
Expand Down Expand Up @@ -42,6 +43,7 @@
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/jwt": "workspace:^",
"@rocket.chat/logger": "workspace:^"
"@rocket.chat/logger": "workspace:^",
"bcrypt": "^5.0.1"
}
}
17 changes: 13 additions & 4 deletions ee/packages/license/src/definition/LicenseBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { LicenseLimitKind } from './ILicenseV3';
import type { LicenseModule } from './LicenseModule';

export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules';

export type BehaviorWithContext = {
behavior: LicenseBehavior;
modules?: LicenseModule[];
};
export type BehaviorWithContext =
| {
behavior: LicenseBehavior;
modules?: LicenseModule[];
reason: 'limit';
limit?: LicenseLimitKind;
}
| {
behavior: LicenseBehavior;
modules?: LicenseModule[];
reason: 'period' | 'url';
};
11 changes: 11 additions & 0 deletions ee/packages/license/src/definition/LicenseValidationOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { LicenseLimitKind } from './ILicenseV3';
import type { LicenseBehavior } from './LicenseBehavior';
import type { LimitContext } from './LimitContext';

export type LicenseValidationOptions = {
behaviors?: LicenseBehavior[];
limits?: LicenseLimitKind[];
suppressLog?: boolean;
isNewLicense?: boolean;
context?: Partial<{ [K in LicenseLimitKind]: Partial<LimitContext<LicenseLimitKind>> }>;
};
4 changes: 3 additions & 1 deletion ee/packages/license/src/definition/LimitContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import type { IUser } from '@rocket.chat/core-typings';

import type { LicenseLimitKind } from './ILicenseV3';

export type LimitContext<T extends LicenseLimitKind> = T extends 'roomsPerGuest' ? { userId: IUser['_id'] } : Record<string, never>;
export type LimitContext<T extends LicenseLimitKind> = { extraCount?: number } & (T extends 'roomsPerGuest'
? { userId: IUser['_id'] }
: Record<string, never>);
15 changes: 15 additions & 0 deletions ee/packages/license/src/definition/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { LicenseLimitKind } from './ILicenseV3';
import type { BehaviorWithContext, LicenseBehavior } from './LicenseBehavior';
import type { LicenseModule } from './LicenseModule';

type ModuleValidation = Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined>;
type BehaviorTriggered = Record<`behavior:${LicenseBehavior}`, { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }>;
type LimitReached = Record<`limitReached:${LicenseLimitKind}`, undefined>;

export type LicenseEvents = ModuleValidation &
BehaviorTriggered &
LimitReached & {
validate: undefined;
invalidate: undefined;
module: { module: LicenseModule; valid: boolean };
};
40 changes: 37 additions & 3 deletions ee/packages/license/src/events/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LicenseLimitKind } from '../definition/ILicenseV3';
import type { BehaviorWithContext } from '../definition/LicenseBehavior';
import type { LicenseModule } from '../definition/LicenseModule';
import type { LicenseManager } from '../license';
import { logger } from '../logger';
Expand All @@ -21,10 +21,44 @@ export function moduleRemoved(this: LicenseManager, module: LicenseModule) {
}
}

export function limitReached(this: LicenseManager, limitKind: LicenseLimitKind) {
export function behaviorTriggered(this: LicenseManager, options: BehaviorWithContext) {
const { behavior, reason, modules: _, ...rest } = options;
try {
this.emit(`limitReached:${limitKind}`);
this.emit(`behavior:${behavior}`, {
reason,
...rest,
});
} catch (error) {
logger.error({ msg: 'Error running behavior triggered event', error });
}

if (behavior !== 'prevent_action') {
return;
}

if (reason !== 'limit' || !(`limit` in rest) || !rest.limit) {
return;
}

try {
this.emit(`limitReached:${rest.limit}`);
} catch (error) {
logger.error({ msg: 'Error running limit reached event', error });
}
}

export function licenseValidated(this: LicenseManager) {
try {
this.emit('validate');
} catch (error) {
logger.error({ msg: 'Error running license validated event', error });
}
}

export function licenseInvalidated(this: LicenseManager) {
try {
this.emit('invalidate');
} catch (error) {
logger.error({ msg: 'Error running license invalidated event', error });
}
}
17 changes: 13 additions & 4 deletions ee/packages/license/src/events/listeners.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { LicenseLimitKind } from '../definition/ILicenseV3';
import type { BehaviorWithContext, LicenseBehavior } from '../definition/LicenseBehavior';
import type { LicenseModule } from '../definition/LicenseModule';
import type { LicenseManager } from '../license';
import { hasModule } from '../modules';
Expand Down Expand Up @@ -58,18 +59,26 @@ export function onToggledFeature(
};
}

export function onModule(this: LicenseManager, cb: (...args: any[]) => void) {
export function onModule(this: LicenseManager, cb: (data: { module: LicenseModule; valid: boolean }) => void) {
this.on('module', cb);
}

export function onValidateLicense(this: LicenseManager, cb: (...args: any[]) => void) {
export function onValidateLicense(this: LicenseManager, cb: () => void) {
this.on('validate', cb);
}

export function onInvalidateLicense(this: LicenseManager, cb: (...args: any[]) => void) {
export function onInvalidateLicense(this: LicenseManager, cb: () => void) {
this.on('invalidate', cb);
}

export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: (...args: any[]) => void) {
export function onBehaviorTriggered(
this: LicenseManager,
behavior: Exclude<LicenseBehavior, 'prevent_installation'>,
cb: (data: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }) => void,
) {
this.on(`behavior:${behavior}`, cb);
}

export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: () => void) {
this.on(`limitReached:${limitKind}`, cb);
}
7 changes: 6 additions & 1 deletion ee/packages/license/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { LimitContext } from './definition/LimitContext';
import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated';
import { onLicense } from './events/deprecated';
import {
onBehaviorTriggered,
onInvalidFeature,
onInvalidateLicense,
onLimitReached,
Expand Down Expand Up @@ -45,6 +46,8 @@ interface License {
onValidateLicense: typeof onValidateLicense;
onInvalidateLicense: typeof onInvalidateLicense;
onLimitReached: typeof onLimitReached;
onBehaviorTriggered: typeof onBehaviorTriggered;
revalidateLicense: () => Promise<void>;

getInfo: (loadCurrentValues: boolean) => Promise<{
license: ILicenseV3 | undefined;
Expand Down Expand Up @@ -78,7 +81,7 @@ export class LicenseImp extends LicenseManager implements License {

getCurrentValueForLicenseLimit = getCurrentValueForLicenseLimit;

public async isLimitReached<T extends LicenseLimitKind>(action: T, context?: Partial<LimitContext<T>>) {
public async isLimitReached<T extends LicenseLimitKind>(action: T, context?: Partial<LimitContext<T>>): Promise<boolean> {
return this.shouldPreventAction(action, context, 0);
}

Expand All @@ -96,6 +99,8 @@ export class LicenseImp extends LicenseManager implements License {

onLimitReached = onLimitReached;

onBehaviorTriggered = onBehaviorTriggered;

// Deprecated:
onLicense = onLicense;

Expand Down
12 changes: 12 additions & 0 deletions ee/packages/license/src/isItemAllowed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { LicenseLimitKind } from './definition/ILicenseV3';
import type { LicenseBehavior } from './definition/LicenseBehavior';
import type { LicenseValidationOptions } from './definition/LicenseValidationOptions';

const isItemAllowed = <T>(item: T, allowList?: T[]): boolean => {
return !allowList || allowList.includes(item);
};

export const isLimitAllowed = (item: LicenseLimitKind, options: LicenseValidationOptions): boolean => isItemAllowed(item, options.limits);

export const isBehaviorAllowed = (item: LicenseBehavior, options: LicenseValidationOptions): boolean =>
isItemAllowed(item, options.behaviors) && (options.isNewLicense || item !== 'prevent_installation');
Loading
Loading