Skip to content

Commit bf26164

Browse files
committed
Support for auth_method connection parameter\nfixes #525
1 parent c70998f commit bf26164

2 files changed

Lines changed: 83 additions & 25 deletions

File tree

caldav/davclient.py

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"ssl_verify_cert",
8080
"ssl_cert",
8181
"auth",
82+
"auth_type",
8283
)
8384
)
8485

@@ -451,6 +452,7 @@ def __init__(
451452
username: Optional[str] = None,
452453
password: Optional[str] = None,
453454
auth: Optional[AuthBase] = None,
455+
auth_type: Optional[str] = None,
454456
timeout: Optional[int] = None,
455457
ssl_verify_cert: Union[bool, str] = True,
456458
ssl_cert: Union[str, Tuple[str, str], None] = None,
@@ -464,6 +466,7 @@ def __init__(
464466
* proxy: A string defining a proxy server: `scheme://hostname:port`. Scheme defaults to http, port defaults to 8080.
465467
* username and password should be passed as arguments or in the URL
466468
* auth, timeout and ssl_verify_cert are passed to niquests.request.
469+
* if auth_type is given, the auth-object will be auto-created. Auth_type can be ``bearer``, ``digest`` or ``basic``. Things are likely to work without ``auth_type`` set, but if nothing else the number of requests to the server will be reduced.
467470
* ssl_verify_cert can be the path of a CA-bundle or False.
468471
* huge_tree: boolean, enable XMLParser huge_tree to handle big events, beware
469472
of security issues, see : https://lxml.de/api/lxml.etree.XMLParser-class.html
@@ -515,10 +518,19 @@ def __init__(
515518

516519
self.username = username
517520
self.password = password
521+
self.auth = auth
522+
self.auth_type = auth_type
523+
518524
## I had problems with passwords with non-ascii letters in it ...
519525
if isinstance(self.password, str):
520526
self.password = self.password.encode("utf-8")
521-
self.auth = auth
527+
if auth and self.auth_type:
528+
logging.error(
529+
"both auth object and auth_type sent to DAVClient. The latter will be ignored."
530+
)
531+
elif self.auth_type:
532+
self.build_auth_object()
533+
522534
# TODO: it's possible to force through a specific auth method here,
523535
# but no test code for this.
524536
self.timeout = timeout
@@ -781,6 +793,44 @@ def extract_auth_types(self, header: str):
781793
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate#syntax
782794
return {h.split()[0] for h in header.lower().split(",")}
783795

796+
def build_auth_object(self, auth_types: Optional[List[str]] = None):
797+
"""Fixes self.auth. If ``self.auth_type`` is given, then
798+
insist on using this one. If not, then assume auth_types to
799+
be a list of acceptable auth types and choose the most
800+
appropriate one (prefer digest or basic if username is given,
801+
and bearer if password is given).
802+
803+
Parameters:
804+
* auth_types - A list/tuple of acceptable auth_types
805+
"""
806+
auth_type = self.auth_type
807+
if not auth_type and not auth_types:
808+
raise error.AuthorizationError(
809+
"No auth-type given. This shouldn't happen. Raise an issue at https://github.com/python-caldav/caldav/issues/ or by email noauthtype@plann.no"
810+
)
811+
if auth_types and auth_type and auth_type not in auth_types:
812+
raise error.AuthorizationError(
813+
reason=f"Configuration specifies to use {auth_type}, but server only accepts {auth_types}"
814+
)
815+
if not auth_type and auth_types:
816+
if self.username and "digest" in auth_types:
817+
auth_type = "digest"
818+
elif self.username and "basic" in auth_types:
819+
auth_type = "basic"
820+
elif self.password and "bearer" in auth_types:
821+
auth_type = "bearer"
822+
elif "bearer" in auth_types:
823+
raise error.AuthorizationError(
824+
reason="Server provides bearer auth, but no password given. The bearer token should be configured as password"
825+
)
826+
827+
if auth_type == "digest":
828+
self.auth = niquests.auth.HTTPDigestAuth(self.username, self.password)
829+
elif auth_type == "basic":
830+
self.auth = niquests.auth.HTTPBasicAuth(self.username, self.password)
831+
elif auth_type == "bearer":
832+
self.auth = HTTPBearerAuth(self.password)
833+
784834
def request(
785835
self,
786836
url: str,
@@ -851,21 +901,12 @@ def request(
851901
r.status_code == 401
852902
and "WWW-Authenticate" in r_headers
853903
and not self.auth
854-
and self.username
904+
and (self.username or self.password)
855905
):
856906
auth_types = self.extract_auth_types(r_headers["WWW-Authenticate"])
907+
self.build_auth_object(auth_types)
857908

858-
if self.username and "digest" in auth_types:
859-
self.auth = niquests.auth.HTTPDigestAuth(self.username, self.password)
860-
elif self.username and "basic" in auth_types:
861-
self.auth = niquests.auth.HTTPBasicAuth(self.username, self.password)
862-
elif self.password and "bearer" in auth_types:
863-
self.auth = HTTPBearerAuth(self.password)
864-
elif "bearer" in auth_types:
865-
raise error.AuthorizationError(
866-
reason="Server provides bearer auth, but no password given. The bearer token should be configured as password"
867-
)
868-
else:
909+
if not self.auth:
869910
raise NotImplementedError(
870911
"The server does not provide any of the currently "
871912
"supported authentication methods: basic, digest, bearer"
@@ -890,17 +931,8 @@ def request(
890931
## introduced this regression)
891932

892933
auth_types = self.extract_auth_types(r_headers["WWW-Authenticate"])
893-
894-
if self.password and self.username and "digest" in auth_types:
895-
self.auth = niquests.auth.HTTPDigestAuth(
896-
self.username, self.password.decode()
897-
)
898-
elif self.password and self.username and "basic" in auth_types:
899-
self.auth = niquests.auth.HTTPBasicAuth(
900-
self.username, self.password.decode()
901-
)
902-
elif self.password and "bearer" in auth_types:
903-
self.auth = HTTPBearerAuth(self.password.decode())
934+
self.password = self.password.decode()
935+
self.build_auth_object(auth_types)
904936

905937
self.username = None
906938
self.password = None

tests/test_caldav.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1872,11 +1872,37 @@ def testSearchTodos(self):
18721872

18731873
## Test that uncomplete works
18741874
## (except for GMX ... their server is weird)
1875-
self.skip_on_compatibility_flag('vtodo-cannot-be-uncompleted')
1875+
self.skip_on_compatibility_flag("vtodo-cannot-be-uncompleted")
18761876
t5.uncomplete()
18771877
some_todos = c.search(todo=True)
18781878
assert len(some_todos) == 4 + pre_cnt
18791879

1880+
def testWrongAuthType(self):
1881+
if (
1882+
not "password" in self.server_params
1883+
or not self.server_params["password"]
1884+
or self.server_params["password"] == "any-password-seems-to-work"
1885+
):
1886+
pytest.skip(
1887+
"Testing with wrong password skipped as calendar server does not require a password"
1888+
)
1889+
1890+
connect_params1 = self.server_params.copy()
1891+
for delme in ("setup", "teardown", "name"):
1892+
if delme in connect_params1:
1893+
connect_params1.pop(delme)
1894+
1895+
connect_params2 = connect_params1.copy()
1896+
1897+
## At least one of those two ought to fail
1898+
## as they are (or should be) incompatible
1899+
connect_params1["auth_type"] = "digest"
1900+
connect_params2["auth_type"] = "bearer"
1901+
1902+
with pytest.raises(error.AuthorizationError):
1903+
client(**connect_params1).principal()
1904+
client(**connect_params2).principal()
1905+
18801906
def testWrongPassword(self):
18811907
if (
18821908
not "password" in self.server_params

0 commit comments

Comments
 (0)