mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
Allow users to set a combined certificte and key file for additional certificates in the SSL context
This commit is contained in:
parent
d1ae82c5c2
commit
16adddc803
@ -501,6 +501,19 @@ HTTP header/value expected by Django, eg `'["HTTP_X_FORWARDED_PROTO", "https"]'`
|
|||||||
Settings this value has security implications. Read the Django documentation
|
Settings this value has security implications. Read the Django documentation
|
||||||
and be sure you understand its usage before setting it.
|
and be sure you understand its usage before setting it.
|
||||||
|
|
||||||
|
`PAPERLESS_EMAIL_CERTIFICATE_FILE=<path>`
|
||||||
|
|
||||||
|
: Configures an additional SSL certificate file containing a [combined key and certificate](https://docs.python.org/3/library/ssl.html#combined-key-and-certificate) file
|
||||||
|
for validating SSL connections against mail providers. This is for use with self-signed certificates against
|
||||||
|
local IMAP servers.
|
||||||
|
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Settings this value has security implications for the security of your email.
|
||||||
|
Understand what it does and be sure you need to before setting.
|
||||||
|
|
||||||
## OCR settings {#ocr}
|
## OCR settings {#ocr}
|
||||||
|
|
||||||
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
||||||
|
@ -177,6 +177,23 @@ def settings_values_check(app_configs, **kwargs):
|
|||||||
)
|
)
|
||||||
return msgs
|
return msgs
|
||||||
|
|
||||||
|
def _email_certificate_validate():
|
||||||
|
msgs = []
|
||||||
|
# Existence checks
|
||||||
|
if (
|
||||||
|
settings.EMAIL_CERTIFICATE_FILE is not None
|
||||||
|
and not settings.EMAIL_CERTIFICATE_FILE.is_file()
|
||||||
|
):
|
||||||
|
msgs.append(
|
||||||
|
Error(
|
||||||
|
f"Email cert {settings.EMAIL_CERTIFICATE_FILE} is not a file",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return msgs
|
||||||
|
|
||||||
return (
|
return (
|
||||||
_ocrmypdf_settings_check() + _timezone_validate() + _barcode_scanner_validate()
|
_ocrmypdf_settings_check()
|
||||||
|
+ _timezone_validate()
|
||||||
|
+ _barcode_scanner_validate()
|
||||||
|
+ _email_certificate_validate()
|
||||||
)
|
)
|
||||||
|
@ -67,11 +67,20 @@ def __get_float(key: str, default: float) -> float:
|
|||||||
return float(os.getenv(key, default))
|
return float(os.getenv(key, default))
|
||||||
|
|
||||||
|
|
||||||
def __get_path(key: str, default: Union[PathLike, str]) -> Path:
|
def __get_path(
|
||||||
|
key: str,
|
||||||
|
default: Optional[Union[PathLike, str]] = None,
|
||||||
|
) -> Optional[Path]:
|
||||||
"""
|
"""
|
||||||
Return a normalized, absolute path based on the environment variable or a default
|
Return a normalized, absolute path based on the environment variable or a default,
|
||||||
|
if provided. If not set and no default, returns None
|
||||||
"""
|
"""
|
||||||
return Path(os.environ.get(key, default)).resolve()
|
if key in os.environ:
|
||||||
|
return Path(os.environ[key]).resolve()
|
||||||
|
elif default is not None:
|
||||||
|
return Path(default).resolve()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def __get_list(
|
def __get_list(
|
||||||
@ -477,6 +486,8 @@ CSRF_COOKIE_NAME = f"{COOKIE_PREFIX}csrftoken"
|
|||||||
SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid"
|
SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid"
|
||||||
LANGUAGE_COOKIE_NAME = f"{COOKIE_PREFIX}django_language"
|
LANGUAGE_COOKIE_NAME = f"{COOKIE_PREFIX}django_language"
|
||||||
|
|
||||||
|
EMAIL_CERTIFICATE_FILE = __get_path("PAPERLESS_EMAIL_CERTIFICATE_FILE")
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Database #
|
# Database #
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless.checks import binaries_check
|
from paperless.checks import binaries_check
|
||||||
from paperless.checks import debug_mode_check
|
from paperless.checks import debug_mode_check
|
||||||
from paperless.checks import paths_check
|
from paperless.checks import paths_check
|
||||||
@ -57,7 +59,7 @@ class TestChecks(DirectoriesMixin, TestCase):
|
|||||||
self.assertEqual(len(debug_mode_check(None)), 1)
|
self.assertEqual(len(debug_mode_check(None)), 1)
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsChecks(DirectoriesMixin, TestCase):
|
class TestSettingsChecksAgainstDefaults(DirectoriesMixin, TestCase):
|
||||||
def test_all_valid(self):
|
def test_all_valid(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -70,6 +72,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
msgs = settings_values_check(None)
|
msgs = settings_values_check(None)
|
||||||
self.assertEqual(len(msgs), 0)
|
self.assertEqual(len(msgs), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOcrSettingsChecks(DirectoriesMixin, TestCase):
|
||||||
@override_settings(OCR_OUTPUT_TYPE="notapdf")
|
@override_settings(OCR_OUTPUT_TYPE="notapdf")
|
||||||
def test_invalid_output_type(self):
|
def test_invalid_output_type(self):
|
||||||
"""
|
"""
|
||||||
@ -160,6 +164,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertIn('OCR clean mode "cleanme"', msg.msg)
|
self.assertIn('OCR clean mode "cleanme"', msg.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimezoneSettingsChecks(DirectoriesMixin, TestCase):
|
||||||
@override_settings(TIME_ZONE="TheMoon\\MyCrater")
|
@override_settings(TIME_ZONE="TheMoon\\MyCrater")
|
||||||
def test_invalid_timezone(self):
|
def test_invalid_timezone(self):
|
||||||
"""
|
"""
|
||||||
@ -178,6 +184,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg)
|
self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBarcodeSettingsChecks(DirectoriesMixin, TestCase):
|
||||||
@override_settings(CONSUMER_BARCODE_SCANNER="Invalid")
|
@override_settings(CONSUMER_BARCODE_SCANNER="Invalid")
|
||||||
def test_barcode_scanner_invalid(self):
|
def test_barcode_scanner_invalid(self):
|
||||||
msgs = settings_values_check(None)
|
msgs = settings_values_check(None)
|
||||||
@ -200,3 +208,26 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
|
|||||||
def test_barcode_scanner_valid(self):
|
def test_barcode_scanner_valid(self):
|
||||||
msgs = settings_values_check(None)
|
msgs = settings_values_check(None)
|
||||||
self.assertEqual(len(msgs), 0)
|
self.assertEqual(len(msgs), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailCertSettingsChecks(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
|
@override_settings(EMAIL_CERTIFICATE_FILE=Path("/tmp/not_actually_here.pem"))
|
||||||
|
def test_not_valid_file(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Default settings
|
||||||
|
- Email certificate is set
|
||||||
|
WHEN:
|
||||||
|
- Email certificate file doesn't exist
|
||||||
|
THEN:
|
||||||
|
- system check error reported for email certificate
|
||||||
|
"""
|
||||||
|
self.assertIsNotFile("/tmp/not_actually_here.pem")
|
||||||
|
|
||||||
|
msgs = settings_values_check(None)
|
||||||
|
|
||||||
|
self.assertEqual(len(msgs), 1)
|
||||||
|
|
||||||
|
msg = msgs[0]
|
||||||
|
|
||||||
|
self.assertIn("Email cert /tmp/not_actually_here.pem is not a file", msg.msg)
|
||||||
|
@ -395,12 +395,16 @@ def get_mailbox(server, port, security) -> MailBox:
|
|||||||
"""
|
"""
|
||||||
Returns the correct MailBox instance for the given configuration.
|
Returns the correct MailBox instance for the given configuration.
|
||||||
"""
|
"""
|
||||||
|
ssl_context = ssl.create_default_context()
|
||||||
|
if settings.EMAIL_CERTIFICATE_FILE is not None: # pragma: nocover
|
||||||
|
ssl_context.load_cert_chain(certfile=settings.EMAIL_CERTIFICATE_FILE)
|
||||||
|
|
||||||
if security == MailAccount.ImapSecurity.NONE:
|
if security == MailAccount.ImapSecurity.NONE:
|
||||||
mailbox = MailBoxUnencrypted(server, port)
|
mailbox = MailBoxUnencrypted(server, port)
|
||||||
elif security == MailAccount.ImapSecurity.STARTTLS:
|
elif security == MailAccount.ImapSecurity.STARTTLS:
|
||||||
mailbox = MailBoxTls(server, port, ssl_context=ssl.create_default_context())
|
mailbox = MailBoxTls(server, port, ssl_context=ssl_context)
|
||||||
elif security == MailAccount.ImapSecurity.SSL:
|
elif security == MailAccount.ImapSecurity.SSL:
|
||||||
mailbox = MailBox(server, port, ssl_context=ssl.create_default_context())
|
mailbox = MailBox(server, port, ssl_context=ssl_context)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Unknown IMAP security") # pragma: nocover
|
raise NotImplementedError("Unknown IMAP security") # pragma: nocover
|
||||||
return mailbox
|
return mailbox
|
||||||
|
Loading…
x
Reference in New Issue
Block a user