Skip to content

Commit 9d0ddc9

Browse files
Merge pull request #50932 from nextcloud/backport/50910/stable31
[stable31] fix(files_external): request strict password auth on credentials enter action
2 parents a157b65 + 2b875bb commit 9d0ddc9

30 files changed

+287
-70
lines changed

apps/files_external/src/actions/enterCredentialsAction.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { AxiosResponse } from '@nextcloud/axios'
77
import type { Node } from '@nextcloud/files'
88
import type { StorageConfig } from '../services/externalStorage'
99

10+
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
1011
import { generateUrl } from '@nextcloud/router'
1112
import { showError, showSuccess, spawnDialog } from '@nextcloud/dialogs'
1213
import { translate as t } from '@nextcloud/l10n'
@@ -18,6 +19,10 @@ import { FileAction, DefaultType } from '@nextcloud/files'
1819
import { STORAGE_STATUS, isMissingAuthConfig } from '../utils/credentialsUtils'
1920
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
2021

22+
// Add password confirmation interceptors as
23+
// the backend requires the user to confirm their password
24+
addPasswordConfirmationInterceptors(axios)
25+
2126
type CredentialResponse = {
2227
login?: string,
2328
password?: string,
@@ -31,8 +36,13 @@ type CredentialResponse = {
3136
* @param password The password
3237
*/
3338
async function setCredentials(node: Node, login: string, password: string): Promise<null|true> {
34-
const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', { id: node.attributes.id }), {
35-
backendOptions: { user: login, password },
39+
const configResponse = await axios.request({
40+
method: 'PUT',
41+
url: generateUrl('apps/files_external/userglobalstorages/{id}', { id: node.attributes.id }),
42+
confirmPassword: PwdConfirmationMode.Strict,
43+
data: {
44+
backendOptions: { user: login, password },
45+
},
3646
}) as AxiosResponse<StorageConfig>
3747

3848
const config = configResponse.data
@@ -49,8 +59,10 @@ async function setCredentials(node: Node, login: string, password: string): Prom
4959
return true
5060
}
5161

62+
export const ACTION_CREDENTIALS_EXTERNAL_STORAGE = 'credentials-external-storage'
63+
5264
export const action = new FileAction({
53-
id: 'credentials-external-storage',
65+
id: ACTION_CREDENTIALS_EXTERNAL_STORAGE,
5466
displayName: () => t('files', 'Enter missing credentials'),
5567
iconSvgInline: () => LoginSvg,
5668

@@ -83,7 +95,14 @@ export const action = new FileAction({
8395
))
8496

8597
if (login && password) {
86-
return await setCredentials(node, login, password)
98+
try {
99+
await setCredentials(node, login, password)
100+
showSuccess(t('files_external', 'Credentials successfully set'))
101+
} catch (error) {
102+
showError(t('files_external', 'Error while setting credentials: {error}', {
103+
error: (error as Error).message,
104+
}))
105+
}
87106
}
88107

89108
return null

apps/files_external/src/actions/inlineStorageCheckAction.ts

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import type { AxiosError } from '@nextcloud/axios'
77
import type { Node } from '@nextcloud/files'
88

9+
import { FileAction } from '@nextcloud/files'
910
import { showWarning } from '@nextcloud/dialogs'
1011
import { translate as t } from '@nextcloud/l10n'
1112
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
@@ -15,7 +16,6 @@ import '../css/fileEntryStatus.scss'
1516
import { getStatus, type StorageConfig } from '../services/externalStorage'
1617
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils'
1718
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
18-
import { FileAction } from '@nextcloud/files'
1919

2020
export const action = new FileAction({
2121
id: 'check-external-storage',
@@ -34,45 +34,51 @@ export const action = new FileAction({
3434
* @param node The node to render inline
3535
*/
3636
async renderInline(node: Node) {
37+
const span = document.createElement('span')
38+
span.className = 'files-list__row-status'
39+
span.innerHTML = t('files_external', 'Checking storage …')
40+
3741
let config = null as unknown as StorageConfig
38-
try {
39-
const response = await getStatus(node.attributes.id, node.attributes.scope === 'system')
40-
config = response.data
41-
Vue.set(node.attributes, 'config', config)
42+
getStatus(node.attributes.id, node.attributes.scope === 'system')
43+
.then(response => {
44+
45+
config = response.data
46+
Vue.set(node.attributes, 'config', config)
47+
48+
if (config.status !== STORAGE_STATUS.SUCCESS) {
49+
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
50+
}
4251

43-
if (config.status !== STORAGE_STATUS.SUCCESS) {
44-
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
45-
}
52+
span.remove()
53+
})
54+
.catch(error => {
55+
// If axios failed or if something else prevented
56+
// us from getting the config
57+
if ((error as AxiosError).response && !config) {
58+
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
59+
basename: node.basename,
60+
}))
61+
}
4662

47-
return null
48-
} catch (error) {
49-
// If axios failed or if something else prevented
50-
// us from getting the config
51-
if ((error as AxiosError).response && !config) {
52-
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
53-
basename: node.basename,
54-
}))
55-
return null
56-
}
63+
// Reset inline status
64+
span.innerHTML = ''
5765

58-
// Checking if we really have an error
59-
const isWarning = isMissingAuthConfig(config)
60-
const overlay = document.createElement('span')
61-
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
66+
// Checking if we really have an error
67+
const isWarning = !config ? false : isMissingAuthConfig(config)
68+
const overlay = document.createElement('span')
69+
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
6270

63-
const span = document.createElement('span')
64-
span.className = 'files-list__row-status'
71+
// Only show an icon for errors, warning like missing credentials
72+
// have a dedicated inline action button
73+
if (!isWarning) {
74+
span.innerHTML = AlertSvg
75+
span.title = (error as Error).message
76+
}
6577

66-
// Only show an icon for errors, warning like missing credentials
67-
// have a dedicated inline action button
68-
if (!isWarning) {
69-
span.innerHTML = AlertSvg
70-
span.title = (error as Error).message
71-
}
78+
span.prepend(overlay)
79+
})
7280

73-
span.prepend(overlay)
74-
return span
75-
}
81+
return span
7682
},
7783

7884
order: 10,

apps/files_external/src/actions/openInFilesAction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { translate as t } from '@nextcloud/l10n'
1010

1111
import { FileAction, DefaultType } from '@nextcloud/files'
1212
import { STORAGE_STATUS } from '../utils/credentialsUtils'
13+
import { getCurrentUser } from '@nextcloud/auth'
1314

1415
export const action = new FileAction({
1516
id: 'open-in-files-external-storage',
@@ -32,7 +33,7 @@ export const action = new FileAction({
3233
t('files_external', 'External mount error'),
3334
(redirect) => {
3435
if (redirect === true) {
35-
const scope = node.attributes.scope === 'personal' ? 'user' : 'admin'
36+
const scope = getCurrentUser()?.isAdmin ? 'admin' : 'user'
3637
window.location.href = generateUrl(`/settings/${scope}/externalstorages`)
3738
}
3839
},

apps/files_external/src/css/fileEntryStatus.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
*/
55
.files-list__row-status {
66
display: flex;
7-
width: 44px;
7+
min-width: 44px;
88
justify-content: center;
99
align-items: center;
1010
height: 100%;
11+
text-overflow: ellipsis;
12+
white-space: nowrap;
13+
overflow: hidden;
1114

1215
svg {
1316
width: 24px;

apps/files_external/src/views/CredentialsDialog.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default defineComponent({
7676
computed: {
7777
dialogButtons() {
7878
return [{
79-
label: t('files_external', 'Submit'),
79+
label: t('files_external', 'Confirm'),
8080
type: 'primary',
8181
nativeType: 'submit',
8282
}]

apps/systemtags/src/components/SystemTagPicker.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ import type { Tag, TagWithId } from '../types'
128128
import { defineComponent } from 'vue'
129129
import { emit } from '@nextcloud/event-bus'
130130
import { getLanguage, n, t } from '@nextcloud/l10n'
131-
import { sanitize } from 'dompurify'
132131
import { showError, showInfo } from '@nextcloud/dialogs'
133132
import debounce from 'debounce'
133+
import domPurify from 'dompurify'
134134
import escapeHTML from 'escape-html'
135135
136136
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
@@ -378,7 +378,7 @@ export default defineComponent({
378378
})
379379
}
380380
const chipHtml = chipCloneEl.outerHTML
381-
return chipHtml.replace('%s', escapeHTML(sanitize(tag.displayName)))
381+
return chipHtml.replace('%s', escapeHTML(domPurify.sanitize(tag.displayName)))
382382
},
383383
384384
formatTagName(tag: TagWithId): string {

cypress/dockerNode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export const startNextcloud = async function(branch: string = getCurrentGitBranc
4949
reject(err)
5050
}
5151
}))
52+
53+
const digest = await (await docker.getImage(SERVER_IMAGE).inspect()).RepoDigests.at(0)
54+
const sha = digest?.split('@').at(1)
55+
console.log('├─ Using image ' + sha)
5256
console.log('└─ Done')
5357
} catch (e) {
5458
console.log('└─ Failed to pull images')

cypress/e2e/files/FilesUtils.ts

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

66
import type { User } from '@nextcloud/cypress'
7-
import { ACTION_COPY_MOVE } from '../../../apps/files/src/actions/moveOrCopyAction.ts'
7+
import { ACTION_COPY_MOVE } from "../../../apps/files/src/actions/moveOrCopyAction"
88

99
export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
1010
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
@@ -15,7 +15,7 @@ export const getActionsForFile = (filename: string) => getRowForFile(filename).f
1515
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
1616
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
1717

18-
export const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
18+
const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
1919
const action = row.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
2020
if (action.length > 0) {
2121
cy.log('Found action in row')
@@ -27,12 +27,15 @@ export const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string)
2727
return cy.get(`#${menuButtonId} [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
2828
}
2929

30-
export const getActionEntryForFileId = (fileid: number, actionId: string) => {
31-
return cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
30+
export const getActionEntryForFileId = (fileid: number, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
31+
// If we cannot find the action in the row, it might be in the action menu
32+
return getRowForFileId(fileid).should('be.visible')
33+
.then(row => searchForActionInRow(row, actionId))
3234
}
33-
34-
export const getActionEntryForFile = (filename: string, actionId: string) => {
35-
return cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
35+
export const getActionEntryForFile = (filename: string, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
36+
// If we cannot find the action in the row, it might be in the action menu
37+
return getRowForFile(filename).should('be.visible')
38+
.then(row => searchForActionInRow(row, actionId))
3639
}
3740

3841
export const triggerActionForFileId = (fileid: number, actionId: string) => {
@@ -174,10 +177,8 @@ export const navigateToFolder = (dirPath: string) => {
174177
if (directory === '') {
175178
continue
176179
}
177-
178180
getRowForFile(directory).should('be.visible').find('[data-cy-files-list-row-name-link]').click()
179181
}
180-
181182
}
182183

183184
export const closeSidebar = () => {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { User } from "@nextcloud/cypress"
7+
8+
export type StorageConfig = {
9+
[key: string]: string
10+
}
11+
12+
export enum StorageBackend {
13+
DAV = 'dav',
14+
SMB = 'smb',
15+
SFTP = 'sftp',
16+
}
17+
18+
export enum AuthBackend {
19+
GlobalAuth = 'password::global',
20+
LoginCredentials = 'password::logincredentials',
21+
Password = 'password::password',
22+
SessionCredentials = 'password::sessioncredentials',
23+
UserGlobalAuth = 'password::global::user',
24+
UserProvided = 'password::userprovided',
25+
}
26+
27+
/**
28+
* Create a storage via occ
29+
*/
30+
export function createStorageWithConfig(mountPoint: string, storageBackend: StorageBackend, authBackend: AuthBackend, configs: StorageConfig, user?: User): Cypress.Chainable {
31+
const configsFlag = Object.keys(configs).map(key => `--config "${key}=${configs[key]}"`).join(' ')
32+
const userFlag = user ? `--user ${user.userId}` : ''
33+
34+
const command = `files_external:create "${mountPoint}" "${storageBackend}" "${authBackend}" ${configsFlag} ${userFlag}`
35+
36+
cy.log(`Creating storage with command: ${command}`)
37+
return cy.runOccCommand(command)
38+
}

0 commit comments

Comments
 (0)