Compare commits

..

13 Commits

Author SHA1 Message Date
Rıdvan Akca
d3bc3a796b feat(auth): add loading state for user and role management 2023-07-31 17:56:31 +03:00
Ali BARIN
9e64af4793 feat(auth): add user and role management 2023-07-26 18:12:54 +00:00
Ali BARIN
b581f539e2 feat(sso): introduce authentication with SAML 2023-07-18 21:07:31 +00:00
Ali BARIN
aac1295c10 feat(authorization): add update connection checks 2023-07-18 21:06:26 +00:00
Ali BARIN
e8f2802ee0 feat(authorization): add read connection checks 2023-07-18 21:06:22 +00:00
Ali BARIN
75b3730a70 feat(authorization): add delete flow checks 2023-07-18 21:06:18 +00:00
Ali BARIN
af29dc9c3f feat(authorization): add create flow checks 2023-07-18 21:06:08 +00:00
Ali BARIN
181cb5f335 feat(authorization): add delete connection checks 2023-07-18 21:06:04 +00:00
Ali BARIN
94e560c262 feat(authorization): add create connection checks 2023-07-18 21:06:00 +00:00
Ali BARIN
f802061722 feat(authorization): add read execution checks 2023-07-18 21:05:53 +00:00
Ali BARIN
58a7f6eec6 feat(authorization): add update flow checks 2023-07-18 21:03:43 +00:00
Ali BARIN
5e11d3cc4d feat(authorization): add read flow checks 2023-07-18 21:03:29 +00:00
Ali BARIN
399fb8312a feat: introduce role based access control 2023-07-17 23:16:01 +03:00
118 changed files with 747 additions and 2264 deletions

View File

@@ -4,7 +4,7 @@ WORKDIR /automatisch
RUN \
apk --no-cache add --virtual build-dependencies python3 build-base && \
yarn global add @automatisch/cli@0.8.0 --network-timeout 1000000 && \
yarn global add @automatisch/cli@0.7.1 --network-timeout 1000000 && \
rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM automatischio/automatisch:0.8.0
FROM automatischio/automatisch:0.7.1
WORKDIR /automatisch
RUN apk add --no-cache openssl dos2unix

View File

@@ -2,7 +2,7 @@
"packages": [
"packages/*"
],
"version": "0.8.0",
"version": "0.7.1",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {

View File

@@ -3,16 +3,36 @@ import logger from '../../src/helpers/logger';
import client from './client';
import User from '../../src/models/user';
import Role from '../../src/models/role';
import Permission from '../../src/models/permission';
import '../../src/config/orm';
async function fetchAdminRole() {
const role = await Role
.query()
.where({
key: 'admin'
})
.limit(1)
.first();
async function seedPermissionsIfNeeded() {
const existingPermissions = await Permission.query().limit(1).first();
if (!existingPermissions) return;
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
await Permission.query().insert([
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
...getPermission('Execution', ['read']),
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
...getPermission('Role', ['create', 'delete', 'read', 'update']),
...getPermission('User', ['create', 'delete', 'read', 'update']),
])
}
async function createOrFetchRole() {
const role = await Role.query().limit(1).first();
if (!role) {
const createdRole = await Role.query().insertAndFetch({
name: 'Admin',
key: 'admin',
});
return createdRole;
}
return role;
}
@@ -23,7 +43,9 @@ export async function createUser(
) {
const UNIQUE_VIOLATION_CODE = '23505';
const role = await fetchAdminRole();
await seedPermissionsIfNeeded();
const role = await createOrFetchRole();
const userParams = {
email,
password,

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/backend",
"version": "0.8.0",
"version": "0.7.1",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": {
@@ -22,7 +22,7 @@
"prebuild": "rm -rf ./dist"
},
"dependencies": {
"@automatisch/web": "^0.8.0",
"@automatisch/web": "^0.7.1",
"@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@graphql-tools/graphql-file-loader": "^7.3.4",
@@ -53,8 +53,6 @@
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7",
"http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.1",
"jsonwebtoken": "^9.0.0",
"knex": "^2.4.0",
"lodash.get": "^4.4.2",
@@ -108,7 +106,7 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@automatisch/types": "^0.8.0",
"@automatisch/types": "^0.7.1",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8",
"@types/cors": "^2.8.12",

View File

@@ -6,7 +6,7 @@ import actions from './actions';
import dynamicData from './dynamic-data';
export default defineApp({
name: 'GitHub',
name: 'Github',
key: 'github',
baseUrl: 'https://github.com',
apiBaseUrl: 'https://api.github.com',

View File

@@ -9,11 +9,11 @@ export default {
// ref:
// - https://docs.gitlab.com/ee/api/projects.html#list-all-projects
// - https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination
const firstPageRequest = $.http.get('/api/v4/projects', {
params: {
simple: true,
pagination: 'keyset',
membership: true,
order_by: 'id',
sort: 'asc',
},

View File

@@ -6,7 +6,7 @@ import triggers from './triggers';
import dynamicData from './dynamic-data';
export default defineApp({
name: 'GitLab',
name: 'Gitlab',
key: 'gitlab',
baseUrl: 'https://gitlab.com',
apiBaseUrl: 'https://gitlab.com',

View File

@@ -1,191 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type THeaders = {
__id: string;
header: string;
}[];
type TSheetsResponse = {
sheets: {
properties: {
sheetId: string;
title: string;
};
}[];
};
type TBody = {
requests: IJSONObject[];
};
export default defineAction({
name: 'Create worksheet',
key: 'createWorksheet',
description:
'Create a blank worksheet with a title. Optionally, provide headers.',
arguments: [
{
label: 'Drive',
key: 'driveId',
type: 'dropdown' as const,
required: false,
description:
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDrives',
},
],
},
},
{
label: 'Spreadsheet',
key: 'spreadsheetId',
type: 'dropdown' as const,
required: true,
dependsOn: ['parameters.driveId'],
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listSpreadsheets',
},
{
name: 'parameters.driveId',
value: '{parameters.driveId}',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string' as const,
required: true,
description: '',
variables: true,
},
{
label: 'Headers',
key: 'headers',
type: 'dynamic' as const,
required: false,
fields: [
{
label: 'Header',
key: 'header',
type: 'string' as const,
required: true,
variables: true,
},
],
},
{
label: 'Overwrite',
key: 'overwrite',
type: 'dropdown' as const,
required: false,
value: false,
description:
'If a worksheet with the specified title exists, its content would be lost. Please, use with caution.',
variables: true,
options: [
{
label: 'Yes',
value: 'true',
},
{
label: 'No',
value: 'false',
},
],
},
],
async run($) {
const {
data: { sheets },
} = await $.http.get<TSheetsResponse>(
`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`
);
const selectedSheet = sheets.find(
(sheet) => sheet.properties.title === $.step.parameters.title
);
const headers = $.step.parameters.headers as THeaders;
const values = headers.map((entry) => entry.header);
const body: TBody = {
requests: [
{
addSheet: {
properties: {
title: $.step.parameters.title,
},
},
},
],
};
if ($.step.parameters.overwrite === 'true' && selectedSheet) {
body.requests.unshift({
deleteSheet: {
sheetId: selectedSheet.properties.sheetId,
},
});
}
const { data } = await $.http.post(
`https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`,
body
);
if (values.length) {
const body = {
requests: [
{
updateCells: {
rows: [
{
values: values.map((header) => ({
userEnteredValue: { stringValue: header },
})),
},
],
fields: '*',
start: {
sheetId:
data.replies[data.replies.length - 1].addSheet.properties
.sheetId,
rowIndex: 0,
columnIndex: 0,
},
},
},
],
};
const { data: response } = await $.http.post(
`https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`,
body
);
$.setActionItem({
raw: response,
});
return;
}
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,5 +1,4 @@
import createSpreadsheet from './create-spreadsheet';
import createSpreadsheetRow from './create-spreadsheet-row';
import createWorksheet from './create-worksheet';
export default [createSpreadsheet, createSpreadsheetRow, createWorksheet];
export default [createSpreadsheet, createSpreadsheetRow];

View File

@@ -1,100 +0,0 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
parent: IJSONObject;
properties: IJSONObject;
children: IJSONArray;
};
export default defineAction({
name: 'Create database item',
key: 'createDatabaseItem',
description: 'Creates an item in a database.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string' as const,
required: false,
description:
'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const name = $.step.parameters.name as string;
const truncatedName = name.slice(0, 2000);
const content = $.step.parameters.content as string;
const truncatedContent = content.slice(0, 2000);
const body: TBody = {
parent: {
database_id: $.step.parameters.databaseId,
},
properties: {},
children: [],
};
if (name) {
body.properties.Name = {
title: [
{
text: {
content: truncatedName,
},
},
],
};
}
if (content) {
body.children = [
{
object: 'block',
paragraph: {
rich_text: [
{
text: {
content: truncatedContent,
},
},
],
},
},
];
}
const { data } = await $.http.post('/v1/pages', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,104 +0,0 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
parent: IJSONObject;
properties: IJSONObject;
children: IJSONArray;
};
export default defineAction({
name: 'Create page',
key: 'createPage',
description: 'Creates a page inside a parent page',
arguments: [
{
label: 'Parent page',
key: 'parentPageId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listParentPages',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string' as const,
required: false,
description:
'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const parentPageId = $.step.parameters.parentPageId as string;
const title = $.step.parameters.title as string;
const truncatedTitle = title.slice(0, 2000);
const content = $.step.parameters.content as string;
const truncatedContent = content.slice(0, 2000);
const body: TBody = {
parent: {
page_id: parentPageId,
},
properties: {},
children: [],
};
if (title) {
body.properties.title = {
type: 'title',
title: [
{
text: {
content: truncatedTitle,
},
},
],
};
}
if (content) {
body.children = [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: truncatedContent,
},
},
],
},
},
];
}
const { data } = await $.http.post('/v1/pages', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,70 +0,0 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
filter: IJSONObject;
sorts: IJSONArray;
};
export default defineAction({
name: 'Find database item',
key: 'findDatabaseItem',
description: 'Searches for an item in a database by property.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const databaseId = $.step.parameters.databaseId as string;
const name = $.step.parameters.name as string;
const truncatedName = name.slice(0, 2000);
const body: TBody = {
filter: {
property: 'Name',
rich_text: {
equals: truncatedName,
},
},
sorts: [
{
timestamp: 'last_edited_time',
direction: 'descending',
},
],
};
const { data } = await $.http.post(
`/v1/databases/${databaseId}/query`,
body
);
$.setActionItem({
raw: data.results[0],
});
},
});

View File

@@ -1,5 +0,0 @@
import createDatabaseItem from './create-database-item';
import createPage from './create-page';
import findDatabaseItem from './find-database-item';
export default [createDatabaseItem, createPage, findDatabaseItem];

View File

@@ -1,4 +1,3 @@
import listDatabases from './list-databases';
import listParentPages from './list-parent-pages';
export default [listDatabases, listParentPages];
export default [listDatabases];

View File

@@ -1,70 +0,0 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type Page = {
id: string;
properties: {
title: {
title: [
{
plain_text: string;
}
];
};
};
parent: {
workspace: boolean;
};
};
type ResponseData = {
results: Page[];
next_cursor?: string;
};
type Payload = {
filter: {
value: 'page';
property: 'object';
};
start_cursor?: string;
};
export default {
name: 'List parent pages',
key: 'listParentPages',
async run($: IGlobalVariable) {
const parentPages: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const payload: Payload = {
filter: {
value: 'page',
property: 'object',
},
};
do {
const response = await $.http.post<ResponseData>('/v1/search', payload);
payload.start_cursor = response.data.next_cursor;
const topLevelPages = response.data.results.filter(
(page) => page.parent.workspace
);
for (const pages of topLevelPages) {
parentPages.data.push({
value: pages.id as string,
name: pages.properties.title.title[0].plain_text as string,
});
}
} while (payload.start_cursor);
return parentPages;
},
};

View File

@@ -3,7 +3,6 @@ import addAuthHeader from './common/add-auth-header';
import addNotionVersionHeader from './common/add-notion-version-header';
import auth from './auth';
import triggers from './triggers';
import actions from './actions';
import dynamicData from './dynamic-data';
export default defineApp({
@@ -15,9 +14,11 @@ export default defineApp({
authDocUrl: 'https://automatisch.io/docs/apps/notion/connection',
primaryColor: '000000',
supportsConnections: true,
beforeRequest: [addAuthHeader, addNotionVersionHeader],
beforeRequest: [
addAuthHeader,
addNotionVersionHeader,
],
auth,
triggers,
actions,
dynamicData,
});

View File

@@ -16,27 +16,13 @@ export async function up(knex: Knex): Promise<void> {
.select('role')
.groupBy('role');
let shouldCreateAdminRole = true;
for (const { role } of uniqueUserRoles) {
// skip empty roles
if (!role) continue;
const lowerCaseRole = lowerCase(role);
if (lowerCaseRole === 'admin') {
shouldCreateAdminRole = false;
}
await knex('roles').insert({
name: capitalize(role),
key: lowerCaseRole,
});
}
if (shouldCreateAdminRole) {
await knex('roles').insert({
name: 'Admin',
key: 'admin',
key: lowerCase(role),
});
}
}

View File

@@ -1,46 +1,23 @@
import { Knex } from 'knex';
const getPermissionForRole = (roleId: string, subject: string, actions: string[], conditions: string[] = []) => actions
.map(action => ({
role_id: roleId,
subject,
action,
conditions,
}));
const getPermission = (subject: string, actions: string[]) => actions.map(action => ({ subject, action }));
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('permissions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('role_id').references('id').inTable('roles');
table.string('action').notNullable();
table.string('subject').notNullable();
table.jsonb('conditions').notNullable().defaultTo([]);
table.timestamps(true, true);
});
const roles = await knex('roles').select(['id', 'key']) as { id: string, key: string }[];
for (const role of roles) {
// `admin` role should have no conditions unlike others by default
const isAdmin = role.key === 'admin';
const roleConditions = isAdmin ? [] : ['isCreator'];
// default permissions
await knex('permissions').insert([
...getPermissionForRole(role.id, 'Connection', ['create', 'read', 'delete', 'update'], roleConditions),
...getPermissionForRole(role.id, 'Execution', ['read'], roleConditions),
...getPermissionForRole(role.id, 'Flow', ['create', 'delete', 'publish', 'read', 'update'], roleConditions),
]);
// admin specific permission
if (isAdmin) {
await knex('permissions').insert([
...getPermissionForRole(role.id, 'User', ['create', 'read', 'delete', 'update']),
...getPermissionForRole(role.id, 'Role', ['create', 'read', 'delete', 'update']),
]);
}
}
await knex('permissions').insert([
...getPermission('Connection', ['create', 'read', 'delete', 'update']),
...getPermission('Execution', ['read']),
...getPermission('Flow', ['create', 'delete', 'publish', 'read', 'update']),
...getPermission('Role', ['create', 'delete', 'read', 'update']),
...getPermission('User', ['create', 'delete', 'read', 'update']),
]);
}
export async function down(knex: Knex): Promise<void> {

View File

@@ -0,0 +1,25 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('roles_permissions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('role_id').references('id').inTable('roles');
table.uuid('permission_id').references('id').inTable('permissions');
});
const roles = await knex('roles').select('id');
const permissions = await knex('permissions').select('id');
for (const role of roles) {
for (const permission of permissions) {
await knex('roles_permissions').insert({
role_id: role.id,
permission_id: permission.id,
});
}
}
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('roles_permissions');
}

View File

@@ -13,7 +13,6 @@ export async function up(knex: Knex): Promise<void> {
table.text('email_attribute_name').notNullable();
table.text('role_attribute_name').notNullable();
table.uuid('default_role_id').references('id').inTable('roles');
table.boolean('active').defaultTo(false);
table.timestamps(true, true);
});

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.alterTable('permissions', (table) => {
table.jsonb('conditions').notNullable().defaultTo([]);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.alterTable('permissions', (table) => {
table.dropColumn('conditions');
});
}

View File

@@ -1,33 +0,0 @@
import { Knex } from 'knex';
const getPermissionForRole = (
roleId: string,
subject: string,
actions: string[]
) =>
actions.map((action) => ({
role_id: roleId,
subject,
action,
conditions: [],
}));
export async function up(knex: Knex): Promise<void> {
const role = (await knex('roles')
.first(['id', 'key'])
.where({ key: 'admin' })
.limit(1)) as { id: string; key: string };
await knex('permissions').insert(
getPermissionForRole(role.id, 'SamlAuthProvider', [
'create',
'read',
'delete',
'update',
])
);
}
export async function down(knex: Knex): Promise<void> {
await knex('permissions').where({ subject: 'SamlAuthProvider' }).delete();
}

View File

@@ -25,8 +25,6 @@ import updateRole from './mutations/update-role.ee';
import updateStep from './mutations/update-step';
import updateUser from './mutations/update-user.ee';
import verifyConnection from './mutations/verify-connection';
import createSamlAuthProvider from './mutations/create-saml-auth-provider.ee';
import updateSamlAuthProvider from './mutations/update-saml-auth-provider.ee';
const mutationResolvers = {
createConnection,
@@ -56,8 +54,6 @@ const mutationResolvers = {
updateRole,
updateStep,
verifyConnection,
createSamlAuthProvider,
updateSamlAuthProvider,
};
export default mutationResolvers;

View File

@@ -1,7 +1,6 @@
import kebabCase from 'lodash/kebabCase';
import Permission from '../../models/permission';
import Role from '../../models/role';
import Context from '../../types/express/context';
import Permission from '../../models/permission';
type Params = {
input: {
@@ -11,9 +10,8 @@ type Params = {
};
};
const createRole = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('create', 'Role');
// TODO: access
const createRole = async (_parent: unknown, params: Params) => {
const { name, description, permissions } = params.input;
const key = kebabCase(name);

View File

@@ -1,54 +0,0 @@
import type { SamlConfig } from '@node-saml/passport-saml';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
issuer: string;
entryPoint: string;
firstnameAttributeName: string;
surnameAttributeName: string;
emailAttributeName: string;
roleAttributeName: string;
defaultRoleId: string;
active: boolean;
};
};
const createSamlAuthProvider = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('create', 'SamlAuthProvider');
const samlAuthProviderPayload: Partial<SamlAuthProvider> = {
...params.input,
};
const existingSamlAuthProvider = await SamlAuthProvider.query()
.limit(1)
.first();
let samlAuthProvider: SamlAuthProvider;
if (!existingSamlAuthProvider) {
samlAuthProvider = await SamlAuthProvider.query().insert(
samlAuthProviderPayload
);
return samlAuthProvider;
}
samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
existingSamlAuthProvider.id,
samlAuthProviderPayload
);
return samlAuthProvider;
};
export default createSamlAuthProvider;

View File

@@ -1,22 +1,17 @@
import User from '../../models/user';
import Role from '../../models/role';
import Context from '../../types/express/context';
type Params = {
input: {
fullName: string;
email: string;
password: string;
role: {
id: string;
};
roleId: string;
};
};
const createUser = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('create', 'User');
const { fullName, email, password } = params.input;
// TODO: access
const createUser = async (_parent: unknown, params: Params) => {
const { fullName, email, password, roleId } = params.input;
const existingUser = await User.query().findOne({ email });
@@ -24,23 +19,12 @@ const createUser = async (_parent: unknown, params: Params, context: Context) =>
throw new Error('User already exists!');
}
const userPayload: Partial<User> = {
const user = await User.query().insert({
fullName,
email,
password,
};
try {
context.currentUser.can('update', 'Role');
userPayload.roleId = params.input.role.id;
} catch {
// void
const role = await Role.query().findOne({ key: 'user' });
userPayload.roleId = role.id;
}
const user = await User.query().insert(userPayload);
roleId,
});
return user;
};

View File

@@ -2,7 +2,8 @@ import { Duration } from 'luxon';
import Context from '../../types/express/context';
import deleteUserQueue from '../../queues/delete-user.ee';
const deleteCurrentUser = async (_parent: unknown, params: never, context: Context) => {
// TODO: access
const deleteUser = async (_parent: unknown, params: never, context: Context) => {
const id = context.currentUser.id;
await context.currentUser.$query().delete();
@@ -19,4 +20,4 @@ const deleteCurrentUser = async (_parent: unknown, params: never, context: Conte
return true;
};
export default deleteCurrentUser;
export default deleteUser;

View File

@@ -1,5 +1,4 @@
import Role from '../../models/role';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
@@ -8,37 +7,22 @@ type Params = {
};
};
// TODO: access
const deleteRole = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'Role');
const role = await Role.query().findById(params.input.id).throwIfNotFound();
const count = await role.$relatedQuery('users').resultSize();
if (count > 0) {
throw new Error('All users must be migrated away from the role!');
}
if (role.isAdmin) {
throw new Error('Admin role cannot be deleted!');
}
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query()
.where({ default_role_id: role.id })
.limit(1)
.first();
if (samlAuthProviderUsingDefaultRole) {
throw new Error(
'You need to change the default role in the SAML configuration before deleting this role.'
);
}
// delete permissions first
await role.$relatedQuery('permissions').delete();
/**
* TODO: consider migrations for users that still have the role or
* do not let the role get deleted if there are still associated users
*/
await role.$query().delete();
return true;

View File

@@ -9,13 +9,12 @@ type Params = {
};
};
// TODO: access
const deleteUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('delete', 'User');
const id = params.input.id;
await User.query().deleteById(id);

View File

@@ -13,7 +13,7 @@ const TOKEN_EXPIRES_IN = '14d';
const login = async (_parent: unknown, params: Params) => {
const user = await User.query().findOne({
email: params.input.email.toLowerCase(),
email: params.input.email,
});
if (user && (await user.login(params.input.password))) {

View File

@@ -1,7 +1,6 @@
import Context from '../../types/express/context';
import Role from '../../models/role';
import Permission from '../../models/permission';
import permissionCatalog from '../../helpers/permission-catalog.ee';
type Params = {
input: {
@@ -12,13 +11,12 @@ type Params = {
};
};
const updateRole = async (
// TODO: access
const updateUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'Role');
const {
id,
name,
@@ -26,66 +24,21 @@ const updateRole = async (
permissions,
} = params.input;
const role = await Role
.query()
.findById(id)
.throwIfNotFound();
const role = await Role.query().findById(id).throwIfNotFound();
try {
const updatedRole = await Role.transaction(async (trx) => {
await role.$relatedQuery('permissions', trx).delete();
// TODO: delete the unrelated items!
await role.$relatedQuery('permissions').unrelate();
if (permissions?.length) {
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);
})
return validSubject && validConditions;
})
.map((permission) => ({
...permission,
roleId: role.id,
}));
await Permission.query().insert(sanitizedPermissions);
// TODO: possibly assert that given permissions do actually exist in catalog
// TODO: possibly optimize it with patching the different permissions compared to current ones
return await role.$query()
.patchAndFetch(
{
name,
description,
permissions,
}
await role
.$query(trx)
.patch(
{
name,
description,
}
);
return await Role
.query(trx)
.leftJoinRelated({
permissions: true
})
.withGraphFetched({
permissions: true
})
.findById(id);
});
return updatedRole;
} catch (err) {
throw new Error('The role could not be updated!');
}
);
};
export default updateRole;
export default updateUser;

View File

@@ -1,45 +0,0 @@
import type { SamlConfig } from '@node-saml/passport-saml';
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
import Context from '../../types/express/context';
type Params = {
input: {
name: string;
certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
issuer: string;
entryPoint: string;
firstnameAttributeName: string;
surnameAttributeName: string;
emailAttributeName: string;
roleAttributeName: string;
defaultRoleId: string;
active: boolean;
};
};
const updateSamlAuthProvider = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'SamlAuthProvider');
const samlAuthProviderPayload: Partial<SamlAuthProvider> = {
...params.input,
};
const existingSamlAuthProvider = await SamlAuthProvider.query()
.limit(1)
.first()
.throwIfNotFound();
const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById(
existingSamlAuthProvider.id,
samlAuthProviderPayload
);
return samlAuthProvider;
};
export default updateSamlAuthProvider;

View File

@@ -12,30 +12,21 @@ type Params = {
};
};
// TODO: access
const updateUser = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'User');
const userPayload: Partial<User> = {
email: params.input.email,
fullName: params.input.fullName,
};
try {
context.currentUser.can('update', 'Role');
userPayload.roleId = params.input.role.id;
} catch {
// void
}
const user = await User.query().patchAndFetchById(
params.input.id,
userPayload
);
const user = await User.query()
.patchAndFetchById(
params.input.id,
{
email: params.input.email,
fullName: params.input.fullName,
roleId: params.input.role.id,
}
);
return user;
};

View File

@@ -1,5 +1,4 @@
import App from '../../models/app';
import Connection from '../../models/connection';
import Context from '../../types/express/context';
type Params = {
@@ -7,17 +6,13 @@ type Params = {
};
const getApp = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
context.currentUser.can('read', 'Connection');
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await connectionBaseQuery
.clone()
const connections = await context.currentUser
.$relatedQuery('connections')
.select('connections.*')
.fullOuterJoinRelated('steps')
.where({

View File

@@ -1,8 +1,6 @@
import { IConnection } from '@automatisch/types';
import App from '../../models/app';
import Context from '../../types/express/context';
import Flow from '../../models/flow';
import Connection from '../../models/connection';
type Params = {
name: string;
@@ -13,27 +11,19 @@ const getConnectedApps = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const flowBaseQuery = conditions.isCreator ? userFlows : allFlows;
context.currentUser.can('read', 'Connection');
let apps = await App.findAll(params.name);
const connections = await connectionBaseQuery
.clone()
const connections = await context.currentUser
.$relatedQuery('connections')
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')
.groupBy('connections.key');
const flows = await flowBaseQuery
.clone()
const flows = await context.currentUser
.$relatedQuery('flows')
.withGraphJoined('steps')
.orderBy('created_at', 'desc');

View File

@@ -1,7 +1,6 @@
import { IDynamicData, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context';
import App from '../../models/app';
import Step from '../../models/step';
import ExecutionStep from '../../models/execution-step';
import globalVariable from '../../helpers/global-variable';
import computeParameters from '../../helpers/compute-parameters';
@@ -17,13 +16,10 @@ const getDynamicData = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
context.currentUser.can('update', 'Flow');
const step = await stepBaseQuery
.clone()
const step = await context.currentUser
.$relatedQuery('steps')
.withGraphFetched({
connection: true,
flow: true,

View File

@@ -1,7 +1,6 @@
import { IDynamicFields, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context';
import App from '../../models/app';
import Step from '../../models/step';
import globalVariable from '../../helpers/global-variable';
type Params = {
@@ -15,13 +14,10 @@ const getDynamicFields = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
context.currentUser.can('update', 'Flow');
const step = await stepBaseQuery
.clone()
const step = await context.currentUser
.$relatedQuery('steps')
.withGraphFetched({
connection: true,
flow: true,

View File

@@ -1,6 +1,5 @@
import Context from '../../types/express/context';
import paginate from '../../helpers/pagination';
import Execution from '../../models/execution';
type Params = {
executionId: string;
@@ -13,13 +12,10 @@ const getExecutionSteps = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('read', 'Execution');
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
context.currentUser.can('read', 'Execution');
const execution = await executionBaseQuery
.clone()
const execution = await context.currentUser
.$relatedQuery('executions')
.withSoftDeleted()
.findById(params.executionId)
.throwIfNotFound();

View File

@@ -1,5 +1,4 @@
import Context from '../../types/express/context';
import Execution from '../../models/execution';
type Params = {
executionId: string;
@@ -10,13 +9,10 @@ const getExecution = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('read', 'Execution');
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
context.currentUser.can('read', 'Execution');
const execution = await executionBaseQuery
.clone()
const execution = await context.currentUser
.$relatedQuery('executions')
.withGraphFetched({
flow: {
steps: true,

View File

@@ -1,6 +1,5 @@
import { raw } from 'objection';
import Context from '../../types/express/context';
import Execution from '../../models/execution';
import paginate from '../../helpers/pagination';
type Params = {
@@ -13,11 +12,7 @@ const getExecutions = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('read', 'Execution');
const userExecutions = context.currentUser.$relatedQuery('executions');
const allExecutions = Execution.query();
const executionBaseQuery = conditions.isCreator ? userExecutions : allExecutions;
context.currentUser.can('read', 'Execution');
const selectStatusStatement = `
case
@@ -28,8 +23,8 @@ const getExecutions = async (
as status
`;
const executions = executionBaseQuery
.clone()
const executions = context.currentUser
.$relatedQuery('executions')
.joinRelated('executionSteps as execution_steps')
.select('executions.*', raw(selectStatusStatement))
.withSoftDeleted()

View File

@@ -1,18 +1,14 @@
import Context from '../../types/express/context';
import Flow from '../../models/flow';
type Params = {
id: string;
};
const getFlow = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
context.currentUser.can('read', 'Flow');
const flow = await baseQuery
.clone()
const flow = await context.currentUser
.$relatedQuery('flows')
.withGraphJoined('[steps.[connection]]')
.orderBy('steps.position', 'asc')
.findOne({ 'flows.id': params.id })

View File

@@ -1,4 +1,3 @@
import Flow from '../../models/flow';
import Context from '../../types/express/context';
import paginate from '../../helpers/pagination';
@@ -11,13 +10,10 @@ type Params = {
};
const getFlows = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Flow');
const userFlows = context.currentUser.$relatedQuery('flows');
const allFlows = Flow.query();
const baseQuery = conditions.isCreator ? userFlows : allFlows;
context.currentUser.can('read', 'Flow');
const flowsQuery = baseQuery
.clone()
const flowsQuery = context.currentUser
.$relatedQuery('flows')
.joinRelated({
steps: true,
})

View File

@@ -1,7 +0,0 @@
import permissionCatalog from '../../helpers/permission-catalog.ee';
const getPermissionCatalog = async () => {
return permissionCatalog;
};
export default getPermissionCatalog;

View File

@@ -0,0 +1,76 @@
const getPermissions = async () => {
const Connection = {
label: 'Connection',
key: 'Connection',
};
const Flow = {
label: 'Flow',
key: 'Flow',
};
const Execution = {
label: 'Execution',
key: 'Execution',
};
const permissions = {
conditions: [
{
key: 'isCreator',
label: 'Is creator'
}
],
actions: [
{
label: 'Create',
action: 'create',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Read',
action: 'read',
subjects: [
Connection.key,
Execution.key,
Flow.key,
]
},
{
label: 'Update',
action: 'update',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Delete',
action: 'delete',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Publish',
action: 'publish',
subjects: [
Flow.key,
]
}
],
subjects: [
Connection,
Flow,
Execution
]
};
return permissions;
};
export default getPermissions;

View File

@@ -5,19 +5,9 @@ type Params = {
id: string
};
// TODO: access
const getRole = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'Role');
return await Role
.query()
.leftJoinRelated({
permissions: true
})
.withGraphFetched({
permissions: true
})
.findById(params.id)
.throwIfNotFound();
return await Role.query().findById(params.id).throwIfNotFound();
};
export default getRole;

View File

@@ -1,10 +1,9 @@
import Context from '../../types/express/context';
import Role from '../../models/role';
// TODO: access
const getRoles = async (_parent: unknown, params: unknown, context: Context) => {
context.currentUser.can('read', 'Role');
return await Role.query().orderBy('name');
return await Role.query();
};
export default getRoles;

View File

@@ -1,7 +1,7 @@
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
const getSamlAuthProviders = async () => {
const providers = await SamlAuthProvider.query().where({ active: true });
const providers = await SamlAuthProvider.query();
return providers;
};

View File

@@ -1,7 +1,6 @@
import { ref } from 'objection';
import ExecutionStep from '../../models/execution-step';
import Step from '../../models/step';
import Context from '../../types/express/context';
import ExecutionStep from '../../models/execution-step';
import { ref } from 'objection';
type Params = {
stepId: string;
@@ -12,18 +11,15 @@ const getStepWithTestExecutions = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Flow');
const userSteps = context.currentUser.$relatedQuery('steps');
const allSteps = Step.query();
const stepBaseQuery = conditions.isCreator ? userSteps : allSteps;
context.currentUser.can('update', 'Flow');
const step = await stepBaseQuery
.clone()
const step = await context.currentUser
.$relatedQuery('steps')
.findOne({ 'steps.id': params.stepId })
.throwIfNotFound();
const previousStepsWithCurrentStep = await stepBaseQuery
.clone()
const previousStepsWithCurrentStep = await context.currentUser
.$relatedQuery('steps')
.withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position)

View File

@@ -5,9 +5,8 @@ type Params = {
id: string
};
// TODO: access
const getUser = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'User');
return await User
.query()
.leftJoinRelated({

View File

@@ -7,9 +7,8 @@ type Params = {
offset: number;
};
// TODO: access
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('read', 'User');
const usersQuery = User
.query()
.leftJoinRelated({

View File

@@ -1,6 +1,5 @@
import Context from '../../types/express/context';
import App from '../../models/app';
import Connection from '../../models/connection';
import globalVariable from '../../helpers/global-variable';
type Params = {
@@ -13,13 +12,10 @@ const testConnection = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
context.currentUser.can('update', 'Connection');
let connection = await connectionBaseQuery
.clone()
let connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.id,
})

View File

@@ -16,7 +16,7 @@ import getUsers from './queries/get-users';
import getInvoices from './queries/get-invoices.ee';
import getPaddleInfo from './queries/get-paddle-info.ee';
import getPaymentPlans from './queries/get-payment-plans.ee';
import getPermissionCatalog from './queries/get-permission-catalog.ee';
import getPermissions from './queries/get-permissions.ee';
import getRole from './queries/get-role.ee';
import getRoles from './queries/get-roles.ee';
import getSamlAuthProviders from './queries/get-saml-auth-providers.ee';
@@ -43,7 +43,7 @@ const queryResolvers = {
getInvoices,
getPaddleInfo,
getPaymentPlans,
getPermissionCatalog,
getPermissions,
getRole,
getRoles,
getSamlAuthProviders,

View File

@@ -42,11 +42,14 @@ type Query {
getTrialStatus: GetTrialStatus
getSubscriptionStatus: GetSubscriptionStatus
getSamlAuthProviders: [GetSamlAuthProviders]
getUsers(limit: Int!, offset: Int!): UserConnection
getUsers(
limit: Int!
offset: Int!
): UserConnection
getUser(id: String!): User
getRoles: [Role]
getRole(id: String!): Role
getPermissionCatalog: PermissionCatalog
getPermissions: Permissions
healthcheck: AppHealth
}
@@ -78,8 +81,6 @@ type Mutation {
updateStep(input: UpdateStepInput): Step
updateUser(input: UpdateUserInput): User
verifyConnection(input: VerifyConnectionInput): Connection
createSamlAuthProvider(input: CreateSamlAuthProviderInput): SamlAuthProvider
updateSamlAuthProvider(input: UpdateSamlAuthProviderInput): SamlAuthProvider
}
"""
@@ -291,20 +292,6 @@ type Execution {
flow: Flow
}
type SamlAuthProvider {
id: String
name: String
certificate: String
signatureAlgorithm: String
issuer: String
entryPoint: String
firstnameAttributeName: String
surnameAttributeName: String
emailAttributeName: String
roleAttributeName: String
active: Boolean
}
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo
@@ -336,34 +323,6 @@ input VerifyConnectionInput {
id: String!
}
input CreateSamlAuthProviderInput {
name: String!
certificate: String!
signatureAlgorithm: String!
issuer: String!
entryPoint: String!
firstnameAttributeName: String!
surnameAttributeName: String!
emailAttributeName: String!
roleAttributeName: String!
defaultRoleId: String!
active: Boolean!
}
input UpdateSamlAuthProviderInput {
name: String!
certificate: String!
signatureAlgorithm: String!
issuer: String!
entryPoint: String!
firstnameAttributeName: String!
surnameAttributeName: String!
emailAttributeName: String!
roleAttributeName: String!
defaultRoleId: String!
active: Boolean!
}
input DeleteConnectionInput {
id: String!
}
@@ -681,13 +640,13 @@ type GetSamlAuthProviders {
}
type Permission {
id: String
action: String
subject: String
conditions: [String]
}
type PermissionCatalog {
# TODO: emphasize it's a catalog item
type Permissions {
actions: [Action]
subjects: [Subject]
conditions: [Condition]
@@ -695,7 +654,7 @@ type PermissionCatalog {
type Action {
label: String
key: String
action: String
subjects: [String]
}

View File

@@ -1,24 +0,0 @@
import axios, { AxiosRequestConfig } from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
const config: AxiosRequestConfig = {};
const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
if (supportsProxy) {
if (httpProxyUrl) {
config.httpAgent = new HttpProxyAgent(process.env.http_proxy);
}
if (httpsProxyUrl) {
config.httpsAgent = new HttpsProxyAgent(process.env.https_proxy);
}
config.proxy = false;
}
const axiosWithProxyInstance = axios.create(config);
export default axiosWithProxyInstance;

View File

@@ -1,4 +1,3 @@
// TODO: replace with axios-with-proxy when needed
import axios from 'axios';
import appConfig from '../../config/app';
import { DateTime } from 'luxon';

View File

@@ -1,4 +1,3 @@
// TODO: replace with axios-with-proxy
import axios from 'axios';
import appConfig from '../config/app';
import memoryCache from 'memory-cache';

View File

@@ -3,7 +3,7 @@ import ExecutionStep from '../models/execution-step';
import get from 'lodash.get';
// INFO: don't remove space in allowed character group!
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ :]+)+}})/g;
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ ]+)+}})/g;
export default function computeParameters(
parameters: Step['parameters'],
@@ -42,7 +42,7 @@ export default function computeParameters(
if (Array.isArray(value)) {
return {
...result,
[key]: value.map((item) => computeParameters(item, executionSteps)),
[key]: value.map(item => computeParameters(item, executionSteps)),
};
}

View File

@@ -1,10 +1,8 @@
import { IHttpClientParams } from '@automatisch/types';
import { AxiosRequestConfig } from 'axios';
import { URL } from 'node:url';
import axios, { AxiosRequestConfig } from 'axios';
export { AxiosInstance as IHttpClient } from 'axios';
import { IHttpClientParams } from '@automatisch/types';
import { URL } from 'url';
import HttpError from '../../errors/http';
import axios from '../axios-with-proxy';
const removeBaseUrlForAbsoluteUrls = (
requestConfig: AxiosRequestConfig

View File

@@ -1,72 +0,0 @@
const Connection = {
label: 'Connection',
key: 'Connection',
};
const Flow = {
label: 'Flow',
key: 'Flow',
};
const Execution = {
label: 'Execution',
key: 'Execution',
};
const permissionCatalog = {
conditions: [
{
key: 'isCreator',
label: 'Is creator'
}
],
actions: [
{
label: 'Create',
key: 'create',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Read',
key: 'read',
subjects: [
Connection.key,
Execution.key,
Flow.key,
]
},
{
label: 'Update',
key: 'update',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Delete',
key: 'delete',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Publish',
key: 'publish',
subjects: [
Flow.key,
]
}
],
subjects: [
Connection,
Flow,
Execution
]
};
export default permissionCatalog;

View File

@@ -1,20 +0,0 @@
import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
import type User from '../models/user'
// Must be kept in sync with `packages/web/src/helpers/userAbility.ts`!
export default function userAbility(user: Partial<User>) {
const permissions = user?.permissions;
const role = user?.role;
// We're not using mongo, but our fields, conditions match
const options = {
conditionsMatcher: mongoQueryMatcher,
fieldMatcher: fieldPatternMatcher
};
if (!role || !permissions) {
return new PureAbility([], options);
}
return new PureAbility<[string, string], string[]>(permissions, options);
}

View File

@@ -2,7 +2,6 @@ import Base from './base';
class Permission extends Base {
id: string;
roleId: string;
action: string;
subject: string;
conditions: string[];
@@ -11,11 +10,10 @@ class Permission extends Base {
static jsonSchema = {
type: 'object',
required: ['roleId', 'action', 'subject'],
required: ['action', 'subject'],
properties: {
id: { type: 'string', format: 'uuid' },
roleId: { type: 'string', format: 'uuid' },
action: { type: 'string', minLength: 1 },
subject: { type: 'string', minLength: 1 },
conditions: { type: 'array', items: { type: 'string' } },

View File

@@ -20,7 +20,7 @@ class Role extends Base {
id: { type: 'string', format: 'uuid' },
name: { type: 'string', minLength: 1 },
key: { type: 'string', minLength: 1 },
description: { type: ['string', 'null'], maxLength: 255 },
description: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
@@ -40,11 +40,15 @@ class Role extends Base {
},
},
permissions: {
relation: Base.HasManyRelation,
relation: Base.ManyToManyRelation,
modelClass: Permission,
join: {
from: 'roles.id',
to: 'permissions.role_id',
through: {
from: 'roles_permissions.role_id',
to: 'roles_permissions.permission_id',
},
to: 'permissions.id',
},
},
});

View File

@@ -8,7 +8,7 @@ class SamlAuthProvider extends Base {
id!: string;
name: string;
certificate: string;
signatureAlgorithm: SamlConfig['signatureAlgorithm'];
signatureAlgorithm: SamlConfig["signatureAlgorithm"];
issuer: string;
entryPoint: string;
firstnameAttributeName: string;
@@ -16,7 +16,6 @@ class SamlAuthProvider extends Base {
emailAttributeName: string;
roleAttributeName: string;
defaultRoleId: string;
active: boolean;
static tableName = 'saml_auth_providers';
@@ -39,18 +38,14 @@ class SamlAuthProvider extends Base {
id: { type: 'string', format: 'uuid' },
name: { type: 'string', minLength: 1 },
certificate: { type: 'string', minLength: 1 },
signatureAlgorithm: {
type: 'string',
enum: ['sha1', 'sha256', 'sha512'],
},
signatureAlgorithm: { type: 'string', enum: ['sha1', 'sha256', 'sha512'] },
issuer: { type: 'string', minLength: 1 },
entryPoint: { type: 'string', minLength: 1 },
firstnameAttributeName: { type: 'string', minLength: 1 },
surnameAttributeName: { type: 'string', minLength: 1 },
emailAttributeName: { type: 'string', minLength: 1 },
roleAttributeName: { type: 'string', minLength: 1 },
defaultRoleId: { type: 'string', format: 'uuid' },
active: { type: 'boolean' },
defaultRoleId: { type: 'string', format: 'uuid' }
},
};
@@ -77,7 +72,7 @@ class SamlAuthProvider extends Base {
entryPoint: this.entryPoint,
issuer: this.issuer,
signatureAlgorithm: this.signatureAlgorithm,
};
}
}
}

View File

@@ -1,22 +1,22 @@
import crypto from 'node:crypto';
import { QueryContext, ModelOptions } from 'objection';
import bcrypt from 'bcrypt';
import { DateTime } from 'luxon';
import crypto from 'node:crypto';
import { ModelOptions, QueryContext } from 'objection';
import { PureAbility, fieldPatternMatcher, mongoQueryMatcher } from '@casl/ability';
import type { Subject } from '@casl/ability';
import appConfig from '../config/app';
import checkLicense from '../helpers/check-license.ee';
import userAbility from '../helpers/user-ability';
import Base from './base';
import Connection from './connection';
import Execution from './execution';
import Flow from './flow';
import Identity from './identity.ee';
import Permission from './permission';
import ExtendedQueryBuilder from './query-builder';
import Role from './role';
import Connection from './connection';
import Flow from './flow';
import Step from './step';
import Subscription from './subscription.ee';
import Role from './role';
import Permission from './permission';
import Execution from './execution';
import Identity from './identity.ee';
import UsageData from './usage-data.ee';
import Subscription from './subscription.ee';
class User extends Base {
id!: string;
@@ -148,11 +148,15 @@ class User extends Base {
},
},
permissions: {
relation: Base.HasManyRelation,
relation: Base.ManyToManyRelation,
modelClass: Permission,
join: {
from: 'users.role_id',
to: 'permissions.role_id',
through: {
from: 'roles_permissions.role_id',
to: 'roles_permissions.permission_id',
},
to: 'permissions.id',
},
},
identities: {
@@ -161,8 +165,8 @@ class User extends Base {
join: {
from: 'identities.user_id',
to: 'users.id',
},
},
}
}
});
login(password: string) {
@@ -288,44 +292,27 @@ class User extends Base {
}
}
async $afterFind(): Promise<any> {
const hasValidLicense = await checkLicense();
if (hasValidLicense) return this;
if (Array.isArray(this.permissions)) {
this.permissions = this.permissions.filter((permission) => {
const isRolePermission = permission.subject === 'Role';
const isSamlAuthProviderPermission =
permission.subject === 'SamlAuthProvider';
return !isRolePermission && !isSamlAuthProviderPermission;
});
get ability() {
if (!this.permissions) {
throw new Error('User.permissions must be fetched!');
}
return this;
// We're not using mongo, but our fields, conditions match
return new PureAbility(this.permissions, {
conditionsMatcher: mongoQueryMatcher,
fieldMatcher: fieldPatternMatcher
});
}
get ability(): ReturnType<typeof userAbility> {
return userAbility(this);
}
can(action: string, subject: string) {
can(action: string, subject: Subject) {
const can = this.ability.can(action, subject);
if (!can) throw new Error('Not authorized!');
const relevantRule = this.ability.relevantRuleFor(action, subject);
const conditions = (relevantRule?.conditions as string[]) || [];
const conditionMap: Record<string, true> = Object.fromEntries(
conditions.map((condition) => [condition, true])
);
return conditionMap;
return can;
}
cannot(action: string, subject: string) {
cannot(action: string, subject: Subject) {
const cannot = this.ability.cannot(action, subject);
if (cannot) throw new Error('Not authorized!');

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/cli",
"version": "0.8.0",
"version": "0.7.1",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"contributors": [
@@ -33,7 +33,7 @@
"version": "oclif readme && git add README.md"
},
"dependencies": {
"@automatisch/backend": "^0.8.0",
"@automatisch/backend": "^0.7.1",
"@oclif/core": "^1",
"@oclif/plugin-help": "^5",
"@oclif/plugin-plugins": "^2.0.1",

View File

@@ -15593,6 +15593,11 @@ winston@^3.6.0, winston@^3.7.1:
triple-beam "^1.3.0"
winston-transport "^4.5.0"
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
workbox-background-sync@6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz#3141afba3cc8aa2ae14c24d0f6811374ba8ff6a9"

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/docs",
"version": "0.8.0",
"version": "0.7.1",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"private": true,

View File

@@ -157,7 +157,6 @@ export default defineConfig({
collapsed: true,
items: [
{ text: 'Triggers', link: '/apps/notion/triggers' },
{ text: 'Actions', link: '/apps/notion/actions' },
{ text: 'Connection', link: '/apps/notion/connection' },
],
},

View File

@@ -5,8 +5,6 @@ items:
desc: Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers.
- name: Create spreadsheet row
desc: Creates a new row in a specific spreadsheet.
- name: Create worksheet
desc: Create a blank worksheet with a title. Optionally, provide headers.
---
<script setup>

View File

@@ -1,16 +0,0 @@
---
favicon: /favicons/notion.svg
items:
- name: Create database item
desc: Creates an item in a database.
- name: Create page
desc: Creates a page inside a parent page.
- name: Find database item
desc: Searches for an item in a database by property.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -1,5 +1,9 @@
# Available Apps
:::warning
We just have a few available integrations at the moment and we also know that workflows you can build with them is limited. But we still made the project public and want to share them with you so you can get a sense of what Automatisch can do. Meanwhile, we're working on adding more integrations and improving the existing ones.
:::
Following integrations are currently supported by Automatisch.
- [DeepL](/apps/deepl/actions)

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/e2e-tests",
"version": "0.8.0",
"version": "0.7.1",
"license": "See LICENSE file",
"private": true,
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",

View File

@@ -96,7 +96,6 @@ export interface IUser {
flows: IFlow[];
steps: IStep[];
role: IRole;
permissions: IPermission[];
}
export interface IRole {
@@ -105,20 +104,6 @@ export interface IRole {
name: string;
description: string;
isAdmin: boolean;
permissions: IPermission[];
}
export interface IPermission {
id: string;
action: string;
subject: string;
conditions: string[];
}
export interface IPermissionCatalog {
actions: { label: string; key: string; subjects: string[] }[];
subjects: { label: string; key: string; }[];
conditions: { label: string; key: string; }[];
}
export interface IFieldDropdown {
@@ -432,11 +417,6 @@ declare module 'axios' {
interface AxiosRequestConfig {
additionalProperties?: Record<string, unknown>;
}
// ref: https://github.com/axios/axios/issues/5095
interface AxiosInstance {
create(config?: CreateAxiosDefaults): AxiosInstance;
}
}
export interface IRequest extends Request {

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/types",
"version": "0.8.0",
"version": "0.7.1",
"license": "See LICENSE file",
"description": "Type definitions for automatisch",
"homepage": "https://github.com/automatisch/automatisch",

View File

@@ -1,13 +1,11 @@
{
"name": "@automatisch/web",
"version": "0.8.0",
"version": "0.7.1",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"dependencies": {
"@apollo/client": "^3.6.9",
"@automatisch/types": "^0.8.0",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@automatisch/types": "^0.7.1",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@hookform/resolvers": "^2.8.8",
@@ -33,7 +31,7 @@
"notistack": "^2.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.45.2",
"react-hook-form": "^7.43.9",
"react-intl": "^5.20.12",
"react-json-tree": "^0.16.2",
"react-router-dom": "^6.0.2",

View File

@@ -8,74 +8,60 @@ import CreateRole from 'pages/CreateRole/index.ee';
import EditRole from 'pages/EditRole/index.ee';
import * as URLS from 'config/urls';
import Can from 'components/Can';
// TODO: consider introducing redirections to `/` as fallback
export default (
<>
<Route
path={URLS.USERS}
element={
<Can I="read" a="User">
<AdminSettingsLayout>
<Users />
</AdminSettingsLayout>
</Can>
<AdminSettingsLayout>
<Users />
</AdminSettingsLayout>
}
/>
<Route
path={URLS.CREATE_USER}
element={
<Can I="create" a="User">
<AdminSettingsLayout>
<CreateUser />
</AdminSettingsLayout>
</Can>
<AdminSettingsLayout>
<CreateUser />
</AdminSettingsLayout>
}
/>
<Route
path={URLS.USER_PATTERN}
element={
<Can I="update" a="User">
<AdminSettingsLayout>
<EditUser />
</AdminSettingsLayout>
</Can>
<AdminSettingsLayout>
<EditUser />
</AdminSettingsLayout>
}
/>
<Route
path={URLS.ROLES}
element={
<Can I="read" a="Role">
<AdminSettingsLayout>
<Roles />
</AdminSettingsLayout>
</Can>
<AdminSettingsLayout>
<Roles />
</AdminSettingsLayout>
}
/>
<Route
path={URLS.CREATE_ROLE}
element={
<Can I="create" a="Role">
<AdminSettingsLayout>
<CreateRole />
</AdminSettingsLayout>
</Can>
<AdminSettingsLayout>
<CreateRole />
</AdminSettingsLayout>
}
/>
<Route
path={URLS.ROLE_PATTERN}
element={
<Can I="update" a="Role">
<AdminSettingsLayout>
<EditRole />
</AdminSettingsLayout>
</Can>
<AdminSettingsLayout>
<EditRole />
</AdminSettingsLayout>
}
/>

View File

@@ -4,7 +4,6 @@ import MenuItem from '@mui/material/MenuItem';
import Menu, { MenuProps } from '@mui/material/Menu';
import { Link } from 'react-router-dom';
import Can from 'components/Can';
import apolloClient from 'graphql/client';
import * as URLS from 'config/urls';
import useAuthentication from 'hooks/useAuthentication';
@@ -55,14 +54,9 @@ function AccountDropdownMenu(
{formatMessage('accountDropdownMenu.settings')}
</MenuItem>
<Can I="read" a="User">
<MenuItem
component={Link}
to={URLS.ADMIN_SETTINGS_DASHBOARD}
>
{formatMessage('accountDropdownMenu.adminSettings')}
</MenuItem>
</Can>
<MenuItem component={Link} to={URLS.ADMIN_SETTINGS_DASHBOARD}>
{formatMessage('accountDropdownMenu.adminSettings')}
</MenuItem>
<MenuItem onClick={logout} data-test="logout-item">
{formatMessage('accountDropdownMenu.logout')}

View File

@@ -12,7 +12,7 @@ import useFormatMessage from 'hooks/useFormatMessage';
import computeAuthStepVariables from 'helpers/computeAuthStepVariables';
import { processStep } from 'helpers/authenticationSteps';
import InputCreator from 'components/InputCreator';
import { generateExternalLink } from '../../helpers/translationValues';
import { generateExternalLink } from '../../helpers/translation-values';
import { Form } from './style';
type AddAppConnectionProps = {

View File

@@ -1,42 +1,34 @@
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import GroupIcon from '@mui/icons-material/Group';
import GroupsIcon from '@mui/icons-material/Groups';
import * as React from 'react';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import * as React from 'react';
import GroupIcon from '@mui/icons-material/Group';
import GroupsIcon from '@mui/icons-material/Groups';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import { SvgIconComponent } from '@mui/icons-material';
import * as URLS from 'config/urls';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import * as URLS from 'config/urls';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type SettingsLayoutProps = {
children: React.ReactNode;
};
type DrawerLink = {
Icon: SvgIconComponent,
primary: string,
to: string,
}
function createDrawerLinks({ canReadRole, canReadUser }: { canReadRole: boolean; canReadUser: boolean; }) {
function createDrawerLinks({ isCloud }: { isCloud: boolean }) {
const items = [
canReadUser ? {
Icon: GroupIcon,
primary: 'adminSettingsDrawer.users',
to: URLS.USERS,
} : null,
canReadRole ? {
Icon: GroupsIcon,
primary: 'adminSettingsDrawer.roles',
to: URLS.ROLES,
} : null
]
.filter(Boolean) as DrawerLink[];
{
Icon: GroupIcon,
primary: 'adminSettingsDrawer.users',
to: URLS.USERS,
},
{
Icon: GroupsIcon,
primary: 'adminSettingsDrawer.roles',
to: URLS.ROLES,
}
]
return items;
}
@@ -52,17 +44,14 @@ const drawerBottomLinks = [
export default function SettingsLayout({
children,
}: SettingsLayoutProps): React.ReactElement {
const { isCloud } = useAutomatischInfo();
const theme = useTheme();
const currentUserAbility = useCurrentUserAbility();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
const openDrawer = () => setDrawerOpen(true);
const closeDrawer = () => setDrawerOpen(false);
const drawerLinks = createDrawerLinks({
canReadUser: currentUserAbility.can('read', 'User'),
canReadRole: currentUserAbility.can('read', 'Role'),
});
const drawerLinks = createDrawerLinks({ isCloud });
return (
<>

View File

@@ -1,22 +0,0 @@
import { Can as OriginalCan } from '@casl/react';
import * as React from 'react';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
type CanProps = {
I: string;
a: string;
passThrough?: boolean;
children: React.ReactNode | ((isAllowed: boolean) => React.ReactNode);
} | {
I: string;
an: string;
passThrough?: boolean;
children: React.ReactNode | ((isAllowed: boolean) => React.ReactNode);
};
export default function Can(props: CanProps) {
const currentUserAbility = useCurrentUserAbility();
return (<OriginalCan ability={currentUserAbility} {...props} />);
};

View File

@@ -20,7 +20,6 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
size={buttonProps.size}
component={buttonProps.component}
to={buttonProps.to}
disabled={buttonProps.disabled}
>
{icon}
</IconButton>

View File

@@ -2,7 +2,7 @@ import { styled } from '@mui/material/styles';
import MuiIconButton, { iconButtonClasses } from '@mui/material/IconButton';
export const IconButton = styled(MuiIconButton)`
&.${iconButtonClasses.colorPrimary}:not(.${iconButtonClasses.disabled}) {
&.${iconButtonClasses.colorPrimary} {
background: ${({ theme }) => theme.palette.primary.main};
color: ${({ theme }) => theme.palette.primary.contrastText};

View File

@@ -1,57 +0,0 @@
import * as React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import Checkbox, { CheckboxProps } from '@mui/material/Checkbox';
type ControlledCheckboxProps = {
name: string;
} & CheckboxProps;
export default function ControlledCheckbox(props: ControlledCheckboxProps): React.ReactElement {
const { control } = useFormContext();
const {
required,
name,
defaultValue = false,
disabled = false,
onBlur,
onChange,
...checkboxProps
} = props;
return (
<Controller
rules={{ required }}
name={name}
defaultValue={defaultValue}
control={control}
render={({
field: {
ref,
onChange: controllerOnChange,
onBlur: controllerOnBlur,
value,
name,
...field
},
}) => {
return (
<Checkbox
{...checkboxProps}
{...field}
checked={!!value}
name={name}
disabled={disabled}
onChange={(...args) => {
controllerOnChange(...args);
onChange?.(...args);
}}
onBlur={(...args) => {
controllerOnBlur();
onBlur?.(...args);
}}
inputRef={ref}
/>
)}}
/>
);
}

View File

@@ -2,54 +2,35 @@ import * as React from 'react';
import { useMutation } from '@apollo/client';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import { useSnackbar } from 'notistack';
import Can from 'components/Can';
import ConfirmationDialog from 'components/ConfirmationDialog';
import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee';
import useFormatMessage from 'hooks/useFormatMessage';
type DeleteRoleButtonProps = {
disabled?: boolean;
roleId: string;
};
export default function DeleteRoleButton(props: DeleteRoleButtonProps) {
const { disabled, roleId } = props;
const { roleId } = props;
const [showConfirmation, setShowConfirmation] = React.useState(false);
const [deleteRole] = useMutation(DELETE_ROLE, {
variables: { input: { id: roleId } },
refetchQueries: ['GetRoles'],
});
const formatMessage = useFormatMessage();
const { enqueueSnackbar } = useSnackbar();
const handleConfirm = React.useCallback(async () => {
try {
await deleteRole();
await deleteRole();
setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), {
variant: 'success',
});
} catch (error) {
throw new Error('Failed while deleting!');
}
setShowConfirmation(false);
}, [deleteRole]);
return (
<>
<Can I="delete" a="Role" passThrough>
{(allowed) => (
<IconButton
disabled={!allowed || disabled}
onClick={() => setShowConfirmation(true)}
size="small"
>
<DeleteIcon />
</IconButton>
)}
</Can>
<IconButton onClick={() => setShowConfirmation(true)} size="small">
<DeleteIcon />
</IconButton>
<ConfirmationDialog
open={showConfirmation}

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { useMutation } from '@apollo/client';
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import { useSnackbar } from 'notistack';
import ConfirmationDialog from 'components/ConfirmationDialog';
import { DELETE_USER } from 'graphql/mutations/delete-user.ee';
@@ -20,19 +19,11 @@ export default function DeleteUserButton(props: DeleteUserButtonProps) {
refetchQueries: ['GetUsers'],
});
const formatMessage = useFormatMessage();
const { enqueueSnackbar } = useSnackbar();
const handleConfirm = React.useCallback(async () => {
try {
await deleteUser();
await deleteUser();
setShowConfirmation(false);
enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), {
variant: 'success',
});
} catch (error) {
throw new Error('Failed while deleting!');
}
setShowConfirmation(false);
}, [deleteUser]);
return (

View File

@@ -6,7 +6,6 @@ import type { PopoverProps } from '@mui/material/Popover';
import MenuItem from '@mui/material/MenuItem';
import { useSnackbar } from 'notistack';
import Can from 'components/Can';
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow';
import * as URLS from 'config/urls';
@@ -73,39 +72,13 @@ export default function ContextMenu(
hideBackdrop={false}
anchorEl={anchorEl}
>
<Can I="read" a="Flow" passThrough>
{(allowed) => (
<MenuItem
disabled={!allowed}
component={Link}
to={URLS.FLOW(flowId)}
>
{formatMessage('flow.view')}
</MenuItem>
)}
</Can>
<MenuItem component={Link} to={URLS.FLOW(flowId)}>
{formatMessage('flow.view')}
</MenuItem>
<Can I="create" a="Flow" passThrough>
{(allowed) => (
<MenuItem
disabled={!allowed}
onClick={onFlowDuplicate}
>
{formatMessage('flow.duplicate')}
</MenuItem>
)}
</Can>
<MenuItem onClick={onFlowDuplicate}>{formatMessage('flow.duplicate')}</MenuItem>
<Can I="delete" a="Flow" passThrough>
{(allowed) => (
<MenuItem
disabled={!allowed}
onClick={onFlowDelete}
>
{formatMessage('flow.delete')}
</MenuItem>
)}
</Can>
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
</Menu>
);
}

View File

@@ -0,0 +1,47 @@
import {
IconButton,
Skeleton,
Stack,
TableCell,
TableRow,
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
type ListLoaderProps = {
rowsNumber: number;
cellNumber: number;
};
const ListLoader = ({ rowsNumber, cellNumber }: ListLoaderProps) => {
return (
<>
{[...Array(rowsNumber)].map((row, index) => (
<TableRow
key={index}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{[...Array(cellNumber)].map((cell, index) => (
<TableCell key={index} scope="row">
<Skeleton />
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton size="small">
<EditIcon />
</IconButton>
<IconButton size="small">
<DeleteIcon />
</IconButton>
</Stack>
</TableCell>
</TableRow>
))}
</>
);
};
export default ListLoader;

View File

@@ -1,142 +0,0 @@
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { useFormContext } from 'react-hook-form';
import { IPermissionCatalog } from '@automatisch/types';
import ControlledCheckbox from 'components/ControlledCheckbox';
import useFormatMessage from 'hooks/useFormatMessage';
type PermissionSettingsProps = {
onClose: () => void;
fieldPrefix: string;
subject: string;
actions: IPermissionCatalog['actions'];
conditions: IPermissionCatalog['conditions'];
}
export default function PermissionSettings(props: PermissionSettingsProps) {
const {
onClose,
fieldPrefix,
subject,
actions,
conditions,
} = props;
const formatMessage = useFormatMessage();
const { getValues, resetField } = useFormContext();
const cancel = () => {
for (const action of actions) {
for (const condition of conditions) {
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
resetField(fieldName);
}
}
onClose();
}
const apply = () => {
for (const action of actions) {
for (const condition of conditions) {
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
const value = getValues(fieldName);
resetField(fieldName, { defaultValue: value });
}
}
onClose();
}
return (
<Dialog open onClose={cancel}>
<DialogTitle>
{formatMessage('permissionSettings.title')}
</DialogTitle>
<DialogContent>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell component="th" />
{actions.map(action => (
<TableCell component="th" key={action.key}>
<Typography
variant="subtitle1"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700
}}
>
{action.label}
</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{conditions.map((condition) => (
<TableRow
key={condition.key}
sx={{ '&:last-child td': { border: 0 } }}
>
<TableCell scope="row">
<Typography
variant="subtitle2"
>
{condition.label}
</Typography>
</TableCell>
{actions.map((action) => (
<TableCell
key={`${action.key}.${condition.key}`}
align="center"
>
<Typography
variant="subtitle2"
>
{action.subjects.includes(subject) && (
<ControlledCheckbox
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
disabled={getValues(`${fieldPrefix}.${action.key}.value`) !== true}
/>
)}
{!action.subjects.includes(subject) && '-'}
</Typography>
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={cancel}>{formatMessage('permissionSettings.cancel')}</Button>
<Button onClick={apply} color="error">
{formatMessage('permissionSettings.apply')}
</Button>
</DialogActions>
</Dialog>
)
}

View File

@@ -1,122 +0,0 @@
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import * as React from 'react';
import ControlledCheckbox from 'components/ControlledCheckbox';
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
import PermissionSettings from './PermissionSettings.ee';
type PermissionCatalogFieldProps = {
name?: string;
disabled?: boolean;
};
const PermissionCatalogField = ({ name = 'permissions', disabled = false }: PermissionCatalogFieldProps) => {
const permissionCatalog = usePermissionCatalog();
const [dialogName, setDialogName] = React.useState<string>();
if (!permissionCatalog) return (<React.Fragment />);
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell component="th" />
{permissionCatalog.actions.map(action => (
<TableCell component="th" key={action.key}>
<Typography
variant="subtitle1"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700
}}
>
{action.label}
</Typography>
</TableCell>
))}
<TableCell component="th" />
</TableRow>
</TableHead>
<TableBody>
{permissionCatalog.subjects.map((subject) => (
<TableRow
key={subject.key}
sx={{ '&:last-child td': { border: 0 } }}
>
<TableCell scope="row">
<Typography
variant="subtitle2"
>
{subject.label}
</Typography>
</TableCell>
{permissionCatalog.actions.map((action) => (
<TableCell
key={`${subject.key}.${action.key}`}
align="center"
>
<Typography
variant="subtitle2"
>
{action.subjects.includes(subject.key) && (
<ControlledCheckbox
disabled={disabled}
name={`${name}.${subject.key}.${action.key}.value`}
/>
)}
{!action.subjects.includes(subject.key) && '-'}
</Typography>
</TableCell>
))}
<TableCell>
<Stack
direction="row"
gap={1}
justifyContent="right"
>
<IconButton
color="info"
size="small"
onClick={() => setDialogName(subject.key)}
disabled={disabled}
>
<SettingsIcon />
</IconButton>
{dialogName === subject.key && (
<PermissionSettings
onClose={() => setDialogName('')}
fieldPrefix={`${name}.${subject.key}`}
subject={subject.key}
actions={permissionCatalog.actions}
conditions={permissionCatalog.conditions}
/>
)}
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
};
export default PermissionCatalogField;

View File

@@ -13,14 +13,15 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit';
import DeleteRoleButton from 'components/DeleteRoleButton/index.ee';
import ListLoader from 'components/ListLoader';
import useFormatMessage from 'hooks/useFormatMessage';
import useRoles from 'hooks/useRoles.ee';
import * as URLS from 'config/urls';
// TODO: introduce loading bar
// TODO: introduce interaction feedback upon deletion (successful + failure)
export default function RoleList(): React.ReactElement {
const formatMessage = useFormatMessage();
const { roles } = useRoles();
const { roles, loading } = useRoles();
return (
<TableContainer component={Paper}>
@@ -49,34 +50,40 @@ export default function RoleList(): React.ReactElement {
</TableRow>
</TableHead>
<TableBody>
{roles.map((role) => (
<TableRow
key={role.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell scope="row">
<Typography variant="subtitle2">{role.name}</Typography>
</TableCell>
{loading ? (
<ListLoader rowsNumber={3} cellNumber={2} />
) : (
roles.map((role) => (
<TableRow
key={role.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell scope="row">
<Typography variant="subtitle2">{role.name}</Typography>
</TableCell>
<TableCell scope="row">
<Typography variant="subtitle2">{role.description}</Typography>
</TableCell>
<TableCell scope="row">
<Typography variant="subtitle2">
{role.description}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton
size="small"
component={Link}
to={URLS.ROLE(role.id)}
>
<EditIcon />
</IconButton>
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton
size="small"
component={Link}
to={URLS.ROLE(role.id)}
>
<EditIcon />
</IconButton>
<DeleteRoleButton disabled={role.isAdmin} roleId={role.id} />
</Stack>
</TableCell>
</TableRow>
))}
<DeleteRoleButton roleId={role.id} />
</Stack>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>

View File

@@ -4,7 +4,7 @@ import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Divider from '@mui/material/Divider';
import * as URLS from 'config/urls';
import appConfig from 'config/app';
import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee';
import useFormatMessage from 'hooks/useFormatMessage';
@@ -24,12 +24,10 @@ function SsoProviders() {
<Button
key={provider.id}
component="a"
href={URLS.SSO_LOGIN(provider.issuer)}
href={`${appConfig.apiUrl}/login/saml/${provider.issuer}`}
variant="outlined"
>
{formatMessage('ssoProviders.loginWithProvider', {
providerName: provider.name
})}
{provider.name}
</Button>
))}
</Stack>

View File

@@ -3,7 +3,7 @@ import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import * as URLS from 'config/urls';
import { generateInternalLink } from 'helpers/translationValues';
import { generateInternalLink } from 'helpers/translation-values';
import useTrialStatus from 'hooks/useTrialStatus.ee';
import useFormatMessage from 'hooks/useFormatMessage';

View File

@@ -13,11 +13,13 @@ import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit';
import DeleteUserButton from 'components/DeleteUserButton/index.ee';
import ListLoader from 'components/ListLoader';
import useUsers from 'hooks/useUsers';
import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls';
// TODO: introduce loading bar
// TODO: introduce translation entries
// TODO: introduce interaction feedback upon deletion (successful + failure)
export default function UserList(): React.ReactElement {
const formatMessage = useFormatMessage();
const { users, loading } = useUsers();
@@ -49,34 +51,38 @@ export default function UserList(): React.ReactElement {
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow
key={user.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell scope="row">
<Typography variant="subtitle2">{user.fullName}</Typography>
</TableCell>
{loading ? (
<ListLoader rowsNumber={3} cellNumber={2} />
) : (
users.map((user) => (
<TableRow
key={user.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell scope="row">
<Typography variant="subtitle2">{user.fullName}</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2">{user.email}</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2">{user.email}</Typography>
</TableCell>
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton
size="small"
component={Link}
to={URLS.USER(user.id)}
>
<EditIcon />
</IconButton>
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton
size="small"
component={Link}
to={URLS.USER(user.id)}
>
<EditIcon />
</IconButton>
<DeleteUserButton userId={user.id} />
</Stack>
</TableCell>
</TableRow>
))}
<DeleteUserButton userId={user.id} />
</Stack>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>

View File

@@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl';
import Typography from '@mui/material/Typography';
import type { AlertProps } from '@mui/material/Alert';
import { generateExternalLink } from '../../helpers/translationValues';
import { generateExternalLink } from '../../helpers/translation-values';
import { WEBHOOK_DOCS } from '../../config/urls';
import TextField from '../TextField';
import { Alert } from './style';

View File

@@ -1,7 +1,7 @@
import { styled } from '@mui/material/styles';
import MuiAlert, { alertClasses } from '@mui/material/Alert';
export const Alert = styled(MuiAlert)(() => ({
export const Alert = styled(MuiAlert)(({ theme }) => ({
[`&.${alertClasses.root}`]: {
fontWeight: 300,
width: '100%',

View File

@@ -17,9 +17,7 @@ const config: Config = {
supportEmailAddress: 'support@automatisch.io'
};
if (!config.apiUrl && !config.graphqlUrl) {
config.apiUrl = '/';
} else if (!config.apiUrl) {
if (!config.apiUrl) {
config.apiUrl = (new URL(config.graphqlUrl)).origin;
}

View File

@@ -1,5 +1,3 @@
import appConfig from './app';
export const CONNECTIONS = '/connections';
export const EXECUTIONS = '/executions';
export const EXECUTION_PATTERN = '/executions/:executionId';
@@ -8,7 +6,6 @@ export const EXECUTION = (executionId: string) =>
export const LOGIN = '/login';
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
export const SSO_LOGIN = (issuer: string) => `${appConfig.apiUrl}/login/saml/${issuer}`;
export const SIGNUP = '/sign-up';
export const FORGOT_PASSWORD = '/forgot-password';
export const RESET_PASSWORD = '/reset-password';

View File

@@ -6,12 +6,6 @@ export const UPDATE_ROLE = gql`
id
name
description
permissions {
id
action
subject
conditions
}
}
}
`;

View File

@@ -7,11 +7,9 @@ export const GET_CURRENT_USER = gql`
fullName
email
role {
id
isAdmin
}
permissions {
id
action
subject
conditions

Some files were not shown because too many files have changed in this diff Show More