PostgreSQL Source Code git master
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
oauth_server.OAuthHandler Class Reference

Public Member Functions

def do_GET (self)
 
str client_id (self)
 
def do_POST (self)
 
JsonObject config (self)
 
JsonObject authorization (self)
 
JsonObject token (self)
 

Data Fields

 path
 

Static Public Attributes

 JsonObject = dict[str, object]
 

Private Member Functions

def _check_issuer (self)
 
def _check_authn (self)
 
dict[str, str_parse_params (self)
 
bool _should_modify (self)
 
def _get_param (self, name, default)
 
str _content_type (self)
 
int _interval (self)
 
str _retry_code (self)
 
str _uri_spelling (self)
 
def _response_padding (self)
 
def _access_token (self)
 
None _send_json (self, JsonObject js)
 
def _token_state (self)
 
def _remove_token_state (self)
 

Private Attributes

 _alt_issuer
 
 _parameterized
 
 _response_code
 
 _params
 
 _test_params
 

Detailed Description

Core implementation of the authorization server. The API is
inheritance-based, with entry points at do_GET() and do_POST(). See the
documentation for BaseHTTPRequestHandler.

Definition at line 19 of file oauth_server.py.

Member Function Documentation

◆ _access_token()

def oauth_server.OAuthHandler._access_token (   self)
private
The actual Bearer token sent back to the client on success. Tests may
override this with the "token" test parameter.

Definition at line 221 of file oauth_server.py.

221 def _access_token(self):
222 """
223 The actual Bearer token sent back to the client on success. Tests may
224 override this with the "token" test parameter.
225 """
226 token = self._get_param("token", None)
227 if token is not None:
228 return token
229
230 token = "9243959234"
231 if self._alt_issuer:
232 token += "-alt"
233
234 return token
235

References oauth_server.OAuthHandler._alt_issuer, and oauth_server.OAuthHandler._get_param().

Referenced by oauth_server.OAuthHandler.token().

◆ _check_authn()

def oauth_server.OAuthHandler._check_authn (   self)
private
Checks the expected value of the Authorization header, if any.

Definition at line 47 of file oauth_server.py.

47 def _check_authn(self):
48 """
49 Checks the expected value of the Authorization header, if any.
50 """
51 secret = self._get_param("expected_secret", None)
52 if secret is None:
53 return
54
55 assert "Authorization" in self.headers
56 method, creds = self.headers["Authorization"].split()
57
58 if method != "Basic":
59 raise RuntimeError(f"client used {method} auth; expected Basic")
60
61 username = urllib.parse.quote_plus(self.client_id)
62 password = urllib.parse.quote_plus(secret)
63 expected_creds = f"{username}:{password}"
64
65 if creds.encode() != base64.b64encode(expected_creds.encode()):
66 raise RuntimeError(
67 f"client sent '{creds}'; expected b64encode('{expected_creds}')"
68 )
69

References oauth_server.OAuthHandler._get_param(), oauth_server.OAuthHandler.client_id(), printTableContent.headers, and async_ctx.headers.

◆ _check_issuer()

def oauth_server.OAuthHandler._check_issuer (   self)
private
Switches the behavior of the provider depending on the issuer URI.

Definition at line 28 of file oauth_server.py.

28 def _check_issuer(self):
29 """
30 Switches the behavior of the provider depending on the issuer URI.
31 """
32 self._alt_issuer = (
33 self.path.startswith("/alternate/")
34 or self.path == "/.well-known/oauth-authorization-server/alternate"
35 )
36 self._parameterized = self.path.startswith("/param/")
37
38 if self._alt_issuer:
39 # The /alternate issuer uses IETF-style .well-known URIs.
40 if self.path.startswith("/.well-known/"):
41 self.path = self.path.removesuffix("/alternate")
42 else:
43 self.path = self.path.removeprefix("/alternate")
44 elif self._parameterized:
45 self.path = self.path.removeprefix("/param")
46

Referenced by oauth_server.OAuthHandler.do_POST().

◆ _content_type()

str oauth_server.OAuthHandler._content_type (   self)
private
Returns "application/json" unless the test has requested something
different.

Definition at line 178 of file oauth_server.py.

178 def _content_type(self) -> str:
179 """
180 Returns "application/json" unless the test has requested something
181 different.
182 """
183 return self._get_param("content_type", "application/json")
184

References oauth_server.OAuthHandler._get_param().

Referenced by oauth_server.OAuthHandler._send_json().

◆ _get_param()

def oauth_server.OAuthHandler._get_param (   self,
  name,
  default 
)
private
If the client has requested a modification to this stage (see
_should_modify()), this method searches the provided test parameters for
a key of the given name, and returns it if found. Otherwise the provided
default is returned.

Definition at line 165 of file oauth_server.py.

165 def _get_param(self, name, default):
166 """
167 If the client has requested a modification to this stage (see
168 _should_modify()), this method searches the provided test parameters for
169 a key of the given name, and returns it if found. Otherwise the provided
170 default is returned.
171 """
172 if self._should_modify() and name in self._test_params:
173 return self._test_params[name]
174
175 return default
176

References oauth_server.OAuthHandler._should_modify(), and oauth_server.OAuthHandler._test_params.

Referenced by oauth_server.OAuthHandler._access_token(), oauth_server.OAuthHandler._check_authn(), oauth_server.OAuthHandler._content_type(), oauth_server.OAuthHandler._interval(), oauth_server.OAuthHandler._response_padding(), oauth_server.OAuthHandler._retry_code(), oauth_server.OAuthHandler._uri_spelling(), and oauth_server.OAuthHandler.token().

◆ _interval()

int oauth_server.OAuthHandler._interval (   self)
private
Returns 0 unless the test has requested something different.

Definition at line 186 of file oauth_server.py.

186 def _interval(self) -> int:
187 """
188 Returns 0 unless the test has requested something different.
189 """
190 return self._get_param("interval", 0)
191

References oauth_server.OAuthHandler._get_param().

Referenced by oauth_server.OAuthHandler.authorization().

◆ _parse_params()

dict[str, str] oauth_server.OAuthHandler._parse_params (   self)
private
Parses apart the form-urlencoded request body and returns the resulting
dict. For use by do_POST().

Definition at line 86 of file oauth_server.py.

86 def _parse_params(self) -> dict[str, str]:
87 """
88 Parses apart the form-urlencoded request body and returns the resulting
89 dict. For use by do_POST().
90 """
91 size = int(self.headers["Content-Length"])
92 form = self.rfile.read(size)
93
94 assert self.headers["Content-Type"] == "application/x-www-form-urlencoded"
95 return urllib.parse.parse_qs(
96 form.decode("utf-8"),
97 strict_parsing=True,
98 keep_blank_values=True,
99 encoding="utf-8",
100 errors="strict",
101 )
102
#define read(a, b, c)
Definition: win32.h:13

References oauth_server.OAuthHandler.do_POST(), printTableContent.headers, async_ctx.headers, and read.

Referenced by oauth_server.OAuthHandler.client_id().

◆ _remove_token_state()

def oauth_server.OAuthHandler._remove_token_state (   self)
private
Removes any cached _TokenState for the current client_id. Call this
after the token exchange ends to get rid of unnecessary state.

Definition at line 284 of file oauth_server.py.

284 def _remove_token_state(self):
285 """
286 Removes any cached _TokenState for the current client_id. Call this
287 after the token exchange ends to get rid of unnecessary state.
288 """
289 if self.client_id in self.server.token_state:
290 del self.server.token_state[self.client_id]
291

References oauth_server.OAuthHandler.client_id(), and PgFdwRelationInfo.server.

Referenced by oauth_server.OAuthHandler.token().

◆ _response_padding()

def oauth_server.OAuthHandler._response_padding (   self)
private
If the huge_response test parameter is set to True, returns a dict
containing a gigantic string value, which can then be folded into a JSON
response.

Definition at line 209 of file oauth_server.py.

209 def _response_padding(self):
210 """
211 If the huge_response test parameter is set to True, returns a dict
212 containing a gigantic string value, which can then be folded into a JSON
213 response.
214 """
215 if not self._get_param("huge_response", False):
216 return dict()
217
218 return {"_pad_": "x" * 1024 * 1024}
219

References oauth_server.OAuthHandler._get_param().

Referenced by oauth_server.OAuthHandler.authorization(), and oauth_server.OAuthHandler.token().

◆ _retry_code()

str oauth_server.OAuthHandler._retry_code (   self)
private
Returns "authorization_pending" unless the test has requested something
different.

Definition at line 193 of file oauth_server.py.

193 def _retry_code(self) -> str:
194 """
195 Returns "authorization_pending" unless the test has requested something
196 different.
197 """
198 return self._get_param("retry_code", "authorization_pending")
199

References oauth_server.OAuthHandler._get_param().

Referenced by oauth_server.OAuthHandler.token().

◆ _send_json()

None oauth_server.OAuthHandler._send_json (   self,
JsonObject  js 
)
private
Sends the provided JSON dict as an application/json response.
self._response_code can be modified to send JSON error responses.

Definition at line 236 of file oauth_server.py.

236 def _send_json(self, js: JsonObject) -> None:
237 """
238 Sends the provided JSON dict as an application/json response.
239 self._response_code can be modified to send JSON error responses.
240 """
241 resp = json.dumps(js).encode("ascii")
242 self.log_message("sending JSON response: %s", resp)
243
244 self.send_response(self._response_code)
245 self.send_header("Content-Type", self._content_type)
246 self.send_header("Content-Length", str(len(resp)))
247 self.end_headers()
248
249 self.wfile.write(resp)
250
const char * str
#define write(a, b, c)
Definition: win32.h:14
const void size_t len

References oauth_server.OAuthHandler._content_type(), oauth_server.OAuthHandler._response_code, len, str, and write.

◆ _should_modify()

bool oauth_server.OAuthHandler._should_modify (   self)
private
Returns True if the client has requested a modification to this stage of
the exchange.

Definition at line 145 of file oauth_server.py.

145 def _should_modify(self) -> bool:
146 """
147 Returns True if the client has requested a modification to this stage of
148 the exchange.
149 """
150 if not hasattr(self, "_test_params"):
151 return False
152
153 stage = self._test_params.get("stage")
154
155 return (
156 stage == "all"
157 or (
158 stage == "discovery"
159 and self.path == "/.well-known/openid-configuration"
160 )
161 or (stage == "device" and self.path == "/authorize")
162 or (stage == "token" and self.path == "/token")
163 )
164

References oauth_server.OAuthHandler._test_params, RewriteMappingFile.path, backup_file_entry.path, PathClauseUsage.path, JsonTablePlanState.path, keepwal_entry.path, file_entry_t.path, fetch_range_request.path, UpgradeTaskReport.path, tablespaceinfo.path, IndexPath.path, BitmapHeapPath.path, BitmapAndPath.path, BitmapOrPath.path, TidPath.path, TidRangePath.path, SubqueryScanPath.path, ForeignPath.path, CustomPath.path, AppendPath.path, MergeAppendPath.path, GroupResultPath.path, MaterialPath.path, MemoizePath.path, UniquePath.path, GatherPath.path, GatherMergePath.path, ProjectionPath.path, ProjectSetPath.path, SortPath.path, GroupPath.path, UpperUniquePath.path, AggPath.path, GroupingSetsPath.path, MinMaxAggPath.path, WindowAggPath.path, SetOpPath.path, RecursiveUnionPath.path, LockRowsPath.path, ModifyTablePath.path, LimitPath.path, MinMaxAggInfo.path, JsonTablePathScan.path, _include_path.path, and oauth_server.OAuthHandler.path.

Referenced by oauth_server.OAuthHandler._get_param(), and oauth_server.OAuthHandler.token().

◆ _token_state()

def oauth_server.OAuthHandler._token_state (   self)
private
A cached _TokenState object for the connected client (as determined by
the request's client_id), or a new one if it doesn't already exist.

This relies on the existence of a defaultdict attached to the server;
see main() below.

Definition at line 274 of file oauth_server.py.

274 def _token_state(self):
275 """
276 A cached _TokenState object for the connected client (as determined by
277 the request's client_id), or a new one if it doesn't already exist.
278
279 This relies on the existence of a defaultdict attached to the server;
280 see main() below.
281 """
282 return self.server.token_state[self.client_id]
283

References oauth_server.OAuthHandler.client_id(), oauth_server.main(), and PgFdwRelationInfo.server.

Referenced by oauth_server.OAuthHandler.authorization(), and oauth_server.OAuthHandler.token().

◆ _uri_spelling()

str oauth_server.OAuthHandler._uri_spelling (   self)
private
Returns "verification_uri" unless the test has requested something
different.

Definition at line 201 of file oauth_server.py.

201 def _uri_spelling(self) -> str:
202 """
203 Returns "verification_uri" unless the test has requested something
204 different.
205 """
206 return self._get_param("uri_spelling", "verification_uri")
207

References oauth_server.OAuthHandler._get_param().

Referenced by oauth_server.OAuthHandler.authorization().

◆ authorization()

JsonObject oauth_server.OAuthHandler.authorization (   self)

Definition at line 292 of file oauth_server.py.

292 def authorization(self) -> JsonObject:
293 uri = "https://example.com/"
294 if self._alt_issuer:
295 uri = "https://example.org/"
296
297 resp = {
298 "device_code": "postgres",
299 "user_code": "postgresuser",
300 self._uri_spelling: uri,
301 "expires_in": 5,
302 **self._response_padding,
303 }
304
305 interval = self._interval
306 if interval is not None:
307 resp["interval"] = interval
308 self._token_state.min_delay = interval
309 else:
310 self._token_state.min_delay = 5 # default
311
312 # Check the scope.
313 if "scope" in self._params:
314 assert self._params["scope"][0], "empty scopes should be omitted"
315
316 return resp
317

References oauth_server.OAuthHandler._alt_issuer, oauth_server.OAuthHandler._interval(), oauth_server.OAuthHandler._params, oauth_server.OAuthHandler._response_padding(), oauth_server.OAuthHandler._token_state(), and oauth_server.OAuthHandler._uri_spelling().

◆ client_id()

str oauth_server.OAuthHandler.client_id (   self)
Returns the client_id sent in the POST body or the Authorization header.
self._parse_params() must have been called first.

Definition at line 104 of file oauth_server.py.

104 def client_id(self) -> str:
105 """
106 Returns the client_id sent in the POST body or the Authorization header.
107 self._parse_params() must have been called first.
108 """
109 if "client_id" in self._params:
110 return self._params["client_id"][0]
111
112 if "Authorization" not in self.headers:
113 raise RuntimeError("client did not send any client_id")
114
115 _, creds = self.headers["Authorization"].split()
116
117 decoded = base64.b64decode(creds).decode("utf-8")
118 username, _ = decoded.split(":", 1)
119
120 return urllib.parse.unquote_plus(username)
121

References oauth_server.OAuthHandler._params, oauth_server.OAuthHandler._parse_params(), printTableContent.headers, and async_ctx.headers.

Referenced by oauth_server.OAuthHandler._check_authn(), oauth_server.OAuthHandler._remove_token_state(), and oauth_server.OAuthHandler._token_state().

◆ config()

JsonObject oauth_server.OAuthHandler.config (   self)

Definition at line 251 of file oauth_server.py.

251 def config(self) -> JsonObject:
252 port = self.server.socket.getsockname()[1]
253
254 issuer = f"http://localhost:{port}"
255 if self._alt_issuer:
256 issuer += "/alternate"
257 elif self._parameterized:
258 issuer += "/param"
259
260 return {
261 "issuer": issuer,
262 "token_endpoint": issuer + "/token",
263 "device_authorization_endpoint": issuer + "/authorize",
264 "response_types_supported": ["token"],
265 "subject_types_supported": ["public"],
266 "id_token_signing_alg_values_supported": ["RS256"],
267 "grant_types_supported": [
268 "authorization_code",
269 "urn:ietf:params:oauth:grant-type:device_code",
270 ],
271 }
272

References oauth_server.OAuthHandler._alt_issuer, oauth_server.OAuthHandler._parameterized, and PgFdwRelationInfo.server.

◆ do_GET()

def oauth_server.OAuthHandler.do_GET (   self)

Definition at line 70 of file oauth_server.py.

70 def do_GET(self):
71 self._response_code = 200
72 self._check_issuer()
73
74 config_path = "/.well-known/openid-configuration"
75 if self._alt_issuer:
76 config_path = "/.well-known/oauth-authorization-server"
77
78 if self.path == config_path:
79 resp = self.config()
80 else:
81 self.send_error(404, "Not Found")
82 return
83
84 self._send_json(resp)
85

◆ do_POST()

def oauth_server.OAuthHandler.do_POST (   self)

Definition at line 122 of file oauth_server.py.

122 def do_POST(self):
123 self._response_code = 200
124 self._check_issuer()
125
126 self._params = self._parse_params()
127 if self._parameterized:
128 # Pull encoded test parameters out of the peer's client_id field.
129 # This is expected to be Base64-encoded JSON.
130 js = base64.b64decode(self.client_id)
131 self._test_params = json.loads(js)
132
133 self._check_authn()
134
135 if self.path == "/authorize":
136 resp = self.authorization()
137 elif self.path == "/token":
138 resp = self.token()
139 else:
140 self.send_error(404)
141 return
142
143 self._send_json(resp)
144

References oauth_server.OAuthHandler._check_issuer(), and oauth_server.OAuthHandler._response_code.

Referenced by oauth_server.OAuthHandler._parse_params().

◆ token()

JsonObject oauth_server.OAuthHandler.token (   self)

Definition at line 318 of file oauth_server.py.

318 def token(self) -> JsonObject:
319 if err := self._get_param("error_code", None):
320 self._response_code = self._get_param("error_status", 400)
321
322 resp = {"error": err}
323 if desc := self._get_param("error_desc", ""):
324 resp["error_description"] = desc
325
326 return resp
327
328 if self._should_modify() and "retries" in self._test_params:
329 retries = self._test_params["retries"]
330
331 # Check to make sure the token interval is being respected.
332 now = time.monotonic()
333 if self._token_state.last_try is not None:
334 delay = now - self._token_state.last_try
335 assert (
336 delay > self._token_state.min_delay
337 ), f"client waited only {delay} seconds between token requests (expected {self._token_state.min_delay})"
338
339 self._token_state.last_try = now
340
341 # If we haven't reached the required number of retries yet, return a
342 # "pending" response.
343 if self._token_state.retries < retries:
344 self._token_state.retries += 1
345
346 self._response_code = 400
347 return {"error": self._retry_code}
348
349 # Clean up any retry tracking state now that the exchange is ending.
350 self._remove_token_state()
351
352 return {
353 "access_token": self._access_token,
354 "token_type": "bearer",
355 **self._response_padding,
356 }
357
358
#define token
Definition: indent_globs.h:126

References oauth_server.OAuthHandler._access_token(), oauth_server.OAuthHandler._get_param(), oauth_server.OAuthHandler._remove_token_state(), oauth_server.OAuthHandler._response_code, oauth_server.OAuthHandler._response_padding(), oauth_server.OAuthHandler._retry_code(), oauth_server.OAuthHandler._should_modify(), oauth_server.OAuthHandler._test_params, and oauth_server.OAuthHandler._token_state().

Field Documentation

◆ _alt_issuer

oauth_server.OAuthHandler._alt_issuer
private

◆ _parameterized

oauth_server.OAuthHandler._parameterized
private

Definition at line 36 of file oauth_server.py.

Referenced by oauth_server.OAuthHandler.config().

◆ _params

oauth_server.OAuthHandler._params
private

◆ _response_code

oauth_server.OAuthHandler._response_code
private

◆ _test_params

oauth_server.OAuthHandler._test_params
private

◆ JsonObject

oauth_server.OAuthHandler.JsonObject = dict[str, object]
static

Definition at line 26 of file oauth_server.py.

◆ path

oauth_server.OAuthHandler.path

Definition at line 34 of file oauth_server.py.

Referenced by oauth_server.OAuthHandler._should_modify().


The documentation for this class was generated from the following file: