Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/datastructures/auth.py: 35%

141 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 07:17 +0000

1from __future__ import annotations 

2 

3import base64 

4import binascii 

5import typing as t 

6 

7from ..http import dump_header 

8from ..http import parse_dict_header 

9from ..http import quote_header_value 

10from .structures import CallbackDict 

11 

12if t.TYPE_CHECKING: 

13 import typing_extensions as te 

14 

15 

16class Authorization: 

17 """Represents the parts of an ``Authorization`` request header. 

18 

19 :attr:`.Request.authorization` returns an instance if the header is set. 

20 

21 An instance can be used with the test :class:`.Client` request methods' ``auth`` 

22 parameter to send the header in test requests. 

23 

24 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be 

25 set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password`` 

26 parameters. 

27 

28 For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the 

29 :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``. 

30 

31 .. versionchanged:: 2.3 

32 The ``token`` parameter and attribute was added to support auth schemes that use 

33 a token instead of parameters, such as ``Bearer``. 

34 

35 .. versionchanged:: 2.3 

36 The object is no longer a ``dict``. 

37 

38 .. versionchanged:: 0.5 

39 The object is an immutable dict. 

40 """ 

41 

42 def __init__( 

43 self, 

44 auth_type: str, 

45 data: dict[str, str | None] | None = None, 

46 token: str | None = None, 

47 ) -> None: 

48 self.type = auth_type 

49 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``.""" 

50 

51 if data is None: 

52 data = {} 

53 

54 self.parameters = data 

55 """A dict of parameters parsed from the header. Either this or :attr:`token` 

56 will have a value for a given scheme. 

57 """ 

58 

59 self.token = token 

60 """A token parsed from the header. Either this or :attr:`parameters` will have a 

61 value for a given scheme. 

62 

63 .. versionadded:: 2.3 

64 """ 

65 

66 def __getattr__(self, name: str) -> str | None: 

67 return self.parameters.get(name) 

68 

69 def __getitem__(self, name: str) -> str | None: 

70 return self.parameters.get(name) 

71 

72 def get(self, key: str, default: str | None = None) -> str | None: 

73 return self.parameters.get(key, default) 

74 

75 def __contains__(self, key: str) -> bool: 

76 return key in self.parameters 

77 

78 def __eq__(self, other: object) -> bool: 

79 if not isinstance(other, Authorization): 

80 return NotImplemented 

81 

82 return ( 

83 other.type == self.type 

84 and other.token == self.token 

85 and other.parameters == self.parameters 

86 ) 

87 

88 @classmethod 

89 def from_header(cls, value: str | None) -> te.Self | None: 

90 """Parse an ``Authorization`` header value and return an instance, or ``None`` 

91 if the value is empty. 

92 

93 :param value: The header value to parse. 

94 

95 .. versionadded:: 2.3 

96 """ 

97 if not value: 

98 return None 

99 

100 scheme, _, rest = value.partition(" ") 

101 scheme = scheme.lower() 

102 rest = rest.strip() 

103 

104 if scheme == "basic": 

105 try: 

106 username, _, password = base64.b64decode(rest).decode().partition(":") 

107 except (binascii.Error, UnicodeError): 

108 return None 

109 

110 return cls(scheme, {"username": username, "password": password}) 

111 

112 if "=" in rest.rstrip("="): 

113 # = that is not trailing, this is parameters. 

114 return cls(scheme, parse_dict_header(rest), None) 

115 

116 # No = or only trailing =, this is a token. 

117 return cls(scheme, None, rest) 

118 

119 def to_header(self) -> str: 

120 """Produce an ``Authorization`` header value representing this data. 

121 

122 .. versionadded:: 2.0 

123 """ 

124 if self.type == "basic": 

125 value = base64.b64encode( 

126 f"{self.username}:{self.password}".encode() 

127 ).decode("utf8") 

128 return f"Basic {value}" 

129 

130 if self.token is not None: 

131 return f"{self.type.title()} {self.token}" 

132 

133 return f"{self.type.title()} {dump_header(self.parameters)}" 

134 

135 def __str__(self) -> str: 

136 return self.to_header() 

137 

138 def __repr__(self) -> str: 

139 return f"<{type(self).__name__} {self.to_header()}>" 

140 

141 

142class WWWAuthenticate: 

143 """Represents the parts of a ``WWW-Authenticate`` response header. 

144 

145 Set :attr:`.Response.www_authenticate` to an instance of list of instances to set 

146 values for this header in the response. Modifying this instance will modify the 

147 header value. 

148 

149 Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be 

150 set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a 

151 token. 

152 

153 For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters` 

154 dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and 

155 ``"key" in auth`` are also provided. 

156 

157 .. versionchanged:: 2.3 

158 The ``token`` parameter and attribute was added to support auth schemes that use 

159 a token instead of parameters, such as ``Bearer``. 

160 

161 .. versionchanged:: 2.3 

162 The object is no longer a ``dict``. 

163 

164 .. versionchanged:: 2.3 

165 The ``on_update`` parameter was removed. 

166 """ 

167 

168 def __init__( 

169 self, 

170 auth_type: str, 

171 values: dict[str, str | None] | None = None, 

172 token: str | None = None, 

173 ): 

174 self._type = auth_type.lower() 

175 self._parameters: dict[str, str | None] = CallbackDict( # type: ignore[misc] 

176 values, lambda _: self._trigger_on_update() 

177 ) 

178 self._token = token 

179 self._on_update: t.Callable[[WWWAuthenticate], None] | None = None 

180 

181 def _trigger_on_update(self) -> None: 

182 if self._on_update is not None: 

183 self._on_update(self) 

184 

185 @property 

186 def type(self) -> str: 

187 """The authorization scheme, like ``basic``, ``digest``, or ``bearer``.""" 

188 return self._type 

189 

190 @type.setter 

191 def type(self, value: str) -> None: 

192 self._type = value 

193 self._trigger_on_update() 

194 

195 @property 

196 def parameters(self) -> dict[str, str | None]: 

197 """A dict of parameters for the header. Only one of this or :attr:`token` should 

198 have a value for a given scheme. 

199 """ 

200 return self._parameters 

201 

202 @parameters.setter 

203 def parameters(self, value: dict[str, str]) -> None: 

204 self._parameters = CallbackDict( # type: ignore[misc] 

205 value, lambda _: self._trigger_on_update() 

206 ) 

207 self._trigger_on_update() 

208 

209 @property 

210 def token(self) -> str | None: 

211 """A dict of parameters for the header. Only one of this or :attr:`token` should 

212 have a value for a given scheme. 

213 """ 

214 return self._token 

215 

216 @token.setter 

217 def token(self, value: str | None) -> None: 

218 """A token for the header. Only one of this or :attr:`parameters` should have a 

219 value for a given scheme. 

220 

221 .. versionadded:: 2.3 

222 """ 

223 self._token = value 

224 self._trigger_on_update() 

225 

226 def __getitem__(self, key: str) -> str | None: 

227 return self.parameters.get(key) 

228 

229 def __setitem__(self, key: str, value: str | None) -> None: 

230 if value is None: 

231 if key in self.parameters: 

232 del self.parameters[key] 

233 else: 

234 self.parameters[key] = value 

235 

236 self._trigger_on_update() 

237 

238 def __delitem__(self, key: str) -> None: 

239 if key in self.parameters: 

240 del self.parameters[key] 

241 self._trigger_on_update() 

242 

243 def __getattr__(self, name: str) -> str | None: 

244 return self[name] 

245 

246 def __setattr__(self, name: str, value: str | None) -> None: 

247 if name in {"_type", "_parameters", "_token", "_on_update"}: 

248 super().__setattr__(name, value) 

249 else: 

250 self[name] = value 

251 

252 def __delattr__(self, name: str) -> None: 

253 del self[name] 

254 

255 def __contains__(self, key: str) -> bool: 

256 return key in self.parameters 

257 

258 def __eq__(self, other: object) -> bool: 

259 if not isinstance(other, WWWAuthenticate): 

260 return NotImplemented 

261 

262 return ( 

263 other.type == self.type 

264 and other.token == self.token 

265 and other.parameters == self.parameters 

266 ) 

267 

268 def get(self, key: str, default: str | None = None) -> str | None: 

269 return self.parameters.get(key, default) 

270 

271 @classmethod 

272 def from_header(cls, value: str | None) -> te.Self | None: 

273 """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None`` 

274 if the value is empty. 

275 

276 :param value: The header value to parse. 

277 

278 .. versionadded:: 2.3 

279 """ 

280 if not value: 

281 return None 

282 

283 scheme, _, rest = value.partition(" ") 

284 scheme = scheme.lower() 

285 rest = rest.strip() 

286 

287 if "=" in rest.rstrip("="): 

288 # = that is not trailing, this is parameters. 

289 return cls(scheme, parse_dict_header(rest), None) 

290 

291 # No = or only trailing =, this is a token. 

292 return cls(scheme, None, rest) 

293 

294 def to_header(self) -> str: 

295 """Produce a ``WWW-Authenticate`` header value representing this data.""" 

296 if self.token is not None: 

297 return f"{self.type.title()} {self.token}" 

298 

299 if self.type == "digest": 

300 items = [] 

301 

302 for key, value in self.parameters.items(): 

303 if key in {"realm", "domain", "nonce", "opaque", "qop"}: 

304 value = quote_header_value(value, allow_token=False) 

305 else: 

306 value = quote_header_value(value) 

307 

308 items.append(f"{key}={value}") 

309 

310 return f"Digest {', '.join(items)}" 

311 

312 return f"{self.type.title()} {dump_header(self.parameters)}" 

313 

314 def __str__(self) -> str: 

315 return self.to_header() 

316 

317 def __repr__(self) -> str: 

318 return f"<{type(self).__name__} {self.to_header()}>"