fix: various auth improvements + other fixes

pull/6775/head
NGPixel 1 year ago
parent 349f4e5730
commit 5a60fb11b5
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -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

@ -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

@ -16,9 +16,7 @@ defaults:
ssl: false
sslOptions:
auto: true
schemas:
wiki: wiki
scheduler: scheduler
schema: wiki
ssl:
enabled: false
pool:

@ -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
})
},
/**

@ -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()'))

@ -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])
}

@ -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)

@ -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 {

@ -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.",

@ -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() {

@ -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
*

@ -461,7 +461,6 @@ async function fetchStrategies (showAll = false) {
}
selfRegistration
}
order
}
}
`,

@ -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]

@ -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
<script setup>
import { cloneDeep } from 'lodash-es'
import gql from 'graphql-tag'
import draggable from 'vuedraggable'
import { Sortable } from 'sortablejs-vue3'
import { useI18n } from 'vue-i18n'
import { useMeta, useQuasar } from 'quasar'
@ -224,11 +224,18 @@ const state = reactive({
welcomeRedirect: '/',
logoutRedirect: '/'
},
providers: [
{ id: 'local', label: 'Local Authentication', provider: 'Username-Password', icon: 'database', isActive: true },
{ id: 'google', label: 'Google', provider: 'Google', icon: 'google', isActive: true },
{ id: 'slack', label: 'Slack', provider: 'Slack', icon: 'slack', isActive: false }
]
providers: []
})
const sortableOptions = {
handle: '.handle',
animation: 150
}
// WATCHERS
watch(() => adminStore.currentSiteId, (newValue) => {
load()
})
// METHODS
@ -236,24 +243,34 @@ const state = reactive({
async function load () {
state.loading++
$q.loading.show()
// const resp = await APOLLO_CLIENT.query({
// query: gql`
// query getSite (
// $id: UUID!
// ) {
// siteById(
// id: $id
// ) {
// id
// }
// }
// `,
// variables: {
// id: adminStore.currentSiteId
// },
// fetchPolicy: 'network-only'
// })
// this.config = cloneDeep(resp?.data?.siteById)
const resp = await APOLLO_CLIENT.query({
query: gql`
query getSiteAuthStrategies (
$siteId: UUID!
) {
authSiteStrategies(
siteId: $siteId
visibleOnly: false
) {
id
activeStrategy {
displayName
strategy {
key
title
icon
}
}
isVisible
}
}
`,
variables: {
siteId: adminStore.currentSiteId
},
fetchPolicy: 'network-only'
})
state.providers = cloneDeep(resp?.data?.authSiteStrategies)
$q.loading.hide()
state.loading--
}
@ -300,6 +317,11 @@ async function save () {
state.loading--
}
function updateAuthPosition (ev) {
const item = state.providers.splice(ev.oldIndex, 1)[0]
state.providers.splice(ev.newIndex, 0, item)
}
async function uploadBg () {
const input = document.createElement('input')
input.type = 'file'

Loading…
Cancel
Save