mirror of
https://github.com/Kozea/Radicale.git
synced 2025-06-26 16:45:52 +00:00
Rebase
This commit is contained in:
commit
606bd30514
11 changed files with 65 additions and 30 deletions
|
@ -1,10 +1,18 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 3.dev
|
## 3.dev
|
||||||
|
|
||||||
|
* Adjustment: option [auth] htpasswd_encryption change default from "md5" to "autodetect"
|
||||||
|
|
||||||
|
## 3.2.3
|
||||||
|
* Add: support for Python 3.13
|
||||||
* Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar
|
* Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar
|
||||||
|
* Fix: typos in code
|
||||||
* Enhancement: Added free-busy report
|
* Enhancement: Added free-busy report
|
||||||
* Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports
|
* Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS on reports
|
||||||
* Enhancement: remove unexpected control codes from uploaded items
|
* Enhancement: remove unexpected control codes from uploaded items
|
||||||
|
* Enhancement: add 'strip_domain' setting for username handling
|
||||||
|
* Enhancement: add option to toggle debug log of rights rule with doesn't match
|
||||||
* Drop: remove unused requirement "typeguard"
|
* Drop: remove unused requirement "typeguard"
|
||||||
* Improve: Refactored some date parsing code
|
* Improve: Refactored some date parsing code
|
||||||
|
|
||||||
|
|
|
@ -122,12 +122,12 @@ The `users` file can be created and managed with
|
||||||
[htpasswd](https://httpd.apache.org/docs/current/programs/htpasswd.html):
|
[htpasswd](https://httpd.apache.org/docs/current/programs/htpasswd.html):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new htpasswd file with the user "user1"
|
# Create a new htpasswd file with the user "user1" using SHA-512 as hash method
|
||||||
$ htpasswd -c /path/to/users user1
|
$ htpasswd -5 -c /path/to/users user1
|
||||||
New password:
|
New password:
|
||||||
Re-type new password:
|
Re-type new password:
|
||||||
# Add another user
|
# Add another user
|
||||||
$ htpasswd /path/to/users user2
|
$ htpasswd -5 /path/to/users user2
|
||||||
New password:
|
New password:
|
||||||
Re-type new password:
|
Re-type new password:
|
||||||
```
|
```
|
||||||
|
@ -138,8 +138,7 @@ Authentication can be enabled with the following configuration:
|
||||||
[auth]
|
[auth]
|
||||||
type = htpasswd
|
type = htpasswd
|
||||||
htpasswd_filename = /path/to/users
|
htpasswd_filename = /path/to/users
|
||||||
# encryption method used in the htpasswd file
|
htpasswd_encryption = autodetect
|
||||||
htpasswd_encryption = md5
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### The simple but insecure way
|
##### The simple but insecure way
|
||||||
|
@ -623,7 +622,7 @@ hosts = 0.0.0.0:5232, [::]:5232
|
||||||
[auth]
|
[auth]
|
||||||
type = htpasswd
|
type = htpasswd
|
||||||
htpasswd_filename = ~/.config/radicale/users
|
htpasswd_filename = ~/.config/radicale/users
|
||||||
htpasswd_encryption = md5
|
htpasswd_encryption = autodetect
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
filesystem_folder = ~/.var/lib/radicale/collections
|
filesystem_folder = ~/.var/lib/radicale/collections
|
||||||
|
@ -641,7 +640,7 @@ The same example configuration via command line arguments looks like:
|
||||||
```bash
|
```bash
|
||||||
python3 -m radicale --server-hosts 0.0.0.0:5232,[::]:5232 \
|
python3 -m radicale --server-hosts 0.0.0.0:5232,[::]:5232 \
|
||||||
--auth-type htpasswd --auth-htpasswd-filename ~/.config/radicale/users \
|
--auth-type htpasswd --auth-htpasswd-filename ~/.config/radicale/users \
|
||||||
--auth-htpasswd-encryption md5
|
--auth-htpasswd-encryption autodetect
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the argument `--config ""` to stop Radicale from loading the default
|
Add the argument `--config ""` to stop Radicale from loading the default
|
||||||
|
@ -778,7 +777,7 @@ Available methods:
|
||||||
The installation of **bcrypt** is required for this.
|
The installation of **bcrypt** is required for this.
|
||||||
|
|
||||||
`md5`
|
`md5`
|
||||||
: This uses an iterated MD5 digest of the password with a salt.
|
: This uses an iterated MD5 digest of the password with a salt (nowadays insecure).
|
||||||
|
|
||||||
`sha256`
|
`sha256`
|
||||||
: This uses an iterated SHA-256 digest of the password with a salt.
|
: This uses an iterated SHA-256 digest of the password with a salt.
|
||||||
|
@ -789,7 +788,7 @@ Available methods:
|
||||||
`autodetect`
|
`autodetect`
|
||||||
: This selects autodetection of method per entry.
|
: This selects autodetection of method per entry.
|
||||||
|
|
||||||
Default: `md5`
|
Default: `autodetect`
|
||||||
|
|
||||||
##### delay
|
##### delay
|
||||||
|
|
||||||
|
@ -1017,6 +1016,12 @@ Log response on level=debug
|
||||||
|
|
||||||
Default: `False`
|
Default: `False`
|
||||||
|
|
||||||
|
##### rights_rule_doesnt_match_on_debug = True
|
||||||
|
|
||||||
|
Log rights rule which doesn't match on level=debug
|
||||||
|
|
||||||
|
Default: `False`
|
||||||
|
|
||||||
#### headers
|
#### headers
|
||||||
|
|
||||||
In this section additional HTTP headers that are sent to clients can be
|
In this section additional HTTP headers that are sent to clients can be
|
||||||
|
|
5
config
5
config
|
@ -80,7 +80,7 @@
|
||||||
# Htpasswd encryption method
|
# Htpasswd encryption method
|
||||||
# Value: plain | bcrypt | md5 | sha256 | sha512 | autodetect
|
# Value: plain | bcrypt | md5 | sha256 | sha512 | autodetect
|
||||||
# bcrypt requires the installation of 'bcrypt' module.
|
# bcrypt requires the installation of 'bcrypt' module.
|
||||||
#htpasswd_encryption = md5
|
#htpasswd_encryption = autodetect
|
||||||
|
|
||||||
# Incorrect authentication delay (seconds)
|
# Incorrect authentication delay (seconds)
|
||||||
#delay = 1
|
#delay = 1
|
||||||
|
@ -176,6 +176,8 @@
|
||||||
# Log response content on level=debug
|
# Log response content on level=debug
|
||||||
#response_content_on_debug = False
|
#response_content_on_debug = False
|
||||||
|
|
||||||
|
# Log rights rule which doesn't match on level=debug
|
||||||
|
#rights_rule_doesnt_match_on_debug = False
|
||||||
|
|
||||||
[headers]
|
[headers]
|
||||||
|
|
||||||
|
@ -196,4 +198,3 @@
|
||||||
# When returning a free-busy report, limit the number of returned
|
# When returning a free-busy report, limit the number of returned
|
||||||
# occurences per event to prevent DOS attacks.
|
# occurences per event to prevent DOS attacks.
|
||||||
#max_freebusy_occurrence = 10000
|
#max_freebusy_occurrence = 10000
|
||||||
|
|
||||||
|
|
|
@ -57,13 +57,15 @@
|
||||||
Require all granted
|
Require all granted
|
||||||
</IfDefine>
|
</IfDefine>
|
||||||
|
|
||||||
## You may want to use apache's authentication (config: [auth] type = remote_user)
|
## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
|
||||||
|
## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
|
||||||
#AuthBasicProvider file
|
#AuthBasicProvider file
|
||||||
#AuthType Basic
|
#AuthType Basic
|
||||||
#AuthName "Enter your credentials"
|
#AuthName "Enter your credentials"
|
||||||
#AuthUserFile /path/to/httpdfile/
|
#AuthUserFile /etc/httpd/conf/htpasswd-radicale
|
||||||
#AuthGroupFile /dev/null
|
#AuthGroupFile /dev/null
|
||||||
#Require valid-user
|
#Require valid-user
|
||||||
|
#RequestHeader set X-Remote-User expr=%{REMOTE_USER}
|
||||||
|
|
||||||
<IfDefine RADICALE_ENFORCE_SSL>
|
<IfDefine RADICALE_ENFORCE_SSL>
|
||||||
<IfModule !ssl_module>
|
<IfModule !ssl_module>
|
||||||
|
@ -106,13 +108,15 @@
|
||||||
Require all granted
|
Require all granted
|
||||||
</IfDefine>
|
</IfDefine>
|
||||||
|
|
||||||
## You may want to use apache's authentication (config: [auth] type = remote_user)
|
## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
|
||||||
|
## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
|
||||||
#AuthBasicProvider file
|
#AuthBasicProvider file
|
||||||
#AuthType Basic
|
#AuthType Basic
|
||||||
#AuthName "Enter your credentials"
|
#AuthName "Enter your credentials"
|
||||||
#AuthUserFile /path/to/httpdfile/
|
#AuthUserFile /etc/httpd/conf/htpasswd-radicale
|
||||||
#AuthGroupFile /dev/null
|
#AuthGroupFile /dev/null
|
||||||
#Require valid-user
|
#Require valid-user
|
||||||
|
#RequestHeader set X-Remote-User expr=%{REMOTE_USER}
|
||||||
|
|
||||||
<IfDefine RADICALE_ENFORCE_SSL>
|
<IfDefine RADICALE_ENFORCE_SSL>
|
||||||
<IfModule !ssl_module>
|
<IfModule !ssl_module>
|
||||||
|
@ -179,11 +183,12 @@ CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
|
||||||
Require all granted
|
Require all granted
|
||||||
</IfDefine>
|
</IfDefine>
|
||||||
|
|
||||||
## You may want to use apache's authentication (config: [auth] type = remote_user)
|
## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
|
||||||
|
## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
|
||||||
#AuthBasicProvider file
|
#AuthBasicProvider file
|
||||||
#AuthType Basic
|
#AuthType Basic
|
||||||
#AuthName "Enter your credentials"
|
#AuthName "Enter your credentials"
|
||||||
#AuthUserFile /path/to/httpdfile/
|
#AuthUserFile /etc/httpd/conf/htpasswd-radicale
|
||||||
#AuthGroupFile /dev/null
|
#AuthGroupFile /dev/null
|
||||||
#Require valid-user
|
#Require valid-user
|
||||||
</Location>
|
</Location>
|
||||||
|
@ -221,11 +226,12 @@ CustomLog logs/ssl_request_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
|
||||||
Require all granted
|
Require all granted
|
||||||
</IfDefine>
|
</IfDefine>
|
||||||
|
|
||||||
## You may want to use apache's authentication (config: [auth] type = remote_user)
|
## You may want to use apache's authentication (config: [auth] type = http_x_remote_user)
|
||||||
|
## e.g. create a new file with a testuser: htpasswd -c -B /etc/httpd/conf/htpasswd-radicale testuser
|
||||||
#AuthBasicProvider file
|
#AuthBasicProvider file
|
||||||
#AuthType Basic
|
#AuthType Basic
|
||||||
#AuthName "Enter your credentials"
|
#AuthName "Enter your credentials"
|
||||||
#AuthUserFile /path/to/httpdfile/
|
#AuthUserFile /etc/httpd/conf/htpasswd-radicale
|
||||||
#AuthGroupFile /dev/null
|
#AuthGroupFile /dev/null
|
||||||
#Require valid-user
|
#Require valid-user
|
||||||
</Location>
|
</Location>
|
||||||
|
|
|
@ -146,7 +146,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
||||||
if self._response_content_on_debug:
|
if self._response_content_on_debug:
|
||||||
logger.debug("Response content:\n%s", answer)
|
logger.debug("Response content:\n%s", answer)
|
||||||
else:
|
else:
|
||||||
logger.debug("Response content: suppressed by config/option [auth] response_content_on_debug")
|
logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug")
|
||||||
headers["Content-Type"] += "; charset=%s" % self._encoding
|
headers["Content-Type"] += "; charset=%s" % self._encoding
|
||||||
answer = answer.encode(self._encoding)
|
answer = answer.encode(self._encoding)
|
||||||
accept_encoding = [
|
accept_encoding = [
|
||||||
|
@ -196,7 +196,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
||||||
logger.debug("Request header:\n%s",
|
logger.debug("Request header:\n%s",
|
||||||
pprint.pformat(self._scrub_headers(environ)))
|
pprint.pformat(self._scrub_headers(environ)))
|
||||||
else:
|
else:
|
||||||
logger.debug("Request header: suppressed by config/option [auth] request_header_on_debug")
|
logger.debug("Request header: suppressed by config/option [logging] request_header_on_debug")
|
||||||
|
|
||||||
# SCRIPT_NAME is already removed from PATH_INFO, according to the
|
# SCRIPT_NAME is already removed from PATH_INFO, according to the
|
||||||
# WSGI specification.
|
# WSGI specification.
|
||||||
|
|
|
@ -51,6 +51,7 @@ class ApplicationBase:
|
||||||
self._encoding = configuration.get("encoding", "request")
|
self._encoding = configuration.get("encoding", "request")
|
||||||
self._log_bad_put_request_content = configuration.get("logging", "bad_put_request_content")
|
self._log_bad_put_request_content = configuration.get("logging", "bad_put_request_content")
|
||||||
self._response_content_on_debug = configuration.get("logging", "response_content_on_debug")
|
self._response_content_on_debug = configuration.get("logging", "response_content_on_debug")
|
||||||
|
self._request_content_on_debug = configuration.get("logging", "request_content_on_debug")
|
||||||
self._hook = hook.load(configuration)
|
self._hook = hook.load(configuration)
|
||||||
|
|
||||||
def _read_xml_request_body(self, environ: types.WSGIEnviron
|
def _read_xml_request_body(self, environ: types.WSGIEnviron
|
||||||
|
@ -66,17 +67,20 @@ class ApplicationBase:
|
||||||
logger.debug("Request content (Invalid XML):\n%s", content)
|
logger.debug("Request content (Invalid XML):\n%s", content)
|
||||||
raise RuntimeError("Failed to parse XML: %s" % e) from e
|
raise RuntimeError("Failed to parse XML: %s" % e) from e
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("Request content:\n%s",
|
if self._request_content_on_debug:
|
||||||
xmlutils.pretty_xml(xml_content))
|
logger.debug("Request content (XML):\n%s",
|
||||||
|
xmlutils.pretty_xml(xml_content))
|
||||||
|
else:
|
||||||
|
logger.debug("Request content (XML): suppressed by config/option [logging] request_content_on_debug")
|
||||||
return xml_content
|
return xml_content
|
||||||
|
|
||||||
def _xml_response(self, xml_content: ET.Element) -> bytes:
|
def _xml_response(self, xml_content: ET.Element) -> bytes:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
if self._response_content_on_debug:
|
if self._response_content_on_debug:
|
||||||
logger.debug("Response content:\n%s",
|
logger.debug("Response content (XML):\n%s",
|
||||||
xmlutils.pretty_xml(xml_content))
|
xmlutils.pretty_xml(xml_content))
|
||||||
else:
|
else:
|
||||||
logger.debug("Response content: suppressed by config/option [auth] response_content_on_debug")
|
logger.debug("Response content (XML): suppressed by config/option [logging] response_content_on_debug")
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
ET.ElementTree(xml_content).write(f, encoding=self._encoding,
|
ET.ElementTree(xml_content).write(f, encoding=self._encoding,
|
||||||
xml_declaration=True)
|
xml_declaration=True)
|
||||||
|
|
|
@ -150,7 +150,7 @@ class ApplicationPartPut(ApplicationBase):
|
||||||
if self._log_bad_put_request_content:
|
if self._log_bad_put_request_content:
|
||||||
logger.warning("Bad PUT request content of %r:\n%s", path, content)
|
logger.warning("Bad PUT request content of %r:\n%s", path, content)
|
||||||
else:
|
else:
|
||||||
logger.debug("Bad PUT request content: suppressed by config/option [auth] bad_put_request_content")
|
logger.debug("Bad PUT request content: suppressed by config/option [logging] bad_put_request_content")
|
||||||
return httputils.BAD_REQUEST
|
return httputils.BAD_REQUEST
|
||||||
(prepared_items, prepared_tag, prepared_write_whole_collection,
|
(prepared_items, prepared_tag, prepared_write_whole_collection,
|
||||||
prepared_props, prepared_exc_info) = prepare(
|
prepared_props, prepared_exc_info) = prepare(
|
||||||
|
|
|
@ -180,7 +180,7 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
||||||
"help": "htpasswd filename",
|
"help": "htpasswd filename",
|
||||||
"type": filepath}),
|
"type": filepath}),
|
||||||
("htpasswd_encryption", {
|
("htpasswd_encryption", {
|
||||||
"value": "md5",
|
"value": "autodetect",
|
||||||
"help": "htpasswd encryption method",
|
"help": "htpasswd encryption method",
|
||||||
"type": str}),
|
"type": str}),
|
||||||
("realm", {
|
("realm", {
|
||||||
|
@ -316,6 +316,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
||||||
"value": "False",
|
"value": "False",
|
||||||
"help": "log response content on level=debug",
|
"help": "log response content on level=debug",
|
||||||
"type": bool}),
|
"type": bool}),
|
||||||
|
("rights_rule_doesnt_match_on_debug", {
|
||||||
|
"value": "False",
|
||||||
|
"help": "log rights rules which doesn't match on level=debug",
|
||||||
|
"type": bool}),
|
||||||
("mask_passwords", {
|
("mask_passwords", {
|
||||||
"value": "True",
|
"value": "True",
|
||||||
"help": "mask passwords in logs",
|
"help": "mask passwords in logs",
|
||||||
|
|
|
@ -14,8 +14,8 @@ def load(configuration):
|
||||||
return utils.load_plugin(
|
return utils.load_plugin(
|
||||||
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
|
INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.warning(e)
|
||||||
logger.warn("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type"))
|
logger.warning("Hook \"%s\" failed to load, falling back to \"none\"." % configuration.get("hook", "type"))
|
||||||
configuration = configuration.copy()
|
configuration = configuration.copy()
|
||||||
configuration.update({"hook": {"type": "none"}}, "hook", privileged=True)
|
configuration.update({"hook": {"type": "none"}}, "hook", privileged=True)
|
||||||
return utils.load_plugin(
|
return utils.load_plugin(
|
||||||
|
|
|
@ -146,7 +146,7 @@ def read_request_body(configuration: "config.Configuration",
|
||||||
if configuration.get("logging", "request_content_on_debug"):
|
if configuration.get("logging", "request_content_on_debug"):
|
||||||
logger.debug("Request content:\n%s", content)
|
logger.debug("Request content:\n%s", content)
|
||||||
else:
|
else:
|
||||||
logger.debug("Request content: suppressed by config/option [auth] request_content_on_debug")
|
logger.debug("Request content: suppressed by config/option [logging] request_content_on_debug")
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ class Rights(rights.BaseRights):
|
||||||
def __init__(self, configuration: config.Configuration) -> None:
|
def __init__(self, configuration: config.Configuration) -> None:
|
||||||
super().__init__(configuration)
|
super().__init__(configuration)
|
||||||
self._filename = configuration.get("rights", "file")
|
self._filename = configuration.get("rights", "file")
|
||||||
|
self._log_rights_rule_doesnt_match_on_debug = configuration.get("logging", "rights_rule_doesnt_match_on_debug")
|
||||||
|
|
||||||
def authorization(self, user: str, path: str) -> str:
|
def authorization(self, user: str, path: str) -> str:
|
||||||
user = user or ""
|
user = user or ""
|
||||||
|
@ -61,6 +62,8 @@ class Rights(rights.BaseRights):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError("Failed to load rights file %r: %s" %
|
raise RuntimeError("Failed to load rights file %r: %s" %
|
||||||
(self._filename, e)) from e
|
(self._filename, e)) from e
|
||||||
|
if not self._log_rights_rule_doesnt_match_on_debug:
|
||||||
|
logger.debug("logging of rules which doesn't match suppressed by config/option [logging] rights_rule_doesnt_match_on_debug")
|
||||||
for section in rights_config.sections():
|
for section in rights_config.sections():
|
||||||
group_match = False
|
group_match = False
|
||||||
try:
|
try:
|
||||||
|
@ -96,5 +99,9 @@ class Rights(rights.BaseRights):
|
||||||
logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
|
logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
|
||||||
user, sane_path, user_pattern, collection_pattern,
|
user, sane_path, user_pattern, collection_pattern,
|
||||||
section)
|
section)
|
||||||
|
if self._log_rights_rule_doesnt_match_on_debug:
|
||||||
|
logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
|
||||||
|
user, sane_path, user_pattern, collection_pattern,
|
||||||
|
section)
|
||||||
logger.info("Rights: %r:%r doesn't match any section", user, sane_path)
|
logger.info("Rights: %r:%r doesn't match any section", user, sane_path)
|
||||||
return ""
|
return ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue