Skip to content

Commit a070301

Browse files
pringelmanncome-nc
authored andcommitted
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>
1 parent cfd5f04 commit a070301

9 files changed

Lines changed: 92 additions & 71 deletions

File tree

apps/oauth2/src/settings-admin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import axios from '@nextcloud/axios'
67
import { loadState } from '@nextcloud/initial-state'
8+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
79
import { createApp } from 'vue'
810
import AdminSettings from './views/AdminSettings.vue'
911

1012
import 'vite/modulepreload-polyfill'
1113

14+
addPasswordConfirmationInterceptors(axios)
15+
1216
const clients = loadState('oauth2', 'clients')
1317

1418
const app = createApp(AdminSettings, {

apps/oauth2/src/views/AdminSettings.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import axios, { isAxiosError } from '@nextcloud/axios'
88
import { getCapabilities } from '@nextcloud/capabilities'
99
import { loadState } from '@nextcloud/initial-state'
1010
import { t } from '@nextcloud/l10n'
11+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
1112
import { generateUrl } from '@nextcloud/router'
1213
import { ref } from 'vue'
1314
import NcButton from '@nextcloud/vue/components/NcButton'
@@ -56,7 +57,7 @@ async function addClient() {
5657
const { data } = await axios.post(generateUrl('apps/oauth2/clients'), {
5758
name: newClient.value.name,
5859
redirectUri: newClient.value.redirectUri,
59-
})
60+
}, { confirmPassword: PwdConfirmationMode.Strict })
6061
clients.value.push(data)
6162
showSecretWarning.value = true
6263

apps/settings/src/components/AdminDelegation/GroupSelect.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<script>
1818
import axios from '@nextcloud/axios'
1919
import { showError } from '@nextcloud/dialogs'
20+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
2021
import { generateUrl } from '@nextcloud/router'
2122
import NcSelect from '@nextcloud/vue/components/NcSelect'
2223
import logger from '../../logger.ts'
@@ -66,7 +67,7 @@ export default {
6667
class: this.setting.class,
6768
}
6869
try {
69-
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data)
70+
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data, { confirmPassword: PwdConfirmationMode.Strict })
7071
} catch (e) {
7172
showError(t('settings', 'Unable to modify setting'))
7273
logger.error('Unable to modify setting', e)

apps/settings/src/components/AdminTwoFactor.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
<script>
7878
import axios from '@nextcloud/axios'
7979
import { loadState } from '@nextcloud/initial-state'
80+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
8081
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
8182
import debounce from 'lodash/debounce.js'
8283
import sortedUniq from 'lodash/sortedUniq.js'
@@ -170,7 +171,7 @@ export default {
170171
enforcedGroups: this.enforcedGroups,
171172
excludedGroups: this.excludedGroups,
172173
}
173-
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data)
174+
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data, { confirmPassword: PwdConfirmationMode.Strict })
174175
.then((resp) => resp.data)
175176
.then((state) => {
176177
this.state = state

apps/settings/src/main-admin-delegation.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import axios from '@nextcloud/axios'
7+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
68
import Vue from 'vue'
79
import App from './components/AdminDelegating.vue'
810

11+
addPasswordConfirmationInterceptors(axios)
12+
913
// bind to window
1014
Vue.prototype.OC = OC
1115
Vue.prototype.t = t

apps/settings/src/main-admin-security.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import { getCSPNonce } from '@nextcloud/auth'
12
/**
23
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
34
* SPDX-License-Identifier: AGPL-3.0-or-later
45
*/
5-
import { getCSPNonce } from '@nextcloud/auth'
6+
import axios from '@nextcloud/axios'
67
import { loadState } from '@nextcloud/initial-state'
8+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
79
import Vue from 'vue'
810
import AdminTwoFactor from './components/AdminTwoFactor.vue'
911
import EncryptionSettings from './components/Encryption/EncryptionSettings.vue'
1012
import store from './store/admin-security.js'
1113

14+
addPasswordConfirmationInterceptors(axios)
15+
1216
__webpack_nonce__ = getCSPNonce()
1317

1418
Vue.prototype.t = t

apps/settings/src/main-apps-users-management.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
*/
55

66
import { getCSPNonce } from '@nextcloud/auth'
7+
import axios from '@nextcloud/axios'
78
import { n, t } from '@nextcloud/l10n'
9+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
810
import { createPinia, PiniaVuePlugin } from 'pinia'
911
import VTooltipPlugin from 'v-tooltip'
1012
import Vue from 'vue'
@@ -14,6 +16,8 @@ import SettingsApp from './views/SettingsApp.vue'
1416
import router from './router/index.ts'
1517
import { useStore } from './store/index.js'
1618

19+
addPasswordConfirmationInterceptors(axios)
20+
1721
// CSP config for webpack dynamic chunk loading
1822

1923
__webpack_nonce__ = getCSPNonce()

apps/settings/src/store/api.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export default {
5252
get(url, options) {
5353
return axios.get(sanitize(url), options)
5454
},
55-
post(url, data) {
56-
return axios.post(sanitize(url), data)
55+
post(url, data, options) {
56+
return axios.post(sanitize(url), data, options)
5757
},
5858
patch(url, data) {
5959
return axios.patch(sanitize(url), data)

apps/settings/src/store/apps.js

Lines changed: 67 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import axios from '@nextcloud/axios'
77
import { showError, showInfo } from '@nextcloud/dialogs'
88
import { loadState } from '@nextcloud/initial-state'
9+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
910
import { generateUrl } from '@nextcloud/router'
1011
import Vue from 'vue'
1112
import logger from '../logger.ts'
@@ -180,81 +181,82 @@ const actions = {
180181
} else {
181182
apps = [appId]
182183
}
183-
return api.requireAdmin().then(() => {
184-
context.commit('startLoading', apps)
185-
context.commit('startLoading', 'install')
186-
187-
const previousState = {}
188-
apps.forEach((_appId) => {
189-
const app = context.state.apps.find((app) => app.id === _appId)
190-
if (app) {
191-
previousState[_appId] = {
192-
active: app.active,
193-
groups: [...(app.groups || [])],
194-
}
195-
context.commit('enableApp', { appId: _appId, groups })
184+
context.commit('startLoading', apps)
185+
context.commit('startLoading', 'install')
186+
187+
const previousState = {}
188+
apps.forEach((_appId) => {
189+
const app = context.state.apps.find((app) => app.id === _appId)
190+
if (app) {
191+
previousState[_appId] = {
192+
active: app.active,
193+
groups: [...(app.groups || [])],
196194
}
197-
})
198-
199-
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups })
200-
.then((response) => {
201-
context.commit('stopLoading', apps)
202-
context.commit('stopLoading', 'install')
203-
204-
// check for server health
205-
return axios.get(generateUrl('apps/files/'))
206-
.then(() => {
207-
if (response.data.update_required) {
208-
showInfo(
209-
t(
210-
'settings',
211-
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
212-
),
213-
{
214-
onClick: () => window.location.reload(),
215-
close: false,
216-
217-
},
218-
)
219-
setTimeout(function() {
220-
location.reload()
221-
}, 5000)
222-
}
223-
})
224-
.catch(() => {
225-
if (!Array.isArray(appId)) {
226-
showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
227-
context.commit('setError', {
228-
appId: apps,
229-
error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
230-
})
231-
context.dispatch('disableApp', { appId })
232-
}
233-
})
234-
})
235-
.catch((error) => {
236-
context.commit('stopLoading', apps)
237-
context.commit('stopLoading', 'install')
195+
context.commit('enableApp', { appId: _appId, groups })
196+
}
197+
})
238198

239-
apps.forEach((_appId) => {
240-
if (previousState[_appId]) {
241-
context.commit('enableApp', {
242-
appId: _appId,
243-
groups: previousState[_appId].groups,
199+
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups }, { confirmPassword: PwdConfirmationMode.Strict })
200+
.then((response) => {
201+
context.commit('stopLoading', apps)
202+
context.commit('stopLoading', 'install')
203+
204+
// check for server health
205+
return axios.get(generateUrl('apps/files/'))
206+
.then(() => {
207+
if (response.data.update_required) {
208+
showInfo(
209+
t(
210+
'settings',
211+
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
212+
),
213+
{
214+
onClick: () => window.location.reload(),
215+
close: false,
216+
217+
},
218+
)
219+
setTimeout(function() {
220+
location.reload()
221+
}, 5000)
222+
}
223+
})
224+
.catch(() => {
225+
if (!Array.isArray(appId)) {
226+
showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
227+
context.commit('setError', {
228+
appId: apps,
229+
error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
244230
})
245-
if (!previousState[_appId].active) {
246-
context.commit('disableApp', _appId)
247-
}
231+
context.dispatch('disableApp', { appId })
248232
}
249233
})
234+
})
235+
.catch((error) => {
236+
context.commit('stopLoading', apps)
237+
context.commit('stopLoading', 'install')
238+
239+
apps.forEach((_appId) => {
240+
if (previousState[_appId]) {
241+
context.commit('enableApp', {
242+
appId: _appId,
243+
groups: previousState[_appId].groups,
244+
})
245+
if (!previousState[_appId].active) {
246+
context.commit('disableApp', _appId)
247+
}
248+
}
249+
})
250250

251+
const message = error.response?.data?.data?.message
252+
if (message) {
251253
context.commit('setError', {
252254
appId: apps,
253-
error: error.response.data.data.message,
255+
error: message,
254256
})
255257
context.commit('APPS_API_FAILURE', { appId, error })
256-
})
257-
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
258+
}
259+
})
258260
},
259261
forceEnableApp(context, { appId }) {
260262
let apps

0 commit comments

Comments
 (0)