Radicale 3.8.2__tar.gz → 3.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {radicale-3.8.2 → radicale-3.9.0}/CHANGELOG.md +28 -0
  2. {radicale-3.8.2 → radicale-3.9.0}/DOCUMENTATION.md +100 -15
  3. {radicale-3.8.2 → radicale-3.9.0}/PKG-INFO +9 -3
  4. {radicale-3.8.2 → radicale-3.9.0}/Radicale.egg-info/PKG-INFO +9 -3
  5. {radicale-3.8.2 → radicale-3.9.0}/Radicale.egg-info/SOURCES.txt +2 -1
  6. {radicale-3.8.2 → radicale-3.9.0}/Radicale.egg-info/requires.txt +9 -2
  7. {radicale-3.8.2 → radicale-3.9.0}/config +6 -2
  8. {radicale-3.8.2 → radicale-3.9.0}/pyproject.toml +20 -10
  9. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/__init__.py +12 -10
  10. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/base.py +6 -4
  11. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/delete.py +7 -11
  12. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/get.py +13 -17
  13. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/head.py +3 -3
  14. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/mkcalendar.py +5 -4
  15. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/mkcol.py +5 -4
  16. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/move.py +8 -14
  17. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/options.py +1 -1
  18. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/post.py +3 -3
  19. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/propfind.py +36 -27
  20. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/proppatch.py +11 -8
  21. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/put.py +9 -10
  22. {radicale-3.8.2 → radicale-3.9.0}/radicale/app/report.py +11 -11
  23. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/__init__.py +48 -18
  24. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/dovecot.py +3 -3
  25. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/ldap.py +17 -16
  26. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/oauth2.py +2 -2
  27. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/pam.py +20 -13
  28. {radicale-3.8.2 → radicale-3.9.0}/radicale/config.py +33 -6
  29. {radicale-3.8.2 → radicale-3.9.0}/radicale/group/htgroup.py +1 -1
  30. {radicale-3.8.2 → radicale-3.9.0}/radicale/item/filter.py +33 -36
  31. {radicale-3.8.2 → radicale-3.9.0}/radicale/rights/__init__.py +8 -6
  32. {radicale-3.8.2 → radicale-3.9.0}/radicale/rights/authenticated.py +3 -2
  33. {radicale-3.8.2 → radicale-3.9.0}/radicale/rights/from_file.py +3 -2
  34. {radicale-3.8.2 → radicale-3.9.0}/radicale/rights/owner_only.py +3 -2
  35. {radicale-3.8.2 → radicale-3.9.0}/radicale/rights/owner_write.py +3 -2
  36. {radicale-3.8.2 → radicale-3.9.0}/radicale/server.py +107 -1
  37. {radicale-3.8.2 → radicale-3.9.0}/radicale/sharing/__init__.py +61 -20
  38. {radicale-3.8.2 → radicale-3.9.0}/radicale/sharing/files.py +2 -0
  39. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/lock.py +2 -0
  40. {radicale-3.8.2 → radicale-3.9.0}/radicale/tests/custom/rights.py +3 -2
  41. {radicale-3.8.2 → radicale-3.9.0}/radicale/tests/custom/web.py +2 -2
  42. {radicale-3.8.2 → radicale-3.9.0}/radicale/utils.py +2 -0
  43. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/__init__.py +2 -2
  44. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal.py +1 -0
  45. radicale-3.9.0/radicale/web/internal_data/css/icons/check-circle.svg +1 -0
  46. radicale-3.9.0/radicale/web/internal_data/css/icons/minus-circle.svg +1 -0
  47. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/main.css +104 -7
  48. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/index.html +8 -41
  49. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/api/api.js +2 -2
  50. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/api/sharing.js +1 -1
  51. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/models/collection.js +3 -1
  52. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/CollectionsScene.js +395 -62
  53. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js +6 -2
  54. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/CreateEditShareScene.js +34 -10
  55. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +0 -16
  56. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/misc.js +1 -15
  57. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/none.py +1 -1
  58. radicale-3.8.2/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +0 -195
  59. {radicale-3.8.2 → radicale-3.9.0}/COPYING.md +0 -0
  60. {radicale-3.8.2 → radicale-3.9.0}/MANIFEST.in +0 -0
  61. {radicale-3.8.2 → radicale-3.9.0}/README.md +0 -0
  62. {radicale-3.8.2 → radicale-3.9.0}/Radicale.egg-info/dependency_links.txt +0 -0
  63. {radicale-3.8.2 → radicale-3.9.0}/Radicale.egg-info/entry_points.txt +0 -0
  64. {radicale-3.8.2 → radicale-3.9.0}/Radicale.egg-info/top_level.txt +0 -0
  65. {radicale-3.8.2 → radicale-3.9.0}/radicale/__init__.py +0 -0
  66. {radicale-3.8.2 → radicale-3.9.0}/radicale/__main__.py +0 -0
  67. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/denyall.py +0 -0
  68. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/htpasswd.py +0 -0
  69. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/http_remote_user.py +0 -0
  70. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/http_x_remote_user.py +0 -0
  71. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/imap.py +0 -0
  72. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/none.py +0 -0
  73. {radicale-3.8.2 → radicale-3.9.0}/radicale/auth/remote_user.py +0 -0
  74. {radicale-3.8.2 → radicale-3.9.0}/radicale/group/__init__.py +0 -0
  75. {radicale-3.8.2 → radicale-3.9.0}/radicale/group/from_auth.py +0 -0
  76. {radicale-3.8.2 → radicale-3.9.0}/radicale/group/none.py +0 -0
  77. {radicale-3.8.2 → radicale-3.9.0}/radicale/hook/__init__.py +0 -0
  78. {radicale-3.8.2 → radicale-3.9.0}/radicale/hook/email/__init__.py +0 -0
  79. {radicale-3.8.2 → radicale-3.9.0}/radicale/hook/none.py +0 -0
  80. {radicale-3.8.2 → radicale-3.9.0}/radicale/hook/rabbitmq/__init__.py +0 -0
  81. {radicale-3.8.2 → radicale-3.9.0}/radicale/httputils.py +0 -0
  82. {radicale-3.8.2 → radicale-3.9.0}/radicale/item/__init__.py +0 -0
  83. {radicale-3.8.2 → radicale-3.9.0}/radicale/log.py +0 -0
  84. {radicale-3.8.2 → radicale-3.9.0}/radicale/pathutils.py +0 -0
  85. {radicale-3.8.2 → radicale-3.9.0}/radicale/py.typed +0 -0
  86. {radicale-3.8.2 → radicale-3.9.0}/radicale/sharing/common.py +0 -0
  87. {radicale-3.8.2 → radicale-3.9.0}/radicale/sharing/csv.py +0 -0
  88. {radicale-3.8.2 → radicale-3.9.0}/radicale/sharing/none.py +0 -0
  89. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/__init__.py +0 -0
  90. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/__init__.py +0 -0
  91. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/base.py +0 -0
  92. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/cache.py +0 -0
  93. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/create_collection.py +0 -0
  94. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/delete.py +0 -0
  95. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/discover.py +0 -0
  96. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/get.py +0 -0
  97. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/history.py +0 -0
  98. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/meta.py +0 -0
  99. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/move.py +0 -0
  100. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/sync.py +0 -0
  101. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/upload.py +0 -0
  102. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem/verify.py +0 -0
  103. {radicale-3.8.2 → radicale-3.9.0}/radicale/storage/multifilesystem_nolock.py +0 -0
  104. {radicale-3.8.2 → radicale-3.9.0}/radicale/tests/custom/__init__.py +0 -0
  105. {radicale-3.8.2 → radicale-3.9.0}/radicale/tests/custom/auth.py +0 -0
  106. {radicale-3.8.2 → radicale-3.9.0}/radicale/tests/custom/storage_simple_sync.py +0 -0
  107. {radicale-3.8.2 → radicale-3.9.0}/radicale/types.py +0 -0
  108. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icon.png +0 -0
  109. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/delete.svg +0 -0
  110. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/download.svg +0 -0
  111. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/edit.svg +0 -0
  112. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/eye.svg +0 -0
  113. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/key.svg +0 -0
  114. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/new.svg +0 -0
  115. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/repeat.svg +0 -0
  116. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/share.svg +0 -0
  117. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/icons/upload.svg +0 -0
  118. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/loading.svg +0 -0
  119. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/css/logo.svg +0 -0
  120. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/api/common.js +0 -0
  121. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/config.js +0 -0
  122. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/constants.js +0 -0
  123. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/main.js +0 -0
  124. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/DeleteConfirmationScene.js +0 -0
  125. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/LoadingScene.js +0 -0
  126. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/LoginScene.js +0 -0
  127. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +0 -0
  128. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/scenes/scene_manager.js +0 -0
  129. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/collection_utils.js +0 -0
  130. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/collections_cache.js +0 -0
  131. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/error.js +0 -0
  132. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/form_validator.js +0 -0
  133. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/permissions.js +0 -0
  134. {radicale-3.8.2 → radicale-3.9.0}/radicale/web/internal_data/js/utils/url_text.js +0 -0
  135. {radicale-3.8.2 → radicale-3.9.0}/radicale/xmlutils.py +0 -0
  136. {radicale-3.8.2 → radicale-3.9.0}/radicale.wsgi +0 -0
  137. {radicale-3.8.2 → radicale-3.9.0}/rights +0 -0
  138. {radicale-3.8.2 → radicale-3.9.0}/setup.cfg +0 -0
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.9.0
4
+ * Add: [server] type: serve with hypercorn, which additionally speaks HTTP/2 over cleartext (H2C) on the same socket (`pip install radicale[h2]`)
5
+ * Adjustment WebUI: show incoming shares as cards
6
+ * Fix: typo in /.mobileconfig
7
+ * Fix: empty text-match filter no longer fails the REPORT
8
+ * Improvement: sharing/api: reject unsupported overlay properties depending on collection type and conversion
9
+ * Improvement: sharing/bday: remove default "(BDAY)" suffix from overlayed "displayname"
10
+ * Fix: WebUI/Sharing: property overrides on transformed shares
11
+ * Fix: PUT request: accept If-Match: * for an existing item
12
+ * Add: new permission 'l' for locking properties by blocking write access to properties (PROPPATCH) and suppressing write-properties (PROPFIND)
13
+ * Add: storage-hook supports now placeholder 'actor' (useful for shared collection)
14
+ * Fix: REPORT with calendar-data parses every item with vobject just to count VEVENTs (10× slower than needed)
15
+ * Fix: Honor zero-offset VALARM triggers in time-range filters
16
+ * Fix: time-range filter on VTODO with only CREATED and COMPLETED
17
+ * Fix: addressbook-query prop-filter defaults to test="anyof"
18
+ * Fix: REPORT expand: non-recurring events with a TZID were time-range filtered by their local time read as UTC
19
+ * Fix: sharing: broken support for custom database_path
20
+ * Fix: auth/login-cache: add lock around remove expired entry
21
+ * Fix: sharing/delete collection: fix uncatched collection URI without trailing '/'
22
+ * Fix: auth/sharing+rights: refactoring "user_groups" handling to be thread-safe
23
+ * Fix: auth/login-cache: partially missing thread-safety
24
+ * Extension: contrib/Apache reference config supports now also HTTP2
25
+ * Extension: HTTP version added to request log
26
+
27
+ ## 3.8.3
28
+ * Fix: [group] htgroup: fix not working htgroup_cache=True
29
+ * Fix: auth: clear rights/user_groups before conditional set depending on group_type/auth_type
30
+
3
31
  ## 3.8.2
4
32
  * Add: tests: sharing API plus CalDAV/CardDAV workflow and corrupt CSV handling (covers #2259)
5
33
  * Add: tests: mocked LDAP, IMAP, OAuth2 and PAM authentication servers (covers #2259)
@@ -306,13 +306,13 @@ To enable and manage the service run:
306
306
 
307
307
  ```bash
308
308
  # Enable the service
309
- $ systemctl enable radicale
309
+ systemctl enable radicale
310
310
  # Start the service
311
- $ systemctl start radicale
311
+ systemctl start radicale
312
312
  # Check the status of the service
313
- $ systemctl status radicale
313
+ systemctl status radicale
314
314
  # View all log messages
315
- $ journalctl --unit radicale.service
315
+ journalctl --unit radicale.service
316
316
  ```
317
317
 
318
318
  #### Linux with systemd as a user
@@ -340,13 +340,13 @@ To enable and manage the service run:
340
340
 
341
341
  ```bash
342
342
  # Enable the service
343
- $ systemctl --user enable radicale
343
+ systemctl --user enable radicale
344
344
  # Start the service
345
- $ systemctl --user start radicale
345
+ systemctl --user start radicale
346
346
  # Check the status of the service
347
- $ systemctl --user status radicale
347
+ systemctl --user status radicale
348
348
  # View all log messages
349
- $ journalctl --user --unit radicale.service
349
+ journalctl --user --unit radicale.service
350
350
  ```
351
351
 
352
352
  #### Windows with "NSSM - the Non-Sucking Service Manager"
@@ -467,6 +467,34 @@ Be reminded that Radicale's default configuration enforces limits on the
467
467
  maximum number of parallel connections, the maximum file size and the rate of
468
468
  incorrect authentication attempts. Connections are terminated after a timeout.
469
469
 
470
+ #### HTTP/2 to the backend
471
+
472
+ The examples above forward to Radicale over HTTP/1.x. The built-in server
473
+ answers HTTP/1.0 and closes after each request; with `type = hypercorn` (see
474
+ [Configuration/Server](#server)) the hop keeps connections alive instead, and it
475
+ can speak **HTTP/2 over cleartext (H2C)** on the same socket: the connection
476
+ starts with the HTTP/2 client preface, so a proxy configured to use HTTP/2 for
477
+ this upstream gets it without terminating TLS itself.
478
+
479
+ The reason to prefer it is request smuggling. Smuggling needs a connection that
480
+ carries more than one request, and two peers that disagree on where the first
481
+ one ends: most commonly a `Content-Length` header pulling against a
482
+ `Transfer-Encoding` header, or duplicate `Transfer-Encoding` headers that the
483
+ two peers combine or drop differently. HTTP/2 leaves nothing to disagree
484
+ about — every frame carries its own length, a request ends at an explicit
485
+ end-of-stream, and a `content-length` that contradicts the data is a stream
486
+ error (RFC 9113). The hop that changes is the last one, so no HTTP/1.1
487
+ downgrade is introduced, and the client-side hop is unchanged: this removes
488
+ the ambiguity on one hop, not on the path end to end.
489
+
490
+ With `ssl = True` `hypercorn` serves HTTP/2 over TLS by ALPN too, so the
491
+ encrypted hop described under [Secure connection between Radicale and the
492
+ reverse proxy](#secure-connection-between-radicale-and-the-reverse-proxy) can
493
+ use it; the proxy still has to be told to use HTTP/2 for that upstream.
494
+
495
+ `type = builtin` stays the default, so a proxy that forwards to it keeps working
496
+ exactly as before.
497
+
470
498
  #### Manage user accounts with the reverse proxy
471
499
 
472
500
  Set the configuration option `type` in the `auth` section to
@@ -798,9 +826,60 @@ The following describes all configuration sections and options.
798
826
  The configuration options in this section are only relevant in standalone
799
827
  mode; they are ignored, when Radicale runs on WSGI.
800
828
 
829
+ ##### type
830
+
831
+ _(>= 3.9.0)_
832
+
833
+ The server to run: `builtin` (the default) or `hypercorn`.
834
+
835
+ The built-in server answers HTTP/1.0 and closes after each request. `hypercorn`
836
+ keeps connections alive and also serves **HTTP/2 over cleartext (H2C)** on the
837
+ same socket, which is what a reverse proxy in front of Radicale needs to speak
838
+ HTTP/2 without terminating TLS itself; it is an optional dependency, installed
839
+ with the `h2` extra using `pip install radicale[h2]` or by underlying
840
+ OS (package name: 'python3-hypercorn'), supported version: hypercorn>=0.17.
841
+ Asking for it when it is not installed fails with an error
842
+ naming the extra instead of falling back to the built-in server.
843
+
844
+ With `hypercorn` the TLS options are used as they are (`ssl`, `certificate`,
845
+ `key`, `certificate_authority`, `ciphersuite`) and `max_content_length` is
846
+ handed over to it — without that, hypercorn's own WSGI body limit of 16 Mbyte
847
+ would answer a larger request with `400` before Radicale sees it. Two details
848
+ change: `protocol` is **ignored** (hypercorn sets its own TLS versions, TLSv1.2
849
+ at the minimum) and an unset `ciphersuite` means hypercorn's default cipher
850
+ list rather than the system one. `max_connections` and `timeout` configure the
851
+ built-in server only, and a request that is still in flight is given three
852
+ seconds when the server stops.
853
+
854
+ Default: `builtin`
855
+
856
+ Note: while `builtin` has internal support for handling IPv4 and IPv6 binding
857
+ separate, `hypercorn` hasn't as of now. Following config runs into error
858
+ message
859
+
860
+ ```ini
861
+ [server]
862
+ hosts = [::]:5232,0.0.0.0:5232
863
+ ```
864
+
865
+ while having (here OS Linux):
866
+
867
+ ``` bash
868
+ # Sysctl status:
869
+ sysctl net.ipv6.bindv6only
870
+ net.ipv6.bindv6only = 0
871
+ ```
872
+
873
+ Solutions:
874
+ - reduce `hosts` config to IPv4 or IPv6
875
+ - adjust toggle on OS Linux by `sysctl -w net.ipv6.bindv6only=1` (don't forget to make this permanent)
876
+
801
877
  ##### hosts
802
878
 
803
- A comma separated list of addresses that the server will bind to.
879
+ A comma separated list of addresses that the server will bind to. A host name
880
+ is bound on every address it resolves to, as with the built-in server; with
881
+ `hypercorn` a failure to bind one of them stops the server, while the built-in
882
+ one logs it and serves the others.
804
883
 
805
884
  Default: `localhost:5232`
806
885
 
@@ -1705,11 +1784,12 @@ tutorial for an example.
1705
1784
  Default: (unset)
1706
1785
 
1707
1786
  Supported placeholders:
1708
- - `%(user)s`: logged-in user
1787
+ - `%(user)s`: logged-in (or in case of sharing: mapped) user
1709
1788
  - `%(cwd)s`: current working directory _(>= 3.5.1)_
1710
1789
  - `%(path)s`: full path of item _(>= 3.5.1)_
1711
1790
  - `%(to_path)s`: full path of destination item (only set on MOVE request) _(>= 3.5.5)_
1712
1791
  - `%(request)s`: request method _(>= 3.5.5)_
1792
+ - `%(actor)s`: logged-in user _(>= 3.9.0)_
1713
1793
 
1714
1794
  The command will be executed with base directory defined in `filesystem_folder` (see above)
1715
1795
 
@@ -1939,7 +2019,7 @@ Log storage cache actions on `level = debug`
1939
2019
 
1940
2020
  Default: `False`
1941
2021
 
1942
- ##### profiling_per_request
2022
+ ##### profiling
1943
2023
 
1944
2024
  _(>= 3.5.10)_
1945
2025
 
@@ -2076,7 +2156,7 @@ _(>= 3.5.5)_
2076
2156
 
2077
2157
  Port on SMTP server to connect to.
2078
2158
 
2079
- Default:
2159
+ Default: `25`
2080
2160
 
2081
2161
  ##### smtp_security
2082
2162
 
@@ -2397,7 +2477,7 @@ _(>= 3.7.0)_
2397
2477
 
2398
2478
  Default permissions for create token-based sharing
2399
2479
 
2400
- Default: `r`
2480
+ Default: `r` _(< 3.7.1)_ / `rp` _(>= 3.7.1)_
2401
2481
 
2402
2482
  Supported: `rfwEePp`
2403
2483
 
@@ -2580,7 +2660,7 @@ address books. Use Radicale's web interface or a client with support for it
2580
2660
  To create a new calendar run something like:
2581
2661
 
2582
2662
  ```bash
2583
- $ curl -u user -X MKCOL 'http://localhost:5232/user/calendar' --data \
2663
+ curl -u user -X MKCOL 'http://localhost:5232/user/calendar' --data \
2584
2664
  '<?xml version="1.0" encoding="UTF-8" ?>
2585
2665
  <create xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:I="http://apple.com/ns/ical/">
2586
2666
  <set>
@@ -2605,7 +2685,7 @@ $ curl -u user -X MKCOL 'http://localhost:5232/user/calendar' --data \
2605
2685
  To create a new address book run something like:
2606
2686
 
2607
2687
  ```bash
2608
- $ curl -u user -X MKCOL 'http://localhost:5232/user/addressbook' --data \
2688
+ curl -u user -X MKCOL 'http://localhost:5232/user/addressbook' --data \
2609
2689
  '<?xml version="1.0" encoding="UTF-8" ?>
2610
2690
  <create xmlns="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav">
2611
2691
  <set>
@@ -2718,6 +2798,11 @@ The following `permissions` are recognized:
2718
2798
  * **t:** deny create of token-based sharing of collection in case `permit_create_token=True` _(>= 3.7.0)_
2719
2799
  * **M:** permit create of map-based sharing of collection in case `permit_create_map= False` _(>= 3.7.0)_
2720
2800
  * **m:** deny create of map-based sharing of collection in case `permit_create_map=True` _(>= 3.7.0)_
2801
+ * **P:** permit properties overlay in case permit_properties_overlay=False _(>= 3.7.0)_
2802
+ * **p:** deny properties overlay in case permit_properties_overlay=True _(>= 3.7.0)_
2803
+ * **E:** enable enforce properties overlay in case enforce_properties_overlay=False _(>= 3.7.0)_
2804
+ * **e:** disable enforce of properties overlay in case enforce_properties_overlay=True _(>= 3.7.0)_
2805
+ * **l:** lock properties (disable write-properties) _(>= 3.8.4)_
2721
2806
 
2722
2807
  ### Storage
2723
2808
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Radicale
3
- Version: 3.8.2
3
+ Version: 3.9.0
4
4
  Summary: CalDAV and CardDAV Server
5
5
  Author-email: Guillaume Ayoub <guillaume.ayoub@kozea.fr>, Unrud <unrud@outlook.com>, Peter Bieringer <pb@bieringer.de>
6
6
  License: GNU GPL v3
@@ -39,6 +39,9 @@ Requires-Dist: bcrypt; extra == "test"
39
39
  Requires-Dist: argon2-cffi; extra == "test"
40
40
  Requires-Dist: pam; extra == "test"
41
41
  Requires-Dist: ldap3; extra == "test"
42
+ Requires-Dist: hypercorn>=0.17; extra == "test"
43
+ Provides-Extra: h2
44
+ Requires-Dist: hypercorn>=0.17; extra == "h2"
42
45
  Provides-Extra: bcrypt
43
46
  Requires-Dist: bcrypt; extra == "bcrypt"
44
47
  Provides-Extra: argon2
@@ -56,8 +59,11 @@ Requires-Dist: pytest-playwright; extra == "dev"
56
59
  Requires-Dist: html5validator; extra == "dev"
57
60
  Provides-Extra: caldav-test
58
61
  Requires-Dist: pytest>=7; extra == "caldav-test"
59
- Requires-Dist: caldav>=3.0; python_version >= "3.10" and extra == "caldav-test"
60
- Requires-Dist: caldav-server-tester<2,>=1.2; python_version >= "3.10" and extra == "caldav-test"
62
+ Requires-Dist: caldav==3.5.0; python_version >= "3.10" and extra == "caldav-test"
63
+ Requires-Dist: caldav-server-tester==1.4.0; python_version >= "3.10" and extra == "caldav-test"
64
+ Requires-Dist: icalendar==7.3.0; python_version >= "3.10" and extra == "caldav-test"
65
+ Requires-Dist: icalendar-searcher==1.2.0; python_version >= "3.10" and extra == "caldav-test"
66
+ Requires-Dist: recurring-ical-events==3.8.2; python_version >= "3.10" and extra == "caldav-test"
61
67
  Dynamic: license-file
62
68
 
63
69
  # Radicale
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Radicale
3
- Version: 3.8.2
3
+ Version: 3.9.0
4
4
  Summary: CalDAV and CardDAV Server
5
5
  Author-email: Guillaume Ayoub <guillaume.ayoub@kozea.fr>, Unrud <unrud@outlook.com>, Peter Bieringer <pb@bieringer.de>
6
6
  License: GNU GPL v3
@@ -39,6 +39,9 @@ Requires-Dist: bcrypt; extra == "test"
39
39
  Requires-Dist: argon2-cffi; extra == "test"
40
40
  Requires-Dist: pam; extra == "test"
41
41
  Requires-Dist: ldap3; extra == "test"
42
+ Requires-Dist: hypercorn>=0.17; extra == "test"
43
+ Provides-Extra: h2
44
+ Requires-Dist: hypercorn>=0.17; extra == "h2"
42
45
  Provides-Extra: bcrypt
43
46
  Requires-Dist: bcrypt; extra == "bcrypt"
44
47
  Provides-Extra: argon2
@@ -56,8 +59,11 @@ Requires-Dist: pytest-playwright; extra == "dev"
56
59
  Requires-Dist: html5validator; extra == "dev"
57
60
  Provides-Extra: caldav-test
58
61
  Requires-Dist: pytest>=7; extra == "caldav-test"
59
- Requires-Dist: caldav>=3.0; python_version >= "3.10" and extra == "caldav-test"
60
- Requires-Dist: caldav-server-tester<2,>=1.2; python_version >= "3.10" and extra == "caldav-test"
62
+ Requires-Dist: caldav==3.5.0; python_version >= "3.10" and extra == "caldav-test"
63
+ Requires-Dist: caldav-server-tester==1.4.0; python_version >= "3.10" and extra == "caldav-test"
64
+ Requires-Dist: icalendar==7.3.0; python_version >= "3.10" and extra == "caldav-test"
65
+ Requires-Dist: icalendar-searcher==1.2.0; python_version >= "3.10" and extra == "caldav-test"
66
+ Requires-Dist: recurring-ical-events==3.8.2; python_version >= "3.10" and extra == "caldav-test"
61
67
  Dynamic: license-file
62
68
 
63
69
  # Radicale
@@ -99,11 +99,13 @@ radicale/web/internal_data/css/icon.png
99
99
  radicale/web/internal_data/css/loading.svg
100
100
  radicale/web/internal_data/css/logo.svg
101
101
  radicale/web/internal_data/css/main.css
102
+ radicale/web/internal_data/css/icons/check-circle.svg
102
103
  radicale/web/internal_data/css/icons/delete.svg
103
104
  radicale/web/internal_data/css/icons/download.svg
104
105
  radicale/web/internal_data/css/icons/edit.svg
105
106
  radicale/web/internal_data/css/icons/eye.svg
106
107
  radicale/web/internal_data/css/icons/key.svg
108
+ radicale/web/internal_data/css/icons/minus-circle.svg
107
109
  radicale/web/internal_data/css/icons/new.svg
108
110
  radicale/web/internal_data/css/icons/repeat.svg
109
111
  radicale/web/internal_data/css/icons/share.svg
@@ -119,7 +121,6 @@ radicale/web/internal_data/js/scenes/CollectionsScene.js
119
121
  radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js
120
122
  radicale/web/internal_data/js/scenes/CreateEditShareScene.js
121
123
  radicale/web/internal_data/js/scenes/DeleteConfirmationScene.js
122
- radicale/web/internal_data/js/scenes/IncomingSharingScene.js
123
124
  radicale/web/internal_data/js/scenes/LoadingScene.js
124
125
  radicale/web/internal_data/js/scenes/LoginScene.js
125
126
  radicale/web/internal_data/js/scenes/ShareCollectionScene.js
@@ -14,8 +14,11 @@ bcrypt
14
14
  pytest>=7
15
15
 
16
16
  [caldav_test:python_version >= "3.10"]
17
- caldav>=3.0
18
- caldav-server-tester<2,>=1.2
17
+ caldav==3.5.0
18
+ caldav-server-tester==1.4.0
19
+ icalendar==7.3.0
20
+ icalendar-searcher==1.2.0
21
+ recurring-ical-events==3.8.2
19
22
 
20
23
  [dev]
21
24
  flake8
@@ -25,6 +28,9 @@ pytest
25
28
  pytest-playwright
26
29
  html5validator
27
30
 
31
+ [h2]
32
+ hypercorn>=0.17
33
+
28
34
  [ldap]
29
35
  ldap3
30
36
 
@@ -38,3 +44,4 @@ bcrypt
38
44
  argon2-cffi
39
45
  pam
40
46
  ldap3
47
+ hypercorn>=0.17
@@ -11,7 +11,11 @@
11
11
 
12
12
  [server]
13
13
 
14
- # CalDAV server hostnames separated by a comma
14
+ # Server to run
15
+ # Value: builtin (HTTP/1.0) | hypercorn (HTTP/1 + HTTP/2 incl. H2C)
16
+ #type = builtin
17
+
18
+ # Server IP addresses or hostnames separated by a comma the server will bind to
15
19
  # IPv4 syntax: address:port
16
20
  # IPv6 syntax: [address]:port
17
21
  # Hostname syntax (using "getaddrinfo" to resolve to IPv4/IPv6 adress(es)): hostname:port
@@ -178,7 +182,7 @@
178
182
  #oauth2_client_secret =
179
183
 
180
184
  # PAM service
181
- #pam_serivce = radicale
185
+ #pam_service = radicale
182
186
 
183
187
  # PAM group user should be member of
184
188
  #pam_group_membership =
@@ -3,7 +3,7 @@ name = "Radicale"
3
3
  # When the version is updated, a new section in the CHANGELOG.md file must be
4
4
  # added too.
5
5
  readme = "README.md"
6
- version = "3.8.2"
6
+ version = "3.9.0"
7
7
  authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}]
8
8
  license = {text = "GNU GPL v3"}
9
9
  description = "CalDAV and CardDAV Server"
@@ -40,13 +40,25 @@ dependencies = [
40
40
 
41
41
 
42
42
  [project.optional-dependencies]
43
- test = ["pytest>=7", "waitress", "bcrypt", "argon2-cffi", "pam", "ldap3"]
43
+ test = ["pytest>=7", "waitress", "bcrypt", "argon2-cffi", "pam", "ldap3", "hypercorn>=0.17"]
44
+ h2 = ["hypercorn>=0.17"]
44
45
  bcrypt = ["bcrypt"]
45
46
  argon2 = ["argon2-cffi"]
46
47
  ldap = ["ldap3"]
47
48
  pam = ["pam"]
48
49
  dev = ["flake8", "isort", "mypy", "pytest", "pytest-playwright", "html5validator"]
49
- caldav_test = ["pytest>=7", "caldav>=3.0; python_version >= '3.10'", "caldav-server-tester>=1.2,<2; python_version >= '3.10'"]
50
+ # Exact pins: the observed feature report depends on the client libraries
51
+ # (feature model, search matching, recurrence expansion), not only on
52
+ # caldav-server-tester, so they are all frozen together. This is the single
53
+ # place to bump them, see caldav_compat_tests/README.md.
54
+ caldav_test = [
55
+ "pytest>=7",
56
+ "caldav==3.5.0; python_version >= '3.10'",
57
+ "caldav-server-tester==1.4.0; python_version >= '3.10'",
58
+ "icalendar==7.3.0; python_version >= '3.10'",
59
+ "icalendar-searcher==1.2.0; python_version >= '3.10'",
60
+ "recurring-ical-events==3.8.2; python_version >= '3.10'",
61
+ ]
50
62
 
51
63
  [project.scripts]
52
64
  radicale = "radicale.__main__:run"
@@ -226,7 +238,7 @@ commands = [["mypy", "--install-types", "--non-interactive", "."]]
226
238
  skip_install = true
227
239
 
228
240
  [tool.tox.env.integ_test]
229
- description = "Server+WebUI: run 'playwright' integration tests (with 'chromium')"
241
+ description = "Server+WebUI: run 'playwright' integration tests (with 'chromium', may require additional system libs, check with 'ldd `find .tox/ -name chrome-headless-shell` | grep found')"
230
242
  deps = ["pytest", "pytest-playwright"]
231
243
  # Store Playwright browsers inside .tox for better caching
232
244
  setenv = { PLAYWRIGHT_BROWSERS_PATH = "{toxworkdir}/.playwright" }
@@ -235,12 +247,10 @@ commands = [["pytest", "integ_tests"]]
235
247
 
236
248
  [tool.tox.env.caldav_compat_test]
237
249
  description = "Server: run caldav compatibility tests"
250
+ # versions are pinned in project.optional-dependencies.caldav_test
238
251
  extras = ["caldav_test"]
239
- deps = [
240
- "caldav",
241
- "caldav_server_tester==1.2.0", # synchronize version with .github/workflows/test.yml#caldav-compat-test
242
- "pytest"
243
- ]
252
+ deps = "{[project.optional-dependencies]caldav_test}"
253
+ allowlist_externals = [ "pytest" ]
244
254
  commands = [["pytest", "caldav_compat_tests"]]
245
255
 
246
256
  [tool.tox.env.js]
@@ -252,7 +262,7 @@ commands = [["npx", "-y", "-p", "typescript@7.0.2", "tsc", "-p", "radicale/web/j
252
262
  skip_install = true
253
263
 
254
264
  [tool.tox.env.html5validator]
255
- description = "WebUI: validate HTML"
265
+ description = "WebUI: validate HTML (requires 'java', e.g. 'java-latest-openjdk-headless')"
256
266
  deps = ["html5validator-2"]
257
267
  commands = [["html5validator", "--root", "radicale/web/internal_data/", "--also-check-css"]]
258
268
 
@@ -472,9 +472,10 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
472
472
  https_info = " " + environ.get("SSL_PROTOCOL", "") + " " + environ.get("SSL_CIPHER", "")
473
473
  else:
474
474
  https_info = ""
475
- logger.info("%s request for %r%s received from %s%s%s",
475
+ http_proto = " " + environ["SERVER_PROTOCOL"]
476
+ logger.info("%s request for %r%s received from %s%s%s%s",
476
477
  request_method, unsafe_path, depthinfo,
477
- remote_host, remote_useragent_txt, https_info)
478
+ remote_host, remote_useragent_txt, http_proto, https_info)
478
479
  if self._request_header_on_debug:
479
480
  if logger.isEnabledFor(logging.DEBUG):
480
481
  logger.debug("Request header:\n%s",
@@ -554,11 +555,12 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
554
555
  self.configuration, environ, base64.b64decode(
555
556
  authorization.encode("ascii"))).split(":", 1)
556
557
 
558
+ user_groups: set = set([])
557
559
  if login and not app_base._check_user_format(self._storage, login, self._validate_user_value, self._urldecode_username):
558
560
  info = "not compliant to %r" % self._validate_user_value
559
561
  user = ""
560
562
  else:
561
- (user, info) = self._auth.login(login, password, context) or ("", "") if login else ("", "")
563
+ (user, info, user_groups) = self._auth.login(login, password, context) or ("", "", set([])) if login else ("", "", set([]))
562
564
  request_info: dict = {
563
565
  "method": request_method,
564
566
  "login": login, # not 'user' in this step
@@ -603,14 +605,14 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
603
605
 
604
606
  if user:
605
607
  group_type = self.configuration.get("group", "type")
606
- if group_type in ["htgroup"]:
607
- self._rights._user_groups = self._group.groups(login) if login else set([])
608
+ if group_type in ["htgroup"] and login:
609
+ user_groups = self._group.groups(login)
610
+ logger.debug("Groups received from %r: %r", group_type, ",".join(user_groups))
608
611
  elif group_type in ["from_auth"]:
609
612
  auth_type = self.configuration.get("auth", "type")
610
613
  if auth_type in ["ldap", "pam"]:
611
614
  try:
612
- logger.debug("Groups received from %r: %r", auth_type, ",".join(self._auth._groups))
613
- self._rights._user_groups = self._auth._groups
615
+ logger.debug("Groups received from %r: %r", auth_type, ",".join(user_groups))
614
616
  except AttributeError:
615
617
  pass
616
618
 
@@ -621,8 +623,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
621
623
  principal = next(iter(self._storage.discover(
622
624
  principal_path, depth="1")), None)
623
625
  if not principal:
624
- if "W" in self._rights.authorization(user, principal_path):
625
- with self._storage.acquire_lock("w", user):
626
+ if "W" in self._rights.authorization(user, user_groups, principal_path):
627
+ with self._storage.acquire_lock("w", user, actor=user):
626
628
  try:
627
629
  new_coll, _, _ = self._storage.create_collection(principal_path)
628
630
  if new_coll:
@@ -677,7 +679,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
677
679
  status, headers, answer, xml_request = httputils.METHOD_NOT_ALLOWED
678
680
  else:
679
681
  status, headers, answer, xml_request = function(
680
- environ, base_prefix, path, user, request_info)
682
+ environ, base_prefix, path, user, user_groups, request_info)
681
683
  except PermissionError as e:
682
684
  logger.error("PermissionError: %s", e)
683
685
  status, headers, answer, xml_request = httputils.INTERNAL_SERVER_ERROR
@@ -267,6 +267,7 @@ class Access:
267
267
  """Helper class to check access rights of an item"""
268
268
 
269
269
  user: str
270
+ user_groups: set
270
271
  path: str
271
272
  parent_path: str
272
273
  permissions: str
@@ -274,13 +275,14 @@ class Access:
274
275
  _parent_permissions: Optional[str]
275
276
  _permissions_filter: Union[str, None] = None
276
277
 
277
- def __init__(self, rights: rights.BaseRights, user: str, path: str, permissions_filter: Union[str, None] = None
278
+ def __init__(self, rights: rights.BaseRights, user: str, user_groups: set, path: str, permissions_filter: Union[str, None] = None
278
279
  ) -> None:
279
280
  self._rights = rights
280
281
  self.user = user
282
+ self.user_groups = user_groups
281
283
  self.path = path
282
284
  self.parent_path = pathutils.parent_path(path)
283
- self.permissions = self._rights.authorization(self.user, self.path)
285
+ self.permissions = self._rights.authorization(self.user, self.user_groups, self.path)
284
286
  if permissions_filter is not None:
285
287
  self._permissions_filter = permissions_filter
286
288
  self.permissions = intersect(
@@ -293,7 +295,7 @@ class Access:
293
295
  return self.permissions
294
296
  if self._parent_permissions is None:
295
297
  self._parent_permissions = self._rights.authorization(
296
- self.user, self.parent_path)
298
+ self.user, self.user_groups, self.parent_path)
297
299
  if self._permissions_filter is not None:
298
300
  self._parent_permissions = intersect(
299
301
  _with_implied_freebusy(self._parent_permissions),
@@ -302,7 +304,7 @@ class Access:
302
304
 
303
305
  def check(self, permission: str,
304
306
  item: Optional[types.CollectionOrItem] = None) -> bool:
305
- if permission not in "rwdDoO":
307
+ if permission not in rights.INTERNAL_PERMISSIONS_ITEM:
306
308
  raise ValueError("Invalid permission argument: %r" % permission)
307
309
  if not item:
308
310
  permissions = permission + permission.upper()
@@ -23,7 +23,7 @@ from http import client
23
23
  from typing import Optional, Union
24
24
  from urllib.parse import quote
25
25
 
26
- from radicale import httputils, sharing, storage, types, xmlutils
26
+ from radicale import httputils, storage, types, xmlutils
27
27
  from radicale.app.base import Access, ApplicationBase
28
28
  from radicale.hook import HookNotificationItem, HookNotificationItemTypes
29
29
  from radicale.log import logger
@@ -66,27 +66,23 @@ def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection,
66
66
  class ApplicationPartDelete(ApplicationBase):
67
67
 
68
68
  def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str,
69
- path: str, user: str, request_info: dict) -> types.WSGIResponse:
69
+ path: str, user: str, user_groups: set, request_info: dict) -> types.WSGIResponse:
70
70
  """Manage DELETE request."""
71
71
  actor = user
72
72
  permissions_filter = None
73
73
  share = None
74
74
  if self._sharing._enabled:
75
75
  # Sharing by token or map (if enabled)
76
- share = self._sharing.sharing_collection_resolver(path, user)
77
- user_lookup = user
78
- if self._rights._user_groups is not None and len(self._rights._user_groups) > 0:
79
- user_lookup += sharing.SHARING_SEPARATOR_GROUP + ','.join(self._rights._user_groups)
80
- share = self._sharing.sharing_collection_resolver(path, user_lookup)
76
+ share = self._sharing.sharing_collection_resolver(path, user, user_groups)
81
77
  if share:
82
- if path.endswith('/'):
83
- logger.notice("delete of a mapped collection is never permitted: %r -> %r", share['PathOrToken'], path)
78
+ if share['PathMapped'].endswith('/'):
79
+ logger.notice("delete of a mapped collection is never permitted: %r -> %r", share['PathOrToken'], share['PathMapped'])
84
80
  return httputils.NOT_ALLOWED
85
81
  # overwrite and run through extended permission check
86
82
  path = share['PathMapped']
87
83
  user = share['Owner']
88
84
  permissions_filter = share['Permissions']
89
- access = Access(self._rights, user, path, permissions_filter)
85
+ access = Access(self._rights, user, user_groups, path, permissions_filter)
90
86
  if not access.check("w"):
91
87
  return httputils.NOT_ALLOWED
92
88
  if self._sharing._enabled:
@@ -95,7 +91,7 @@ class ApplicationPartDelete(ApplicationBase):
95
91
  if collections_share_list is not None and len(collections_share_list) > 0:
96
92
  logger.notice("delete of a mapped collection in use (%d) is not permitted: %r", len(collections_share_list), path)
97
93
  return httputils.CONFLICT
98
- with self._storage.acquire_lock("w", user, path=path, request="DELETE"):
94
+ with self._storage.acquire_lock("w", user, path=path, request="DELETE", actor=actor):
99
95
  item = next(iter(self._storage.discover(path)), None)
100
96
  if not item:
101
97
  return httputils.NOT_FOUND