Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ module.exports = {
jsdoc: {
mode: 'typescript',
},
'import/resolver': {
typescript: {}, // this loads <rootdir>/tsconfig.json to eslint
},
},
overrides: [
// Allow any in tests
Expand All @@ -43,6 +46,6 @@ module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
},
}
},
],
}
3 changes: 1 addition & 2 deletions apps/settings/lib/Settings/Personal/Security/WebAuthn.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ public function getForm() {
$this->mapper->findAllForUid($this->userId)
);

return new TemplateResponse('settings', 'settings/personal/security/webauthn', [
]);
return new TemplateResponse('settings', 'settings/personal/security/webauthn');
}

public function getSection(): ?string {
Expand Down
4 changes: 1 addition & 3 deletions apps/settings/lib/SetupChecks/PhpModules.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class PhpModules implements ISetupCheck {
'zlib',
];
protected const RECOMMENDED_MODULES = [
'bcmath',
'exif',
'gmp',
'intl',
Expand All @@ -58,8 +57,7 @@ protected function getRecommendedModuleDescription(string $module): string {
return match($module) {
'intl' => $this->l10n->t('increases language translation performance and fixes sorting of non-ASCII characters'),
'sodium' => $this->l10n->t('for Argon2 for password hashing'),
'bcmath' => $this->l10n->t('for WebAuthn passwordless login'),
'gmp' => $this->l10n->t('for WebAuthn passwordless login, and SFTP storage'),
'gmp' => $this->l10n->t('required for SFTP storage and recommended for WebAuthn performance'),
'exif' => $this->l10n->t('for picture rotation in server and metadata extraction in the Photos app'),
default => '',
};
Expand Down
6 changes: 3 additions & 3 deletions apps/settings/src/service/WebAuthnRegistrationSerice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { RegistrationResponseJSON } from '@simplewebauthn/types'
import type { PublicKeyCredentialCreationOptionsJSON, RegistrationResponseJSON } from '@simplewebauthn/types'

import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
Expand All @@ -21,9 +21,9 @@ export async function startRegistration() {

try {
logger.debug('Fetching webauthn registration data')
const { data } = await axios.get(url)
const { data } = await axios.get<PublicKeyCredentialCreationOptionsJSON>(url)
logger.debug('Start webauthn registration')
const attrs = await registerWebAuthn(data)
const attrs = await registerWebAuthn({ optionsJSON: data })
return attrs
} catch (e) {
logger.error(e as Error)
Expand Down
14 changes: 10 additions & 4 deletions core/src/components/login/PasswordLessLoginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
<template>
<form v-if="(isHttps || isLocalhost) && supportsWebauthn"
ref="loginForm"
aria-labelledby="password-less-login-form-title"
class="password-less-login-form"
method="post"
name="login"
@submit.prevent="submit">
<h2>{{ t('core', 'Log in with a device') }}</h2>
<h2 id="password-less-login-form-title">
{{ t('core', 'Log in with a device') }}
</h2>

<NcTextField required
:value="user"
:autocomplete="autoCompleteAllowed ? 'on' : 'off'"
Expand Down Expand Up @@ -41,9 +45,11 @@
</NcEmptyContent>
</template>

<script>
<script type="ts">
import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
import { defineComponent } from 'vue'
import {
NoValidCredentials,
startAuthentication,
finishAuthentication,
} from '../../services/WebAuthnAuthenticationService.ts'
Expand All @@ -56,7 +62,7 @@ import LoginButton from './LoginButton.vue'
import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue'
import logger from '../../logger'

export default {
export default defineComponent({
name: 'PasswordLessLoginForm',
components: {
LoginButton,
Expand Down Expand Up @@ -143,7 +149,7 @@ export default {
// noop
},
},
}
})
</script>

<style lang="scss" scoped>
Expand Down
2 changes: 1 addition & 1 deletion core/src/services/WebAuthnAuthenticationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function startAuthentication(loginName: string) {
logger.error('No valid credentials returned for webauthn')
throw new NoValidCredentials()
}
return await startWebauthnAuthentication(data)
return await startWebauthnAuthentication({ optionsJSON: data })
}

/**
Expand Down
14 changes: 12 additions & 2 deletions cypress/dockerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export const startNextcloud = async function(branch: string = getCurrentGitBranc
Type: 'tmpfs',
ReadOnly: false,
}],
PortBindings: {
'80/tcp': [{
HostIP: '0.0.0.0',
HostPort: '8083',
}],
},
},
Env: [
`BRANCH=${branch}`,
Expand Down Expand Up @@ -242,11 +248,15 @@ export const getContainerIP = async function(
while (ip === '' && tries < 10) {
tries++

await container.inspect(function(err, data) {
container.inspect(function(err, data) {
if (err) {
throw err
}
ip = data?.NetworkSettings?.IPAddress || ''
if (data?.HostConfig.PortBindings?.['80/tcp']?.[0]?.HostPort) {
ip = `localhost:${data.HostConfig.PortBindings['80/tcp'][0].HostPort}`
} else {
ip = data?.NetworkSettings?.IPAddress || ''
}
})

if (ip !== '') {
Expand Down
41 changes: 16 additions & 25 deletions cypress/e2e/files/LivePhotosUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,25 @@ type SetupInfo = {
}

/**
*
* @param user
* @param fileName
* @param domain
* @param requesttoken
* @param metadata
*/
function setMetadata(user: User, fileName: string, requesttoken: string, metadata: object) {
cy.url().then(url => {
const hostname = new URL(url).hostname
cy.request({
method: 'PROPPATCH',
url: `http://${hostname}/remote.php/dav/files/${user.userId}/${fileName}`,
auth: { user: user.userId, pass: user.password },
headers: {
requesttoken,
},
body: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
</d:prop>
</d:set>
</d:propertyupdate>`,
})
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?/, '')
cy.request({
method: 'PROPPATCH',
url: `${base}/remote.php/dav/files/${user.userId}/${fileName}`,
auth: { user: user.userId, pass: user.password },
headers: {
requesttoken,
},
body: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
<d:set>
<d:prop>
${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
</d:prop>
</d:set>
</d:propertyupdate>`,
})

}

/**
Expand Down
22 changes: 13 additions & 9 deletions cypress/e2e/files_external/files-user-credentials.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ describe('Files user credentials', { testIsolation: true }, () => {
let user2: User
let storageUser: User

beforeEach(() => {
})

before(() => {
cy.runOccCommand('app:enable files_external')

Expand All @@ -43,8 +40,10 @@ describe('Files user credentials', { testIsolation: true }, () => {
})

it('Create a user storage with user credentials', () => {
const url = Cypress.config('baseUrl') + '/remote.php/dav/files/' + storageUser.userId
createStorageWithConfig(storageUser.userId, StorageBackend.DAV, AuthBackend.UserProvided, { host: url.replace('index.php/', ''), secure: 'false' })
// Its not the public server address but the address so the server itself can connect to it
const base = 'http://localhost'
const host = `${base}/remote.php/dav/files/${storageUser.userId}`
createStorageWithConfig(storageUser.userId, StorageBackend.DAV, AuthBackend.UserProvided, { host, secure: 'false' })

cy.login(user1)
cy.visit('/apps/files/extstoragemounts')
Expand Down Expand Up @@ -72,6 +71,7 @@ describe('Files user credentials', { testIsolation: true }, () => {

// Auth dialog should be closed and the set credentials button should be gone
cy.get('@authDialog').should('not.exist', { timeout: 2000 })

getActionEntryForFile(storageUser.userId, ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')

// Finally, the storage should be accessible
Expand All @@ -81,8 +81,10 @@ describe('Files user credentials', { testIsolation: true }, () => {
})

it('Create a user storage with GLOBAL user credentials', () => {
const url = Cypress.config('baseUrl') + '/remote.php/dav/files/' + storageUser.userId
createStorageWithConfig('storage1', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host: url.replace('index.php/', ''), secure: 'false' })
// Its not the public server address but the address so the server itself can connect to it
const base = 'http://localhost'
const host = `${base}/remote.php/dav/files/${storageUser.userId}`
createStorageWithConfig('storage1', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host, secure: 'false' })

cy.login(user2)
cy.visit('/apps/files/extstoragemounts')
Expand Down Expand Up @@ -119,8 +121,10 @@ describe('Files user credentials', { testIsolation: true }, () => {
})

it('Create another user storage while reusing GLOBAL user credentials', () => {
const url = Cypress.config('baseUrl') + '/remote.php/dav/files/' + storageUser.userId
createStorageWithConfig('storage2', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host: url.replace('index.php/', ''), secure: 'false' })
// Its not the public server address but the address so the server itself can connect to it
const base = 'http://localhost'
const host = `${base}/remote.php/dav/files/${storageUser.userId}`
createStorageWithConfig('storage2', StorageBackend.DAV, AuthBackend.UserGlobalAuth, { host, secure: 'false' })

cy.login(user2)
cy.visit('/apps/files/extstoragemounts')
Expand Down
25 changes: 15 additions & 10 deletions cypress/e2e/files_versions/version_deletion.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ describe('Versions restoration', () => {
})

it('Does not work without delete permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined

Expand All @@ -68,24 +67,30 @@ describe('Versions restoration', () => {
navigateToFolder(folderName)
openVersionsPanel(randomFilePath)

cy.url().then(url => { hostname = new URL(url).hostname })
getRowForFile(randomFileName).invoke('attr', 'data-cy-files-list-row-fileid').then(_fileId => { fileId = _fileId })
cy.get('[data-files-versions-version]').eq(1).invoke('attr', 'data-files-versions-version').then(_versionId => { versionId = _versionId })
getRowForFile(randomFileName)
.should('be.visible')
.invoke('attr', 'data-cy-files-list-row-fileid')
.then(($fileId) => { fileId = $fileId })

cy.get('[data-files-versions-version]')
.eq(1)
.invoke('attr', 'data-files-versions-version')
.then(($versionId) => { versionId = $versionId })

cy.logout()
cy.then(() => {
cy.logout()
cy.request({
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
return cy.request({
method: 'DELETE',
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
},
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,
})
.then(({ status }) => {
expect(status).to.equal(403)
})
}).then(({ status }) => {
expect(status).to.equal(403)
})
})
})
Expand Down
27 changes: 16 additions & 11 deletions cypress/e2e/files_versions/version_download.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,31 +52,36 @@ describe('Versions download', () => {
})

it('Does not work without download permission through direct API access', () => {
let hostname: string
let fileId: string|undefined
let versionId: string|undefined

setupTestSharedFileFromUser(user, randomFileName, { download: false })
.then(recipient => {
.then((recipient) => {
openVersionsPanel(randomFileName)

cy.url().then(url => { hostname = new URL(url).hostname })
getRowForFile(randomFileName).invoke('attr', 'data-cy-files-list-row-fileid').then(_fileId => { fileId = _fileId })
cy.get('[data-files-versions-version]').eq(1).invoke('attr', 'data-files-versions-version').then(_versionId => { versionId = _versionId })
getRowForFile(randomFileName)
.should('be.visible')
.invoke('attr', 'data-cy-files-list-row-fileid')
.then(($fileId) => { fileId = $fileId })

cy.get('[data-files-versions-version]')
.eq(1)
.invoke('attr', 'data-files-versions-version')
.then(($versionId) => { versionId = $versionId })

cy.logout()
cy.then(() => {
cy.logout()
cy.request({
const base = Cypress.config('baseUrl')!.replace(/\/index\.php\/?$/, '')
return cy.request({
url: `${base}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
auth: { user: recipient.userId, pass: recipient.password },
headers: {
cookie: '',
},
url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`,
failOnStatusCode: false,
})
.then(({ status }) => {
expect(status).to.equal(403)
})
}).then(({ status }) => {
expect(status).to.equal(403)
})
})
})
Expand Down
Loading
Loading