diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 69405101..855c671a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -902,8 +902,15 @@ Available types are: Requires validation, otherwise clients can supply the header themselves, which then is unconditionally trusted. +* `http_remote_user` _(>= 3.5.9)_ + Takes the username from the Remote-User HTTP header `HTTP_REMOTE_USER` and disables + Radicale's internal HTTP authentication. This can be used to provide the + username from a reverse proxy which authenticated the client upfront. + Requires validation, otherwise clients can supply the header themselves, + which then is unconditionally trusted. + * `http_x_remote_user` - Takes the username from the `X-Remote-User` HTTP header and disables + Takes the username from the X-Remote-User HTTP header `HTTP_X_REMOTE_USER` and disables Radicale's internal HTTP authentication. This can be used to provide the username from a reverse proxy which authenticated the client upfront. Requires validation, otherwise clients can supply the header themselves, diff --git a/config b/config index 77df29b9..93291650 100644 --- a/config +++ b/config @@ -63,7 +63,7 @@ [auth] # Authentication method -# Value: none | htpasswd | remote_user | http_x_remote_user | dovecot | ldap | oauth2 | pam | denyall +# Value: none | htpasswd | remote_user | http_remote_user | http_x_remote_user | dovecot | ldap | oauth2 | pam | denyall #type = denyall # Cache logins for until expiration time diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index bb418431..0b895820 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -275,7 +275,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead, logger.debug("Called by reverse proxy, remove base prefix %r from path: %r => %r", base_prefix, path, path_new) path = path_new else: - if self._auth_type in ['remote_user', 'http_x_remote_user'] and self._web_type == 'internal': + if self._auth_type in ['remote_user', 'http_remote_user', 'http_x_remote_user'] and self._web_type == 'internal': logger.warning("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching (may cause authentication issues using internal WebUI)", base_prefix, path) else: logger.debug("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching", base_prefix, path) diff --git a/radicale/auth/__init__.py b/radicale/auth/__init__.py index c32a8306..7eadb18a 100644 --- a/radicale/auth/__init__.py +++ b/radicale/auth/__init__.py @@ -23,7 +23,7 @@ Authentication module. Authentication is based on usernames and passwords. If something more advanced is needed an external WSGI server or reverse proxy can be used -(see ``remote_user`` or ``http_x_remote_user`` backend). +(see ``remote_user``, ``http_remote_user`` or ``http_x_remote_user`` backend). Take a look at the class ``BaseAuth`` if you want to implement your own. @@ -40,6 +40,7 @@ from radicale import config, types, utils from radicale.log import logger INTERNAL_TYPES: Sequence[str] = ("none", "remote_user", "http_x_remote_user", + "http_remote_user", "denyall", "htpasswd", "ldap", @@ -59,6 +60,7 @@ CACHE_LOGIN_TYPES: Sequence[str] = ( INSECURE_IF_NO_LOOPBACK_TYPES: Sequence[str] = ( "remote_user", + "http_remote_user", "http_x_remote_user", ) diff --git a/radicale/auth/http_remote_user.py b/radicale/auth/http_remote_user.py new file mode 100644 index 00000000..40695f7b --- /dev/null +++ b/radicale/auth/http_remote_user.py @@ -0,0 +1,36 @@ +# This file is part of Radicale - CalDAV and CardDAV server +# Copyright © 2025-2025 Peter Bieringer +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +Authentication backend that takes the username from the +``HTTP_REMOTE_USER`` header. + +It's intended for use with a reverse proxy. Be aware as this will be insecure +if the reverse proxy is not configured properly. + +""" + +from typing import Tuple, Union + +from radicale import types +from radicale.auth import none + + +class Auth(none.Auth): + + def get_external_login(self, environ: types.WSGIEnviron) -> Union[ + Tuple[()], Tuple[str, str]]: + return environ.get("HTTP_REMOTE_USER", ""), "" diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py index 5ffc540d..86b61062 100644 --- a/radicale/tests/test_auth.py +++ b/radicale/tests/test_auth.py @@ -263,6 +263,23 @@ class TestBaseAuthRequests(BaseTest): href_element = prop.find(xmlutils.make_clark("D:href")) assert href_element is not None and href_element.text == "/test/" + def test_http_remote_user(self) -> None: + self.configure({"auth": {"type": "http_remote_user"}}) + _, responses = self.propfind("/", """\ + + + + + +""", HTTP_REMOTE_USER="test") + assert responses is not None + response = responses["/"] + assert not isinstance(response, int) + status, prop = response["D:current-user-principal"] + assert status == 200 + href_element = prop.find(xmlutils.make_clark("D:href")) + assert href_element is not None and href_element.text == "/test/" + def test_http_x_remote_user(self) -> None: self.configure({"auth": {"type": "http_x_remote_user"}}) _, responses = self.propfind("/", """\