Skip to content

Commit 9f6cb94

Browse files
authored
fix(urls): terminate url regex with $ so that there is no cascading attempts at URLs (#1103)
When using -a=foo for an app (see deis/workflow-cli#22) the app name will be sent as =foo but since =foo does not match the URL app name regex it will simply keep trying to match to the next URL in the hierarchy that can take POST which happens to match to app create URL Anchoring all URLs to match to the end via $ prevents those cascading behaviours
1 parent 9582496 commit 9f6cb94

4 files changed

Lines changed: 76 additions & 49 deletions

File tree

rootfs/api/tests/test_app.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,33 @@ def test_gather_app_settings(self, mock_requests):
663663
assert isinstance(s['deploy_timeout'], int)
664664
assert isinstance(s['pod_termination_grace_period_seconds'], int)
665665

666+
def test_app_name_bad_regex(self, mock_requests):
667+
"""
668+
Create a normal app and then try to do a build on it but include
669+
extra chars (equal for example) in the name and make sure no new
670+
apps are created and that the operation errors out
671+
"""
672+
# create app
673+
app_id = self.create_app()
674+
675+
# verify that there is only 1 app and it is the one expected
676+
response = self.client.get("/v2/apps")
677+
self.assertEqual(response.status_code, 200, response)
678+
self.assertEqual(response.data['count'], 1, response.data)
679+
self.assertEqual(response.data['results'][0]['id'], app_id, response.data)
680+
681+
# deploy to an app that doesn't exist should fail with 404
682+
url = "/v2/apps/{}/builds".format('={}'.format(app_id))
683+
body = {'image': 'autotest/example'}
684+
response = self.client.post(url, body)
685+
self.assertEqual(response.status_code, 404, response)
686+
687+
# verify again that there is only 1 app
688+
response = self.client.get("/v2/apps")
689+
self.assertEqual(response.status_code, 200, response)
690+
self.assertEqual(response.data['count'], 1, response.data)
691+
692+
666693
FAKE_LOG_DATA = bytes("""
667694
2013-08-15 12:41:25 [33454] [INFO] Starting gunicorn 17.5
668695
2013-08-15 12:41:25 [33454] [INFO] Listening at: http://0.0.0.0:5000 (33454)

rootfs/api/tests/test_hooks.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def test_build_hook(self, mock_requests):
127127
app_id = self.create_app()
128128

129129
build = {'username': 'autotest', 'app': app_id}
130-
url = '/v2/hooks/builds'.format(**locals())
130+
url = '/v2/hooks/build'.format(**locals())
131131
body = {'receive_user': 'autotest',
132132
'receive_repo': app_id,
133133
'image': '{app_id}:v2'.format(**locals())}
@@ -146,7 +146,7 @@ def test_build_hook_slug_url(self, mock_requests):
146146
"""Test creating a slug_url build via an API Hook"""
147147
app_id = self.create_app()
148148
build = {'username': 'autotest', 'app': app_id}
149-
url = '/v2/hooks/builds'.format(**locals())
149+
url = '/v2/hooks/build'.format(**locals())
150150
body = {'receive_user': 'autotest',
151151
'receive_repo': app_id,
152152
'image': 'http://example.com/slugs/foo-12345354.tar.gz'}
@@ -168,7 +168,7 @@ def test_build_hook_procfile(self, mock_requests):
168168
app_id = self.create_app()
169169

170170
build = {'username': 'autotest', 'app': app_id}
171-
url = '/v2/hooks/builds'.format(**locals())
171+
url = '/v2/hooks/build'.format(**locals())
172172
PROCFILE = {'web': 'node server.js', 'worker': 'node worker.js'}
173173
SHA = 'ecdff91c57a0b9ab82e89634df87e293d259a3aa'
174174
body = {'receive_user': 'autotest',
@@ -213,7 +213,7 @@ def test_build_hook_dockerfile(self, mock_requests):
213213
"""Test creating a Dockerfile build via an API Hook"""
214214
app_id = self.create_app()
215215
build = {'username': 'autotest', 'app': app_id}
216-
url = '/v2/hooks/builds'.format(**locals())
216+
url = '/v2/hooks/build'.format(**locals())
217217
SHA = 'ecdff91c57a0b9ab82e89634df87e293d259a3aa'
218218
DOCKERFILE = """FROM busybox
219219
CMD /bin/true"""
@@ -296,7 +296,7 @@ def test_admin_can_hook(self, mock_requests):
296296
'image': '{app_id}:v2'.format(**locals()),
297297
'sha': 'ecdff91c57a0b9ab82e89634df87e293d259a3aa',
298298
'dockerfile': DOCKERFILE}
299-
url = '/v2/hooks/builds'
299+
url = '/v2/hooks/build'
300300
response = self.client.post(url, body,
301301
HTTP_X_DEIS_BUILDER_AUTH=settings.BUILDER_KEY)
302302
self.assertEqual(response.status_code, 200, response.data)

rootfs/api/tests/test_perm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def test_create(self):
174174
self.assertEqual(len(response.data['results']), 1)
175175
# check that user 2 can't see any of the app's builds, configs,
176176
# containers, limits, or releases
177-
for model in ['builds', 'config', 'containers', 'releases']:
177+
for model in ['builds', 'config', 'pods', 'releases']:
178178
response = self.client.get("/v2/apps/{}/{}/".format(app_id, model))
179179
msg = "Failed: status '%s', and data '%s'" % (response.status_code, response.data)
180180
self.assertEqual(response.status_code, 403, msg=msg)

rootfs/api/urls.py

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,109 +17,109 @@
1717
urlpatterns = [
1818
url(r'^', include(router.urls)),
1919
# application release components
20-
url(r"^apps/(?P<id>{})/config/?".format(settings.APP_URL_REGEX),
20+
url(r"^apps/(?P<id>{})/config/?$".format(settings.APP_URL_REGEX),
2121
views.ConfigViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
22-
url(r"^apps/(?P<id>{})/builds/(?P<uuid>[-_\w]+)/?".format(settings.APP_URL_REGEX),
22+
url(r"^apps/(?P<id>{})/builds/(?P<uuid>[-_\w]+)/?$".format(settings.APP_URL_REGEX),
2323
views.BuildViewSet.as_view({'get': 'retrieve'})),
24-
url(r"^apps/(?P<id>{})/builds/?".format(settings.APP_URL_REGEX),
24+
url(r"^apps/(?P<id>{})/builds/?$".format(settings.APP_URL_REGEX),
2525
views.BuildViewSet.as_view({'get': 'list', 'post': 'create'})),
26-
url(r"^apps/(?P<id>{})/releases/v(?P<version>[0-9]+)/?".format(settings.APP_URL_REGEX),
26+
url(r"^apps/(?P<id>{})/releases/v(?P<version>[0-9]+)/?$".format(settings.APP_URL_REGEX),
2727
views.ReleaseViewSet.as_view({'get': 'retrieve'})),
28-
url(r"^apps/(?P<id>{})/releases/rollback/?".format(settings.APP_URL_REGEX),
28+
url(r"^apps/(?P<id>{})/releases/rollback/?$".format(settings.APP_URL_REGEX),
2929
views.ReleaseViewSet.as_view({'post': 'rollback'})),
30-
url(r"^apps/(?P<id>{})/releases/?".format(settings.APP_URL_REGEX),
30+
url(r"^apps/(?P<id>{})/releases/?$".format(settings.APP_URL_REGEX),
3131
views.ReleaseViewSet.as_view({'get': 'list'})),
3232
# restart pods
33-
url(r"^apps/(?P<id>{})/pods/restart/?".format(settings.APP_URL_REGEX),
33+
url(r"^apps/(?P<id>{})/pods/restart/?$".format(settings.APP_URL_REGEX),
3434
views.PodViewSet.as_view({'post': 'restart'})),
35-
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w.]+)/restart/?".format(settings.APP_URL_REGEX),
35+
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w.]+)/restart/?$".format(settings.APP_URL_REGEX),
3636
views.PodViewSet.as_view({'post': 'restart'})),
37-
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w]+)/(?P<name>[-_\w]+)/restart/?".format(
37+
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w]+)/(?P<name>[-_\w]+)/restart/?$".format(
3838
settings.APP_URL_REGEX),
3939
views.PodViewSet.as_view({'post': 'restart'})),
4040
# list pods
41-
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w]+)/(?P<name>[-_\w]+)/?".format(
41+
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w]+)/(?P<name>[-_\w]+)/?$".format(
4242
settings.APP_URL_REGEX),
4343
views.PodViewSet.as_view({'get': 'list'})),
44-
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w.]+)/?".format(settings.APP_URL_REGEX),
44+
url(r"^apps/(?P<id>{})/pods/(?P<type>[-_\w.]+)/?$".format(settings.APP_URL_REGEX),
4545
views.PodViewSet.as_view({'get': 'list'})),
46-
url(r"^apps/(?P<id>{})/pods/?".format(settings.APP_URL_REGEX),
46+
url(r"^apps/(?P<id>{})/pods/?$".format(settings.APP_URL_REGEX),
4747
views.PodViewSet.as_view({'get': 'list'})),
4848
# application domains
49-
url(r"^apps/(?P<id>{})/domains/(?P<domain>\**\.?[-\._\w]+)/?".format(settings.APP_URL_REGEX),
49+
url(r"^apps/(?P<id>{})/domains/(?P<domain>\**\.?[-\._\w]+)/?$".format(settings.APP_URL_REGEX),
5050
views.DomainViewSet.as_view({'delete': 'destroy'})),
51-
url(r"^apps/(?P<id>{})/domains/?".format(settings.APP_URL_REGEX),
51+
url(r"^apps/(?P<id>{})/domains/?$".format(settings.APP_URL_REGEX),
5252
views.DomainViewSet.as_view({'post': 'create', 'get': 'list'})),
5353
# application actions
54-
url(r"^apps/(?P<id>{})/scale/?".format(settings.APP_URL_REGEX),
54+
url(r"^apps/(?P<id>{})/scale/?$".format(settings.APP_URL_REGEX),
5555
views.AppViewSet.as_view({'post': 'scale'})),
56-
url(r"^apps/(?P<id>{})/logs/?".format(settings.APP_URL_REGEX),
56+
url(r"^apps/(?P<id>{})/logs/?$".format(settings.APP_URL_REGEX),
5757
views.AppViewSet.as_view({'get': 'logs'})),
58-
url(r"^apps/(?P<id>{})/run/?".format(settings.APP_URL_REGEX),
58+
url(r"^apps/(?P<id>{})/run/?$".format(settings.APP_URL_REGEX),
5959
views.AppViewSet.as_view({'post': 'run'})),
6060
# application settings
61-
url(r"^apps/(?P<id>{})/settings/?".format(settings.APP_URL_REGEX),
61+
url(r"^apps/(?P<id>{})/settings/?$".format(settings.APP_URL_REGEX),
6262
views.AppSettingsViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
6363
# application ip whitelist
64-
url(r"^apps/(?P<id>{})/whitelist/?".format(settings.APP_URL_REGEX),
64+
url(r"^apps/(?P<id>{})/whitelist/?$".format(settings.APP_URL_REGEX),
6565
views.WhitelistViewSet.as_view({'post': 'create', 'get': 'list', 'delete': 'delete'})),
6666
# application TLS settings
67-
url(r"^apps/(?P<id>{})/tls/?".format(settings.APP_URL_REGEX),
67+
url(r"^apps/(?P<id>{})/tls/?$".format(settings.APP_URL_REGEX),
6868
views.TLSViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
6969
# apps sharing
70-
url(r"^apps/(?P<id>{})/perms/(?P<username>[-_\w]+)/?".format(settings.APP_URL_REGEX),
70+
url(r"^apps/(?P<id>{})/perms/(?P<username>[-_\w]+)/?$".format(settings.APP_URL_REGEX),
7171
views.AppPermsViewSet.as_view({'delete': 'destroy'})),
72-
url(r"^apps/(?P<id>{})/perms/?".format(settings.APP_URL_REGEX),
72+
url(r"^apps/(?P<id>{})/perms/?$".format(settings.APP_URL_REGEX),
7373
views.AppPermsViewSet.as_view({'get': 'list', 'post': 'create'})),
7474
# apps base endpoint
75-
url(r"^apps/(?P<id>{})/?".format(settings.APP_URL_REGEX),
75+
url(r"^apps/(?P<id>{})/?$".format(settings.APP_URL_REGEX),
7676
views.AppViewSet.as_view({'get': 'retrieve', 'post': 'update', 'delete': 'destroy'})),
77-
url(r'^apps/?',
77+
url(r'^apps/?$',
7878
views.AppViewSet.as_view({'get': 'list', 'post': 'create'})),
7979
# key
80-
url(r'^keys/(?P<id>.+)/?',
80+
url(r'^keys/(?P<id>.+)/?$',
8181
views.KeyViewSet.as_view({'get': 'retrieve', 'delete': 'destroy'})),
82-
url(r'^keys/?',
82+
url(r'^keys/?$',
8383
views.KeyViewSet.as_view({'get': 'list', 'post': 'create'})),
8484
# hooks
85-
url(r'^hooks/keys/(?P<id>{})/(?P<username>[-_\w]+)?'.format(settings.APP_URL_REGEX),
85+
url(r'^hooks/keys/(?P<id>{})/(?P<username>[-_\w]+)?$'.format(settings.APP_URL_REGEX),
8686
views.KeyHookViewSet.as_view({'get': 'users'})),
87-
url(r'^hooks/keys/(?P<id>{})/?'.format(settings.APP_URL_REGEX),
87+
url(r'^hooks/keys/(?P<id>{})/?$'.format(settings.APP_URL_REGEX),
8888
views.KeyHookViewSet.as_view({'get': 'app'})),
89-
url(r'^hooks/key/(?P<fingerprint>.+)/?',
89+
url(r'^hooks/key/(?P<fingerprint>.+)/?$',
9090
views.KeyHookViewSet.as_view({'get': 'public_key'})),
91-
url(r'^hooks/build/?',
91+
url(r'^hooks/build/?$',
9292
views.BuildHookViewSet.as_view({'post': 'create'})),
93-
url(r'^hooks/config/?',
93+
url(r'^hooks/config/?$',
9494
views.ConfigHookViewSet.as_view({'post': 'create'})),
9595
# authn / authz
96-
url(r'^auth/register/?',
96+
url(r'^auth/register/?$',
9797
views.UserRegistrationViewSet.as_view({'post': 'create'})),
98-
url(r'^auth/cancel/?',
98+
url(r'^auth/cancel/?$',
9999
views.UserManagementViewSet.as_view({'delete': 'destroy'})),
100-
url(r'^auth/passwd/?',
100+
url(r'^auth/passwd/?$',
101101
views.UserManagementViewSet.as_view({'post': 'passwd'})),
102-
url(r'^auth/whoami/?',
102+
url(r'^auth/whoami/?$',
103103
views.UserManagementViewSet.as_view({'get': 'list'})),
104-
url(r'^auth/login/',
104+
url(r'^auth/login/$',
105105
views_obtain_auth_token),
106-
url(r'^auth/tokens/',
106+
url(r'^auth/tokens/$',
107107
views.TokenManagementViewSet.as_view({'post': 'regenerate'})),
108108
# admin sharing
109-
url(r'^admin/perms/(?P<username>[-_\w]+)/?',
109+
url(r'^admin/perms/(?P<username>[-_\w]+)/?$',
110110
views.AdminPermsViewSet.as_view({'delete': 'destroy'})),
111-
url(r'^admin/perms/?',
111+
url(r'^admin/perms/?$',
112112
views.AdminPermsViewSet.as_view({'get': 'list', 'post': 'create'})),
113113
# certificates
114-
url(r'^certs/(?P<name>[-_*.\w]+)/domain/(?P<domain>\**\.?[-\._\w]+)?',
114+
url(r'^certs/(?P<name>[-_*.\w]+)/domain/(?P<domain>\**\.?[-\._\w]+)?$',
115115
views.CertificateViewSet.as_view({'delete': 'detach', 'post': 'attach'})),
116-
url(r'^certs/(?P<name>[-_*.\w]+)/?',
116+
url(r'^certs/(?P<name>[-_*.\w]+)/?$',
117117
views.CertificateViewSet.as_view({
118118
'get': 'retrieve',
119119
'delete': 'destroy'
120120
})),
121-
url(r'^certs/?',
121+
url(r'^certs/?$',
122122
views.CertificateViewSet.as_view({'get': 'list', 'post': 'create'})),
123123
# list users
124-
url(r'^users/?', views.UserView.as_view({'get': 'list'})),
124+
url(r'^users/?$', views.UserView.as_view({'get': 'list'})),
125125
]

0 commit comments

Comments
 (0)