Skip to content

Commit 118f0ad

Browse files
authored
Merge pull request #135 from bcgov/dev
Custom domains
2 parents 89985a9 + 74ff810 commit 118f0ad

35 files changed

+1278
-250
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/clients/kong.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def get_service_routes (service_id):
2929
def get_local_certs_by_ns (ns):
3030
return recurse_get_records ([], "/certificates?tags=gwa.ns.%s" % ns)
3131

32+
def get_public_certs_by_ns (ns):
33+
return recurse_get_records ([], "/certificates?tags=ns.%s" % ns)
34+
3235
def get_acls ():
3336
return recurse_get_records ([], "/acls")
3437

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: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +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-
return {
64-
"id": "g001"
65-
}
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"}
6673
def get_group(id):
67-
return {
68-
"attributes": {
69-
"perm-domains": [ ".api.gov.bc.ca", ".cluster.local" ]
74+
if id == "g001":
75+
return {
76+
"attributes": {
77+
"perm-domains": [ ".api.gov.bc.ca", ".cluster.local" ]
78+
}
7079
}
71-
}
80+
elif id == "g002":
81+
return {
82+
"attributes": {
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" ]
94+
}
95+
}
96+
elif id == "g004":
97+
return {
98+
"attributes": {
99+
"perm-domains": [ ".api.gov.bc.ca", ".custom.gov.bc.ca" ]
100+
}
101+
}
102+
72103
mocker.patch("v2.services.namespaces.admin_api", return_value=mock_kc_admin)
73104

74105
def mock_kong(mocker):
@@ -92,14 +123,35 @@ def json():
92123
return Response
93124
elif (path == 'http://kong/certificates?tags=gwa.ns.mytest' or
94125
path == 'http://kong/certificates?tags=gwa.ns.sescookie' or
95-
path == 'http://kong/certificates?tags=gwa.ns.dclass'):
126+
path == 'http://kong/certificates?tags=gwa.ns.dclass' or
127+
path == 'http://kong/certificates?tags=gwa.ns.customcert'):
96128
class Response:
97129
def json():
98130
return {
99131
"data": [],
100132
"next": None
101133
}
102134
return Response
135+
elif (path == 'http://kong/certificates?tags=ns.customcert'):
136+
class Response:
137+
def json():
138+
return {
139+
"next": None,
140+
"data": [
141+
{
142+
"id": "41d14845-669f-4dcd-aff2-926fb32a4b25",
143+
"snis": [
144+
"test.custom.gov.bc.ca"
145+
],
146+
"tags": [
147+
"ns.customcert",
148+
],
149+
"cert": "CERT",
150+
"key": "KEY"
151+
}
152+
]
153+
}
154+
return Response
103155

104156
else:
105157
raise Exception(path)
@@ -157,7 +209,8 @@ class Response:
157209
"aps.route.dataclass.high": [],
158210
"aps.route.dataclass.public": []
159211
},
160-
'select_tag': 'ns.sescookie.dev'
212+
'select_tag': 'ns.sescookie.dev',
213+
'certificates': []
161214
}
162215

163216
assert json.dumps(kwargs['json'], sort_keys=True) == json.dumps(matched, sort_keys=True)
@@ -175,7 +228,39 @@ class Response:
175228
"aps.route.dataclass.high": ['myapi.api.gov.bc.ca'],
176229
"aps.route.dataclass.public": []
177230
},
178-
'select_tag': 'ns.dclass.dev'
231+
'select_tag': 'ns.dclass.dev',
232+
'certificates': []
233+
}
234+
235+
assert json.dumps(kwargs['json'], sort_keys=True) == json.dumps(matched, sort_keys=True)
236+
return Response
237+
elif (url == 'http://kube-api/namespaces/customcert/routes'):
238+
class Response:
239+
status_code = 201
240+
matched = {
241+
'hosts': ['test.custom.gov.bc.ca'],
242+
'ns_attributes': {'perm-domains': ['.api.gov.bc.ca', '.custom.gov.bc.ca']},
243+
'overrides': {
244+
'aps.route.session.cookie.enabled': [],
245+
"aps.route.dataclass.low": [],
246+
"aps.route.dataclass.medium": [],
247+
"aps.route.dataclass.high": [],
248+
"aps.route.dataclass.public": []
249+
},
250+
'select_tag': 'ns.customcert',
251+
'certificates': [
252+
{
253+
"id": "41d14845-669f-4dcd-aff2-926fb32a4b25",
254+
"snis": [
255+
"test.custom.gov.bc.ca"
256+
],
257+
"tags": [
258+
"ns.customcert",
259+
],
260+
"cert": "CERT",
261+
"key": "KEY"
262+
}
263+
]
179264
}
180265

181266
assert json.dumps(kwargs['json'], sort_keys=True) == json.dumps(matched, sort_keys=True)
@@ -190,24 +275,16 @@ class Response:
190275
raise Exception(url)
191276

192277
def mock_requests_get(self, url, **kwards):
193-
if (url == 'http://kube-api/namespaces/mytest/local_tls'):
194-
class Response:
195-
status_code = 200
196-
def json():
197-
return {}
198-
return Response
199-
elif (url == 'http://kube-api/namespaces/sescookie/local_tls'):
200-
class Response:
201-
status_code = 200
202-
def json():
203-
return {}
204-
return Response
205-
elif (url == 'http://kube-api/namespaces/dclass/local_tls'):
278+
if (url == 'http://kube-api/namespaces/mytest/local_tls' or
279+
url == 'http://kube-api/namespaces/sescookie/local_tls' or
280+
url == 'http://kube-api/namespaces/dclass/local_tls' or
281+
url == 'http://kube-api/namespaces/customcert/local_tls'):
206282
class Response:
207283
status_code = 200
208284
def json():
209285
return {}
210286
return Response
287+
211288
else:
212289
raise Exception(url)
213290

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,31 @@ def test_happy_with_data_class_gateway_call(client):
121121
assert response.status_code == 200
122122
assert json.dumps(response.json) == '{"message": "Sync successful.", "results": "Deck reported no changes"}'
123123

124+
def test_happy_with_custom_domain_gateway_call(client):
125+
configFile = '''
126+
services:
127+
- name: my-service
128+
host: myupstream.local
129+
ca_certificates: [ "0000-0000-0000-0000" ]
130+
certificate: "41d14845-669f-4dcd-aff2-926fb32a4b25"
131+
tags: ["ns.customcert"]
132+
routes:
133+
- name: route-1
134+
hosts: [ test.custom.gov.bc.ca ]
135+
tags: ["ns.customcert"]
136+
plugins:
137+
- name: acl-auth
138+
tags: ["ns.customcert"]
139+
'''
140+
141+
data={
142+
"configFile": configFile,
143+
"dryRun": False
144+
}
145+
response = client.put('/v2/namespaces/customcert/gateway', json=data)
146+
assert response.status_code == 200
147+
assert json.dumps(response.json) == '{"message": "Sync successful.", "results": "Deck reported no changes"}'
148+
124149
def test_success_mtls_reference(client):
125150
configFile = '''
126151
services:

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)")

0 commit comments

Comments
 (0)