Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/datastructures/auth.py: 34%
211 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
1from __future__ import annotations
3import base64
4import binascii
5import typing as t
6import warnings
7from functools import wraps
9from ..http import dump_header
10from ..http import parse_dict_header
11from ..http import parse_set_header
12from ..http import quote_header_value
13from .structures import CallbackDict
14from .structures import HeaderSet
16if t.TYPE_CHECKING:
17 import typing_extensions as te
20class Authorization:
21 """Represents the parts of an ``Authorization`` request header.
23 :attr:`.Request.authorization` returns an instance if the header is set.
25 An instance can be used with the test :class:`.Client` request methods' ``auth``
26 parameter to send the header in test requests.
28 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be
29 set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password``
30 parameters.
32 For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the
33 :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``.
35 .. versionchanged:: 2.3
36 The ``token`` parameter and attribute was added to support auth schemes that use
37 a token instead of parameters, such as ``Bearer``.
39 .. versionchanged:: 2.3
40 The object is no longer a ``dict``.
42 .. versionchanged:: 0.5
43 The object is an immutable dict.
44 """
46 def __init__(
47 self,
48 auth_type: str,
49 data: dict[str, str] | None = None,
50 token: str | None = None,
51 ) -> None:
52 self.type = auth_type
53 """The authorization scheme, like ``Basic``, ``Digest``, or ``Bearer``."""
55 if data is None:
56 data = {}
58 self.parameters = data
59 """A dict of parameters parsed from the header. Either this or :attr:`token`
60 will have a value for a give scheme.
61 """
63 self.token = token
64 """A token parsed from the header. Either this or :attr:`parameters` will have a
65 value for a given scheme.
67 .. versionadded:: 2.3
68 """
70 def __getattr__(self, name: str) -> str | None:
71 return self.parameters.get(name)
73 def __getitem__(self, name: str) -> str | None:
74 return self.parameters.get(name)
76 def get(self, key: str, default: str | None = None) -> str | None:
77 return self.parameters.get(key, default)
79 def __contains__(self, key: str) -> bool:
80 return key in self.parameters
82 def __eq__(self, other: object) -> bool:
83 if not isinstance(other, Authorization):
84 return NotImplemented
86 return (
87 other.type == self.type
88 and other.token == self.token
89 and other.parameters == self.parameters
90 )
92 @classmethod
93 def from_header(cls, value: str | None) -> te.Self | None:
94 """Parse an ``Authorization`` header value and return an instance, or ``None``
95 if the value is empty.
97 :param value: The header value to parse.
99 .. versionadded:: 2.3
100 """
101 if not value:
102 return None
104 scheme, _, rest = value.partition(" ")
105 scheme = scheme.lower()
106 rest = rest.strip()
108 if scheme == "basic":
109 try:
110 username, _, password = base64.b64decode(rest).decode().partition(":")
111 except (binascii.Error, UnicodeError):
112 return None
114 return cls(scheme, {"username": username, "password": password})
116 if "=" in rest.rstrip("="):
117 # = that is not trailing, this is parameters.
118 return cls(scheme, parse_dict_header(rest), None)
120 # No = or only trailing =, this is a token.
121 return cls(scheme, None, rest)
123 def to_header(self) -> str:
124 """Produce an ``Authorization`` header value representing this data.
126 .. versionadded:: 2.0
127 """
128 if self.type == "basic":
129 value = base64.b64encode(
130 f"{self.username}:{self.password}".encode()
131 ).decode("utf8")
132 return f"Basic {value}"
134 if self.token is not None:
135 return f"{self.type.title()} {self.token}"
137 return f"{self.type.title()} {dump_header(self.parameters)}"
139 def __str__(self) -> str:
140 return self.to_header()
142 def __repr__(self) -> str:
143 return f"<{type(self).__name__} {self.to_header()}>"
146def auth_property(name: str, doc: str | None = None) -> property:
147 """A static helper function for Authentication subclasses to add
148 extra authentication system properties onto a class::
150 class FooAuthenticate(WWWAuthenticate):
151 special_realm = auth_property('special_realm')
153 .. deprecated:: 2.3
154 Will be removed in Werkzeug 3.0.
155 """
156 warnings.warn(
157 "'auth_property' is deprecated and will be removed in Werkzeug 3.0.",
158 DeprecationWarning,
159 stacklevel=2,
160 )
162 def _set_value(self, value): # type: ignore[no-untyped-def]
163 if value is None:
164 self.pop(name, None)
165 else:
166 self[name] = str(value)
168 return property(lambda x: x.get(name), _set_value, doc=doc)
171class WWWAuthenticate:
172 """Represents the parts of a ``WWW-Authenticate`` response header.
174 Set :attr:`.Response.www_authenticate` to an instance of list of instances to set
175 values for this header in the response. Modifying this instance will modify the
176 header value.
178 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be
179 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a
180 token.
182 For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters`
183 dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and
184 ``"key" in auth`` are also provided.
186 .. versionchanged:: 2.3
187 The ``token`` parameter and attribute was added to support auth schemes that use
188 a token instead of parameters, such as ``Bearer``.
190 .. versionchanged:: 2.3
191 The object is no longer a ``dict``.
193 .. versionchanged:: 2.3
194 The ``on_update`` parameter was removed.
195 """
197 def __init__(
198 self,
199 auth_type: str | None = None,
200 values: dict[str, str] | None = None,
201 token: str | None = None,
202 ):
203 if auth_type is None:
204 warnings.warn(
205 "An auth type must be given as the first parameter. Assuming 'basic' is"
206 " deprecated and will be removed in Werkzeug 3.0.",
207 DeprecationWarning,
208 stacklevel=2,
209 )
210 auth_type = "basic"
212 self._type = auth_type.lower()
213 self._parameters: dict[str, str] = CallbackDict( # type: ignore[misc]
214 values, lambda _: self._trigger_on_update()
215 )
216 self._token = token
217 self._on_update: t.Callable[[WWWAuthenticate], None] | None = None
219 def _trigger_on_update(self) -> None:
220 if self._on_update is not None:
221 self._on_update(self)
223 @property
224 def type(self) -> str:
225 """The authorization scheme, like ``Basic``, ``Digest``, or ``Bearer``."""
226 return self._type
228 @type.setter
229 def type(self, value: str) -> None:
230 self._type = value
231 self._trigger_on_update()
233 @property
234 def parameters(self) -> dict[str, str]:
235 """A dict of parameters for the header. Only one of this or :attr:`token` should
236 have a value for a give scheme.
237 """
238 return self._parameters
240 @parameters.setter
241 def parameters(self, value: dict[str, str]) -> None:
242 self._parameters = CallbackDict( # type: ignore[misc]
243 value, lambda _: self._trigger_on_update()
244 )
245 self._trigger_on_update()
247 @property
248 def token(self) -> str | None:
249 """A dict of parameters for the header. Only one of this or :attr:`token` should
250 have a value for a give scheme.
251 """
252 return self._token
254 @token.setter
255 def token(self, value: str | None) -> None:
256 """A token for the header. Only one of this or :attr:`parameters` should have a
257 value for a given scheme.
259 .. versionadded:: 2.3
260 """
261 self._token = value
262 self._trigger_on_update()
264 def set_basic(self, realm: str = "authentication required") -> None:
265 """Clear any existing data and set a ``Basic`` challenge.
267 .. deprecated:: 2.3
268 Will be removed in Werkzeug 3.0. Create and assign an instance instead.
269 """
270 warnings.warn(
271 "The 'set_basic' method is deprecated and will be removed in Werkzeug 3.0."
272 " Create and assign an instance instead."
273 )
274 self._type = "basic"
275 dict.clear(self.parameters) # type: ignore[arg-type]
276 dict.update(
277 self.parameters, # type: ignore[arg-type]
278 {"realm": realm}, # type: ignore[dict-item]
279 )
280 self._token = None
281 self._trigger_on_update()
283 def set_digest(
284 self,
285 realm: str,
286 nonce: str,
287 qop: t.Sequence[str] = ("auth",),
288 opaque: str | None = None,
289 algorithm: str | None = None,
290 stale: bool = False,
291 ) -> None:
292 """Clear any existing data and set a ``Digest`` challenge.
294 .. deprecated:: 2.3
295 Will be removed in Werkzeug 3.0. Create and assign an instance instead.
296 """
297 warnings.warn(
298 "The 'set_digest' method is deprecated and will be removed in Werkzeug 3.0."
299 " Create and assign an instance instead."
300 )
301 self._type = "digest"
302 dict.clear(self.parameters) # type: ignore[arg-type]
303 parameters = {
304 "realm": realm,
305 "nonce": nonce,
306 "qop": ", ".join(qop),
307 "stale": "TRUE" if stale else "FALSE",
308 }
310 if opaque is not None:
311 parameters["opaque"] = opaque
313 if algorithm is not None:
314 parameters["algorithm"] = algorithm
316 dict.update(self.parameters, parameters) # type: ignore[arg-type]
317 self._token = None
318 self._trigger_on_update()
320 def __getitem__(self, key: str) -> str | None:
321 return self.parameters.get(key)
323 def __setitem__(self, key: str, value: str | None) -> None:
324 if value is None:
325 if key in self.parameters:
326 del self.parameters[key]
327 else:
328 self.parameters[key] = value
330 self._trigger_on_update()
332 def __delitem__(self, key: str) -> None:
333 if key in self.parameters:
334 del self.parameters[key]
335 self._trigger_on_update()
337 def __getattr__(self, name: str) -> str | None:
338 return self[name]
340 def __setattr__(self, name: str, value: str | None) -> None:
341 if name in {"_type", "_parameters", "_token", "_on_update"}:
342 super().__setattr__(name, value)
343 else:
344 self[name] = value
346 def __delattr__(self, name: str) -> None:
347 del self[name]
349 def __contains__(self, key: str) -> bool:
350 return key in self.parameters
352 def __eq__(self, other: object) -> bool:
353 if not isinstance(other, WWWAuthenticate):
354 return NotImplemented
356 return (
357 other.type == self.type
358 and other.token == self.token
359 and other.parameters == self.parameters
360 )
362 def get(self, key: str, default: str | None = None) -> str | None:
363 return self.parameters.get(key, default)
365 @classmethod
366 def from_header(cls, value: str | None) -> te.Self | None:
367 """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None``
368 if the value is empty.
370 :param value: The header value to parse.
372 .. versionadded:: 2.3
373 """
374 if not value:
375 return None
377 scheme, _, rest = value.partition(" ")
378 scheme = scheme.lower()
379 rest = rest.strip()
381 if "=" in rest.rstrip("="):
382 # = that is not trailing, this is parameters.
383 return cls(scheme, parse_dict_header(rest), None)
385 # No = or only trailing =, this is a token.
386 return cls(scheme, None, rest)
388 def to_header(self) -> str:
389 """Produce a ``WWW-Authenticate`` header value representing this data."""
390 if self.token is not None:
391 return f"{self.type.title()} {self.token}"
393 if self.type == "digest":
394 items = []
396 for key, value in self.parameters.items():
397 if key in {"realm", "domain", "nonce", "opaque", "qop"}:
398 value = quote_header_value(value, allow_token=False)
399 else:
400 value = quote_header_value(value)
402 items.append(f"{key}={value}")
404 return f"Digest {', '.join(items)}"
406 return f"{self.type.title()} {dump_header(self.parameters)}"
408 def __str__(self) -> str:
409 return self.to_header()
411 def __repr__(self) -> str:
412 return f"<{type(self).__name__} {self.to_header()}>"
414 @property
415 def qop(self) -> set[str]:
416 """The ``qop`` parameter as a set.
418 .. deprecated:: 2.3
419 Will be removed in Werkzeug 3.0. It will become the same as other
420 parameters, returning a string.
421 """
422 warnings.warn(
423 "The 'qop' property is deprecated and will be removed in Werkzeug 3.0."
424 " It will become the same as other parameters, returning a string.",
425 DeprecationWarning,
426 stacklevel=2,
427 )
429 def on_update(value: HeaderSet) -> None:
430 if not value:
431 if "qop" in self:
432 del self["qop"]
434 return
436 self.parameters["qop"] = value.to_header()
438 return parse_set_header(self.parameters.get("qop"), on_update)
440 @property
441 def stale(self) -> bool | None:
442 """The ``stale`` parameter as a boolean.
444 .. deprecated:: 2.3
445 Will be removed in Werkzeug 3.0. It will become the same as other
446 parameters, returning a string.
447 """
448 warnings.warn(
449 "The 'stale' property is deprecated and will be removed in Werkzeug 3.0."
450 " It will become the same as other parameters, returning a string.",
451 DeprecationWarning,
452 stacklevel=2,
453 )
455 if "stale" in self.parameters:
456 return self.parameters["stale"].lower() == "true"
458 return None
460 @stale.setter
461 def stale(self, value: bool | str | None) -> None:
462 if value is None:
463 if "stale" in self.parameters:
464 del self.parameters["stale"]
466 return
468 if isinstance(value, bool):
469 warnings.warn(
470 "Setting the 'stale' property to a boolean is deprecated and will be"
471 " removed in Werkzeug 3.0.",
472 DeprecationWarning,
473 stacklevel=2,
474 )
475 self.parameters["stale"] = "TRUE" if value else "FALSE"
476 else:
477 self.parameters["stale"] = value
479 auth_property = staticmethod(auth_property)
482def _deprecated_dict_method(f): # type: ignore[no-untyped-def]
483 @wraps(f)
484 def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
485 warnings.warn(
486 "Treating 'Authorization' and 'WWWAuthenticate' as a dict is deprecated and"
487 " will be removed in Werkzeug 3.0. Use the 'parameters' attribute instead.",
488 DeprecationWarning,
489 stacklevel=2,
490 )
491 return f(*args, **kwargs)
493 return wrapper
496for name in (
497 "__iter__",
498 "clear",
499 "copy",
500 "items",
501 "keys",
502 "pop",
503 "popitem",
504 "setdefault",
505 "update",
506 "values",
507):
508 f = _deprecated_dict_method(getattr(dict, name))
509 setattr(Authorization, name, f)
510 setattr(WWWAuthenticate, name, f)