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

1from __future__ import annotations 

2 

3import base64 

4import binascii 

5import typing as t 

6import warnings 

7from functools import wraps 

8 

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 

15 

16if t.TYPE_CHECKING: 

17 import typing_extensions as te 

18 

19 

20class Authorization: 

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

22 

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

24 

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

26 parameter to send the header in test requests. 

27 

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. 

31 

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``. 

34 

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``. 

38 

39 .. versionchanged:: 2.3 

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

41 

42 .. versionchanged:: 0.5 

43 The object is an immutable dict. 

44 """ 

45 

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``.""" 

54 

55 if data is None: 

56 data = {} 

57 

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 """ 

62 

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. 

66 

67 .. versionadded:: 2.3 

68 """ 

69 

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

71 return self.parameters.get(name) 

72 

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

74 return self.parameters.get(name) 

75 

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

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

78 

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

80 return key in self.parameters 

81 

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

83 if not isinstance(other, Authorization): 

84 return NotImplemented 

85 

86 return ( 

87 other.type == self.type 

88 and other.token == self.token 

89 and other.parameters == self.parameters 

90 ) 

91 

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. 

96 

97 :param value: The header value to parse. 

98 

99 .. versionadded:: 2.3 

100 """ 

101 if not value: 

102 return None 

103 

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

105 scheme = scheme.lower() 

106 rest = rest.strip() 

107 

108 if scheme == "basic": 

109 try: 

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

111 except (binascii.Error, UnicodeError): 

112 return None 

113 

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

115 

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

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

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

119 

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

121 return cls(scheme, None, rest) 

122 

123 def to_header(self) -> str: 

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

125 

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}" 

133 

134 if self.token is not None: 

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

136 

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

138 

139 def __str__(self) -> str: 

140 return self.to_header() 

141 

142 def __repr__(self) -> str: 

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

144 

145 

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:: 

149 

150 class FooAuthenticate(WWWAuthenticate): 

151 special_realm = auth_property('special_realm') 

152 

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 ) 

161 

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) 

167 

168 return property(lambda x: x.get(name), _set_value, doc=doc) 

169 

170 

171class WWWAuthenticate: 

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

173 

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. 

177 

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. 

181 

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. 

185 

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``. 

189 

190 .. versionchanged:: 2.3 

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

192 

193 .. versionchanged:: 2.3 

194 The ``on_update`` parameter was removed. 

195 """ 

196 

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" 

211 

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 

218 

219 def _trigger_on_update(self) -> None: 

220 if self._on_update is not None: 

221 self._on_update(self) 

222 

223 @property 

224 def type(self) -> str: 

225 """The authorization scheme, like ``Basic``, ``Digest``, or ``Bearer``.""" 

226 return self._type 

227 

228 @type.setter 

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

230 self._type = value 

231 self._trigger_on_update() 

232 

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 

239 

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() 

246 

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 

253 

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. 

258 

259 .. versionadded:: 2.3 

260 """ 

261 self._token = value 

262 self._trigger_on_update() 

263 

264 def set_basic(self, realm: str = "authentication required") -> None: 

265 """Clear any existing data and set a ``Basic`` challenge. 

266 

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() 

282 

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. 

293 

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 } 

309 

310 if opaque is not None: 

311 parameters["opaque"] = opaque 

312 

313 if algorithm is not None: 

314 parameters["algorithm"] = algorithm 

315 

316 dict.update(self.parameters, parameters) # type: ignore[arg-type] 

317 self._token = None 

318 self._trigger_on_update() 

319 

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

321 return self.parameters.get(key) 

322 

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 

329 

330 self._trigger_on_update() 

331 

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

333 if key in self.parameters: 

334 del self.parameters[key] 

335 self._trigger_on_update() 

336 

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

338 return self[name] 

339 

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 

345 

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

347 del self[name] 

348 

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

350 return key in self.parameters 

351 

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

353 if not isinstance(other, WWWAuthenticate): 

354 return NotImplemented 

355 

356 return ( 

357 other.type == self.type 

358 and other.token == self.token 

359 and other.parameters == self.parameters 

360 ) 

361 

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

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

364 

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. 

369 

370 :param value: The header value to parse. 

371 

372 .. versionadded:: 2.3 

373 """ 

374 if not value: 

375 return None 

376 

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

378 scheme = scheme.lower() 

379 rest = rest.strip() 

380 

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

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

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

384 

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

386 return cls(scheme, None, rest) 

387 

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}" 

392 

393 if self.type == "digest": 

394 items = [] 

395 

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) 

401 

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

403 

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

405 

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

407 

408 def __str__(self) -> str: 

409 return self.to_header() 

410 

411 def __repr__(self) -> str: 

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

413 

414 @property 

415 def qop(self) -> set[str]: 

416 """The ``qop`` parameter as a set. 

417 

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 ) 

428 

429 def on_update(value: HeaderSet) -> None: 

430 if not value: 

431 if "qop" in self: 

432 del self["qop"] 

433 

434 return 

435 

436 self.parameters["qop"] = value.to_header() 

437 

438 return parse_set_header(self.parameters.get("qop"), on_update) 

439 

440 @property 

441 def stale(self) -> bool | None: 

442 """The ``stale`` parameter as a boolean. 

443 

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 ) 

454 

455 if "stale" in self.parameters: 

456 return self.parameters["stale"].lower() == "true" 

457 

458 return None 

459 

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"] 

465 

466 return 

467 

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 

478 

479 auth_property = staticmethod(auth_property) 

480 

481 

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) 

492 

493 return wrapper 

494 

495 

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)