Skip to content

Commit 7c3f19b

Browse files
authored
Support specifying session configs in http and graphql endpoints (#9110)
For http we add a `config` parameter, and for graphql we add a `__config__` key to `variables` (mirroring the newer and more graphql standard compliant way for globals). I made dbview responsible for loading the config, like it is for binary proto state (with `decode_state`).
1 parent 4fa537c commit 7c3f19b

File tree

16 files changed

+299
-86
lines changed

16 files changed

+299
-86
lines changed

docs/cloud/http_gql.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ HTTP
7878
----
7979
8080
- :ref:`Overview <ref_edgeql_http>`
81-
- :ref:`ref_edgeqlql_protocol`
81+
- :ref:`ref_edgeql_protocol`
8282
- :ref:`ref_edgeql_http_health_checks`
8383
8484

docs/reference/using/graphql/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Requests can contain the following fields:
109109
keys must be the fully qualified names of the globals to set (e.g.,
110110
``default::current_user`` for the global ``current_user`` in the ``default``
111111
module).
112+
- ``variables["__config__"]`` - a JSON object containing session configuration values. **Optional**. **Added in 7.0.**
112113
- ``operationName`` - the name of the operation that must be
113114
executed. **Optional** If the GraphQL query contains several named
114115
operations, it is required.

docs/reference/using/http.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,25 @@ example showing how you might send the query ``select Person {*};`` using cURL:
8686
8787
.. lint-on
8888
89-
.. _ref_edgeqlql_protocol:
89+
.. _ref_edgeql_protocol:
9090
9191
Querying
9292
========
9393
9494
|Gel| supports GET and POST methods for handling EdgeQL over HTTP protocol. Both GET and POST methods use the following fields:
9595
9696
- ``query`` - contains the EdgeQL query string
97-
- ``variables``- contains a JSON object where the keys are the parameter names from the query and the values are the arguments to be used in this execution of the query.
98-
- ``globals``- contains a JSON object where the keys are the fully qualified global names and the values are the desired values for those globals.
97+
- ``variables``- contains a JSON object where the keys are the parameter names from the query and the values are the arguments to be used in this execution of the query. **Optional**
98+
- ``globals``- contains a JSON object where the keys are the fully qualified global names and the values are the desired values for those globals. **Optional**
99+
100+
- ``config`` - contains a JSON object where the keys are configuration option names and the values are the desired values for those configs. **Optional**. **Added in 7.0.**
99101
100102
The protocol supports HTTP Keep-Alive.
101103
102104
GET request
103105
-----------
104106
105-
The HTTP GET request passes the fields as query parameters: ``query`` string and JSON-encoded ``variables`` mapping.
107+
The HTTP GET request passes the fields as query parameters: ``query`` string and JSON-encoded ``variables``, ``globals``, and ``config`` mappings.
106108
107109
108110
POST request
@@ -115,7 +117,8 @@ The POST request should use ``application/json`` content type and submit the fol
115117
{
116118
"query": "select Person {*} filter .name = <str>$name;",
117119
"variables": { "name": "John" },
118-
"globals": { "default::global_name": "value" }
120+
"globals": { "default::global_name": "value" },
121+
"config": { "default_transaction_isolation": "RepeatableRead" }
119122
}
120123
121124

docs/resources/changelog/7_x.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
===========
2-
v7.0 beta 1
3-
===========
1+
====
2+
v7.0
3+
====
44

55
:edb-alt-title: Gel v7
66

@@ -10,7 +10,7 @@ automatically suggested:
1010

1111
.. code-block:: bash
1212
13-
$ gel project init --server-version 7.0-beta.1
13+
$ gel project init --server-version 7.0-rc.3
1414
1515
1616
Upgrading
@@ -121,6 +121,14 @@ Third-party SQL tools can use the pg_connector role and access
121121
policies will be disable.
122122

123123

124+
Session configuration for GraphQL and EdgeQL HTTP interfaces
125+
------------------------------------------------------------
126+
127+
The :ref:`GraphQL <ref_graphql_protocol>` and :ref:`EdgeQL HTTP <ref_edgeql_protocol>` interfaces now support passing in session configuration variables.
128+
129+
If you are exposing HTTP/GraphQL endpoints to users and relying on them not being able to perform configuration, you will need to instead have them use roles that have not been given permission to modify sensitive configs.
130+
131+
124132
Simpler scoping rules deprecation warnings
125133
------------------------------------------
126134

edb/graphql/extension.pyx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async def handle_request(
102102
operation_name = None
103103
variables = None
104104
globals = None
105+
config = None
105106
deprecated_globals = None
106107
query = None
107108
query_bytes_len = 0
@@ -179,6 +180,12 @@ async def handle_request(
179180
if variables is not None:
180181
globals = variables.get('__globals__')
181182

183+
if variables is not None:
184+
config = variables.get('__config__')
185+
186+
if config is not None and not isinstance(config, dict):
187+
raise TypeError('"__config__" must be a JSON object')
188+
182189
if globals is not None and not isinstance(globals, dict):
183190
raise TypeError('"__globals__" must be a JSON object')
184191
if (
@@ -210,7 +217,9 @@ async def handle_request(
210217
response.content_type = b'application/json'
211218
try:
212219
result = await _execute(
213-
db, role_name, tenant, query, operation_name, variables, globals)
220+
db, role_name, tenant, query, operation_name, variables, globals,
221+
config,
222+
)
214223
except Exception as ex:
215224
if debug.flags.server:
216225
markup.dump(ex)
@@ -239,14 +248,15 @@ async def handle_request(
239248

240249

241250
async def compile(
242-
dbview.Database db,
251+
dbview.DatabaseConnectionView dbv,
243252
tenant,
244253
query: str,
245254
tokens: Optional[List[Tuple[int, int, int, str]]],
246255
substitutions: Optional[Dict[str, Tuple[str, int, int]]],
247256
operation_name: Optional[str],
248257
variables: Dict[str, Any],
249258
):
259+
db = dbv._db
250260
server = tenant.server
251261
compiler_pool = server.get_compiler_pool()
252262
started_at = time.monotonic()
@@ -256,8 +266,9 @@ async def compile(
256266
db.user_schema_pickle,
257267
tenant.get_global_schema_pickle(),
258268
db.reflection_cache,
259-
db.db_config,
260-
db._index.get_compilation_system_config(),
269+
dbv.get_database_config(),
270+
dbv.get_compilation_system_config(),
271+
dbv.get_session_config(),
261272
query,
262273
tokens,
263274
substitutions,
@@ -274,7 +285,7 @@ async def compile(
274285
)
275286

276287
async def _execute(
277-
db, role_name, tenant, query, operation_name, variables, globals
288+
db, role_name, tenant, query, operation_name, variables, globals, config
278289
):
279290
dbver = db.dbver
280291
query_cache = tenant.server._http_query_cache
@@ -316,7 +327,27 @@ async def _execute(
316327
print(rewritten)
317328
print(f'variables: {vars}')
318329

319-
cache_key = ('graphql', prepared_query, (), operation_name, dbver)
330+
await db.introspection()
331+
332+
dbv: dbview.DatabaseConnectionView = await tenant.new_dbview(
333+
dbname=db.name,
334+
query_cache=False,
335+
protocol_version=edbdef.CURRENT_PROTOCOL,
336+
role_name=role_name,
337+
)
338+
dbv.is_transient = True
339+
dbv.decode_json_session_config(config)
340+
341+
# Put the compilation-affecting session config into the cache key.
342+
# N.B: We skip putting system/database config in here, since dbver
343+
# gets bumped whenever those change.
344+
config_key = db.server.compilation_config_serializer.encode_configs(
345+
dbv.get_session_config()
346+
)
347+
348+
cache_key = (
349+
'graphql', prepared_query, (), operation_name, dbver, config_key
350+
)
320351
use_prep_stmt = False
321352

322353
entry: CacheEntry = None
@@ -328,15 +359,15 @@ async def _execute(
328359
print("REDIRECT", entry.key_vars)
329360

330361
key_vars2 = tuple(vars[k] for k in entry.key_vars)
331-
cache_key2 = (prepared_query, key_vars2, operation_name, dbver)
362+
cache_key2 = (
363+
prepared_query, key_vars2, operation_name, dbver, config_key
364+
)
332365
entry = query_cache.get(cache_key2, None)
333366

334-
await db.introspection()
335-
336367
if entry is None:
337368
if rewritten is not None:
338369
qug, gql_op = await compile(
339-
db,
370+
dbv,
340371
tenant,
341372
query,
342373
rewritten.tokens(gql_lexer.TokenKind),
@@ -346,7 +377,7 @@ async def _execute(
346377
)
347378
else:
348379
qug, gql_op = await compile(
349-
db,
380+
dbv,
350381
tenant,
351382
query,
352383
None,
@@ -381,13 +412,6 @@ async def _execute(
381412

382413
compiled = dbview.CompiledQuery(query_unit_group=qug)
383414

384-
dbv = await tenant.new_dbview(
385-
dbname=db.name,
386-
query_cache=False,
387-
protocol_version=edbdef.CURRENT_PROTOCOL,
388-
role_name=role_name,
389-
)
390-
391415
async with tenant.with_pgcon(db.name) as pgcon:
392416
try:
393417
return await execute.execute_json(

edb/server/compiler_pool/worker.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020
from __future__ import annotations
21-
from typing import Any, Optional
21+
from typing import Any, Mapping, Optional
2222

2323
import pickle
2424

@@ -262,6 +262,7 @@ def compile_graphql(
262262
global_schema: Optional[bytes],
263263
database_config: Optional[bytes],
264264
system_config: Optional[bytes],
265+
session_config: Mapping[str, Any],
265266
*compile_args: Any,
266267
**compile_kwargs: Any,
267268
) -> tuple[compiler.QueryUnitGroup, graphql.TranspiledOperation]:
@@ -303,7 +304,7 @@ def compile_graphql(
303304
inline_typenames=False,
304305
inline_objectids=False,
305306
modaliases=None,
306-
session_config=None,
307+
session_config=session_config,
307308
)
308309

309310
unit_group, _ = COMPILER.compile(

edb/server/dbview/dbview.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@ cdef class DatabaseConnectionView:
253253
cdef bint is_state_desc_changed(self)
254254
cdef describe_state(self)
255255
cdef encode_state(self)
256+
cdef check_session_config_perms(self, keys)
256257
cdef decode_state(self, type_id, data)
258+
cdef decode_json_session_config(self, json_session_config)
257259
cdef bint needs_commit_after_state_sync(self)
258260

259261
cdef check_capabilities(

edb/server/dbview/dbview.pyx

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,21 @@ cdef class DatabaseConnectionView:
974974
state['globals'] = {k: v.value for k, v in globals_.items()}
975975
return serializer.type_id, serializer.encode(state)
976976

977+
cdef check_session_config_perms(self, keys):
978+
is_superuser, permissions = self.get_permissions()
979+
if not is_superuser:
980+
settings = self.get_config_spec()
981+
for k in keys:
982+
setting = settings[k]
983+
if setting.session_restricted and not (
984+
setting.session_permission
985+
and setting.session_permission in permissions
986+
):
987+
raise errors.DisabledCapabilityError(
988+
f'role {self._role_name} does not have permission to '
989+
f'configure session config variable {k}'
990+
)
991+
977992
cdef decode_state(self, type_id, data):
978993
serializer = self.get_state_serializer()
979994
self._command_state_serializer = serializer
@@ -1001,27 +1016,15 @@ cdef class DatabaseConnectionView:
10011016
aliases[None] = state.get('module', defines.DEFAULT_MODULE_ALIAS)
10021017
aliases = immutables.Map(aliases)
10031018

1004-
is_superuser, permissions = self.get_permissions()
1005-
if not is_superuser:
1006-
settings = self.get_config_spec()
1007-
for k in state.get('config', ()):
1008-
setting = settings[k]
1009-
if setting.session_restricted and not (
1010-
setting.session_permission
1011-
and setting.session_permission in permissions
1012-
):
1013-
raise errors.DisabledCapabilityError(
1014-
f'role {self._role_name} does not have permission to '
1015-
f'configure session config variable {k}'
1016-
)
1017-
1019+
config_obj = state.get('config', {})
1020+
self.check_session_config_perms(config_obj)
10181021
session_config = immutables.Map({
10191022
k: config.SettingValue(
10201023
name=k,
10211024
value=v,
10221025
source='session',
10231026
scope=qltypes.ConfigScope.SESSION,
1024-
) for k, v in state.get('config', {}).items()
1027+
) for k, v in config_obj.items()
10251028
})
10261029
globals_ = immutables.Map({
10271030
k: config.SettingValue(
@@ -1038,6 +1041,25 @@ cdef class DatabaseConnectionView:
10381041
aliases, session_config, globals_, type_id, data
10391042
)
10401043

1044+
cdef decode_json_session_config(self, json_session_config):
1045+
if not json_session_config:
1046+
return
1047+
1048+
settings = self.get_config_spec()
1049+
1050+
self.check_session_config_perms(json_session_config)
1051+
1052+
session_config = self.get_session_config()
1053+
for k, v in json_session_config.items():
1054+
op = config.Operation(
1055+
config.OpCode.CONFIG_SET,
1056+
qltypes.ConfigScope.SESSION,
1057+
k,
1058+
v,
1059+
)
1060+
session_config = op.apply(settings, session_config)
1061+
self.set_session_config(session_config)
1062+
10411063
cdef bint needs_commit_after_state_sync(self):
10421064
return any(
10431065
tx_conf in self._config
@@ -1402,25 +1424,14 @@ cdef class DatabaseConnectionView:
14021424

14031425
for op in ops:
14041426
if op.scope is config.ConfigScope.INSTANCE:
1427+
assert conn is not None
14051428
await self._db._index.apply_system_config_op(conn, op)
14061429
elif op.scope is config.ConfigScope.DATABASE:
14071430
self.set_database_config(
14081431
op.apply(settings, self.get_database_config()),
14091432
)
14101433
elif op.scope is config.ConfigScope.SESSION:
1411-
is_superuser, permissions = self.get_permissions()
1412-
if not is_superuser:
1413-
setting = op.get_setting(settings)
1414-
if setting.session_restricted and not (
1415-
setting.session_permission
1416-
and setting.session_permission in permissions
1417-
):
1418-
raise errors.DisabledCapabilityError(
1419-
f'role {self._role_name} does not have permission '
1420-
f'to configure session config variable '
1421-
f'{setting.name}'
1422-
)
1423-
1434+
self.check_session_config_perms([op.setting_name])
14241435
self.set_session_config(
14251436
op.apply(settings, self.get_session_config()),
14261437
)

0 commit comments

Comments
 (0)