feat: introduce app configs with shared auth clients (#1213)
This commit is contained in:
91
packages/backend/src/models/app-auth-client.ts
Normal file
91
packages/backend/src/models/app-auth-client.ts
Normal 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;
|
70
packages/backend/src/models/app-config.ts
Normal file
70
packages/backend/src/models/app-config.ts
Normal 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;
|
@@ -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;
|
||||
|
||||
|
@@ -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',
|
||||
|
Reference in New Issue
Block a user