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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

141 statements  

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

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( 

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(value, lambda _: self._trigger_on_update()) 

205 self._trigger_on_update() 

206 

207 @property 

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

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

210 have a value for a given scheme. 

211 """ 

212 return self._token 

213 

214 @token.setter 

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

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

217 value for a given scheme. 

218 

219 .. versionadded:: 2.3 

220 """ 

221 self._token = value 

222 self._trigger_on_update() 

223 

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

225 return self.parameters.get(key) 

226 

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

228 if value is None: 

229 if key in self.parameters: 

230 del self.parameters[key] 

231 else: 

232 self.parameters[key] = value 

233 

234 self._trigger_on_update() 

235 

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

237 if key in self.parameters: 

238 del self.parameters[key] 

239 self._trigger_on_update() 

240 

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

242 return self[name] 

243 

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

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

246 super().__setattr__(name, value) 

247 else: 

248 self[name] = value 

249 

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

251 del self[name] 

252 

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

254 return key in self.parameters 

255 

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

257 if not isinstance(other, WWWAuthenticate): 

258 return NotImplemented 

259 

260 return ( 

261 other.type == self.type 

262 and other.token == self.token 

263 and other.parameters == self.parameters 

264 ) 

265 

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

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

268 

269 @classmethod 

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

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

272 if the value is empty. 

273 

274 :param value: The header value to parse. 

275 

276 .. versionadded:: 2.3 

277 """ 

278 if not value: 

279 return None 

280 

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

282 scheme = scheme.lower() 

283 rest = rest.strip() 

284 

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

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

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

288 

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

290 return cls(scheme, None, rest) 

291 

292 def to_header(self) -> str: 

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

294 if self.token is not None: 

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

296 

297 if self.type == "digest": 

298 items = [] 

299 

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

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

302 value = quote_header_value(value, allow_token=False) 

303 else: 

304 value = quote_header_value(value) 

305 

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

307 

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

309 

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

311 

312 def __str__(self) -> str: 

313 return self.to_header() 

314 

315 def __repr__(self) -> str: 

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