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:
OlegIvaniv
2023-07-12 14:11:46 +02:00
committed by GitHub
parent 26046f6fe8
commit e5620ab1e4
33 changed files with 271 additions and 94 deletions

View File

@@ -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 });

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -45,6 +45,8 @@ beforeAll(async () => {
authAgent = utils.createAuthAgent(app);
authOwnerAgent = authAgent(owner);
authMemberAgent = authAgent(member);
config.set('userManagement.isInstanceOwnerSetUp', true);
});
beforeEach(async () => {

View File

@@ -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);

View File

@@ -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 () => {

View File

@@ -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,
};

View File

@@ -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 {}
}
}

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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 () => {