Skip to content

Commit 3520210

Browse files
committed
feat(core): add support for encrypted db connection in webinstaller
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 86b45bb commit 3520210

4 files changed

Lines changed: 157 additions & 0 deletions

File tree

core/Controller/SetupController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ public function display(array $post): void {
8181
'dbtablespace' => '',
8282
'dbhost' => 'localhost',
8383
'dbtype' => '',
84+
'dbsslmode' => '',
85+
'dbsslca' => '',
86+
'dbsslcert' => '',
87+
'dbsslkey' => '',
88+
'dbsslcrl' => '',
89+
'dbsslnoverify' => false,
8490
'hasAutoconfig' => false,
8591
'serverRoot' => \OC::$SERVERROOT,
8692
'version' => implode('.', $this->serverVersion->getVersion()),

core/src/install.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ export type SetupConfig = {
2424
dbhost: string
2525
dbtype: DbType | ''
2626

27+
/** Encryption mode of the connection, pgsql only */
28+
dbsslmode: string
29+
/** Path to the CA certificate the database server is verified against */
30+
dbsslca: string
31+
/** Path to the client certificate used to authenticate against the database */
32+
dbsslcert: string
33+
/** Path to the private key of the client certificate */
34+
dbsslkey: string
35+
/** Path to the certificate revocation list, pgsql only */
36+
dbsslcrl: string
37+
/** Skip verifying that the server certificate matches the host, mysql only */
38+
dbsslnoverify: boolean
39+
2740
databases: Partial<Record<DbType, string>>
2841

2942
hasAutoconfig: boolean

core/src/views/Setup.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const defaultConfig = Object.freeze({
2020
dbtablespace: '',
2121
dbhost: '',
2222
dbtype: '',
23+
dbsslmode: '',
24+
dbsslca: '',
25+
dbsslcert: '',
26+
dbsslkey: '',
27+
dbsslcrl: '',
28+
dbsslnoverify: false,
2329
databases: {
2430
sqlite: 'SQLite',
2531
mysql: 'MySQL/MariaDB',
@@ -155,6 +161,67 @@ describe('Default setup page', () => {
155161
})
156162
})
157163

164+
describe('Encrypted database connection', () => {
165+
beforeEach(cleanup)
166+
beforeEach(() => {
167+
removeInitialState()
168+
mockInitialState('core', 'links', links)
169+
})
170+
171+
it.each(['sqlite', 'oci'])('Is not offered for %s', async (dbtype) => {
172+
mockInitialState('core', 'config', {
173+
...defaultConfig,
174+
dbtype,
175+
databases: { sqlite: 'SQLite', mysql: 'MySQL/MariaDB', pgsql: 'PostgreSQL', oci: 'Oracle' },
176+
} as SetupConfig)
177+
const component = render(SetupView)
178+
179+
await expect(component.findByText('Encrypted database connection')).rejects.toThrow()
180+
})
181+
182+
it('Offers the PDO options for mysql', async () => {
183+
mockInitialState('core', 'config', { ...defaultConfig, dbtype: 'mysql' } as SetupConfig)
184+
const component = render(SetupView)
185+
186+
await expect(component.findByText('Encrypted database connection')).resolves.not.toThrow()
187+
await expect(component.findByRole('textbox', { name: /CA certificate path/ })).resolves.not.toThrow()
188+
await expect(component.findByRole('textbox', { name: /Client certificate path/ })).resolves.not.toThrow()
189+
await expect(component.findByRole('textbox', { name: /Client certificate key path/ })).resolves.not.toThrow()
190+
await expect(component.findByRole('checkbox', { name: /Do not verify that the server certificate/ })).resolves.not.toThrow()
191+
192+
// Both are PostgreSQL specific
193+
await expect(component.findByRole('textbox', { name: /Encryption mode/ })).rejects.toThrow()
194+
await expect(component.findByRole('textbox', { name: /Certificate revocation list path/ })).rejects.toThrow()
195+
})
196+
197+
it('Offers the libpq parameters for pgsql', async () => {
198+
mockInitialState('core', 'config', { ...defaultConfig, dbtype: 'pgsql' } as SetupConfig)
199+
const component = render(SetupView)
200+
201+
await expect(component.findByRole('textbox', { name: /Encryption mode/ })).resolves.not.toThrow()
202+
await expect(component.findByRole('textbox', { name: /CA certificate path/ })).resolves.not.toThrow()
203+
await expect(component.findByRole('textbox', { name: /Client certificate path/ })).resolves.not.toThrow()
204+
await expect(component.findByRole('textbox', { name: /Client certificate key path/ })).resolves.not.toThrow()
205+
await expect(component.findByRole('textbox', { name: /Certificate revocation list path/ })).resolves.not.toThrow()
206+
207+
// MySQL specific
208+
await expect(component.findByRole('checkbox', { name: /Do not verify that the server certificate/ })).rejects.toThrow()
209+
})
210+
211+
it('Renders the submitted values on error', async () => {
212+
mockInitialState('core', 'config', {
213+
...defaultConfig,
214+
dbtype: 'pgsql',
215+
dbsslmode: 'verify-full',
216+
dbsslca: '/ca.pem',
217+
} as SetupConfig)
218+
const component = render(SetupView)
219+
220+
expect((await component.findByRole('textbox', { name: /Encryption mode/ }) as HTMLInputElement).value).toBe('verify-full')
221+
expect((await component.findByRole('textbox', { name: /CA certificate path/ }) as HTMLInputElement).value).toBe('/ca.pem')
222+
})
223+
})
224+
158225
describe('Setup page with errors and warning', () => {
159226
beforeEach(cleanup)
160227
beforeEach(() => {

core/src/views/Setup.vue

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,69 @@
186186
name="dbhost"
187187
spellcheck="false" />
188188
</fieldset>
189+
190+
<!-- Encrypted database connection -->
191+
<details v-if="supportsEncryptedConnection" data-cy-setup-form-database-encryption>
192+
<summary>{{ t('core', 'Encrypted database connection') }}</summary>
193+
194+
<fieldset>
195+
<legend class="hidden-visually">
196+
{{ t('core', 'Encrypted database connection') }}
197+
</legend>
198+
199+
<NcTextField
200+
v-if="config.dbtype === 'pgsql'"
201+
v-model="config.dbsslmode"
202+
:helper-text="t('core', 'Supported modes: disable, allow, prefer, require, verify-ca, verify-full.')"
203+
:label="t('core', 'Encryption mode')"
204+
autocapitalize="none"
205+
autocomplete="off"
206+
name="dbsslmode"
207+
spellcheck="false" />
208+
209+
<NcTextField
210+
v-model="config.dbsslca"
211+
:helper-text="t('core', 'Has to be readable by the web server.')"
212+
:label="t('core', 'CA certificate path')"
213+
autocapitalize="none"
214+
autocomplete="off"
215+
name="dbsslca"
216+
spellcheck="false" />
217+
218+
<NcTextField
219+
v-model="config.dbsslcert"
220+
:label="t('core', 'Client certificate path')"
221+
autocapitalize="none"
222+
autocomplete="off"
223+
name="dbsslcert"
224+
spellcheck="false" />
225+
226+
<NcTextField
227+
v-model="config.dbsslkey"
228+
:label="t('core', 'Client certificate key path')"
229+
autocapitalize="none"
230+
autocomplete="off"
231+
name="dbsslkey"
232+
spellcheck="false" />
233+
234+
<NcTextField
235+
v-if="config.dbtype === 'pgsql'"
236+
v-model="config.dbsslcrl"
237+
:label="t('core', 'Certificate revocation list path')"
238+
autocapitalize="none"
239+
autocomplete="off"
240+
name="dbsslcrl"
241+
spellcheck="false" />
242+
243+
<NcCheckboxRadioSwitch
244+
v-if="config.dbtype === 'mysql'"
245+
v-model="config.dbsslnoverify"
246+
name="dbsslnoverify"
247+
type="checkbox">
248+
{{ t('core', 'Do not verify that the server certificate matches the database host') }}
249+
</NcCheckboxRadioSwitch>
250+
</fieldset>
251+
</details>
189252
</fieldset>
190253
</details>
191254

@@ -324,6 +387,14 @@ export default defineComponent({
324387
return 'success'
325388
},
326389
390+
/**
391+
* Only MySQL/MariaDB and PostgreSQL can be configured to use an encrypted
392+
* connection through the installer, see OC\Setup\AbstractDatabase.
393+
*/
394+
supportsEncryptedConnection(): boolean {
395+
return this.config?.dbtype === 'mysql' || this.config?.dbtype === 'pgsql'
396+
},
397+
327398
firstAndOnlyDatabase(): string | null {
328399
const dbNames = Object.values(this.config?.databases || {})
329400
if (dbNames.length === 1) {

0 commit comments

Comments
 (0)