From 5a60fb11b52a9ad4a81ba904b9c3a0e0fc05cb1a Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sat, 2 Sep 2023 05:03:01 +0000 Subject: [PATCH] fix: various auth improvements + other fixes --- .devcontainer/docker-compose.yml | 2 +- config.sample.yml | 8 +- server/app/data.yml | 4 +- server/core/db.mjs | 6 +- server/db/migrations/3.0.0.mjs | 4 +- server/graph/resolvers/authentication.mjs | 47 ++++++++++- server/graph/resolvers/page.mjs | 22 ++--- server/graph/schemas/authentication.graphql | 9 +- server/locales/en.json | 6 +- server/models/authentication.mjs | 7 +- server/models/pages.mjs | 27 ++---- ux/src/components/AuthLoginPanel.vue | 1 - ux/src/pages/AdminAuth.vue | 18 ++-- ux/src/pages/AdminLogin.vue | 92 +++++++++++++-------- 14 files changed, 145 insertions(+), 108 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index fe5c464aac..6474243cf9 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -28,7 +28,7 @@ services: # (Adding the "ports" property to this file will not forward from a Codespace.) db: - image: postgres:16beta1 + image: postgres:16rc1 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data diff --git a/config.sample.yml b/config.sample.yml index f58b6d6b61..40a913bb30 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -2,7 +2,7 @@ # Wiki.js - CONFIGURATION # ####################################################################### # Full documentation + examples: -# https://js.wiki/docs/install +# https://next.js.wiki/docs/install # --------------------------------------------------------------------- # Port the server should listen to @@ -13,7 +13,7 @@ port: 3000 # --------------------------------------------------------------------- # Database # --------------------------------------------------------------------- -# PostgreSQL 11 or later required +# PostgreSQL 12 or later required db: host: localhost @@ -21,9 +21,7 @@ db: user: postgres pass: postgres db: postgres - schemas: - wiki: wiki - scheduler: scheduler + schema: wiki ssl: false # Optional diff --git a/server/app/data.yml b/server/app/data.yml index aa6043c2c7..535ed24f1c 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -16,9 +16,7 @@ defaults: ssl: false sslOptions: auto: true - schemas: - wiki: wiki - scheduler: scheduler + schema: wiki ssl: enabled: false pool: diff --git a/server/core/db.mjs b/server/core/db.mjs index 4c225e19e2..c1bfa5ab6f 100644 --- a/server/core/db.mjs +++ b/server/core/db.mjs @@ -86,7 +86,7 @@ export default { useNullAsDefault: true, asyncStackTraces: WIKI.IS_DEBUG, connection: this.config, - searchPath: [WIKI.config.db.schemas.wiki], + searchPath: [WIKI.config.db.schema], pool: { ...workerMode ? { min: 0, max: 1 } : WIKI.config.pool, async afterCreate(conn, done) { @@ -223,12 +223,12 @@ export default { */ async syncSchemas () { WIKI.logger.info('Ensuring DB schema exists...') - await this.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schemas.wiki}`) + await this.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schema}`) WIKI.logger.info('Ensuring DB migrations have been applied...') return this.knex.migrate.latest({ tableName: 'migrations', migrationSource, - schemaName: WIKI.config.db.schemas.wiki + schemaName: WIKI.config.db.schema }) }, /** diff --git a/server/db/migrations/3.0.0.mjs b/server/db/migrations/3.0.0.mjs index bed044f393..a91b216236 100644 --- a/server/db/migrations/3.0.0.mjs +++ b/server/db/migrations/3.0.0.mjs @@ -67,8 +67,8 @@ export async function up (knex) { table.string('displayName').notNullable().defaultTo('') table.jsonb('config').notNullable().defaultTo('{}') table.boolean('selfRegistration').notNullable().defaultTo(false) - table.jsonb('domainWhitelist').notNullable().defaultTo('[]') - table.jsonb('autoEnrollGroups').notNullable().defaultTo('[]') + table.string('allowedEmailRegex') + table.specificType('autoEnrollGroups', 'uuid[]') }) .createTable('commentProviders', table => { table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) diff --git a/server/graph/resolvers/authentication.mjs b/server/graph/resolvers/authentication.mjs index 3a5fcfcae7..82d1c4c45f 100644 --- a/server/graph/resolvers/authentication.mjs +++ b/server/graph/resolvers/authentication.mjs @@ -57,7 +57,7 @@ export default { async authSiteStrategies (obj, args, context, info) { const site = await WIKI.db.sites.query().findById(args.siteId) const activeStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true }) - return activeStrategies.map(str => { + const siteStrategies = _.sortBy(activeStrategies.map(str => { const siteAuth = _.find(site.config.authStrategies, ['id', str.id]) || {} return { id: str.id, @@ -65,7 +65,8 @@ export default { order: siteAuth.order ?? 0, isVisible: siteAuth.isVisible ?? false } - }) + }), ['order']) + return args.visibleOnly ? siteStrategies.filter(s => s.isVisible) : siteStrategies } }, Mutation: { @@ -196,6 +197,10 @@ export default { */ async setApiState (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + WIKI.config.api.isEnabled = args.enabled await WIKI.configSvc.saveToDb(['api']) return { @@ -210,6 +215,10 @@ export default { */ async revokeApiKey (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + await WIKI.db.apiKeys.query().findById(args.id).patch({ isRevoked: true }) @@ -227,11 +236,14 @@ export default { */ async updateAuthStrategies (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + const previousStrategies = await WIKI.db.authentication.getStrategies() for (const str of args.strategies) { const newStr = { displayName: str.displayName, - order: str.order, isEnabled: str.isEnabled, config: _.reduce(str.config, (result, value, key) => { _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null)) @@ -280,6 +292,10 @@ export default { */ async regenerateCertificates (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + await WIKI.auth.regenerateCertificates() return { responseResult: generateSuccess('Certificates have been regenerated successfully.') @@ -293,6 +309,10 @@ export default { */ async resetGuestUser (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + await WIKI.auth.resetGuestUser() return { responseResult: generateSuccess('Guest user has been reset successfully.') @@ -302,7 +322,28 @@ export default { } } }, + // ------------------------------------------------------------------ + // TYPE: AuthenticationActiveStrategy + // ------------------------------------------------------------------ AuthenticationActiveStrategy: { + config (obj, args, context) { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + return obj.config ?? {} + }, + allowedEmailRegex (obj, args, context) { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + return obj.allowedEmailRegex ?? '' + }, + autoEnrollGroups (obj, args, context) { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + return obj.autoEnrollGroups ?? [] + }, strategy (obj, args, context) { return _.find(WIKI.data.authentication, ['key', obj.module]) } diff --git a/server/graph/resolvers/page.mjs b/server/graph/resolvers/page.mjs index fbd4ce7376..804b1f57b2 100644 --- a/server/graph/resolvers/page.mjs +++ b/server/graph/resolvers/page.mjs @@ -483,7 +483,7 @@ export default { user: context.req.user }) return { - responseResult: generateSuccess('Page has been converted.') + operation: generateSuccess('Page has been converted.') } } catch (err) { return generateError(err) @@ -499,7 +499,7 @@ export default { user: context.req.user }) return { - responseResult: generateSuccess('Page has been moved.') + operation: generateSuccess('Page has been moved.') } } catch (err) { return generateError(err) @@ -515,7 +515,7 @@ export default { user: context.req.user }) return { - responseResult: generateSuccess('Page has been deleted.') + operation: generateSuccess('Page has been deleted.') } } catch (err) { return generateError(err) @@ -534,7 +534,7 @@ export default { throw new Error('This tag does not exist.') } return { - responseResult: generateSuccess('Tag has been deleted.') + operation: generateSuccess('Tag has been deleted.') } } catch (err) { return generateError(err) @@ -555,7 +555,7 @@ export default { throw new Error('This tag does not exist.') } return { - responseResult: generateSuccess('Tag has been updated successfully.') + operation: generateSuccess('Tag has been updated successfully.') } } catch (err) { return generateError(err) @@ -569,7 +569,7 @@ export default { await WIKI.db.pages.flushCache() WIKI.events.outbound.emit('flushCache') return { - responseResult: generateSuccess('Pages Cache has been flushed successfully.') + operation: generateSuccess('Pages Cache has been flushed successfully.') } } catch (err) { return generateError(err) @@ -582,7 +582,7 @@ export default { try { const count = await WIKI.db.pages.migrateToLocale(args) return { - responseResult: generateSuccess('Migrated content to target locale successfully.'), + operation: generateSuccess('Migrated content to target locale successfully.'), count } } catch (err) { @@ -596,7 +596,7 @@ export default { try { await WIKI.db.pages.rebuildTree() return { - responseResult: generateSuccess('Page tree rebuilt successfully.') + operation: generateSuccess('Page tree rebuilt successfully.') } } catch (err) { return generateError(err) @@ -613,7 +613,7 @@ export default { } await WIKI.db.pages.renderPage(page) return { - responseResult: generateSuccess('Page rendered successfully.') + operation: generateSuccess('Page rendered successfully.') } } catch (err) { return generateError(err) @@ -649,7 +649,7 @@ export default { }) return { - responseResult: generateSuccess('Page version restored successfully.') + operation: generateSuccess('Page version restored successfully.') } } catch (err) { return generateError(err) @@ -662,7 +662,7 @@ export default { try { await WIKI.db.pageHistory.purge(args.olderThan) return { - responseResult: generateSuccess('Page history purged successfully.') + operation: generateSuccess('Page history purged successfully.') } } catch (err) { return generateError(err) diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql index 54cc5dbc85..43049e71f1 100644 --- a/server/graph/schemas/authentication.graphql +++ b/server/graph/schemas/authentication.graphql @@ -106,14 +106,13 @@ type AuthenticationActiveStrategy { isEnabled: Boolean config: JSON selfRegistration: Boolean - domainWhitelist: [String] - autoEnrollGroups: [Int] + allowedEmailRegex: String + autoEnrollGroups: [UUID] } type AuthenticationSiteStrategy { id: UUID activeStrategy: AuthenticationActiveStrategy - order: Int isVisible: Boolean } @@ -146,8 +145,8 @@ input AuthenticationStrategyInput { order: Int! isEnabled: Boolean! selfRegistration: Boolean! - domainWhitelist: [String]! - autoEnrollGroups: [Int]! + allowedEmailRegex: String! + autoEnrollGroups: [UUID]! } type AuthenticationApiKey { diff --git a/server/locales/en.json b/server/locales/en.json index 9569eee7d4..22276f9346 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -66,16 +66,16 @@ "admin.audit.title": "Audit Log", "admin.auth.activeStrategies": "Active Strategies", "admin.auth.addStrategy": "Add Strategy", + "admin.auth.allowedEmailRegex": "Allowed Email Address Regex", + "admin.auth.allowedEmailRegexHint": "(optional) Only allow users to register with an email address that matches the regex expression.", "admin.auth.allowedWebOrigins": "Allowed Web Origins", "admin.auth.autoEnrollGroups": "Assign to group(s)", - "admin.auth.autoEnrollGroupsHint": "Automatically assign new users to these groups. New users are always added to the Users group regardless of this setting.", + "admin.auth.autoEnrollGroupsHint": "(optional) Automatically assign new users to these groups. New users are always added to the Users group regardless of this setting.", "admin.auth.callbackUrl": "Callback URL / Redirect URI", "admin.auth.configReference": "Configuration Reference", "admin.auth.configReferenceSubtitle": "Some strategies may require some configuration values to be set on your provider. These are provided for reference only and may not be needed by the current strategy.", "admin.auth.displayName": "Display Name", "admin.auth.displayNameHint": "The title shown to the end user for this authentication strategy.", - "admin.auth.domainsWhitelist": "Email Address Allowlist", - "admin.auth.domainsWhitelistHint": "Only allow users to register with an email address that matches the regex expression.", "admin.auth.enabled": "Enabled", "admin.auth.enabledForced": "This strategy cannot be disabled.", "admin.auth.enabledHint": "Should this strategy be available to sites for login.", diff --git a/server/models/authentication.mjs b/server/models/authentication.mjs index 380162d10c..f13d902ed8 100644 --- a/server/models/authentication.mjs +++ b/server/models/authentication.mjs @@ -34,12 +34,7 @@ export class Authentication extends Model { } static async getStrategies({ enabledOnly = false } = {}) { - const strategies = await WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {}) - return strategies.map(str => ({ - ...str, - domainWhitelist: get(str.domainWhitelist, 'v', []), - autoEnrollGroups: get(str.autoEnrollGroups, 'v', []) - })) + return WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {}) } static async refreshStrategiesFromDisk() { diff --git a/server/models/pages.mjs b/server/models/pages.mjs index e522a95b05..14681be9ef 100644 --- a/server/models/pages.mjs +++ b/server/models/pages.mjs @@ -1045,15 +1045,12 @@ export class Page extends Model { await WIKI.db.pages.deletePageFromCache(page.hash) WIKI.events.outbound.emit('deletePageFromCache', page.hash) - // -> Delete from Search Index - await WIKI.data.searchEngine.deleted(page) - // -> Delete from Storage if (!opts.skipStorage) { - await WIKI.db.storage.pageEvent({ - event: 'deleted', - page - }) + // await WIKI.db.storage.pageEvent({ + // event: 'deleted', + // page + // }) } // -> Reconnect Links @@ -1076,6 +1073,8 @@ export class Page extends Model { * @returns {Promise} Promise with no value */ static async reconnectLinks (opts) { + return + // TODO: fix this const pageHref = `/${opts.locale}/${opts.path}` let replaceArgs = { from: '', @@ -1142,20 +1141,6 @@ export class Page extends Model { } } - /** - * Rebuild page tree for new/updated/deleted page - * - * @returns {Promise} Promise with no value - */ - static async rebuildTree() { - const rebuildJob = await WIKI.scheduler.registerJob({ - name: 'rebuild-tree', - immediate: true, - worker: true - }) - return rebuildJob.finished - } - /** * Trigger the rendering of a page * diff --git a/ux/src/components/AuthLoginPanel.vue b/ux/src/components/AuthLoginPanel.vue index a2fc866c54..eaa2635297 100644 --- a/ux/src/components/AuthLoginPanel.vue +++ b/ux/src/components/AuthLoginPanel.vue @@ -461,7 +461,6 @@ async function fetchStrategies (showAll = false) { } selfRegistration } - order } } `, diff --git a/ux/src/pages/AdminAuth.vue b/ux/src/pages/AdminAuth.vue index 23ec0ea199..f1ce6bc817 100644 --- a/ux/src/pages/AdminAuth.vue +++ b/ux/src/pages/AdminAuth.vue @@ -171,15 +171,15 @@ q-page.admin-mail q-item blueprint-icon(icon='private') q-item-section - q-item-label {{t(`admin.auth.domainsWhitelist`)}} - q-item-label(caption) {{t(`admin.auth.domainsWhitelistHint`)}} + q-item-label {{t(`admin.auth.allowedEmailRegex`)}} + q-item-label(caption) {{t(`admin.auth.allowedEmailRegexHint`)}} q-item-section q-input( outlined - v-model='state.strategy.domainWhitelist' + v-model='state.strategy.allowedEmailRegex' dense hide-bottom-space - :aria-label='t(`admin.auth.domainsWhitelist`)' + :aria-label='t(`admin.auth.allowedEmailRegex`)' prefix='/' suffix='/' ) @@ -193,8 +193,8 @@ q-page.admin-mail q-banner.q-mt-md( v-if='!state.strategy.config || Object.keys(state.strategy.config).length < 1' rounded - :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`' - ) {{t('admin.auth.noConfigOption')}} + :class='$q.dark.isActive ? `bg-dark-4 text-grey-5` : `bg-grey-2 text-grey-7`' + ): em {{t('admin.auth.noConfigOption')}} template( v-for='(cfg, cfgKey, idx) in state.strategy.config' ) @@ -213,7 +213,7 @@ q-page.admin-mail color='primary' checked-icon='las la-check' unchecked-icon='las la-times' - :aria-label='t(`admin.general.allowComments`)' + :aria-label='cfg.title' :disable='cfg.readOnly' ) q-item(v-else) @@ -432,7 +432,7 @@ async function load () { isEnabled config selfRegistration - domainWhitelist + allowedEmailRegex autoEnrollGroups } } @@ -505,7 +505,7 @@ function addStrategy (str) { isEnabled: true, displayName: str.title, selfRegistration: true, - domainWhitelist: [], + allowedEmailRegex: '', autoEnrollGroups: [] } state.activeStrategies = [...state.activeStrategies, newStr] diff --git a/ux/src/pages/AdminLogin.vue b/ux/src/pages/AdminLogin.vue index 6fb7794b63..cbb1addec8 100644 --- a/ux/src/pages/AdminLogin.vue +++ b/ux/src/pages/AdminLogin.vue @@ -145,30 +145,30 @@ q-page.admin-login q-card-section .text-subtitle1 {{t('admin.login.providers')}} q-card-section.admin-login-providers.q-pt-none - draggable( - class='q-list rounded-borders' + sortable( + class='q-list' :list='state.providers' - :animation='150' - handle='.handle' - @end='dragStarted = false' item-key='id' + :options='sortableOptions' + @end='updateAuthPosition' ) template(#item='{element}') q-item q-item-section(side) - q-icon.handle(name='las la-bars') - blueprint-icon(:icon='element.icon') + q-icon.handle(name='mdi-drag-horizontal') + q-item-section(side) + q-icon(:name='`img:` + element.activeStrategy.strategy.icon') q-item-section - q-item-label {{element.label}} - q-item-label(caption) {{element.provider}} + q-item-label {{element.activeStrategy.displayName}} + q-item-label(caption) {{element.activeStrategy.strategy.title}} q-item-section(side) q-toggle( - v-model='element.isActive' + v-model='element.isVisible' color='primary' checked-icon='las la-check' unchecked-icon='las la-times' label='Visible' - :aria-label='element.label' + :aria-label='element.activeStrategy.displayName' ) q-item.q-pt-none q-item-section @@ -183,7 +183,7 @@ q-page.admin-login