-
Notifications
You must be signed in to change notification settings - Fork 7.1k
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(core): Add license support to n8n #4566
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
b0c1b18
add sdk
mutdmour 9565367
add license manager
mutdmour 0fc8f56
Merge branch 'master' of github.com:n8n-io/n8n into N8N-5283-license-sdk
mutdmour 36fa6bd
type fix
mutdmour 4a49a21
add basic func
mutdmour 4feebf2
store to db
mutdmour d057e8e
update default
mutdmour 342b665
activate license
mutdmour d3d9c38
add sharing flag
mutdmour c07b172
fix setup
mutdmour cce9739
clear license
mutdmour 75bb748
update conosle log to info
mutdmour 9bd05ff
refactor
mutdmour 0c0167c
use npm dependency
mutdmour b4d5856
update error logs
mutdmour 644c70b
add simple test
mutdmour e155e3b
add license tests
mutdmour 109af64
update tests
mutdmour fe94f5c
merge in master
mutdmour c828c02
update pnpm package
mutdmour b8a0a67
fix error handling types
mutdmour 4d9d68d
Update packages/cli/src/config/schema.ts
mutdmour ebc9cc4
make feature enum
mutdmour 4db9bdc
Merge branch 'N8N-5283-license-sdk' of github.com:n8n-io/n8n into N8N…
mutdmour 29bf191
Merge branch 'master' of github.com:n8n-io/n8n into N8N-5283-license-sdk
mutdmour 4bbf784
merge in master
mutdmour 8ec0b6b
add warning
mutdmour cb2ac94
merge
mutdmour 389f93c
update sdk
mutdmour d632154
Update packages/cli/src/config/schema.ts
mutdmour File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { LicenseManager, TLicenseContainerStr } from '@n8n_io/license-sdk'; | ||
import { ILogger } from 'n8n-workflow'; | ||
import { getLogger } from './Logger'; | ||
import config from '@/config'; | ||
import * as Db from '@/Db'; | ||
import { LICENSE_FEATURES, SETTINGS_LICENSE_CERT_KEY } from './constants'; | ||
|
||
async function loadCertStr(): Promise<TLicenseContainerStr> { | ||
const databaseSettings = await Db.collections.Settings.findOne({ | ||
where: { | ||
key: SETTINGS_LICENSE_CERT_KEY, | ||
}, | ||
}); | ||
|
||
return databaseSettings?.value ?? ''; | ||
} | ||
|
||
async function saveCertStr(value: TLicenseContainerStr): Promise<void> { | ||
await Db.collections.Settings.upsert( | ||
{ | ||
key: SETTINGS_LICENSE_CERT_KEY, | ||
value, | ||
loadOnStartup: false, | ||
}, | ||
['key'], | ||
); | ||
} | ||
|
||
export class License { | ||
private logger: ILogger; | ||
|
||
private manager: LicenseManager | undefined; | ||
|
||
constructor() { | ||
this.logger = getLogger(); | ||
} | ||
|
||
async init(instanceId: string, version: 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: 1, | ||
productIdentifier: `n8n-${version}`, | ||
autoRenewEnabled, | ||
autoRenewOffset, | ||
logger: this.logger, | ||
loadCertStr, | ||
saveCertStr, | ||
deviceFingerprint: () => instanceId, | ||
}); | ||
|
||
await this.manager.initialize(); | ||
} catch (e: unknown) { | ||
if (e instanceof Error) { | ||
this.logger.error('Could not initialize license manager sdk', e); | ||
} | ||
} | ||
} | ||
|
||
async activate(activationKey: string): Promise<void> { | ||
if (!this.manager) { | ||
return; | ||
} | ||
|
||
if (this.manager.isValid()) { | ||
return; | ||
} | ||
|
||
try { | ||
await this.manager.activate(activationKey); | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
this.logger.error('Could not activate license', e); | ||
} | ||
} | ||
} | ||
|
||
async renew() { | ||
if (!this.manager) { | ||
return; | ||
} | ||
|
||
try { | ||
await this.manager.renew(); | ||
} catch (e) { | ||
if (e instanceof Error) { | ||
this.logger.error('Could not renew license', e); | ||
} | ||
} | ||
} | ||
|
||
isFeatureEnabled(feature: string): boolean { | ||
if (!this.manager) { | ||
return false; | ||
} | ||
|
||
return this.manager.hasFeatureEnabled(feature); | ||
} | ||
|
||
isSharingEnabled() { | ||
return this.isFeatureEnabled(LICENSE_FEATURES.SHARING); | ||
} | ||
} | ||
|
||
let licenseInstance: License | undefined; | ||
|
||
export function getLicense(): License { | ||
mutdmour marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (licenseInstance === undefined) { | ||
licenseInstance = new License(); | ||
} | ||
|
||
return licenseInstance; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Command } from '@oclif/command'; | ||
|
||
import { LoggerProxy } from 'n8n-workflow'; | ||
|
||
import * as Db from '@/Db'; | ||
|
||
import { getLogger } from '@/Logger'; | ||
import { SETTINGS_LICENSE_CERT_KEY } from '@/constants'; | ||
|
||
export class ClearLicenseCommand extends Command { | ||
static description = 'Clear license'; | ||
|
||
static examples = [`$ n8n clear:license`]; | ||
|
||
async run() { | ||
const logger = getLogger(); | ||
LoggerProxy.init(logger); | ||
|
||
try { | ||
await Db.init(); | ||
|
||
console.info('Clearing license from database.'); | ||
await Db.collections.Settings.delete({ | ||
key: SETTINGS_LICENSE_CERT_KEY, | ||
}); | ||
console.info('Done. Restart n8n to take effect.'); | ||
} catch (e: unknown) { | ||
console.error('Error updating database. See log messages for details.'); | ||
logger.error('\nGOT ERROR'); | ||
logger.info('===================================='); | ||
if (e instanceof Error) { | ||
logger.error(e.message); | ||
if (e.stack) { | ||
logger.error(e.stack); | ||
} | ||
} | ||
this.exit(1); | ||
} | ||
|
||
this.exit(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { LicenseManager } from '@n8n_io/license-sdk'; | ||
import config from '@/config'; | ||
import { License } from '@/License'; | ||
|
||
jest.mock('@n8n_io/license-sdk'); | ||
|
||
const MOCK_SERVER_URL = 'https://server.com/v1'; | ||
const MOCK_RENEW_OFFSET = 259200; | ||
const MOCK_INSTANCE_ID = 'instance-id'; | ||
const MOCK_N8N_VERSION = '0.27.0'; | ||
const MOCK_ACTIVATION_KEY = 'activation-key'; | ||
const MOCK_FEATURE_FLAG = 'feat:mock'; | ||
|
||
describe('License', () => { | ||
beforeAll(() => { | ||
config.set('license.serverUrl', MOCK_SERVER_URL); | ||
config.set('license.autoRenewEnabled', true); | ||
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET); | ||
}); | ||
|
||
let license; | ||
|
||
beforeEach(async () => { | ||
license = new License(); | ||
await license.init(MOCK_INSTANCE_ID, MOCK_N8N_VERSION); | ||
}); | ||
|
||
test('initializes license manager', async () => { | ||
expect(LicenseManager).toHaveBeenCalledWith({ | ||
autoRenewEnabled: true, | ||
autoRenewOffset: MOCK_RENEW_OFFSET, | ||
deviceFingerprint: expect.any(Function), | ||
productIdentifier: `n8n-${MOCK_N8N_VERSION}`, | ||
logger: expect.anything(), | ||
loadCertStr: expect.any(Function), | ||
saveCertStr: expect.any(Function), | ||
server: MOCK_SERVER_URL, | ||
tenantId: 1, | ||
}); | ||
}); | ||
|
||
test('activates license if current license is not valid', async () => { | ||
LicenseManager.prototype.isValid.mockReturnValue(false); | ||
|
||
await license.activate(MOCK_ACTIVATION_KEY); | ||
|
||
expect(LicenseManager.prototype.isValid).toHaveBeenCalled(); | ||
expect(LicenseManager.prototype.activate).toHaveBeenCalledWith(MOCK_ACTIVATION_KEY); | ||
}); | ||
|
||
test('does not activate license if current license is valid', async () => { | ||
LicenseManager.prototype.isValid.mockReturnValue(true); | ||
|
||
await license.activate(MOCK_ACTIVATION_KEY); | ||
|
||
expect(LicenseManager.prototype.isValid).toHaveBeenCalled(); | ||
expect(LicenseManager.prototype.activate).not.toHaveBeenCalledWith(); | ||
}); | ||
|
||
test('renews license', async () => { | ||
await license.renew(); | ||
|
||
expect(LicenseManager.prototype.renew).toHaveBeenCalled(); | ||
}); | ||
|
||
test('check if feature is enabled', async () => { | ||
await license.isFeatureEnabled(MOCK_FEATURE_FLAG); | ||
|
||
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG); | ||
}); | ||
|
||
test('check if sharing feature is enabled', async () => { | ||
await license.isFeatureEnabled(MOCK_FEATURE_FLAG); | ||
|
||
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding the cloud-specific checks here? That way we could concentrate these special checks here and not have them scattered across the codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which checks do you mean.. the cloud check we need is already here.. checking for config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wait I get it.. you want to move config checks here.. not sure it makes sense.. I would have a separate module that manages features..