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>
pull/59764/head
Peter Ringelmann 5 months ago committed by Côme Chilliet
parent cfd5f04116
commit a070301443
No known key found for this signature in database
GPG Key ID: A3E2F658B28C760A
  1. 4
      apps/oauth2/src/settings-admin.ts
  2. 3
      apps/oauth2/src/views/AdminSettings.vue
  3. 3
      apps/settings/src/components/AdminDelegation/GroupSelect.vue
  4. 3
      apps/settings/src/components/AdminTwoFactor.vue
  5. 4
      apps/settings/src/main-admin-delegation.js
  6. 6
      apps/settings/src/main-admin-security.js
  7. 4
      apps/settings/src/main-apps-users-management.ts
  8. 4
      apps/settings/src/store/api.js
  9. 132
      apps/settings/src/store/apps.js

@ -3,12 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import axios from '@nextcloud/axios'
import { loadState } from '@nextcloud/initial-state'
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
import { createApp } from 'vue'
import AdminSettings from './views/AdminSettings.vue'
import 'vite/modulepreload-polyfill'
addPasswordConfirmationInterceptors(axios)
const clients = loadState('oauth2', 'clients')
const app = createApp(AdminSettings, {

@ -8,6 +8,7 @@ import axios, { isAxiosError } from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import { ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
@ -56,7 +57,7 @@ async function addClient() {
const { data } = await axios.post(generateUrl('apps/oauth2/clients'), {
name: newClient.value.name,
redirectUri: newClient.value.redirectUri,
})
}, { confirmPassword: PwdConfirmationMode.Strict })
clients.value.push(data)
showSecretWarning.value = true

@ -17,6 +17,7 @@
<script>
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import logger from '../../logger.ts'
@ -66,7 +67,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)

@ -77,6 +77,7 @@
<script>
import axios from '@nextcloud/axios'
import { loadState } from '@nextcloud/initial-state'
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
import debounce from 'lodash/debounce.js'
import sortedUniq from 'lodash/sortedUniq.js'
@ -170,7 +171,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,14 +1,18 @@
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)
__webpack_nonce__ = getCSPNonce()
Vue.prototype.t = t

@ -4,7 +4,9 @@
*/
import { getCSPNonce } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import { n, t } from '@nextcloud/l10n'
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
import { createPinia, PiniaVuePlugin } from 'pinia'
import VTooltipPlugin from 'v-tooltip'
import Vue from 'vue'
@ -14,6 +16,8 @@ import SettingsApp from './views/SettingsApp.vue'
import router from './router/index.ts'
import { useStore } from './store/index.js'
addPasswordConfirmationInterceptors(axios)
// CSP config for webpack dynamic chunk loading
__webpack_nonce__ = getCSPNonce()

@ -52,8 +52,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)

@ -6,6 +6,7 @@
import axios from '@nextcloud/axios'
import { showError, showInfo } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import Vue from 'vue'
import logger from '../logger.ts'
@ -180,81 +181,82 @@ const actions = {
} else {
apps = [appId]
}
return api.requireAdmin().then(() => {
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 })
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 || [])],
}
})
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups })
.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,
},
)
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')
context.commit('enableApp', { appId: _appId, groups })
}
})
apps.forEach((_appId) => {
if (previousState[_appId]) {
context.commit('enableApp', {
appId: _appId,
groups: previousState[_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,
},
)
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'),
})
if (!previousState[_appId].active) {
context.commit('disableApp', _appId)
}
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 }) {
let apps

Loading…
Cancel
Save