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>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import type { Application } from 'express';
|
||||
import type { SuperAgentTest } from 'supertest';
|
||||
import { Container } from 'typedi';
|
||||
import { License } from '@/License';
|
||||
import validator from 'validator';
|
||||
import config from '@/config';
|
||||
import * as Db from '@/Db';
|
||||
@@ -84,6 +86,26 @@ describe('POST /login', () => {
|
||||
const authToken = utils.getAuthToken(response);
|
||||
expect(authToken).toBeDefined();
|
||||
});
|
||||
|
||||
test('should throw AuthError for non-owner if not within users limit quota', async () => {
|
||||
jest.spyOn(Container.get(License), 'isWithinUsersLimit').mockReturnValueOnce(false);
|
||||
const member = await testDb.createUserShell(globalMemberRole);
|
||||
|
||||
const response = await authAgent(member).get('/login');
|
||||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
test('should not throw AuthError for owner if not within users limit quota', async () => {
|
||||
jest.spyOn(Container.get(License), 'isWithinUsersLimit').mockReturnValueOnce(false);
|
||||
const ownerUser = await testDb.createUser({
|
||||
password: randomValidPassword(),
|
||||
globalRole: globalOwnerRole,
|
||||
isOwner: true,
|
||||
});
|
||||
|
||||
const response = await authAgent(ownerUser).get('/login');
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /login', () => {
|
||||
@@ -292,6 +314,18 @@ describe('GET /resolve-signup-token', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should return 403 if user quota reached', async () => {
|
||||
jest.spyOn(Container.get(License), 'isWithinUsersLimit').mockReturnValueOnce(false);
|
||||
const memberShell = await testDb.createUserShell(globalMemberRole);
|
||||
|
||||
const response = await authOwnerAgent
|
||||
.get('/resolve-signup-token')
|
||||
.query({ inviterId: owner.id })
|
||||
.query({ inviteeId: memberShell.id });
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
test('should fail with invalid inputs', async () => {
|
||||
const { id: inviteeId } = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from './shared/constants';
|
||||
import * as testDb from './shared/testDb';
|
||||
import * as utils from './shared/utils';
|
||||
import config from '@/config';
|
||||
|
||||
let authlessAgent: SuperAgentTest;
|
||||
let authMemberAgent: SuperAgentTest;
|
||||
@@ -16,6 +17,8 @@ beforeAll(async () => {
|
||||
|
||||
authlessAgent = utils.createAgent(app);
|
||||
authMemberAgent = utils.createAuthAgent(app)(member);
|
||||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { randomCredentialPayload } from './shared/random';
|
||||
import * as testDb from './shared/testDb';
|
||||
import type { AuthAgent, SaveCredentialFunction } from './shared/types';
|
||||
import * as utils from './shared/utils';
|
||||
import config from '@/config';
|
||||
|
||||
let globalMemberRole: Role;
|
||||
let owner: User;
|
||||
@@ -39,6 +40,7 @@ beforeAll(async () => {
|
||||
|
||||
saveCredential = testDb.affixRoleToSaveCredential(credentialOwnerRole);
|
||||
sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -45,6 +45,8 @@ beforeAll(async () => {
|
||||
authAgent = utils.createAuthAgent(app);
|
||||
authOwnerAgent = authAgent(owner);
|
||||
authMemberAgent = authAgent(member);
|
||||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ beforeAll(async () => {
|
||||
authOwnerAgent = authAgent(owner);
|
||||
authMemberAgent = authAgent(member);
|
||||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
config.set('license.serverUrl', MOCK_SERVER_URL);
|
||||
config.set('license.autoRenewEnabled', true);
|
||||
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as utils from '../shared/utils';
|
||||
import { sampleConfig } from './sampleMetadata';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
import { SamlService } from '@/sso/saml/saml.service.ee';
|
||||
import config from '@/config';
|
||||
import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes';
|
||||
import type { AuthenticationMethod } from 'n8n-workflow';
|
||||
|
||||
@@ -32,6 +33,8 @@ beforeAll(async () => {
|
||||
authOwnerAgent = utils.createAuthAgent(app)(owner);
|
||||
authMemberAgent = utils.createAgent(app, { auth: true, user: someUser });
|
||||
noAuthMemberAgent = utils.createAgent(app, { auth: false, user: someUser });
|
||||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -181,6 +181,7 @@ export async function createUser(attributes: Partial<User> = {}): Promise<User>
|
||||
firstName: firstName ?? randomName(),
|
||||
lastName: lastName ?? randomName(),
|
||||
globalRoleId: (globalRole ?? (await getGlobalMemberRole())).id,
|
||||
globalRole,
|
||||
...rest,
|
||||
};
|
||||
|
||||
|
||||
@@ -683,8 +683,10 @@ export function createAgent(
|
||||
if (options?.apiPath === undefined || options?.apiPath === 'internal') {
|
||||
void agent.use(prefix(REST_PATH_SEGMENT));
|
||||
if (options?.auth && options?.user) {
|
||||
const { token } = issueJWT(options.user);
|
||||
agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`);
|
||||
try {
|
||||
const { token } = issueJWT(options.user);
|
||||
agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Application } from 'express';
|
||||
import type { User } from '@/databases/entities/User';
|
||||
import * as testDb from './shared/testDb';
|
||||
import * as utils from './shared/utils';
|
||||
import config from '@/config';
|
||||
|
||||
import type { AuthAgent } from './shared/types';
|
||||
import { License } from '@/License';
|
||||
@@ -16,6 +17,7 @@ let variablesSpy: jest.SpyInstance<boolean>;
|
||||
const licenseLike = {
|
||||
isVariablesEnabled: jest.fn().mockReturnValue(true),
|
||||
getVariablesLimit: jest.fn().mockReturnValue(-1),
|
||||
isWithinUsersLimit: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -28,6 +30,7 @@ beforeAll(async () => {
|
||||
memberUser = await testDb.createUser();
|
||||
|
||||
authAgent = utils.createAuthAgent(app);
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { makeWorkflow } from './shared/utils';
|
||||
import { randomCredentialPayload } from './shared/random';
|
||||
import { License } from '@/License';
|
||||
import { getSharedWorkflowIds } from '../../src/WorkflowHelpers';
|
||||
import config from '@/config';
|
||||
|
||||
let owner: User;
|
||||
let member: User;
|
||||
@@ -45,6 +46,7 @@ beforeAll(async () => {
|
||||
sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
|
||||
|
||||
await utils.initNodeTypes();
|
||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const MOCK_SERVER_URL = 'https://server.com/v1';
|
||||
const MOCK_RENEW_OFFSET = 259200;
|
||||
const MOCK_INSTANCE_ID = 'instance-id';
|
||||
const MOCK_ACTIVATION_KEY = 'activation-key';
|
||||
const MOCK_FEATURE_FLAG = 'feat:mock';
|
||||
const MOCK_FEATURE_FLAG = 'feat:sharing';
|
||||
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
|
||||
|
||||
describe('License', () => {
|
||||
@@ -71,9 +71,9 @@ describe('License', () => {
|
||||
});
|
||||
|
||||
test('check fetching feature values', async () => {
|
||||
await license.getFeatureValue(MOCK_FEATURE_FLAG, false);
|
||||
license.getFeatureValue(MOCK_FEATURE_FLAG);
|
||||
|
||||
expect(LicenseManager.prototype.getFeatureValue).toHaveBeenCalledWith(MOCK_FEATURE_FLAG, false);
|
||||
expect(LicenseManager.prototype.getFeatureValue).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||
});
|
||||
|
||||
test('check management jwt', async () => {
|
||||
|
||||
Reference in New Issue
Block a user