mirror of
https://github.com/nextcloud/server.git
synced 2026-05-22 10:06:37 -04:00
fix(frontend): add strict password confirmation for sensitive admin actions
Register axios password confirmation interceptors in the apps management, admin delegation, admin security, and OAuth2 settings bundles, and pass PwdConfirmationMode.Strict on requests to endpoints protected with #[PasswordConfirmationRequired(strict: true)], so that the user password is verified via Basic auth on the request itself rather than relying on the session timestamp. Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
This commit is contained in:
parent
0ea55bad6f
commit
058a60774e
9 changed files with 90 additions and 71 deletions
|
|
@ -73,6 +73,7 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
|||
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
|
@ -123,6 +124,7 @@ export default {
|
|||
name: this.newClient.name,
|
||||
redirectUri: this.newClient.redirectUri,
|
||||
},
|
||||
{ confirmPassword: PwdConfirmationMode.Strict },
|
||||
).then(response => {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.clients.push(response.data)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
|
||||
|
||||
Vue.prototype.t = t
|
||||
Vue.prototype.OC = OC
|
||||
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
const clients = loadState('oauth2', 'clients')
|
||||
|
||||
const View = Vue.extend(App)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
|
|||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
import logger from '../../logger.ts'
|
||||
|
||||
export default {
|
||||
|
|
@ -59,7 +60,7 @@ export default {
|
|||
class: this.setting.class,
|
||||
}
|
||||
try {
|
||||
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data)
|
||||
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data, { confirmPassword: PwdConfirmationMode.Strict })
|
||||
} catch (e) {
|
||||
showError(t('settings', 'Unable to modify setting'))
|
||||
logger.error('Unable to modify setting', e)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
|||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
|
||||
import sortedUniq from 'lodash/sortedUniq.js'
|
||||
import uniq from 'lodash/uniq.js'
|
||||
|
|
@ -156,7 +157,7 @@ export default {
|
|||
enforcedGroups: this.enforcedGroups,
|
||||
excludedGroups: this.excludedGroups,
|
||||
}
|
||||
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data)
|
||||
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data, { confirmPassword: PwdConfirmationMode.Strict })
|
||||
.then(resp => resp.data)
|
||||
.then(state => {
|
||||
this.state = state
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
|
||||
import Vue from 'vue'
|
||||
import App from './components/AdminDelegating.vue'
|
||||
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
// bind to window
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.t = t
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
|
||||
import Vue from 'vue'
|
||||
|
||||
import AdminTwoFactor from './components/AdminTwoFactor.vue'
|
||||
import EncryptionSettings from './components/Encryption/EncryptionSettings.vue'
|
||||
import store from './store/admin-security.js'
|
||||
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import VTooltipPlugin from 'v-tooltip'
|
||||
|
|
@ -15,6 +17,8 @@ import { useStore } from './store/index.js'
|
|||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import { PiniaVuePlugin, createPinia } from 'pinia'
|
||||
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
// CSP config for webpack dynamic chunk loading
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ export default {
|
|||
get(url, options) {
|
||||
return axios.get(sanitize(url), options)
|
||||
},
|
||||
post(url, data) {
|
||||
return axios.post(sanitize(url), data)
|
||||
post(url, data, options) {
|
||||
return axios.post(sanitize(url), data, options)
|
||||
},
|
||||
patch(url, data) {
|
||||
return axios.patch(sanitize(url), data)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import axios from '@nextcloud/axios'
|
|||
import { generateUrl } from '@nextcloud/router'
|
||||
import { showError, showInfo } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
|
||||
const state = {
|
||||
apps: [],
|
||||
|
|
@ -180,84 +181,82 @@ const actions = {
|
|||
} else {
|
||||
apps = [appId]
|
||||
}
|
||||
return api.requireAdmin().then((response) => {
|
||||
context.commit('startLoading', apps)
|
||||
context.commit('startLoading', 'install')
|
||||
context.commit('startLoading', apps)
|
||||
context.commit('startLoading', 'install')
|
||||
|
||||
const previousState = {}
|
||||
apps.forEach((_appId) => {
|
||||
const app = context.state.apps.find((app) => app.id === _appId)
|
||||
if (app) {
|
||||
previousState[_appId] = {
|
||||
active: app.active,
|
||||
groups: [...(app.groups || [])],
|
||||
}
|
||||
context.commit('enableApp', { appId: _appId, groups })
|
||||
const previousState = {}
|
||||
apps.forEach((_appId) => {
|
||||
const app = context.state.apps.find((app) => app.id === _appId)
|
||||
if (app) {
|
||||
previousState[_appId] = {
|
||||
active: app.active,
|
||||
groups: [...(app.groups || [])],
|
||||
}
|
||||
})
|
||||
context.commit('enableApp', { appId: _appId, groups })
|
||||
}
|
||||
})
|
||||
|
||||
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups })
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
apps.forEach(_appId => {
|
||||
context.commit('enableApp', { appId: _appId, groups })
|
||||
})
|
||||
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups }, { confirmPassword: PwdConfirmationMode.Strict })
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
|
||||
// check for server health
|
||||
return axios.get(generateUrl('apps/files/'))
|
||||
.then(() => {
|
||||
if (response.data.update_required) {
|
||||
showInfo(
|
||||
t(
|
||||
'settings',
|
||||
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
|
||||
),
|
||||
{
|
||||
onClick: () => window.location.reload(),
|
||||
close: false,
|
||||
// check for server health
|
||||
return axios.get(generateUrl('apps/files/'))
|
||||
.then(() => {
|
||||
if (response.data.update_required) {
|
||||
showInfo(
|
||||
t(
|
||||
'settings',
|
||||
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
|
||||
),
|
||||
{
|
||||
onClick: () => window.location.reload(),
|
||||
close: false,
|
||||
|
||||
},
|
||||
)
|
||||
setTimeout(function() {
|
||||
location.reload()
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!Array.isArray(appId)) {
|
||||
showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
|
||||
})
|
||||
context.dispatch('disableApp', { appId })
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
|
||||
apps.forEach((_appId) => {
|
||||
if (previousState[_appId]) {
|
||||
context.commit('enableApp', {
|
||||
appId: _appId,
|
||||
groups: previousState[_appId].groups,
|
||||
})
|
||||
if (!previousState[_appId].active) {
|
||||
context.commit('disableApp', _appId)
|
||||
}
|
||||
},
|
||||
)
|
||||
setTimeout(function() {
|
||||
location.reload()
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!Array.isArray(appId)) {
|
||||
showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
|
||||
})
|
||||
context.dispatch('disableApp', { appId })
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
|
||||
apps.forEach((_appId) => {
|
||||
if (previousState[_appId]) {
|
||||
context.commit('enableApp', {
|
||||
appId: _appId,
|
||||
groups: previousState[_appId].groups,
|
||||
})
|
||||
if (!previousState[_appId].active) {
|
||||
context.commit('disableApp', _appId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const message = error.response?.data?.data?.message
|
||||
if (message) {
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: error.response.data.data.message,
|
||||
error: message,
|
||||
})
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
|
||||
}
|
||||
})
|
||||
},
|
||||
forceEnableApp(context, { appId, groups }) {
|
||||
let apps
|
||||
|
|
|
|||
Loading…
Reference in a new issue