Files
Automata/packages/cli/src/License.ts
OlegIvaniv e5620ab1e4 feat(API): Implement users account quota guards (#6434)
* feat(cli): Implement users account quota guards

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Remove comment

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Address PR comments

- Getting `usersQuota` from `Settings` repo
- Revert `isUserManagementEnabled` helper
- Fix FE listing of users

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Refactor isWithinUserQuota getter and fix tests

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Revert testDb.ts changes

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Cleanup & improve types

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix duplicated method

* Fix failing test

* Remove `isUserManagementEnabled` completely

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Check for globalRole.name to determine if user is owner

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Fix unit tests

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Set isInstanceOwnerSetUp in specs

* Fix SettingsUserView UM

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* refactor: License typings suggestions for users quota guards (#6636)

refactor: License typings suggestions

* Update packages/cli/src/Ldap/helpers.ts

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* Update packages/cli/test/integration/shared/utils.ts

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* Address PR comments

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

* Use 403 for all user quota related errors

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>

---------

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
2023-07-12 14:11:46 +02:00

205 lines
4.8 KiB
TypeScript

import type { TEntitlement, TLicenseBlock } from '@n8n_io/license-sdk';
import { LicenseManager } from '@n8n_io/license-sdk';
import type { ILogger } from 'n8n-workflow';
import { getLogger } from './Logger';
import config from '@/config';
import * as Db from '@/Db';
import {
LICENSE_FEATURES,
LICENSE_QUOTAS,
N8N_VERSION,
SETTINGS_LICENSE_CERT_KEY,
UNLIMITED_LICENSE_QUOTA,
} from './constants';
import { Service } from 'typedi';
import type { BooleanLicenseFeature, NumericLicenseFeature } from './Interfaces';
type FeatureReturnType = Partial<
{
planName: string;
} & { [K in NumericLicenseFeature]: number } & { [K in BooleanLicenseFeature]: boolean }
>;
@Service()
export class License {
private logger: ILogger;
private manager: LicenseManager | undefined;
constructor() {
this.logger = getLogger();
}
async init(instanceId: string) {
if (this.manager) {
return;
}
const server = config.getEnv('license.serverUrl');
const autoRenewEnabled = config.getEnv('license.autoRenewEnabled');
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
try {
this.manager = new LicenseManager({
server,
tenantId: config.getEnv('license.tenantId'),
productIdentifier: `n8n-${N8N_VERSION}`,
autoRenewEnabled,
autoRenewOffset,
logger: this.logger,
loadCertStr: async () => this.loadCertStr(),
saveCertStr: async (value: TLicenseBlock) => this.saveCertStr(value),
deviceFingerprint: () => instanceId,
});
await this.manager.initialize();
} catch (e: unknown) {
if (e instanceof Error) {
this.logger.error('Could not initialize license manager sdk', e);
}
}
}
async loadCertStr(): Promise<TLicenseBlock> {
// if we have an ephemeral license, we don't want to load it from the database
const ephemeralLicense = config.get('license.cert');
if (ephemeralLicense) {
return ephemeralLicense;
}
const databaseSettings = await Db.collections.Settings.findOne({
where: {
key: SETTINGS_LICENSE_CERT_KEY,
},
});
return databaseSettings?.value ?? '';
}
async saveCertStr(value: TLicenseBlock): Promise<void> {
// if we have an ephemeral license, we don't want to save it to the database
if (config.get('license.cert')) return;
await Db.collections.Settings.upsert(
{
key: SETTINGS_LICENSE_CERT_KEY,
value,
loadOnStartup: false,
},
['key'],
);
}
async activate(activationKey: string): Promise<void> {
if (!this.manager) {
return;
}
await this.manager.activate(activationKey);
}
async renew() {
if (!this.manager) {
return;
}
await this.manager.renew();
}
isFeatureEnabled(feature: BooleanLicenseFeature) {
return this.manager?.hasFeatureEnabled(feature) ?? false;
}
isSharingEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.SHARING);
}
isLogStreamingEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.LOG_STREAMING);
}
isLdapEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.LDAP);
}
isSamlEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.SAML);
}
isAdvancedExecutionFiltersEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS);
}
isVariablesEnabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.VARIABLES);
}
isSourceControlLicensed() {
return this.isFeatureEnabled(LICENSE_FEATURES.SOURCE_CONTROL);
}
isAPIDisabled() {
return this.isFeatureEnabled(LICENSE_FEATURES.API_DISABLED);
}
getCurrentEntitlements() {
return this.manager?.getCurrentEntitlements() ?? [];
}
getFeatureValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
}
getManagementJwt(): string {
if (!this.manager) {
return '';
}
return this.manager.getManagementJwt();
}
/**
* Helper function to get the main plan for a license
*/
getMainPlan(): TEntitlement | undefined {
if (!this.manager) {
return undefined;
}
const entitlements = this.getCurrentEntitlements();
if (!entitlements.length) {
return undefined;
}
return entitlements.find(
(entitlement) => (entitlement.productMetadata?.terms as { isMainPlan?: boolean })?.isMainPlan,
);
}
// Helper functions for computed data
getUsersLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getTriggerLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getVariablesLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
}
getPlanName(): string {
return this.getFeatureValue('planName') ?? 'Community';
}
getInfo(): string {
if (!this.manager) {
return 'n/a';
}
return this.manager.toString();
}
isWithinUsersLimit() {
return this.getUsersLimit() === UNLIMITED_LICENSE_QUOTA;
}
}