Skip to content

Commit 853857c

Browse files
committed
Merge branch 'dev' of https://github.com/bcgov/gwa-api into feature/custom-certs
2 parents a67f005 + 529c39d commit 853857c

File tree

8 files changed

+176
-124
lines changed

8 files changed

+176
-124
lines changed

microservices/gatewayApi/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ The `data_planes_config.json` example:
5353
"data_planes": {
5454
"dp-silver-kong-proxy": {
5555
"kube-api": "https://api.cloud",
56-
"kube-ns": "xxxxxx-dev"
56+
"kube-ns": "xxxxxx-dev",
57+
"validate-upstreams": false
5758
}
5859
}
5960
}

microservices/gatewayApi/config/test.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@
1919
"test-default-dp": {
2020
"kube-api": "http://kube-api",
2121
"kube-ns": "abcd-1234"
22+
},
23+
"strict-dp": {
24+
"kube-api": "http://kube-api",
25+
"kube-ns": "abcd-1234",
26+
"validate-upstreams": true
2227
}
2328
},
2429
"kubeApiCreds": {
2530
"kubeApiPass": "password",
2631
"kubeApiUser": "username"
2732
}
28-
}
33+
}

microservices/gatewayApi/tests/conftest.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,46 @@ def decorated_function(*args, **kwargs):
6060
def mock_keycloak(mocker):
6161
class mock_kc_admin:
6262
def get_group_by_path(path, search_in_subgroups):
63-
if path.endswith("customcert"):
63+
if path == "/ns/mytest":
64+
return {"id": "g001"}
65+
elif path == "/ns/mytest2":
66+
return {"id": "g002"}
67+
elif path == "/ns/mytest3":
68+
return {"id": "g003"}
69+
elif path == "/ns/customcert":
70+
return {"id": "g004"}
71+
else:
72+
return {"id": "g001"}
73+
def get_group(id):
74+
if id == "g001":
6475
return {
65-
"id": "g002"
76+
"attributes": {
77+
"perm-domains": [ ".api.gov.bc.ca", ".cluster.local" ]
78+
}
6679
}
67-
return {
68-
"id": "g001"
69-
}
70-
def get_group(id):
71-
if id == "g002":
80+
elif id == "g002":
7281
return {
7382
"attributes": {
74-
"perm-domains": [ ".api.gov.bc.ca", ".custom.gov.bc.ca" ]
83+
"perm-data-plane": ["strict-dp"],
84+
"perm-upstreams": [],
85+
"perm-domains": [ ".api.gov.bc.ca", ".cluster.local" ]
86+
}
87+
}
88+
elif id == "g003":
89+
return {
90+
"attributes": {
91+
"perm-data-plane": ["strict-dp"],
92+
"perm-upstreams": ['ns1'],
93+
"perm-domains": [ ".api.gov.bc.ca", ".cluster.local" ]
7594
}
7695
}
77-
return {
78-
"attributes": {
79-
"perm-domains": [ ".api.gov.bc.ca", ".cluster.local" ]
96+
elif id == "g004":
97+
return {
98+
"attributes": {
99+
"perm-domains": [ ".api.gov.bc.ca", ".custom.gov.bc.ca" ]
100+
}
80101
}
81-
}
102+
82103
mocker.patch("v2.services.namespaces.admin_api", return_value=mock_kc_admin)
83104

84105
def mock_kong(mocker):

microservices/gatewayApi/tests/routes/v2/test_gateway_err_validations.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,34 @@ def test_invalid_upstream(client):
114114
response = client.put('/v2/namespaces/mytest/gateway', json=data)
115115
assert response.status_code == 400
116116
assert json.dumps(response.json) == '{"error": "Validation Errors:\\nservice upstream is invalid (e1)"}'
117+
118+
def test_invalid_strict_dp_upstream(client):
119+
configFile = '''
120+
services:
121+
- name: my-service
122+
host: myservice.ns1.svc
123+
tags: ["ns.mytest2", "another"]
124+
'''
125+
126+
data={
127+
"configFile": configFile,
128+
"dryRun": True
129+
}
130+
response = client.put('/v2/namespaces/mytest2/gateway', json=data)
131+
assert response.status_code == 400
132+
assert json.dumps(response.json) == '{"error": "Validation Errors:\\nservice upstream is invalid (e6)"}'
133+
134+
def test_valid_strict_dp_upstream(client):
135+
configFile = '''
136+
services:
137+
- name: my-service
138+
host: myservice.ns1.svc
139+
tags: ["ns.mytest3", "another"]
140+
'''
141+
142+
data={
143+
"configFile": configFile,
144+
"dryRun": True
145+
}
146+
response = client.put('/v2/namespaces/mytest3/gateway', json=data)
147+
assert response.status_code == 200

microservices/gatewayApi/tests/routes/v1/test_validate_upstream.py renamed to microservices/gatewayApi/tests/utils/test_validate_upstream.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import yaml
88
import pytest
9-
from v1.routes.gateway import validate_upstream
9+
from utils.validators import validate_upstream
1010

1111
def test_upstream_good(app):
1212
payload = '''
@@ -126,3 +126,31 @@ def test_upstream_protected_service_allow(app):
126126
y = yaml.load(payload, Loader=yaml.FullLoader)
127127
validate_upstream (y, { "perm-protected-ns": ["deny"]}, ['my-namespace'])
128128

129+
def test_upstream_pass_validation(app):
130+
payload = '''
131+
services:
132+
- name: my-service
133+
tags: ["ns.mytest", "another"]
134+
host: myapi.my-namespace.svc
135+
'''
136+
y = yaml.load(payload, Loader=yaml.FullLoader)
137+
138+
validate_upstream (y, { "perm-upstreams": ["my-namespace"]}, [], True)
139+
140+
def test_upstream_fail_validation(app):
141+
payload = '''
142+
services:
143+
- name: my-service
144+
tags: ["ns.mytest", "another"]
145+
host: myapi.my-namespace.svc
146+
'''
147+
y = yaml.load(payload, Loader=yaml.FullLoader)
148+
149+
with pytest.raises(Exception, match=r"service upstream is invalid \(e6\)"):
150+
validate_upstream (y, {}, [], True)
151+
152+
with pytest.raises(Exception, match=r"service upstream is invalid \(e6\)"):
153+
validate_upstream (y, { "perm-upstreams": ["other-namespace"]}, [], True)
154+
155+
with pytest.raises(Exception, match=r"service upstream is invalid \(e6\)"):
156+
validate_upstream (y, { "perm-upstreams": [""]}, [], True)

microservices/gatewayApi/utils/validators.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import re
3+
from urllib.parse import urlparse
34

45
namespace_validation_rule='^[a-z][a-z0-9-]{4,14}$'
56

@@ -16,3 +17,58 @@ def host_valid(input_string):
1617
# match = regex.match(str(input_string))
1718
# return bool(match is not None)
1819

20+
def validate_upstream(yaml, ns_attributes, protected_kube_namespaces, do_validate_upstreams: bool = False):
21+
errors = []
22+
23+
perm_upstreams = ns_attributes.get('perm-upstreams', [])
24+
25+
allow_protected_ns = ns_attributes.get('perm-protected-ns', ['deny'])[0] == 'allow'
26+
27+
# A service host must not contain a list of protected
28+
if 'services' in yaml:
29+
for service in yaml['services']:
30+
if 'url' in service:
31+
try:
32+
u = urlparse(service["url"])
33+
if u.hostname is None:
34+
errors.append("service upstream has invalid url specified (e1)")
35+
else:
36+
validate_upstream_host(u.hostname, errors, allow_protected_ns, protected_kube_namespaces, do_validate_upstreams, perm_upstreams)
37+
except Exception as e:
38+
errors.append("service upstream has invalid url specified (e2)")
39+
40+
if 'host' in service:
41+
host = service["host"]
42+
validate_upstream_host(host, errors, allow_protected_ns, protected_kube_namespaces, do_validate_upstreams, perm_upstreams)
43+
44+
if len(errors) != 0:
45+
raise Exception('\n'.join(errors))
46+
47+
48+
def validate_upstream_host(_host, errors, allow_protected_ns, protected_kube_namespaces, do_validate_upstreams, perm_upstreams):
49+
host = _host.lower()
50+
51+
restricted = ['localhost', '127.0.0.1', '0.0.0.0']
52+
53+
if host in restricted:
54+
errors.append("service upstream is invalid (e1)")
55+
elif host.endswith('svc'):
56+
partials = host.split('.')
57+
# get the namespace, and make sure it is not in the protected_kube_namespaces list
58+
if len(partials) != 3:
59+
errors.append("service upstream is invalid (e2)")
60+
elif partials[1] in protected_kube_namespaces and allow_protected_ns is False:
61+
errors.append("service upstream is invalid (e3)")
62+
elif do_validate_upstreams and (partials[1] in perm_upstreams) is False:
63+
errors.append("service upstream is invalid (e6)")
64+
elif host.endswith('svc.cluster.local'):
65+
partials = host.split('.')
66+
# get the namespace, and make sure it is not in the protected_kube_namespaces list
67+
if len(partials) != 5:
68+
errors.append("service upstream is invalid (e4)")
69+
elif partials[1] in protected_kube_namespaces and allow_protected_ns is False:
70+
errors.append("service upstream is invalid (e5)")
71+
elif do_validate_upstreams and (partials[1] in perm_upstreams) is False:
72+
errors.append("service upstream is invalid (e6)")
73+
elif do_validate_upstreams:
74+
errors.append("service upstream is invalid (e6)")

microservices/gatewayApi/v1/routes/gateway.py

Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from clients.ocp_routes import get_host_list, get_route_overrides
2525
from clients.ocp_gateway_secret import prep_submitted_config, prep_and_apply_secret, write_submitted_config
2626

27-
from utils.validators import host_valid
27+
from utils.validators import host_valid, validate_upstream
2828
from utils.transforms import plugins_transformations
2929
from utils.masking import mask
3030

@@ -270,7 +270,14 @@ def write_config(namespace: str) -> object:
270270
# Validate upstream URLs are valid
271271
try:
272272
protected_kube_namespaces = json.loads(app.config['protectedKubeNamespaces'])
273-
validate_upstream(gw_config, ns_attributes, protected_kube_namespaces)
273+
274+
dp = get_data_plane(ns_attributes)
275+
276+
do_validate_upstreams = app.config['data_planes'][dp].get("validate-upstreams", False)
277+
278+
log.debug("Validate upstreams %s %s" % (dp, do_validate_upstreams))
279+
280+
validate_upstream(gw_config, ns_attributes, protected_kube_namespaces, do_validate_upstreams)
274281
except Exception as ex:
275282
traceback.print_exc()
276283
log.error("%s - %s" % (namespace, " Upstream Validation Errors: %s" % ex))
@@ -476,55 +483,6 @@ def transform_host(host):
476483
else:
477484
return host
478485

479-
480-
def validate_upstream(yaml, ns_attributes, protected_kube_namespaces):
481-
errors = []
482-
483-
allow_protected_ns = ns_attributes.get('perm-protected-ns', ['deny'])[0] == 'allow'
484-
485-
# A host must not contain a list of protected
486-
if 'services' in yaml:
487-
for service in yaml['services']:
488-
if 'url' in service:
489-
try:
490-
u = urlparse(service["url"])
491-
if u.hostname is None:
492-
errors.append("service upstream has invalid url specified (e1)")
493-
else:
494-
validate_upstream_host(u.hostname, errors, allow_protected_ns, protected_kube_namespaces)
495-
except Exception as e:
496-
errors.append("service upstream has invalid url specified (e2)")
497-
498-
if 'host' in service:
499-
host = service["host"]
500-
validate_upstream_host(host, errors, allow_protected_ns, protected_kube_namespaces)
501-
502-
if len(errors) != 0:
503-
raise Exception('\n'.join(errors))
504-
505-
506-
def validate_upstream_host(_host, errors, allow_protected_ns, protected_kube_namespaces):
507-
host = _host.lower()
508-
509-
restricted = ['localhost', '127.0.0.1', '0.0.0.0']
510-
511-
if host in restricted:
512-
errors.append("service upstream is invalid (e1)")
513-
if host.endswith('svc'):
514-
partials = host.split('.')
515-
# get the namespace, and make sure it is not in the protected_kube_namespaces list
516-
if len(partials) != 3:
517-
errors.append("service upstream is invalid (e2)")
518-
elif partials[1] in protected_kube_namespaces and allow_protected_ns is False:
519-
errors.append("service upstream is invalid (e3)")
520-
if host.endswith('svc.cluster.local'):
521-
partials = host.split('.')
522-
# get the namespace, and make sure it is not in the protected_kube_namespaces list
523-
if len(partials) != 5:
524-
errors.append("service upstream is invalid (e4)")
525-
elif partials[1] in protected_kube_namespaces and allow_protected_ns is False:
526-
errors.append("service upstream is invalid (e5)")
527-
528486
# Handle the two cases:
529487
# - pass in an empty config expecting all routes to be deleted ('upstreams' not in yaml)
530488
# - pass in a config with services ('services' in yaml)

0 commit comments

Comments
 (0)