feat: introduce app configs with shared auth clients (#1213)

This commit is contained in:
Ali BARIN
2023-08-16 15:46:43 +02:00
committed by GitHub
parent 25983e046c
commit 3b54b29a99
47 changed files with 1504 additions and 113 deletions

View File

@@ -0,0 +1,91 @@
import { IJSONObject } from '@automatisch/types';
import { AES, enc } from 'crypto-js';
import { ModelOptions, QueryContext } from 'objection';
import appConfig from '../config/app';
import AppConfig from './app-config';
import Base from './base';
class AppAuthClient extends Base {
id!: string;
name: string;
active: boolean;
appConfigId!: string;
authDefaults: string;
formattedAuthDefaults?: IJSONObject;
appConfig?: AppConfig;
static tableName = 'app_auth_clients';
static jsonSchema = {
type: 'object',
required: ['name', 'appConfigId', 'formattedAuthDefaults'],
properties: {
id: { type: 'string', format: 'uuid' },
appConfigId: { type: 'string', format: 'uuid' },
active: { type: 'boolean' },
authDefaults: { type: ['string', 'null'] },
formattedAuthDefaults: { type: 'object' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_config_id',
to: 'app_configs.id',
},
},
});
encryptData(): void {
if (!this.eligibleForEncryption()) return;
this.authDefaults = AES.encrypt(
JSON.stringify(this.formattedAuthDefaults),
appConfig.encryptionKey
).toString();
delete this.formattedAuthDefaults;
}
decryptData(): void {
if (!this.eligibleForDecryption()) return;
this.formattedAuthDefaults = JSON.parse(
AES.decrypt(this.authDefaults, appConfig.encryptionKey).toString(enc.Utf8)
);
}
eligibleForEncryption(): boolean {
return this.formattedAuthDefaults ? true : false;
}
eligibleForDecryption(): boolean {
return this.authDefaults ? true : false;
}
// TODO: Make another abstraction like beforeSave instead of using
// beforeInsert and beforeUpdate separately for the same operation.
async $beforeInsert(queryContext: QueryContext): Promise<void> {
await super.$beforeInsert(queryContext);
this.encryptData();
}
async $beforeUpdate(
opt: ModelOptions,
queryContext: QueryContext
): Promise<void> {
await super.$beforeUpdate(opt, queryContext);
this.encryptData();
}
async $afterFind(): Promise<void> {
this.decryptData();
}
}
export default AppAuthClient;

View File

@@ -0,0 +1,70 @@
import App from './app';
import Base from './base';
import AppAuthClient from './app-auth-client';
class AppConfig extends Base {
id!: string;
key!: string;
allowCustomConnection: boolean;
shared: boolean;
disabled: boolean;
app?: App;
appAuthClients?: AppAuthClient[];
static tableName = 'app_configs';
static jsonSchema = {
type: 'object',
required: ['key'],
properties: {
id: { type: 'string', format: 'uuid' },
key: { type: 'string' },
allowCustomConnection: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
},
};
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
static relationMappings = () => ({
appAuthClients: {
relation: Base.HasManyRelation,
modelClass: AppAuthClient,
join: {
from: 'app_configs.id',
to: 'app_auth_clients.app_config_id',
},
},
});
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);
}
}
export default AppConfig;

View File

@@ -3,6 +3,8 @@ import type { RelationMappings } from 'objection';
import { AES, enc } from 'crypto-js';
import { IRequest } from '@automatisch/types';
import App from './app';
import AppConfig from './app-config';
import AppAuthClient from './app-auth-client';
import Base from './base';
import User from './user';
import Step from './step';
@@ -25,6 +27,9 @@ class Connection extends Base {
user?: User;
steps?: Step[];
triggerSteps?: Step[];
appAuthClientId?: string;
appAuthClient?: AppAuthClient;
appConfig?: AppConfig;
static tableName = 'connections';
@@ -38,6 +43,7 @@ class Connection extends Base {
data: { type: 'string' },
formattedData: { type: 'object' },
userId: { type: 'string', format: 'uuid' },
appAuthClientId: { type: 'string', format: 'uuid' },
verified: { type: 'boolean', default: false },
draft: { type: 'boolean' },
deletedAt: { type: 'string' },
@@ -46,6 +52,10 @@ class Connection extends Base {
},
};
static get virtualAttributes() {
return ['reconnectable'];
}
static relationMappings = (): RelationMappings => ({
user: {
relation: Base.BelongsToOneRelation,
@@ -74,8 +84,36 @@ class Connection extends Base {
builder.where('type', '=', 'trigger');
},
},
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',
},
},
});
get reconnectable() {
if (this.appAuthClientId) {
return this.appAuthClient.active;
}
if (this.appConfig) {
return !this.appConfig.disabled && this.appConfig.allowCustomConnection;
}
return true;
}
encryptData(): void {
if (!this.eligibleForEncryption()) return;

View File

@@ -294,6 +294,7 @@ class User extends Base {
if (Array.isArray(this.permissions)) {
this.permissions = this.permissions.filter((permission) => {
const restrictedSubjects = [
'App',
'Role',
'SamlAuthProvider',
'Config',