From fc4561221dfeaf2e617ed327e2bc2a6a2004877a Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 7 May 2024 13:55:55 +0000 Subject: [PATCH 1/8] feat: add DISABLE_SEED_USER to bypass yarn db:seed:user command --- packages/backend/bin/database/utils.js | 8 ++++++++ packages/backend/src/config/app.js | 1 + 2 files changed, 9 insertions(+) diff --git a/packages/backend/bin/database/utils.js b/packages/backend/bin/database/utils.js index 4d373353..14124276 100644 --- a/packages/backend/bin/database/utils.js +++ b/packages/backend/bin/database/utils.js @@ -21,6 +21,14 @@ export async function createUser( email = 'user@automatisch.io', password = 'sample' ) { + if (appConfig.disableSeedUser) { + logger.info('Seed user is disabled.'); + + process.exit(0); + + return; + } + const UNIQUE_VIOLATION_CODE = '23505'; const role = await fetchAdminRole(); diff --git a/packages/backend/src/config/app.js b/packages/backend/src/config/app.js index 26241f6f..c2050d82 100644 --- a/packages/backend/src/config/app.js +++ b/packages/backend/src/config/app.js @@ -98,6 +98,7 @@ const appConfig = { disableFavicon: process.env.DISABLE_FAVICON === 'true', additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK, additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT, + disableSeedUser: process.env.DISABLE_SEED_USER === 'true', }; if (!appConfig.encryptionKey) { From ae188bc5639fec8c76e306a40103d676c8951991 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 7 May 2024 15:35:58 +0000 Subject: [PATCH 2/8] feat: add migration to mark userful instances installation completed --- ..._userful_instances_installation_completed.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/backend/src/db/migrations/20240507135944_mark_userful_instances_installation_completed.js diff --git a/packages/backend/src/db/migrations/20240507135944_mark_userful_instances_installation_completed.js b/packages/backend/src/db/migrations/20240507135944_mark_userful_instances_installation_completed.js new file mode 100644 index 00000000..0ffbfd6d --- /dev/null +++ b/packages/backend/src/db/migrations/20240507135944_mark_userful_instances_installation_completed.js @@ -0,0 +1,17 @@ +export async function up(knex) { + const users = await knex('users').limit(1); + + // no user implies installation is not completed yet. + if (users.length === 0) return; + + await knex('config').insert({ + key: 'installation.completed', + value: { + data: true + } + }); +}; + +export async function down(knex) { + await knex('config').where({ key: 'installation.completed' }).delete(); +}; From 717c81fa2b0ef2214f4dd2ca625ec271b91879ac Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 7 May 2024 15:36:17 +0000 Subject: [PATCH 3/8] test(global-hooks): truncate config table --- packages/backend/test/setup/global-hooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/setup/global-hooks.js b/packages/backend/test/setup/global-hooks.js index d6f8a562..7e4eee85 100644 --- a/packages/backend/test/setup/global-hooks.js +++ b/packages/backend/test/setup/global-hooks.js @@ -8,7 +8,7 @@ global.beforeAll(async () => { logger.silent = true; // Remove default roles and permissions before running the test suite - await knex.raw('TRUNCATE TABLE roles, permissions CASCADE'); + await knex.raw('TRUNCATE TABLE config, roles, permissions CASCADE'); }); global.beforeEach(async () => { From b30f97db3e69c5ee48df1fa554f5ae78750401bc Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 7 May 2024 15:39:22 +0000 Subject: [PATCH 4/8] feat: add POST /api/v1/installation/users to seed user --- .../api/v1/installation/users/create-user.js | 12 ++++ .../v1/installation/users/create-user.test.js | 62 +++++++++++++++++++ .../src/helpers/authorize-installation.js | 9 +++ packages/backend/src/models/config.js | 22 +++++++ packages/backend/src/models/role.js | 4 ++ packages/backend/src/models/user.js | 13 ++++ .../src/routes/api/v1/installation/users.js | 14 +++++ packages/backend/src/routes/index.js | 3 + packages/backend/test/factories/config.js | 4 ++ 9 files changed, 143 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/installation/users/create-user.js create mode 100644 packages/backend/src/controllers/api/v1/installation/users/create-user.test.js create mode 100644 packages/backend/src/helpers/authorize-installation.js create mode 100644 packages/backend/src/routes/api/v1/installation/users.js diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.js new file mode 100644 index 00000000..ee6a5ce7 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.js @@ -0,0 +1,12 @@ +import User from '../../../../../models/user.js'; +import Config from '../../../../../models/config.js'; + +export default async (request, response) => { + const { email, password, fullName } = request.body; + + await User.createAdminUser({ email, password, fullName }); + + await Config.markInstallationCompleted(); + + response.status(204).end(); +}; diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js new file mode 100644 index 00000000..17de0174 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import Config from '../../../../../models/config.js'; +import User from '../../../../../models/user.js'; +import { createRole } from '../../../../../../test/factories/role'; +import { createInstallationCompletedConfig } from '../../../../../../test/factories/config'; + +describe('POST /api/v1/installation/users', () => { + let adminRole; + + beforeEach(async () => { + adminRole = await createRole({ + name: 'Admin', + key: 'admin', + }) + }); + + describe('for incomplete installations', () => { + it('should respond with HTTP 204 with correct payload', async () => { + expect(await Config.isInstallationCompleted()).toBe(false); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(204); + + const user = await User.query().findOne({ email: 'user@automatisch.io' }); + + expect(user.roleId).toBe(adminRole.id); + expect(await Config.isInstallationCompleted()).toBe(true); + }); + }); + + describe('for completed installations', () => { + beforeEach(async () => { + await createInstallationCompletedConfig(); + }); + + it('should respond with HTTP 403 when installation completed', async () => { + expect(await Config.isInstallationCompleted()).toBe(true); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(403); + + const user = await User.query().findOne({ email: 'user@automatisch.io' }); + + expect(user).toBeUndefined(); + expect(await Config.isInstallationCompleted()).toBe(true); + }); + }) +}); diff --git a/packages/backend/src/helpers/authorize-installation.js b/packages/backend/src/helpers/authorize-installation.js new file mode 100644 index 00000000..09a2b733 --- /dev/null +++ b/packages/backend/src/helpers/authorize-installation.js @@ -0,0 +1,9 @@ +import Config from '../models/config.js'; + +export async function authorizeInstallation(request, response, next) { + if (await Config.isInstallationCompleted()) { + return response.status(403).end(); + } else { + next(); + } +}; diff --git a/packages/backend/src/models/config.js b/packages/backend/src/models/config.js index 949c6aaf..b65b6ece 100644 --- a/packages/backend/src/models/config.js +++ b/packages/backend/src/models/config.js @@ -13,6 +13,28 @@ class Config extends Base { value: { type: 'object' }, }, }; + + static async isInstallationCompleted() { + const installationCompletedEntry = await this + .query() + .where({ + key: 'installation.completed' + }) + .first(); + + const installationCompleted = installationCompletedEntry?.value?.data === true; + + return installationCompleted; + } + + static async markInstallationCompleted() { + return await this.query().insert({ + key: 'installation.completed', + value: { + data: true, + }, + }); + } } export default Config; diff --git a/packages/backend/src/models/role.js b/packages/backend/src/models/role.js index af6ccafa..08b19673 100644 --- a/packages/backend/src/models/role.js +++ b/packages/backend/src/models/role.js @@ -45,6 +45,10 @@ class Role extends Base { get isAdmin() { return this.key === 'admin'; } + + static async findAdmin() { + return await this.query().findOne({ key: 'admin' }); + } } export default Role; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 893f057e..636034a0 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -373,6 +373,19 @@ class User extends Base { return apps; } + static async createAdminUser({ email, password, fullName }) { + const adminRole = await Role.findAdmin(); + + const adminUser = await this.query().insert({ + email, + password, + fullName, + roleId: adminRole.id + }); + + return adminUser; + } + async $beforeInsert(queryContext) { await super.$beforeInsert(queryContext); diff --git a/packages/backend/src/routes/api/v1/installation/users.js b/packages/backend/src/routes/api/v1/installation/users.js new file mode 100644 index 00000000..f2b16feb --- /dev/null +++ b/packages/backend/src/routes/api/v1/installation/users.js @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authorizeInstallation } from '../../../../helpers/authorize-installation.js'; +import createUserAction from '../../../../controllers/api/v1/installation/users/create-user.js'; + +const router = Router(); + +router.post( + '/', + authorizeInstallation, + asyncHandler(createUserAction) +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index a7f8c30a..c67a9cf3 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -18,6 +18,7 @@ import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee. import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; import adminUsersRouter from './api/v1/admin/users.ee.js'; +import installationUsersRouter from './api/v1/installation/users.js'; const router = Router(); @@ -40,5 +41,7 @@ router.use('/api/v1/admin/users', adminUsersRouter); router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter); +router.use('/api/v1/installation/users', installationUsersRouter); + export default router; diff --git a/packages/backend/test/factories/config.js b/packages/backend/test/factories/config.js index a8d59787..5a8e316c 100644 --- a/packages/backend/test/factories/config.js +++ b/packages/backend/test/factories/config.js @@ -11,3 +11,7 @@ export const createConfig = async (params = {}) => { return config; }; + +export const createInstallationCompletedConfig = async () => { + return await createConfig({ key: 'installation.completed', value: { data: true } }); +} From c80791267f2d3a8166b572a3b2414a866b45dbc1 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 May 2024 14:00:12 +0000 Subject: [PATCH 5/8] refactor(installation): improve allow installation guard --- packages/backend/bin/database/utils.js | 3 +++ .../v1/installation/users/create-user.test.js | 24 ++++++++++++++++++- .../backend/src/helpers/allow-installation.js | 16 +++++++++++++ .../src/helpers/authorize-installation.js | 9 ------- .../src/routes/api/v1/installation/users.js | 4 ++-- 5 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 packages/backend/src/helpers/allow-installation.js delete mode 100644 packages/backend/src/helpers/authorize-installation.js diff --git a/packages/backend/bin/database/utils.js b/packages/backend/bin/database/utils.js index 14124276..0a3ae129 100644 --- a/packages/backend/bin/database/utils.js +++ b/packages/backend/bin/database/utils.js @@ -2,6 +2,7 @@ import appConfig from '../../src/config/app.js'; import logger from '../../src/helpers/logger.js'; import client from './client.js'; import User from '../../src/models/user.js'; +import Config from '../../src/models/config.js'; import Role from '../../src/models/role.js'; import '../../src/config/orm.js'; import process from 'process'; @@ -45,6 +46,8 @@ export async function createUser( if (userCount === 0) { const user = await User.query().insertAndFetch(userParams); logger.info(`User has been saved: ${user.email}`); + + await Config.markInstallationCompleted(); } else { logger.info('No need to seed a user.'); } diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js index 17de0174..a157dbb3 100644 --- a/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js @@ -4,6 +4,7 @@ import app from '../../../../../app.js'; import Config from '../../../../../models/config.js'; import User from '../../../../../models/user.js'; import { createRole } from '../../../../../../test/factories/role'; +import { createUser } from '../../../../../../test/factories/user'; import { createInstallationCompletedConfig } from '../../../../../../test/factories/config'; describe('POST /api/v1/installation/users', () => { @@ -17,7 +18,7 @@ describe('POST /api/v1/installation/users', () => { }); describe('for incomplete installations', () => { - it('should respond with HTTP 204 with correct payload', async () => { + it('should respond with HTTP 204 with correct payload when no user', async () => { expect(await Config.isInstallationCompleted()).toBe(false); await request(app) @@ -34,6 +35,27 @@ describe('POST /api/v1/installation/users', () => { expect(user.roleId).toBe(adminRole.id); expect(await Config.isInstallationCompleted()).toBe(true); }); + + it('should respond with HTTP 403 with correct payload when one user exists at least', async () => { + expect(await Config.isInstallationCompleted()).toBe(false); + + await createUser(); + + const usersCountBefore = await User.query().resultSize(); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(403); + + const usersCountAfter = await User.query().resultSize(); + + expect(usersCountBefore).toEqual(usersCountAfter); + }); }); describe('for completed installations', () => { diff --git a/packages/backend/src/helpers/allow-installation.js b/packages/backend/src/helpers/allow-installation.js new file mode 100644 index 00000000..33826a4b --- /dev/null +++ b/packages/backend/src/helpers/allow-installation.js @@ -0,0 +1,16 @@ +import Config from '../models/config.js'; +import User from '../models/user.js'; + +export async function allowInstallation(request, response, next) { + if (await Config.isInstallationCompleted()) { + return response.status(403).end(); + } + + const hasAnyUsers = await User.query().resultSize() > 0; + + if (hasAnyUsers) { + return response.status(403).end(); + } + + next(); +}; diff --git a/packages/backend/src/helpers/authorize-installation.js b/packages/backend/src/helpers/authorize-installation.js deleted file mode 100644 index 09a2b733..00000000 --- a/packages/backend/src/helpers/authorize-installation.js +++ /dev/null @@ -1,9 +0,0 @@ -import Config from '../models/config.js'; - -export async function authorizeInstallation(request, response, next) { - if (await Config.isInstallationCompleted()) { - return response.status(403).end(); - } else { - next(); - } -}; diff --git a/packages/backend/src/routes/api/v1/installation/users.js b/packages/backend/src/routes/api/v1/installation/users.js index f2b16feb..9a3c8fd7 100644 --- a/packages/backend/src/routes/api/v1/installation/users.js +++ b/packages/backend/src/routes/api/v1/installation/users.js @@ -1,13 +1,13 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; -import { authorizeInstallation } from '../../../../helpers/authorize-installation.js'; +import { allowInstallation } from '../../../../helpers/allow-installation.js'; import createUserAction from '../../../../controllers/api/v1/installation/users/create-user.js'; const router = Router(); router.post( '/', - authorizeInstallation, + allowInstallation, asyncHandler(createUserAction) ); From 5a83fc33ecee2c6b22c1735ea05fcba8bc1cab52 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 May 2024 14:09:09 +0000 Subject: [PATCH 6/8] refactor(User): rename createAdminUser with createAdmin --- .../src/controllers/api/v1/installation/users/create-user.js | 2 +- packages/backend/src/models/user.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.js index ee6a5ce7..0e7d05c7 100644 --- a/packages/backend/src/controllers/api/v1/installation/users/create-user.js +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.js @@ -4,7 +4,7 @@ import Config from '../../../../../models/config.js'; export default async (request, response) => { const { email, password, fullName } = request.body; - await User.createAdminUser({ email, password, fullName }); + await User.createAdmin({ email, password, fullName }); await Config.markInstallationCompleted(); diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 636034a0..1abbde14 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -373,7 +373,7 @@ class User extends Base { return apps; } - static async createAdminUser({ email, password, fullName }) { + static async createAdmin({ email, password, fullName }) { const adminRole = await Role.findAdmin(); const adminUser = await this.query().insert({ From 46b85519c1ffb829c87057b4ad35f0fba89937bd Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 May 2024 14:10:21 +0000 Subject: [PATCH 7/8] refactor(User/createAdmin): mark installation completed --- .../src/controllers/api/v1/installation/users/create-user.js | 3 --- packages/backend/src/models/user.js | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.js index 0e7d05c7..84172310 100644 --- a/packages/backend/src/controllers/api/v1/installation/users/create-user.js +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.js @@ -1,12 +1,9 @@ import User from '../../../../../models/user.js'; -import Config from '../../../../../models/config.js'; export default async (request, response) => { const { email, password, fullName } = request.body; await User.createAdmin({ email, password, fullName }); - await Config.markInstallationCompleted(); - response.status(204).end(); }; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 1abbde14..c3900b4f 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -10,6 +10,7 @@ import Base from './base.js'; import App from './app.js'; import AccessToken from './access-token.js'; import Connection from './connection.js'; +import Config from './config.js'; import Execution from './execution.js'; import Flow from './flow.js'; import Identity from './identity.ee.js'; @@ -383,6 +384,8 @@ class User extends Base { roleId: adminRole.id }); + await Config.markInstallationCompleted(); + return adminUser; } From 4144944ab21ecae2573eb40d56e9aac6dee414ac Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 13 May 2024 14:12:46 +0000 Subject: [PATCH 8/8] refactor(migrations): rename installation completed migration --- ...=> 20240507135944_update_installation_completed_for_config.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/backend/src/db/migrations/{20240507135944_mark_userful_instances_installation_completed.js => 20240507135944_update_installation_completed_for_config.js} (100%) diff --git a/packages/backend/src/db/migrations/20240507135944_mark_userful_instances_installation_completed.js b/packages/backend/src/db/migrations/20240507135944_update_installation_completed_for_config.js similarity index 100% rename from packages/backend/src/db/migrations/20240507135944_mark_userful_instances_installation_completed.js rename to packages/backend/src/db/migrations/20240507135944_update_installation_completed_for_config.js