Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http-api: T6736: move REST API to a node distinct from GraphQL API #4110

Merged
merged 9 commits into from
Oct 4, 2024
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<!-- include start from include/version/https-version.xml.i -->
<syntaxVersion component='https' version='6'></syntaxVersion>
<syntaxVersion component='https' version='7'></syntaxVersion>
<!-- include end -->
53 changes: 30 additions & 23 deletions interface-definitions/service_https.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,29 @@
</tagNode>
</children>
</node>
<leafNode name="strict">
<node name="rest">
<properties>
<help>Enforce strict path checking</help>
<valueless/>
<help>REST API</help>
</properties>
</leafNode>
<leafNode name="debug">
<properties>
<help>Debug</help>
<valueless/>
<hidden/>
</properties>
</leafNode>
<children>
<leafNode name="strict">
<properties>
<help>Enforce strict path checking</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="debug">
<properties>
<help>Debug</help>
<valueless/>
<hidden/>
</properties>
</leafNode>
</children>
</node>
<node name="graphql">
<properties>
<help>GraphQL support</help>
<help>GraphQL API</help>
</properties>
<children>
<leafNode name="introspection">
Expand Down Expand Up @@ -109,19 +116,19 @@
</leafNode>
</children>
</node>
</children>
</node>
<node name="cors">
<properties>
<help>Set CORS options</help>
</properties>
<children>
<leafNode name="allow-origin">
<node name="cors">
<properties>
<help>Allow resource request from origin</help>
<multi/>
<help>Set CORS options</help>
</properties>
</leafNode>
<children>
<leafNode name="allow-origin">
<properties>
<help>Allow resource request from origin</help>
<multi/>
</properties>
</leafNode>
</children>
</node>
</children>
</node>
</children>
Expand Down
85 changes: 67 additions & 18 deletions smoketest/scripts/cli/test_service_https.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@

PROCESS_NAME = 'nginx'


class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
Expand Down Expand Up @@ -120,19 +121,29 @@ def test_certificate(self):
# verify() - certificates do not exist (yet)
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')])
self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')])
self.cli_set(
pki_base
+ ['certificate', cert_name, 'certificate', cert_data.replace('\n', '')]
)
self.cli_set(
pki_base
+ ['certificate', cert_name, 'private', 'key', key_data.replace('\n', '')]
)

self.cli_set(base_path + ['certificates', 'dh-params', dh_name])
# verify() - dh-params do not exist (yet)
with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')])
self.cli_set(
pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n', '')]
)
# verify() - dh-param minimum length is 2048 bit
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')])
self.cli_set(
pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n', '')]
)

self.cli_commit()
self.assertTrue(process_named_running(PROCESS_NAME))
Expand All @@ -154,13 +165,15 @@ def test_api_auth(self):
key = 'MySuperSecretVyOS'
self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])

self.cli_set(base_path + ['api', 'rest'])

self.cli_set(base_path + ['listen-address', address])

self.cli_commit()

nginx_config = read_file('/etc/nginx/sites-enabled/default')
self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default
self.assertIn('ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default

url = f'https://{address}/retrieve'
payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
Expand All @@ -180,11 +193,16 @@ def test_api_auth(self):
self.assertEqual(r.status_code, 401)

# Check path config
payload = {'data': '{"op": "showConfig", "path": ["system", "login"]}', 'key': f'{key}'}
payload = {
'data': '{"op": "showConfig", "path": ["system", "login"]}',
'key': f'{key}',
}
r = request('POST', url, verify=False, headers=headers, data=payload)
response = r.json()
vyos_user_exists = 'vyos' in response.get('data', {}).get('user', {})
self.assertTrue(vyos_user_exists, "The 'vyos' user does not exist in the response.")
self.assertTrue(
vyos_user_exists, "The 'vyos' user does not exist in the response."
)

# GraphQL auth test: a missing key will return status code 400, as
# 'key' is a non-nullable field in the schema; an incorrect key is
Expand All @@ -208,7 +226,13 @@ def test_api_auth(self):
}}
"""

r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key})
r = request(
'POST',
graphql_url,
verify=False,
headers=headers,
json={'query': query_valid_key},
)
success = r.json()['data']['SystemStatus']['success']
self.assertTrue(success)

Expand All @@ -224,7 +248,13 @@ def test_api_auth(self):
}
"""

r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key})
r = request(
'POST',
graphql_url,
verify=False,
headers=headers,
json={'query': query_invalid_key},
)
success = r.json()['data']['SystemStatus']['success']
self.assertFalse(success)

Expand All @@ -240,7 +270,13 @@ def test_api_auth(self):
}
"""

r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key})
r = request(
'POST',
graphql_url,
verify=False,
headers=headers,
json={'query': query_no_key},
)
success = r.json()['data']['SystemStatus']['success']
self.assertFalse(success)

Expand All @@ -261,7 +297,9 @@ def test_api_auth(self):
}
}
"""
r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation})
r = request(
'POST', graphql_url, verify=False, headers=headers, json={'query': mutation}
)

token = r.json()['data']['AuthToken']['data']['result']['token']

Expand All @@ -284,7 +322,9 @@ def test_api_auth(self):
}
"""

r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query})
r = request(
'POST', graphql_url, verify=False, headers=headers, json={'query': query}
)
success = r.json()['data']['ShowVersion']['success']
self.assertTrue(success)

Expand All @@ -304,6 +344,7 @@ def test_api_add_delete(self):
self.assertEqual(r.status_code, 503)

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()
sleep(2)

Expand All @@ -326,6 +367,7 @@ def test_api_show(self):
headers = {}

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

payload = {
Expand All @@ -343,6 +385,7 @@ def test_api_generate(self):
headers = {}

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

payload = {
Expand All @@ -362,17 +405,18 @@ def test_api_configure(self):
conf_address = '192.0.2.44/32'

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

payload_path = [
"interfaces",
"dummy",
f"{conf_interface}",
"address",
f"{conf_address}",
'interfaces',
'dummy',
f'{conf_interface}',
'address',
f'{conf_address}',
]

payload = {'data': json.dumps({"op": "set", "path": payload_path}), 'key': key}
payload = {'data': json.dumps({'op': 'set', 'path': payload_path}), 'key': key}

r = request('POST', url, verify=False, headers=headers, data=payload)
self.assertEqual(r.status_code, 200)
Expand All @@ -385,6 +429,7 @@ def test_api_config_file(self):
headers = {}

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

payload = {
Expand All @@ -402,6 +447,7 @@ def test_api_reset(self):
headers = {}

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

payload = {
Expand All @@ -419,6 +465,7 @@ def test_api_image(self):
headers = {}

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

payload = {
Expand Down Expand Up @@ -462,6 +509,7 @@ def test_api_config_file_load_http(self):
headers = {}

self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
self.cli_set(base_path + ['api', 'rest'])
self.cli_commit()

# load config via HTTP requires nginx config
Expand Down Expand Up @@ -498,5 +546,6 @@ def test_api_config_file_load_http(self):
call(f'sudo rm -f {nginx_tmp_site}')
call('sudo systemctl reload nginx')


if __name__ == '__main__':
unittest.main(verbosity=5)
43 changes: 43 additions & 0 deletions src/migration-scripts/https/6-to-7
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2024 VyOS maintainers and contributors <[email protected]>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this library. If not, see <http://www.gnu.org/licenses/>.

# T6736: move REST API to distinct node


from vyos.configtree import ConfigTree


base = ['service', 'https', 'api']

def migrate(config: ConfigTree) -> None:
if not config.exists(base):
# Nothing to do
return

# Move REST API configuration to new node
# REST API was previously enabled if base path exists
config.set(['service', 'https', 'api', 'rest'])
for entry in ('debug', 'strict'):
if config.exists(base + [entry]):
config.set(base + ['rest', entry])
config.delete(base + [entry])

# Move CORS settings under GraphQL
# CORS is not implemented for REST API
if config.exists(base + ['cors']):
old_base = base + ['cors']
new_base = base + ['graphql', 'cors']
config.copy(old_base, new_base)
config.delete(old_base)
Empty file added src/services/api/__init__.py
Empty file.
Loading
Loading