Compare commits

...

50 Commits

Author SHA1 Message Date
dependabot[bot]
015d65ac98 chore(deps): bump http-proxy-middleware from 2.0.1 to 2.0.7
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.1 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.1...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-25 03:27:15 +00:00
Ömer Faruk Aydın
47510e24d5 Merge pull request #2110 from automatisch/aut-1293
test(app-config): write model tests
2024-10-25 01:18:26 +02:00
Faruk AYDIN
91c9ef3068 fix: Pass app config parameters to be used for hooks 2024-10-25 01:07:44 +02:00
Faruk AYDIN
240854e4ac fix: Use uuid for down migration of app config id removal 2024-10-25 00:59:00 +02:00
Jakub P.
0e4fc7efbc fix: use key instead of id in appConfig 2024-10-24 19:42:54 +02:00
Faruk AYDIN
b47e859225 test: Add additional cases for triggerAppConfigUpdate method 2024-10-24 17:10:47 +02:00
Faruk AYDIN
62a1072682 fix: Use triggerAppConfigUpdate spy instead of 2024-10-24 17:07:23 +02:00
Faruk AYDIN
c6f2a97591 test: Add missing associations test for app auth client 2024-10-24 17:04:45 +02:00
Faruk AYDIN
d66be231b3 refactor: Remove redundant updateConnectionAllowedProperty 2024-10-24 17:00:47 +02:00
Faruk AYDIN
f73ffc8711 test: Add idColumn test to app config model 2024-10-24 16:59:17 +02:00
Faruk AYDIN
e4c17c1bc7 refactor: Use belongsTo relation for app config association 2024-10-24 16:59:17 +02:00
Faruk AYDIN
997e729535 refactor: Use hooks with refetched record for app config 2024-10-24 16:59:17 +02:00
Faruk AYDIN
e0e313b8d1 refactor: Remove id from app config factory 2024-10-24 16:59:17 +02:00
Faruk AYDIN
f0bd763e72 feat: Remove id field from app config model 2024-10-24 16:59:17 +02:00
Faruk AYDIN
6a7a90536b feat: Make key field primary key for app config model 2024-10-24 16:59:17 +02:00
Ali BARIN
ac8ddedfb5 test(connection): use new properties from app-config 2024-10-24 16:59:17 +02:00
Ali BARIN
6fcd126ff8 test(app-auth-client): cover lifecycle hooks and triggerAppConfigUpdate method 2024-10-24 16:59:17 +02:00
Faruk AYDIN
55d0966d48 fix: Pass app key while triggering app config update 2024-10-24 16:59:17 +02:00
Faruk AYDIN
2583e08f7a fix: Don't compute connectionAllowed column twice 2024-10-24 16:59:17 +02:00
Faruk AYDIN
de72e62470 fix: Pass app config key to fix associations 2024-10-24 16:59:17 +02:00
Faruk AYDIN
91993dbb07 refactor: AppConfig model and corresponding tests 2024-10-24 16:59:17 +02:00
Faruk AYDIN
d87ee4daa3 refactor: Move and adjust getApp tests for app config model 2024-10-24 16:59:17 +02:00
Ali BARIN
6791e002ff test(app-config): remove redundant virtual attributes test case 2024-10-24 16:59:17 +02:00
Ali BARIN
4ca84aa515 refactor(app-auth-client): remove redundant column selection 2024-10-24 16:59:17 +02:00
Ali BARIN
8189cbc171 fix(app-config): use correct case in connection_allowed 2024-10-24 16:59:17 +02:00
Ali BARIN
73edb45ff7 refactor(app-config): rename allowCustomConnection as customConnectionAllowed 2024-10-24 16:59:17 +02:00
Ali BARIN
0bbe362660 refactor(app-config): rename allow_custom_connection as custom_connection_allowed 2024-10-24 16:59:17 +02:00
Ali BARIN
a76bee51fc refactor(app-config): remove canCustomConnect virtual attribute 2024-10-24 16:59:17 +02:00
Ali BARIN
6e42b52414 refactor(app-config): rename canConnect as connectionAllowed 2024-10-24 16:59:17 +02:00
Ali BARIN
aed61209fa feat(app-config): update canConnect upon dependent changes 2024-10-24 16:59:17 +02:00
Ali BARIN
f5d796ea77 feat(app-config): persist relational virtual attrs 2024-10-24 16:59:17 +02:00
Ali BARIN
ecb04b4ba9 test(app-config): write model tests 2024-10-24 16:59:17 +02:00
Ali BARIN
dabb01e237 Merge pull request #2127 from automatisch/AUT-1231
feat: add error snackbar when creating or updating saml auth provider
2024-10-22 12:13:05 +02:00
Ali BARIN
c2d27d0fd4 Merge pull request #2136 from automatisch/aut-1322
test(connection): write remaining model tests
2024-10-21 17:41:31 +02:00
Ali BARIN
e62bd75fdf Merge pull request #2122 from automatisch/AUT-1239
feat: add error snackbar for updating role mappings
2024-10-21 16:44:24 +02:00
Ali BARIN
2e917bd62b Merge pull request #2120 from automatisch/AUT-1097
feat: allow both number and string values as sampleValue
2024-10-21 16:41:58 +02:00
Ali BARIN
e0492c4264 Merge pull request #2129 from automatisch/AUT-1253
feat: add error snackbar for errors originating from registerUser function
2024-10-21 16:41:31 +02:00
Ali BARIN
7db68e2f96 test(connection): write remaining model tests 2024-10-21 13:10:29 +00:00
Ömer Faruk Aydın
e9b05a37d1 Merge pull request #2135 from automatisch/aut-1322-2
test(connection): cover model lifecycle hooks
2024-10-21 12:49:49 +02:00
Ali BARIN
5613259536 test(connection): cover model lifecycle hooks 2024-10-21 12:41:57 +02:00
Ömer Faruk Aydın
3209ff16ac Merge pull request #2130 from automatisch/aut-1322
test(connection): write model tests
2024-10-21 12:41:04 +02:00
Faruk AYDIN
a49c8602d1 refactor: Remove redundant test cases for connection model 2024-10-21 12:32:46 +02:00
Ali BARIN
7caa055e00 test(connection): write model tests 2024-10-21 10:27:43 +02:00
Ömer Faruk Aydın
0d62bc6c78 Merge pull request #2134 from automatisch/permission-tests
Permission tests
2024-10-18 17:33:57 +02:00
Faruk AYDIN
bc0861fd9e test: Implement tests for permission model 2024-10-18 15:34:06 +02:00
Faruk AYDIN
f280052d93 refactor: Permission model sanitize method 2024-10-18 15:33:47 +02:00
kasia.oczkowska
21da49f79d feat: add error snackbar for errors originating from registerUser function 2024-10-18 13:41:57 +01:00
kasia.oczkowska
19a5ccf942 feat: add error snackbar when creating or updating saml auth provider 2024-10-16 14:31:05 +01:00
kasia.oczkowska
2981fa5946 feat: add error snackbar for updating role mappings 2024-10-10 13:34:40 +01:00
kasia.oczkowska
05a3016557 feat: allow both number and string values as sampleValue 2024-10-10 09:21:03 +01:00
43 changed files with 1469 additions and 160 deletions

View File

@@ -10,11 +10,11 @@ export default async (request, response) => {
};
const appConfigParams = (request) => {
const { allowCustomConnection, shared, disabled } = request.body;
const { customConnectionAllowed, shared, disabled } = request.body;
return {
key: request.params.appKey,
allowCustomConnection,
customConnectionAllowed,
shared,
disabled,
};

View File

@@ -23,7 +23,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
it('should return created app config', async () => {
const appConfig = {
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};
@@ -44,7 +44,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
it('should return HTTP 422 for already existing app config', async () => {
const appConfig = {
key: 'gitlab',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};

View File

@@ -8,16 +8,19 @@ export default async (request, response) => {
})
.throwIfNotFound();
await appConfig.$query().patchAndFetch(appConfigParams(request));
await appConfig.$query().patchAndFetch({
...appConfigParams(request),
key: request.params.appKey,
});
renderObject(response, appConfig);
};
const appConfigParams = (request) => {
const { allowCustomConnection, shared, disabled } = request.body;
const { customConnectionAllowed, shared, disabled } = request.body;
return {
allowCustomConnection,
customConnectionAllowed,
shared,
disabled,
};

View File

@@ -24,7 +24,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return updated app config', async () => {
const appConfig = {
key: 'gitlab',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};
@@ -34,7 +34,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
const newAppConfigValues = {
shared: false,
disabled: true,
allowCustomConnection: false,
customConnectionAllowed: false,
};
const response = await request(app)
@@ -55,7 +55,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
const appConfig = {
shared: false,
disabled: true,
allowCustomConnection: false,
customConnectionAllowed: false,
};
await request(app)
@@ -68,7 +68,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return HTTP 422 for invalid app config data', async () => {
const appConfig = {
key: 'gitlab',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
};

View File

@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
allowCustomConnection: true,
customConnectionAllowed: true,
});
});
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
allowCustomConnection: false,
customConnectionAllowed: false,
});
});

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
appConfig = await createAppConfig({
key: 'deepl',
allowCustomConnection: true,
customConnectionAllowed: true,
shared: true,
disabled: false,
});

View File

@@ -8,7 +8,7 @@ export default async (request, response) => {
})
.throwIfNotFound();
connection = await connection.update(connectionParams(request));
connection = await connection.updateFormattedData(connectionParams(request));
renderObject(response, connection);
};

View File

@@ -0,0 +1,37 @@
export async function up(knex) {
await knex.schema.alterTable('app_configs', (table) => {
table.boolean('connection_allowed').defaultTo(false);
});
const appConfigs = await knex('app_configs').select('*');
for (const appConfig of appConfigs) {
const appAuthClients = await knex('app_auth_clients').where(
'app_key',
appConfig.key
);
const hasSomeActiveAppAuthClients = !!appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = appConfig.shared;
const active = appConfig.disabled === false;
const connectionAllowedConditions = [
hasSomeActiveAppAuthClients,
shared,
active,
];
const connectionAllowed = connectionAllowedConditions.every(Boolean);
await knex('app_configs')
.where('id', appConfig.id)
.update({ connection_allowed: connectionAllowed });
}
}
export async function down(knex) {
await knex.schema.alterTable('app_configs', (table) => {
table.dropColumn('connection_allowed');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', (table) => {
table.renameColumn('allow_custom_connection', 'custom_connection_allowed');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', (table) => {
table.renameColumn('custom_connection_allowed', 'allow_custom_connection');
});
}

View File

@@ -0,0 +1,13 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropPrimary();
table.primary('key');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropPrimary();
table.primary('id');
});
}

View File

@@ -0,0 +1,11 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropColumn('id');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.uuid('id').defaultTo(knex.raw('gen_random_uuid()'));
});
}

View File

@@ -0,0 +1,41 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"connectionAllowed": {
"default": false,
"type": "boolean",
},
"createdAt": {
"type": "string",
},
"customConnectionAllowed": {
"default": false,
"type": "boolean",
},
"disabled": {
"default": false,
"type": "boolean",
},
"id": {
"format": "uuid",
"type": "string",
},
"key": {
"type": "string",
},
"shared": {
"default": false,
"type": "boolean",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"key",
],
"type": "object",
}
`;

View File

@@ -0,0 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Permission model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"action": {
"minLength": 1,
"type": "string",
},
"conditions": {
"items": {
"type": "string",
},
"type": "array",
},
"createdAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"roleId": {
"format": "uuid",
"type": "string",
},
"subject": {
"minLength": 1,
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"roleId",
"action",
"subject",
],
"type": "object",
}
`;

View File

@@ -2,6 +2,7 @@ import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js';
import Base from './base.js';
import AppConfig from './app-config.js';
class AppAuthClient extends Base {
static tableName = 'app_auth_clients';
@@ -21,6 +22,17 @@ class AppAuthClient extends Base {
},
};
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_key',
to: 'app_configs.key',
},
},
});
encryptData() {
if (!this.eligibleForEncryption()) return;
@@ -48,6 +60,17 @@ class AppAuthClient extends Base {
return this.authDefaults ? true : false;
}
async triggerAppConfigUpdate() {
const appConfig = await this.$relatedQuery('appConfig');
// This is a workaround to update connection allowed column for AppConfig
await appConfig?.$query().patch({
key: appConfig.key,
shared: appConfig.shared,
disabled: appConfig.disabled,
});
}
// TODO: Make another abstraction like beforeSave instead of using
// beforeInsert and beforeUpdate separately for the same operation.
async $beforeInsert(queryContext) {
@@ -55,11 +78,23 @@ class AppAuthClient extends Base {
this.encryptData();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
await this.triggerAppConfigUpdate();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
this.encryptData();
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
await this.triggerAppConfigUpdate();
}
async $afterFind() {
this.decryptData();
}

View File

@@ -2,9 +2,12 @@ import { describe, it, expect, vi } from 'vitest';
import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import AppConfig from './app-config.js';
import AppAuthClient from './app-auth-client.js';
import Base from './base.js';
import appConfig from '../config/app.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
import { createAppConfig } from '../../test/factories/app-config.js';
describe('AppAuthClient model', () => {
it('tableName should return correct name', () => {
@@ -15,6 +18,23 @@ describe('AppAuthClient model', () => {
expect(AppAuthClient.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = AppAuthClient.relationMappings();
const expectedRelations = {
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_key',
to: 'app_configs.key',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => {
vi.spyOn(
@@ -140,6 +160,63 @@ describe('AppAuthClient model', () => {
});
});
describe('triggerAppConfigUpdate', () => {
it('should trigger an update in related app config', async () => {
await createAppConfig({ key: 'gitlab' });
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
});
const appConfigBeforeUpdateSpy = vi.spyOn(
AppConfig.prototype,
'$beforeUpdate'
);
await appAuthClient.triggerAppConfigUpdate();
expect(appConfigBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should update related AppConfig after creating an instance', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
shared: true,
});
await createAppAuthClient({
appKey: 'gitlab',
active: true,
});
const refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(true);
});
it('should update related AppConfig after updating an instance', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
shared: true,
});
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
active: false,
});
let refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(false);
await appAuthClient.$query().patchAndFetch({ active: true });
refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(true);
});
});
it('$beforeInsert should call AppAuthClient.encryptData', async () => {
const appAuthClientBeforeInsertSpy = vi.spyOn(
AppAuthClient.prototype,
@@ -151,6 +228,17 @@ describe('AppAuthClient model', () => {
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('$afterInsert should call AppAuthClient.triggerAppConfigUpdate', async () => {
const appAuthClientAfterInsertSpy = vi.spyOn(
AppAuthClient.prototype,
'triggerAppConfigUpdate'
);
await createAppAuthClient();
expect(appAuthClientAfterInsertSpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
const appAuthClient = await createAppAuthClient();
@@ -164,6 +252,19 @@ describe('AppAuthClient model', () => {
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterUpdate should call AppAuthClient.triggerAppConfigUpdate', async () => {
const appAuthClient = await createAppAuthClient();
const appAuthClientAfterUpdateSpy = vi.spyOn(
AppAuthClient.prototype,
'triggerAppConfigUpdate'
);
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
expect(appAuthClientAfterUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterFind should call AppAuthClient.decryptData', async () => {
const appAuthClient = await createAppAuthClient();

View File

@@ -5,6 +5,10 @@ import Base from './base.js';
class AppConfig extends Base {
static tableName = 'app_configs';
static get idColumn() {
return 'key';
}
static jsonSchema = {
type: 'object',
required: ['key'],
@@ -12,7 +16,8 @@ class AppConfig extends Base {
properties: {
id: { type: 'string', format: 'uuid' },
key: { type: 'string' },
allowCustomConnection: { type: 'boolean', default: false },
connectionAllowed: { type: 'boolean', default: false },
customConnectionAllowed: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
createdAt: { type: 'string' },
@@ -31,31 +36,44 @@ class AppConfig extends Base {
},
});
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
get canCustomConnect() {
return !this.disabled && this.allowCustomConnection;
}
get canConnect() {
const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = this.shared;
const active = this.disabled === false;
const conditions = [hasSomeActiveAppAuthClients, shared, active];
return conditions.every(Boolean);
}
async getApp() {
if (!this.key) return null;
return await App.findOneByKey(this.key);
}
async computeAndAssignConnectionAllowedProperty() {
this.connectionAllowed = await this.computeConnectionAllowedProperty();
}
async computeConnectionAllowedProperty() {
const appAuthClients = await this.$relatedQuery('appAuthClients');
const hasSomeActiveAppAuthClients =
appAuthClients?.some((appAuthClient) => appAuthClient.active) || false;
const conditions = [
hasSomeActiveAppAuthClients,
this.shared,
!this.disabled,
];
const connectionAllowed = conditions.every(Boolean);
return connectionAllowed;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.computeAndAssignConnectionAllowedProperty();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.computeAndAssignConnectionAllowedProperty();
}
}
export default AppConfig;

View File

@@ -0,0 +1,180 @@
import { vi, describe, it, expect } from 'vitest';
import Base from './base.js';
import AppConfig from './app-config.js';
import App from './app.js';
import AppAuthClient from './app-auth-client.js';
import { createAppConfig } from '../../test/factories/app-config.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
describe('AppConfig model', () => {
it('tableName should return correct name', () => {
expect(AppConfig.tableName).toBe('app_configs');
});
it('idColumn should return key field', () => {
expect(AppConfig.idColumn).toBe('key');
});
it('jsonSchema should have correct validations', () => {
expect(AppConfig.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = AppConfig.relationMappings();
const expectedRelations = {
appAuthClients: {
relation: Base.HasManyRelation,
modelClass: AppAuthClient,
join: {
from: 'app_configs.key',
to: 'app_auth_clients.app_key',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
describe('getApp', () => {
it('getApp should return null if there is no key', async () => {
const appConfig = new AppConfig();
const app = await appConfig.getApp();
expect(app).toBeNull();
});
it('getApp should return app with provided key', async () => {
const appConfig = new AppConfig();
appConfig.key = 'deepl';
const app = await appConfig.getApp();
const expectedApp = await App.findOneByKey(appConfig.key);
expect(app).toStrictEqual(expectedApp);
});
});
describe('computeAndAssignConnectionAllowedProperty', () => {
it('should call computeConnectionAllowedProperty and assign the result', async () => {
const appConfig = await createAppConfig();
const computeConnectionAllowedPropertySpy = vi
.spyOn(appConfig, 'computeConnectionAllowedProperty')
.mockResolvedValue(true);
await appConfig.computeAndAssignConnectionAllowedProperty();
expect(computeConnectionAllowedPropertySpy).toHaveBeenCalled();
expect(appConfig.connectionAllowed).toBe(true);
});
});
describe('computeConnectionAllowedProperty', () => {
it('should return true when app is enabled, shared and allows custom connection with an active app auth client', async () => {
await createAppAuthClient({
appKey: 'deepl',
active: true,
});
await createAppAuthClient({
appKey: 'deepl',
active: false,
});
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(true);
});
it('should return false if there is no active app auth client', async () => {
await createAppAuthClient({
appKey: 'deepl',
active: false,
});
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it('should return false if there is no app auth clients', async () => {
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it('should return false when app is disabled', async () => {
const appConfig = await createAppConfig({
disabled: true,
customConnectionAllowed: true,
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it(`should return false when app doesn't allow custom connection`, async () => {
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: false,
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
});
it('$beforeInsert should call computeAndAssignConnectionAllowedProperty', async () => {
const computeAndAssignConnectionAllowedPropertySpy = vi
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
.mockResolvedValue(true);
await createAppConfig();
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call computeAndAssignConnectionAllowedProperty', async () => {
const appConfig = await createAppConfig();
const computeAndAssignConnectionAllowedPropertySpy = vi
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
.mockResolvedValue(true);
await appConfig.$query().patch({
key: 'deepl',
});
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
});
});

View File

@@ -89,7 +89,7 @@ class Connection extends Base {
}
if (this.appConfig) {
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
}
return true;
@@ -122,10 +122,20 @@ class Connection extends Base {
return this.data ? true : false;
}
async checkEligibilityForCreation() {
const app = await App.findOneByKey(this.key);
async getApp() {
if (!this.key) return null;
const appConfig = await AppConfig.query().findOne({ key: this.key });
return await App.findOneByKey(this.key);
}
async getAppConfig() {
return await AppConfig.query().findOne({ key: this.key });
}
async checkEligibilityForCreation() {
const app = await this.getApp();
const appConfig = await this.getAppConfig();
if (appConfig) {
if (appConfig.disabled) {
@@ -134,7 +144,7 @@ class Connection extends Base {
);
}
if (!appConfig.allowCustomConnection && this.formattedData) {
if (!appConfig.customConnectionAllowed && this.formattedData) {
throw new NotAuthorizedError(
`New custom connections have been disabled for ${app.name}!`
);
@@ -160,12 +170,6 @@ class Connection extends Base {
return this;
}
async getApp() {
if (!this.key) return null;
return await App.findOneByKey(this.key);
}
async testAndUpdateConnection() {
const app = await this.getApp();
const $ = await globalVariable({ connection: this, app });
@@ -224,7 +228,7 @@ class Connection extends Base {
async reset() {
const formattedData = this?.formattedData?.screenName
? { screenName: this.formattedData.screenName }
: null;
: {};
const updatedConnection = await this.$query().patchAndFetch({
formattedData,
@@ -233,7 +237,7 @@ class Connection extends Base {
return updatedConnection;
}
async update({ formattedData, appAuthClientId }) {
async updateFormattedData({ formattedData, appAuthClientId }) {
if (appAuthClientId) {
const appAuthClient = await AppAuthClient.query()
.findById(appAuthClientId)

View File

@@ -3,11 +3,16 @@ import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js';
import AppAuthClient from './app-auth-client.js';
import App from './app.js';
import AppConfig from './app-config.js';
import Base from './base.js';
import Connection from './connection';
import Step from './step.js';
import User from './user.js';
import Telemetry from '../helpers/telemetry/index.js';
import { createConnection } from '../../test/factories/connection.js';
import { createAppConfig } from '../../test/factories/app-config.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
describe('Connection model', () => {
it('tableName should return correct name', () => {
@@ -26,57 +31,138 @@ describe('Connection model', () => {
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
it('relationMappings should return correct associations', () => {
const relationMappings = Connection.relationMappings();
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = Connection.relationMappings();
const expectedRelations = {
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'connections.user_id',
to: 'users.id',
const expectedRelations = {
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'connections.user_id',
to: 'users.id',
},
},
},
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
},
},
},
triggerSteps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
triggerSteps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
},
filter: expect.any(Function),
},
filter: expect.any(Function),
},
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'connections.key',
to: 'app_configs.key',
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'connections.key',
to: 'app_configs.key',
},
},
},
appAuthClient: {
relation: Base.BelongsToOneRelation,
modelClass: AppAuthClient,
join: {
from: 'connections.app_auth_client_id',
to: 'app_auth_clients.id',
appAuthClient: {
relation: Base.BelongsToOneRelation,
modelClass: AppAuthClient,
join: {
from: 'connections.app_auth_client_id',
to: 'app_auth_clients.id',
},
},
},
};
};
expect(relationMappings).toStrictEqual(expectedRelations);
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('triggerSteps should return only trigger typed steps', () => {
const relations = Connection.relationMappings();
const whereSpy = vi.fn();
relations.triggerSteps.filter({ where: whereSpy });
expect(whereSpy).toHaveBeenCalledWith('type', '=', 'trigger');
});
});
describe.todo('reconnectable');
describe('reconnectable', () => {
it('should return active status of app auth client when created via app auth client', async () => {
const appAuthClient = await createAppAuthClient({
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
appAuthClientId: appAuthClient.id,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appAuthClient: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return true when app config is not disabled and allows custom connection', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
});
const connection = await createConnection({
key: appConfig.key,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return false when app config is disabled or does not allow custom connection', async () => {
const connection = await createConnection({
key: 'gitlab',
formattedData: {
token: 'sample-token',
},
});
await createAppConfig({
key: 'gitlab',
disabled: true,
customConnectionAllowed: false,
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(false);
});
});
describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => {
@@ -160,4 +246,558 @@ describe('Connection model', () => {
expect(connection.data).not.toEqual(formattedData);
});
});
describe('eligibleForEncryption', () => {
it('should return true when formattedData property exists', async () => {
const connection = new Connection();
connection.formattedData = { clientId: 'sample-id' };
expect(connection.eligibleForEncryption()).toBe(true);
});
it("should return false when formattedData property doesn't exist", async () => {
const connection = new Connection();
connection.formattedData = undefined;
expect(connection.eligibleForEncryption()).toBe(false);
});
});
describe('eligibleForDecryption', () => {
it('should return true when data property exists', async () => {
const connection = new Connection();
connection.data = 'encrypted-data';
expect(connection.eligibleForDecryption()).toBe(true);
});
it("should return false when data property doesn't exist", async () => {
const connection = new Connection();
connection.data = undefined;
expect(connection.eligibleForDecryption()).toBe(false);
});
});
describe('getApp', () => {
it('should return connection app when valid key exists', async () => {
const connection = new Connection();
connection.key = 'gitlab';
const connectionApp = await connection.getApp();
const app = await App.findOneByKey('gitlab');
expect(connectionApp).toStrictEqual(app);
});
it('should throw an error when invalid key exists', async () => {
const connection = new Connection();
connection.key = 'invalid-key';
await expect(() => connection.getApp()).rejects.toThrowError(
`An application with the "invalid-key" key couldn't be found.`
);
});
it('should return null when no key exists', async () => {
const connection = new Connection();
await expect(connection.getApp()).resolves.toBe(null);
});
});
it('getAppConfig should return connection app config', async () => {
const connection = new Connection();
connection.key = 'gitlab';
const appConfig = await createAppConfig({ key: 'gitlab' });
const connectionAppConfig = await connection.getAppConfig();
expect(connectionAppConfig).toStrictEqual(appConfig);
});
describe('checkEligibilityForCreation', () => {
it('should return connection if no app config exists', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
const connection = new Connection();
expect(await connection.checkEligibilityForCreation()).toBe(connection);
});
it('should throw an error when app does not exist', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockRejectedValue(
new Error(
`An application with the "unexisting-app" key couldn't be found.`
)
);
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue();
const connection = new Connection();
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
`An application with the "unexisting-app" key couldn't be found.`
);
});
it('should throw an error when app config is disabled', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: true,
});
const connection = new Connection();
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The application has been disabled for new connections!'
);
});
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
customConnectionAllowed: false,
});
const connection = new Connection();
connection.formattedData = {};
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'New custom connections have been disabled for gitlab!'
);
});
it('should throw an error when app config is not shared with app auth client', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
shared: false,
});
const connection = new Connection();
connection.appAuthClientId = 'sample-id';
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The connection with the given app auth client is not allowed!'
);
});
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
shared: true,
});
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
key: 'gitlab',
appAuthClientId: appAuthClient.id,
formattedData: null,
});
await connection.checkEligibilityForCreation();
expect(connection.formattedData).toStrictEqual({
clientId: 'sample-id',
});
});
});
describe('testAndUpdateConnection', () => {
it('should verify connection and persist it', async () => {
const connection = await createConnection({ verified: false });
const isStillVerifiedSpy = vi.fn().mockReturnValue(true);
const originalApp = await connection.getApp();
const getAppSpy = vi
.spyOn(connection, 'getApp')
.mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
isStillVerified: isStillVerifiedSpy,
},
};
});
const updatedConnection = await connection.testAndUpdateConnection();
expect(getAppSpy).toHaveBeenCalledOnce();
expect(isStillVerifiedSpy).toHaveBeenCalledOnce();
expect(updatedConnection.verified).toBe(true);
});
it('should unverify connection and persist it', async () => {
const connection = await createConnection({ verified: true });
const isStillVerifiedSpy = vi
.fn()
.mockRejectedValue(new Error('Wrong credentials!'));
const originalApp = await connection.getApp();
const getAppSpy = vi
.spyOn(connection, 'getApp')
.mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
isStillVerified: isStillVerifiedSpy,
},
};
});
const updatedConnection = await connection.testAndUpdateConnection();
expect(getAppSpy).toHaveBeenCalledOnce();
expect(isStillVerifiedSpy).toHaveBeenCalledOnce();
expect(updatedConnection.verified).toBe(false);
});
});
describe('verifyAndUpdateConnection', () => {
it('should verify connection with valid token', async () => {
const connection = await createConnection({
verified: false,
draft: true,
});
const verifyCredentialsSpy = vi.fn().mockResolvedValue(true);
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyCredentials: verifyCredentialsSpy,
},
};
});
const updatedConnection = await connection.verifyAndUpdateConnection();
expect(verifyCredentialsSpy).toHaveBeenCalledOnce();
expect(updatedConnection.verified).toBe(true);
expect(updatedConnection.draft).toBe(false);
});
it('should throw an error with invalid token', async () => {
const connection = await createConnection({
verified: false,
draft: true,
});
const verifyCredentialsSpy = vi
.fn()
.mockRejectedValue(new Error('Invalid token!'));
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyCredentials: verifyCredentialsSpy,
},
};
});
await expect(() =>
connection.verifyAndUpdateConnection()
).rejects.toThrowError('Invalid token!');
expect(verifyCredentialsSpy).toHaveBeenCalledOnce();
});
});
describe('verifyWebhook', () => {
it('should verify webhook on remote', async () => {
const connection = await createConnection({ key: 'typeform' });
const verifyWebhookSpy = vi.fn().mockResolvedValue('verified-webhook');
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyWebhook: verifyWebhookSpy,
},
};
});
expect(await connection.verifyWebhook()).toBe('verified-webhook');
});
it('should return true if connection does not have value in key property', async () => {
const connection = await createConnection({ key: null });
expect(await connection.verifyWebhook()).toBe(true);
});
it('should throw an error at failed webhook verification', async () => {
const connection = await createConnection({ key: 'typeform' });
const verifyWebhookSpy = vi.fn().mockRejectedValue('unverified-webhook');
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
verifyWebhook: verifyWebhookSpy,
},
};
});
await expect(() => connection.verifyWebhook()).rejects.toThrowError(
'unverified-webhook'
);
});
});
it('generateAuthUrl should return authentication url', async () => {
const connection = await createConnection({
key: 'typeform',
formattedData: {
url: 'https://automatisch.io/authentication-url',
},
});
const generateAuthUrlSpy = vi.fn();
const originalApp = await connection.getApp();
vi.spyOn(connection, 'getApp').mockImplementation(() => {
return {
...originalApp,
auth: {
...originalApp.auth,
generateAuthUrl: generateAuthUrlSpy,
},
};
});
expect(await connection.generateAuthUrl()).toStrictEqual({
url: 'https://automatisch.io/authentication-url',
});
});
describe('reset', () => {
it('should keep screen name when exists and reset the rest of the formatted data', async () => {
const connection = await createConnection({
formattedData: {
screenName: 'Sample connection',
token: 'sample-token',
},
});
await connection.reset();
const refetchedConnection = await connection.$query();
expect(refetchedConnection.formattedData).toStrictEqual({
screenName: 'Sample connection',
});
});
it('should empty formatted data object when screen name does not exist', async () => {
const connection = await createConnection({
formattedData: {
token: 'sample-token',
},
});
await connection.reset();
const refetchedConnection = await connection.$query();
expect(refetchedConnection.formattedData).toStrictEqual({});
});
});
describe('updateFormattedData', () => {
it('should extend connection data with app auth client auth defaults', async () => {
const appAuthClient = await createAppAuthClient({
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
appAuthClientId: appAuthClient.id,
formattedData: {
token: 'sample-token',
},
});
const updatedConnection = await connection.updateFormattedData({
appAuthClientId: appAuthClient.id,
});
expect(updatedConnection.formattedData).toStrictEqual({
clientId: 'sample-id',
token: 'sample-token',
});
});
});
describe('$beforeInsert', () => {
it('should call super.$beforeInsert', async () => {
const superBeforeInsertSpy = vi
.spyOn(Base.prototype, '$beforeInsert')
.mockResolvedValue();
await createConnection();
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('should call checkEligibilityForCreation', async () => {
const checkEligibilityForCreationSpy = vi
.spyOn(Connection.prototype, 'checkEligibilityForCreation')
.mockResolvedValue();
await createConnection();
expect(checkEligibilityForCreationSpy).toHaveBeenCalledOnce();
});
it('should call encryptData', async () => {
const encryptDataSpy = vi
.spyOn(Connection.prototype, 'encryptData')
.mockResolvedValue();
await createConnection();
expect(encryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$beforeUpdate', () => {
it('should call super.$beforeUpdate', async () => {
const superBeforeUpdateSpy = vi
.spyOn(Base.prototype, '$beforeUpdate')
.mockResolvedValue();
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should call encryptData', async () => {
const connection = await createConnection();
const encryptDataSpy = vi
.spyOn(Connection.prototype, 'encryptData')
.mockResolvedValue();
await connection.$query().patch({ verified: false });
expect(encryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$afterFind', () => {
it('should call decryptData', async () => {
const connection = await createConnection();
const decryptDataSpy = vi
.spyOn(Connection.prototype, 'decryptData')
.mockResolvedValue();
await connection.$query();
expect(decryptDataSpy).toHaveBeenCalledOnce();
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
await createConnection();
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call Telemetry.connectionCreated', async () => {
const telemetryConnectionCreatedSpy = vi
.spyOn(Telemetry, 'connectionCreated')
.mockImplementation(() => {});
const connection = await createConnection();
expect(telemetryConnectionCreatedSpy).toHaveBeenCalledWith(connection);
});
});
describe('$afterUpdate', () => {
it('should call super.$afterUpdate', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterUpdate');
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call Telemetry.connectionUpdated', async () => {
const telemetryconnectionUpdatedSpy = vi
.spyOn(Telemetry, 'connectionCreated')
.mockImplementation(() => {});
const connection = await createConnection();
await connection.$query().patch({ verified: false });
expect(telemetryconnectionUpdatedSpy).toHaveBeenCalledWith(connection);
});
});
});

View File

@@ -19,25 +19,39 @@ class Permission extends Base {
},
};
static sanitize(permissions) {
static filter(permissions) {
const sanitizedPermissions = permissions.filter((permission) => {
const { action, subject, conditions } = permission;
const relevantAction = permissionCatalog.actions.find(
(actionCatalogItem) => actionCatalogItem.key === action
);
const validSubject = relevantAction.subjects.includes(subject);
const validConditions = conditions.every((condition) => {
return !!permissionCatalog.conditions.find(
(conditionCatalogItem) => conditionCatalogItem.key === condition
);
});
const relevantAction = this.findAction(action);
const validSubject = this.isSubjectValid(subject, relevantAction);
const validConditions = this.areConditionsValid(conditions);
return validSubject && validConditions;
return relevantAction && validSubject && validConditions;
});
return sanitizedPermissions;
}
static findAction(action) {
return permissionCatalog.actions.find(
(actionCatalogItem) => actionCatalogItem.key === action
);
}
static isSubjectValid(subject, action) {
return action && action.subjects.includes(subject);
}
static areConditionsValid(conditions) {
return conditions.every((condition) => this.isConditionValid(condition));
}
static isConditionValid(condition) {
return !!permissionCatalog.conditions.find(
(conditionCatalogItem) => conditionCatalogItem.key === condition
);
}
}
export default Permission;

View File

@@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import Permission from './permission';
import permissionCatalog from '../helpers/permission-catalog.ee.js';
describe('Permission model', () => {
it('tableName should return correct name', () => {
expect(Permission.tableName).toBe('permissions');
});
it('jsonSchema should have correct validations', () => {
expect(Permission.jsonSchema).toMatchSnapshot();
});
it('filter should return only valid permissions based on permission catalog', () => {
const permissions = [
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'delete', subject: 'Connection', conditions: [] },
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'update', subject: 'Execution', conditions: [] }, // Invalid subject
{ action: 'read', subject: 'Execution', conditions: ['invalid'] }, // Invalid condition
{ action: 'invalid', subject: 'Execution', conditions: [] }, // Invalid action
];
const result = Permission.filter(permissions);
expect(result).toStrictEqual([
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'delete', subject: 'Connection', conditions: [] },
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
]);
});
describe('findAction', () => {
it('should return action from permission catalog', () => {
const action = Permission.findAction('create');
expect(action.key).toStrictEqual('create');
});
it('should return undefined for invalid actions', () => {
const invalidAction = Permission.findAction('invalidAction');
expect(invalidAction).toBeUndefined();
});
});
describe('isSubjectValid', () => {
it('should return true for valid subjects', () => {
const validAction = permissionCatalog.actions.find(
(action) => action.key === 'create'
);
const validSubject = Permission.isSubjectValid('Connection', validAction);
expect(validSubject).toBe(true);
});
it('should return false for invalid subjects', () => {
const validAction = permissionCatalog.actions.find(
(action) => action.key === 'create'
);
const invalidSubject = Permission.isSubjectValid(
'Execution',
validAction
);
expect(invalidSubject).toBe(false);
});
});
describe('areConditionsValid', () => {
it('should return true for valid conditions', () => {
const validConditions = Permission.areConditionsValid(['isCreator']);
expect(validConditions).toBe(true);
});
it('should return false for invalid conditions', () => {
const invalidConditions = Permission.areConditionsValid([
'invalidCondition',
]);
expect(invalidConditions).toBe(false);
});
});
describe('isConditionValid', () => {
it('should return true for valid conditions', () => {
const validCondition = Permission.isConditionValid('isCreator');
expect(validCondition).toBe(true);
});
it('should return false for invalid conditions', () => {
const invalidCondition = Permission.isConditionValid('invalidCondition');
expect(invalidCondition).toBe(false);
});
});
});

View File

@@ -63,14 +63,14 @@ class Role extends Base {
await this.$relatedQuery('permissions', trx).delete();
if (permissions?.length) {
const sanitizedPermissions = Permission.sanitize(permissions).map(
const validPermissions = Permission.filter(permissions).map(
(permission) => ({
...permission,
roleId: this.id,
})
);
await Permission.query().insert(sanitizedPermissions);
await Permission.query().insert(validPermissions);
}
await this.$query(trx).patch({

View File

@@ -2,11 +2,10 @@ const appConfigSerializer = (appConfig) => {
return {
id: appConfig.id,
key: appConfig.key,
allowCustomConnection: appConfig.allowCustomConnection,
customConnectionAllowed: appConfig.customConnectionAllowed,
shared: appConfig.shared,
disabled: appConfig.disabled,
canConnect: appConfig.canConnect,
canCustomConnect: appConfig.canCustomConnect,
connectionAllowed: appConfig.connectionAllowed,
createdAt: appConfig.createdAt.getTime(),
updatedAt: appConfig.updatedAt.getTime(),
};

View File

@@ -13,11 +13,10 @@ describe('appConfig serializer', () => {
const expectedPayload = {
id: appConfig.id,
key: appConfig.key,
allowCustomConnection: appConfig.allowCustomConnection,
customConnectionAllowed: appConfig.customConnectionAllowed,
shared: appConfig.shared,
disabled: appConfig.disabled,
canConnect: appConfig.canConnect,
canCustomConnect: appConfig.canCustomConnect,
connectionAllowed: appConfig.connectionAllowed,
createdAt: appConfig.createdAt.getTime(),
updatedAt: appConfig.updatedAt.getTime(),
};

View File

@@ -10,7 +10,6 @@ const formattedAuthDefaults = {
export const createAppAuthClient = async (params = {}) => {
params.name = params?.name || faker.person.fullName();
params.id = params?.id || faker.string.uuid();
params.appKey = params?.appKey || 'deepl';
params.active = params?.active ?? true;
params.formattedAuthDefaults =

View File

@@ -2,7 +2,7 @@ const createAppConfigMock = (appConfig) => {
return {
data: {
key: appConfig.key,
allowCustomConnection: appConfig.allowCustomConnection,
customConnectionAllowed: appConfig.customConnectionAllowed,
shared: appConfig.shared,
disabled: appConfig.disabled,
},

View File

@@ -3,11 +3,10 @@ const getAppConfigMock = (appConfig) => {
data: {
id: appConfig.id,
key: appConfig.key,
allowCustomConnection: appConfig.allowCustomConnection,
customConnectionAllowed: appConfig.customConnectionAllowed,
shared: appConfig.shared,
disabled: appConfig.disabled,
canConnect: appConfig.canConnect,
canCustomConnect: appConfig.canCustomConnect,
connectionAllowed: appConfig.connectionAllowed,
createdAt: appConfig.createdAt.getTime(),
updatedAt: appConfig.updatedAt.getTime(),
},

View File

@@ -8,11 +8,15 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage {
constructor(page) {
super(page);
this.allowCustomConnectionsSwitch = this.page.locator('[name="allowCustomConnection"]');
this.allowCustomConnectionsSwitch = this.page.locator(
'[name="customConnectionAllowed"]'
);
this.allowSharedConnectionsSwitch = this.page.locator('[name="shared"]');
this.disableConnectionsSwitch = this.page.locator('[name="disabled"]');
this.saveButton = this.page.getByTestId('submit-button');
this.successSnackbar = this.page.getByTestId('snackbar-save-admin-apps-settings-success');
this.successSnackbar = this.page.getByTestId(
'snackbar-save-admin-apps-settings-success'
);
}
async allowCustomConnections() {

View File

@@ -20,7 +20,7 @@ function AdminApplicationCreateAuthClient(props) {
const {
mutateAsync: createAppConfig,
isPending: isCreateAppConfigPending,
error: createAppConfigError
error: createAppConfigError,
} = useAdminCreateAppConfig(props.appKey);
const {
@@ -30,16 +30,15 @@ function AdminApplicationCreateAuthClient(props) {
} = useAdminCreateAppAuthClient(appKey);
const submitHandler = async (values) => {
let appConfigId = appConfig?.data?.id;
let appConfigKey = appConfig?.data?.key;
if (!appConfigId) {
if (!appConfigKey) {
const { data: appConfigData } = await createAppConfig({
allowCustomConnection: true,
customConnectionAllowed: true,
shared: false,
disabled: false,
});
appConfigId = appConfigData.id;
appConfigKey = appConfigData.key;
}
const { name, active, ...formattedAuthDefaults } = values;

View File

@@ -46,7 +46,8 @@ function AdminApplicationSettings(props) {
const defaultValues = useMemo(
() => ({
allowCustomConnection: appConfig?.data?.allowCustomConnection || false,
customConnectionAllowed:
appConfig?.data?.customConnectionAllowed || false,
shared: appConfig?.data?.shared || false,
disabled: appConfig?.data?.disabled || false,
}),
@@ -61,8 +62,8 @@ function AdminApplicationSettings(props) {
<Paper sx={{ p: 2, mt: 4 }}>
<Stack spacing={2} direction="column">
<Switch
name="allowCustomConnection"
label={formatMessage('adminAppsSettings.allowCustomConnection')}
name="customConnectionAllowed"
label={formatMessage('adminAppsSettings.customConnectionAllowed')}
FormControlLabelProps={{
labelPlacement: 'start',
}}

View File

@@ -93,14 +93,17 @@ function ChooseConnectionSubstep(props) {
appWithConnections?.map((connection) => optionGenerator(connection)) ||
[];
if (!appConfig?.data || appConfig?.data?.canCustomConnect) {
if (
!appConfig?.data ||
(!appConfig.data?.disabled && appConfig.data?.customConnectionAllowed)
) {
options.push({
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
value: ADD_CONNECTION_VALUE,
});
}
if (appConfig?.data?.canConnect) {
if (appConfig?.data?.connectionAllowed) {
options.push({
label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'),
value: ADD_SHARED_CONNECTION_VALUE,

View File

@@ -40,7 +40,7 @@ SuggestionItem.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
sampleValue: PropTypes.string,
sampleValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
).isRequired,
onSuggestionClick: PropTypes.func.isRequired,

View File

@@ -53,15 +53,13 @@ function SignUpForm() {
}, [authentication.isAuthenticated]);
const handleSubmit = async (values) => {
const { fullName, email, password } = values;
await registerUser({
fullName,
email,
password,
});
try {
const { fullName, email, password } = values;
await registerUser({
fullName,
email,
password,
});
const { data } = await createAccessToken({
email,
password,
@@ -69,9 +67,27 @@ function SignUpForm() {
const { token } = data;
authentication.updateToken(token);
} catch (error) {
enqueueSnackbar(error?.message || formatMessage('signupForm.error'), {
variant: 'error',
});
const errors = error?.response?.data?.errors
? Object.values(error.response.data.errors)
: [];
if (errors.length) {
for (const [error] of errors) {
enqueueSnackbar(error, {
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-sign-up-error',
},
});
}
} else {
enqueueSnackbar(error?.message || formatMessage('signupForm.error'), {
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-sign-up-error',
},
});
}
}
};

View File

@@ -32,7 +32,8 @@ Variable.propTypes = {
children: PropTypes.node.isRequired,
element: PropTypes.shape({
name: PropTypes.string.isRequired,
sampleValue: PropTypes.string.isRequired,
sampleValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
}),
disabled: PropTypes.bool,
};

View File

@@ -1,5 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import api from 'helpers/api';
import { enqueueSnackbar } from 'notistack';
export default function useAdminCreateSamlAuthProvider() {
const queryClient = useQueryClient();
@@ -15,6 +16,20 @@ export default function useAdminCreateSamlAuthProvider() {
queryKey: ['admin', 'samlAuthProviders'],
});
},
onError: (error) => {
const errors = Object.entries(
error.response.data.errors || [['', 'Failed while saving!']],
);
for (const error of errors) {
enqueueSnackbar(`${error[0] ? error[0] + ': ' : ''} ${error[1]}`, {
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-create-saml-auth-provider-error',
},
});
}
},
});
return query;

View File

@@ -1,5 +1,6 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import api from 'helpers/api';
import { enqueueSnackbar } from 'notistack';
export default function useAdminUpdateSamlAuthProvider(samlAuthProviderId) {
const queryClient = useQueryClient();
@@ -18,6 +19,20 @@ export default function useAdminUpdateSamlAuthProvider(samlAuthProviderId) {
queryKey: ['admin', 'samlAuthProviders'],
});
},
onError: (error) => {
const errors = Object.entries(
error.response.data.errors || [['', 'Failed while saving!']],
);
for (const error of errors) {
enqueueSnackbar(`${error[0] ? error[0] + ': ' : ''} ${error[1]}`, {
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-update-saml-auth-provider-error',
},
});
}
},
});
return query;

View File

@@ -292,7 +292,7 @@
"adminApps.connections": "Connections",
"adminApps.authClients": "Auth clients",
"adminApps.settings": "Settings",
"adminAppsSettings.allowCustomConnection": "Allow custom connection",
"adminAppsSettings.customConnectionAllowed": "Allow custom connection",
"adminAppsSettings.shared": "Shared",
"adminAppsSettings.disabled": "Disabled",
"adminAppsSettings.save": "Save",

View File

@@ -77,14 +77,15 @@ export default function Application() {
const connectionOptions = React.useMemo(() => {
const shouldHaveCustomConnection =
appConfig?.data?.canConnect && appConfig?.data?.canCustomConnect;
appConfig?.data?.connectionAllowed &&
appConfig?.data?.customConnectionAllowed;
const options = [
{
label: formatMessage('app.addConnection'),
key: 'addConnection',
'data-test': 'add-connection-button',
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.canConnect),
to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.connectionAllowed),
disabled: !currentUserAbility.can('create', 'Connection'),
},
];
@@ -155,8 +156,9 @@ export default function Application() {
disabled={
!allowed ||
(appConfig?.data &&
!appConfig?.data?.canConnect &&
!appConfig?.data?.canCustomConnect) ||
!appConfig?.data?.disabled &&
!appConfig?.data?.connectionAllowed &&
!appConfig?.data?.customConnectionAllowed) ||
connectionOptions.every(({ disabled }) => disabled)
}
options={connectionOptions}

View File

@@ -56,6 +56,19 @@ function RoleMappings({ provider, providerLoading }) {
});
}
} catch (error) {
const errors = Object.values(
error.response.data.errors || [['Failed while saving!']],
);
for (const [error] of errors) {
enqueueSnackbar(error, {
variant: 'error',
SnackbarProps: {
'data-test': 'snackbar-update-role-mappings-error',
},
});
}
throw new Error('Failed while saving!');
}
};

View File

@@ -66,10 +66,10 @@ function RoleMappingsFieldArray() {
<MuiTextField
{...params}
label={formatMessage('roleMappingsForm.role')}
required
/>
)}
loading={isRolesLoading}
required
/>
</Stack>
<IconButton

View File

@@ -65,7 +65,7 @@ function SamlConfiguration({ provider, providerLoading }) {
'data-test': 'snackbar-save-saml-provider-success',
},
});
} catch (error) {
} catch {
throw new Error('Failed while saving!');
}
};

View File

@@ -459,9 +459,8 @@ export const SamlAuthProviderRolePropType = PropTypes.shape({
export const AppConfigPropType = PropTypes.shape({
id: PropTypes.string,
key: PropTypes.string,
allowCustomConnection: PropTypes.bool,
canConnect: PropTypes.bool,
canCustomConnect: PropTypes.bool,
customConnectionAllowed: PropTypes.bool,
connectionAllowed: PropTypes.bool,
shared: PropTypes.bool,
disabled: PropTypes.bool,
});
@@ -469,7 +468,7 @@ export const AppConfigPropType = PropTypes.shape({
export const AppAuthClientPropType = PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
appConfigId: PropTypes.string,
appConfigKey: PropTypes.string,
authDefaults: PropTypes.string,
formattedAuthDefaults: PropTypes.object,
active: PropTypes.bool,

View File

@@ -4111,10 +4111,10 @@
resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz"
integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==
"@types/http-proxy@^1.17.5":
version "1.17.8"
resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz"
integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==
"@types/http-proxy@^1.17.8":
version "1.17.15"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36"
integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==
dependencies:
"@types/node" "*"
@@ -9451,11 +9451,11 @@ http-proxy-agent@^7.0.0:
debug "^4.3.4"
http-proxy-middleware@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz"
integrity sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==
version "2.0.7"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6"
integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==
dependencies:
"@types/http-proxy" "^1.17.5"
"@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1"
is-glob "^4.0.1"
is-plain-obj "^3.0.0"