edgedb/tests/test_http_edgeql.py
Michael J. Sullivan ccd0a3a2e1
Support auth for HTTP endpoints (#6352)
This makes all extension endpoints except for 'auth' will go through
authentication, since it seems like the safe default.

We add a new SIMPLE_HTTP transport method for HTTP endpoints.
(Unfortunately, edgedb+http already uses HTTP, even though
HTTP would be a better name for *this* one.)

We also add a new Password auth method that only works for
SIMPLE_HTTP, and which is the default method for SIMPLE_HTTP.
JWT authentication is also supported, but SCRAM is not.

This is a backwards incompatible change! Unless "Trust" is configured
as a method for HTTP transports (which it will be in dev instances),
HTTP endpoints will stop working when not authenticated to.

Fixes #6345.

Password authentication uses HTTP basic auth.

JWT authentication is done by adding a header of the form:
```
Authorization: bearer <JWT>
```

The username for JWT and Trust auth can be specified with the X-EdgeDB-User
header.
2023-10-25 21:11:45 -07:00

410 lines
13 KiB
Python

#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import urllib
import json
import decimal
import edgedb
from edb.testbase import http as tb
class TestHttpEdgeQL(tb.EdgeQLTestCase):
SCHEMA_DEFAULT = os.path.join(os.path.dirname(__file__), 'schemas',
'graphql.esdl')
SCHEMA_OTHER = os.path.join(os.path.dirname(__file__), 'schemas',
'graphql_other.esdl')
SCHEMA_OTHER_DEEP = os.path.join(os.path.dirname(__file__), 'schemas',
'graphql_schema_other_deep.esdl')
SETUP = os.path.join(os.path.dirname(__file__), 'schemas',
'graphql_setup.edgeql')
# EdgeQL/HTTP queries cannot run in a transaction
TRANSACTION_ISOLATION = False
def test_http_edgeql_proto_errors_01(self):
with self.http_con() as con:
data, headers, status = self.http_con_request(
con, {}, path='non-existant',
headers={
'Authorization': self.make_auth_header(),
},
)
self.assertEqual(status, 404)
self.assertEqual(headers['connection'], 'close')
self.assertIn(b'Unknown path', data)
with self.assertRaises(OSError):
self.http_con_request(con, {}, path='non-existant2')
def test_http_edgeql_proto_errors_02(self):
with self.http_con() as con:
data, headers, status = self.http_con_request(
con,
{},
headers={
'Authorization': self.make_auth_header(),
},
)
self.assertEqual(status, 400)
self.assertEqual(headers['connection'], 'close')
self.assertIn(b'query is missing', data)
with self.assertRaises(OSError):
self.http_con_request(con, {}, path='non-existant')
def test_http_edgeql_proto_errors_03(self):
with self.http_con() as con:
con.send(b'blah\r\n\r\n\r\n\r\n')
data, headers, status = self.http_con_request(
con,
{'query': 'blah', 'variables': 'bazz'},
headers={
'Authorization': self.make_auth_header(),
},
)
self.assertEqual(status, 400)
self.assertEqual(headers['connection'], 'close')
self.assertIn(b'HttpParserInvalidMethodError', data)
with self.assertRaises(OSError):
self.http_con_request(con, {}, path='non-existant')
def test_http_edgeql_query_01(self):
for _ in range(10): # repeat to test prepared pgcon statements
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
r"""
SELECT Setting {
name,
value
}
ORDER BY .value ASC;
""",
[
{'name': 'template', 'value': 'blue'},
{'name': 'perks', 'value': 'full'},
{'name': 'template', 'value': 'none'},
],
use_http_post=use_http_post
)
def test_http_edgeql_query_02(self):
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
r"""
SELECT Setting {
name,
value
}
FILTER .name = <str>$name;
""",
[
{'name': 'perks', 'value': 'full'},
],
variables={'name': 'perks'},
use_http_post=use_http_post
)
def test_http_edgeql_query_03(self):
self.assert_edgeql_query_result(
r"""
SELECT User {
name,
age,
groups: { name }
}
FILTER .name = <str>$name AND .age = <int64>$age;
""",
[
{'name': 'Bob', 'age': 21, 'groups': []},
],
variables=dict(name='Bob', age=21)
)
def test_http_edgeql_query_04(self):
with self.assertRaisesRegex(
edgedb.QueryError,
r'parameter \$name is required'):
self.edgeql_query(
r"""
SELECT Setting {
name,
value
}
FILTER .name = <str>$name;
"""
)
with self.assertRaisesRegex(
edgedb.QueryError,
r'parameter \$name is required'):
self.edgeql_query(
r"""
SELECT Setting {
name,
value
}
FILTER .name = <str>$name;
""",
variables={'name': None})
def test_http_edgeql_query_05(self):
with self.assertRaisesRegex(edgedb.InvalidReferenceError,
r'UNRECOGNIZABLE'):
self.edgeql_query(
r"""
SELECT UNRECOGNIZABLE {
value
};
"""
)
def test_http_edgeql_query_06(self):
queries = [
'START TRANSACTION;',
'SET ALIAS blah AS MODULE std;',
'CREATE TYPE default::Tmp { CREATE PROPERTY tmp -> std::str; };',
]
for query in queries:
with self.assertRaisesRegex(
edgedb.ProtocolError,
# can fail on transaction commands or on session configuration
'cannot execute.*',
):
self.edgeql_query(query)
def test_http_edgeql_query_07(self):
self.assert_edgeql_query_result(
r"""
SELECT Setting {
name,
value
}
FILTER .name = "NON EXISTENT";
""",
[],
)
def test_http_edgeql_query_08(self):
self.assert_edgeql_query_result(
r"""
SELECT 1;
SELECT 2;
""",
[2],
)
def test_http_edgeql_query_09(self):
self.assert_edgeql_query_result(
r"""
SELECT <bigint>$number;
""",
[123456789123456789123456789],
variables={'number': 123456789123456789123456789}
)
def test_http_edgeql_query_10(self):
self.assert_edgeql_query_result(
r'''SELECT (INTROSPECT TYPEOF <int64>$x).name;''',
['std::int64'],
variables={'x': 7},
)
def test_http_edgeql_query_11(self):
self.assert_edgeql_query_result(
r'''SELECT <str>$x ++ (INTROSPECT TYPEOF <int64>$y).name;''',
['xstd::int64'],
variables={'x': 'x', 'y': 7},
)
def test_http_edgeql_query_12(self):
self.assert_edgeql_query_result(
r'''SELECT <str>$x''',
['xx'],
variables={'x': 'xx'},
)
self.assert_edgeql_query_result(
r'''SELECT <REQUIRED str>$x''',
['yy'],
variables={'x': 'yy'},
)
self.assert_edgeql_query_result(
r'''SELECT <OPTIONAL str>$x ?? '-default-' ''',
['-default-'],
variables={'x': None},
)
with self.assertRaisesRegex(
edgedb.QueryError,
r'parameter \$x is required'):
self.edgeql_query(
r'''SELECT <REQUIRED str>$x ?? '-default' ''',
variables={'x': None},
)
with self.assertRaisesRegex(
edgedb.QueryError,
r'parameter \$x is required'):
self.edgeql_query(
r'''SELECT <str>$x ?? '-default' ''',
variables={'x': None},
)
def test_http_edgeql_query_13(self):
self.assert_edgeql_query_result(
r'''select (<array<int64>>$foo, <tuple<int64, str>>$bar)''',
[[[1, 2, 3], [1, 'test']]],
variables=dict(
foo=[1, 2, 3],
bar=(1, 'test'),
)
)
def test_http_edgeql_query_14(self):
with self.assertRaisesRegex(
edgedb.ConstraintViolationError,
r'Minimum allowed value for positive_int_t is 0'):
self.edgeql_query(
r'''SELECT <positive_int_t>-1''',
)
def test_http_edgeql_query_globals_01(self):
Q = r'''select GlobalTest { gstr, garray, gid, gdef, gdef2 }'''
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
Q,
[{'gstr': 'WOO',
'gid': '84ed3d8b-5eb2-4d31-9e1e-efb66180445c', 'gdef': '',
'gdef2': None, 'garray': ['x', 'y', 'z']}],
use_http_post=use_http_post,
globals={
'default::test_global_str': "WOO",
'default::test_global_id': (
'84ed3d8b-5eb2-4d31-9e1e-efb66180445c'),
'default::test_global_def': None,
'default::test_global_def2': None,
'default::test_global_array': ['x', 'y', 'z'],
},
)
self.assert_edgeql_query_result(
Q,
[{'gdef': 'x', 'gdef2': 'x'}],
use_http_post=use_http_post,
globals={
'default::test_global_def': 'x',
'default::test_global_def2': 'x',
},
)
self.assert_edgeql_query_result(
Q,
[{'gstr': None, 'garray': None, 'gid': None,
'gdef': '', 'gdef2': ''}],
use_http_post=use_http_post,
)
def test_http_edgeql_query_globals_02(self):
Q = r'''select (global test_global_str) ++ <str>$test'''
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
Q,
['foo!'],
variables={'test': '!'},
globals={'default::test_global_str': 'foo'},
use_http_post=use_http_post,
)
def test_http_edgeql_query_globals_03(self):
Q = r'''select get_glob()'''
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
Q,
['foo'],
globals={'default::test_global_str': 'foo'},
use_http_post=use_http_post,
)
def test_http_edgeql_query_globals_04(self):
Q = r'''select get_glob()'''
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
Q,
[],
use_http_post=use_http_post,
)
def test_http_edgeql_query_func_01(self):
Q = r'''select id_func('foo')'''
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
Q,
['foo'],
use_http_post=use_http_post,
)
def test_http_edgeql_query_duration_01(self):
Q = r"""select <duration>'15m' < <duration>'1h'"""
for use_http_post in [True, False]:
self.assert_edgeql_query_result(
Q,
[True],
use_http_post=use_http_post,
)
def test_http_edgeql_decimal_params_01(self):
req_data = b'''{
"query": "select <array<decimal>>$test",
"variables": {
"test": [1234567890123456789.01234567890123456789]
}
}'''
req = urllib.request.Request(self.http_addr, method='POST')
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', self.make_auth_header())
response = urllib.request.urlopen(
req, req_data, context=self.tls_context
)
resp_data = json.loads(response.read(), parse_float=decimal.Decimal)
self.assert_data_shape(resp_data, {
'data': [
[decimal.Decimal('1234567890123456789.01234567890123456789')]
]
})