Skip to content

Commit 807a3cc

Browse files
pringelmannbackportbot[bot]
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 78909ea commit 807a3cc

9 files changed

Lines changed: 91 additions & 48 deletions

File tree

apps/oauth2/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
9090
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
9191
import { loadState } from '@nextcloud/initial-state'
9292
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
93+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
9394
9495
export default {
9596
name: 'App',
@@ -140,6 +141,7 @@ export default {
140141
name: this.newClient.name,
141142
redirectUri: this.newClient.redirectUri,
142143
},
144+
{ confirmPassword: PwdConfirmationMode.Strict },
143145
).then(response => {
144146
// eslint-disable-next-line vue/no-mutating-props
145147
this.clients.push(response.data)

apps/oauth2/src/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@
2222
*
2323
*/
2424

25+
import axios from '@nextcloud/axios'
2526
import Vue from 'vue'
2627
import App from './App.vue'
2728
import { loadState } from '@nextcloud/initial-state'
29+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
2830

2931
Vue.prototype.t = t
3032
Vue.prototype.OC = OC
3133

34+
addPasswordConfirmationInterceptors(axios)
35+
3236
const clients = loadState('oauth2', 'clients')
3337

3438
const View = Vue.extend(App)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
1414
import { generateUrl } from '@nextcloud/router'
1515
import axios from '@nextcloud/axios'
1616
import { showError } from '@nextcloud/dialogs'
17+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
1718
import logger from '../../logger.ts'
1819
1920
export default {
@@ -55,7 +56,7 @@ export default {
5556
class: this.setting.class,
5657
}
5758
try {
58-
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data)
59+
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data, { confirmPassword: PwdConfirmationMode.Strict })
5960
} catch (e) {
6061
showError(t('settings', 'Unable to modify setting'))
6162
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
@@ -72,6 +72,7 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
7272
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
7373
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
7474
import { loadState } from '@nextcloud/initial-state'
75+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
7576
7677
import sortedUniq from 'lodash/sortedUniq.js'
7778
import uniq from 'lodash/uniq.js'
@@ -152,7 +153,7 @@ export default {
152153
enforcedGroups: this.enforcedGroups,
153154
excludedGroups: this.excludedGroups,
154155
}
155-
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data)
156+
axios.put(generateUrl('/settings/api/admin/twofactorauth'), data, { confirmPassword: PwdConfirmationMode.Strict })
156157
.then(resp => resp.data)
157158
.then(state => {
158159
this.state = state

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
*
2121
*/
2222

23+
import axios from '@nextcloud/axios'
24+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
2325
import Vue from 'vue'
2426
import App from './components/AdminDelegating.vue'
2527

28+
addPasswordConfirmationInterceptors(axios)
29+
2630
// bind to window
2731
Vue.prototype.OC = OC
2832
Vue.prototype.t = t

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@
2222
*
2323
*/
2424

25+
import axios from '@nextcloud/axios'
2526
import { loadState } from '@nextcloud/initial-state'
27+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
2628
import Vue from 'vue'
2729

2830
import AdminTwoFactor from './components/AdminTwoFactor.vue'
2931
import EncryptionSettings from './components/Encryption/EncryptionSettings.vue'
3032
import store from './store/admin-security.js'
3133

34+
addPasswordConfirmationInterceptors(axios)
35+
3236
// eslint-disable-next-line camelcase
3337
__webpack_nonce__ = btoa(OC.requestToken)
3438

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
*
2323
*/
2424

25+
import axios from '@nextcloud/axios'
26+
import { addPasswordConfirmationInterceptors } from '@nextcloud/password-confirmation'
2527
import Vue from 'vue'
2628
import VTooltip from 'v-tooltip'
2729
import { sync } from 'vuex-router-sync'

apps/settings/src/store/api.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ export default {
7171
get(url, options) {
7272
return axios.get(sanitize(url), options)
7373
},
74-
post(url, data) {
75-
return axios.post(sanitize(url), data)
74+
post(url, data, options) {
75+
return axios.post(sanitize(url), data, options)
7676
},
7777
patch(url, data) {
7878
return axios.patch(sanitize(url), data)

apps/settings/src/store/apps.js

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import axios from '@nextcloud/axios'
2828
import { generateUrl } from '@nextcloud/router'
2929
import { showError, showInfo } from '@nextcloud/dialogs'
3030
import { loadState } from '@nextcloud/initial-state'
31+
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
3132

3233
const state = {
3334
apps: [],
@@ -186,58 +187,82 @@ const actions = {
186187
} else {
187188
apps = [appId]
188189
}
189-
return api.requireAdmin().then((response) => {
190-
context.commit('startLoading', apps)
191-
context.commit('startLoading', 'install')
192-
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups })
193-
.then((response) => {
194-
context.commit('stopLoading', apps)
195-
context.commit('stopLoading', 'install')
196-
apps.forEach(_appId => {
197-
context.commit('enableApp', { appId: _appId, groups })
190+
context.commit('startLoading', apps)
191+
context.commit('startLoading', 'install')
192+
193+
const previousState = {}
194+
apps.forEach((_appId) => {
195+
const app = context.state.apps.find((app) => app.id === _appId)
196+
if (app) {
197+
previousState[_appId] = {
198+
active: app.active,
199+
groups: [...(app.groups || [])],
200+
}
201+
context.commit('enableApp', { appId: _appId, groups })
202+
}
203+
})
204+
205+
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups }, { confirmPassword: PwdConfirmationMode.Strict })
206+
.then((response) => {
207+
context.commit('stopLoading', apps)
208+
context.commit('stopLoading', 'install')
209+
210+
// check for server health
211+
return axios.get(generateUrl('apps/files/'))
212+
.then(() => {
213+
if (response.data.update_required) {
214+
showInfo(
215+
t(
216+
'settings',
217+
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
218+
),
219+
{
220+
onClick: () => window.location.reload(),
221+
close: false,
222+
223+
},
224+
)
225+
setTimeout(function() {
226+
location.reload()
227+
}, 5000)
228+
}
229+
})
230+
.catch(() => {
231+
if (!Array.isArray(appId)) {
232+
showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
233+
context.commit('setError', {
234+
appId: apps,
235+
error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
236+
})
237+
context.dispatch('disableApp', { appId })
238+
}
198239
})
240+
})
241+
.catch((error) => {
242+
context.commit('stopLoading', apps)
243+
context.commit('stopLoading', 'install')
199244

200-
// check for server health
201-
return axios.get(generateUrl('apps/files/'))
202-
.then(() => {
203-
if (response.data.update_required) {
204-
showInfo(
205-
t(
206-
'settings',
207-
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
208-
),
209-
{
210-
onClick: () => window.location.reload(),
211-
close: false,
212-
213-
},
214-
)
215-
setTimeout(function() {
216-
location.reload()
217-
}, 5000)
218-
}
219-
})
220-
.catch(() => {
221-
if (!Array.isArray(appId)) {
222-
showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
223-
context.commit('setError', {
224-
appId: apps,
225-
error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
226-
})
227-
context.dispatch('disableApp', { appId })
228-
}
245+
apps.forEach((_appId) => {
246+
if (previousState[_appId]) {
247+
context.commit('enableApp', {
248+
appId: _appId,
249+
groups: previousState[_appId].groups,
229250
})
251+
if (!previousState[_appId].active) {
252+
context.commit('disableApp', _appId)
253+
}
254+
}
230255
})
231-
.catch((error) => {
232-
context.commit('stopLoading', apps)
233-
context.commit('stopLoading', 'install')
256+
257+
const message = error.response?.data?.data?.message
258+
if (message) {
234259
context.commit('setError', {
235260
appId: apps,
236-
error: error.response.data.data.message,
261+
error: message,
237262
})
238263
context.commit('APPS_API_FAILURE', { appId, error })
239-
})
240-
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
264+
}
265+
})
241266
},
242267
forceEnableApp(context, { appId, groups }) {
243268
let apps

0 commit comments

Comments
 (0)