Repository: deis/deis

Issue:
fix(controller): app URLs should disallow uppercase digits and underscores
Since Deis app names become URLs directly, they must match a regex like `[-a-z0-9]+`, as we test for at https://github.com/deis/deis/blob/master/controller/api/tests/test_app.py#L100. But the regexes for apps in controller/api/urls.py use `[-_\w]`, which does some nice locale adjustments but allows uppercase chars and underscore.



Discussion:

Resolving patch:
diff --git a/controller/api/serializers.py b/controller/api/serializers.py
--- a/controller/api/serializers.py
+++ b/controller/api/serializers.py
@@ -6,6 +6,7 @@
 
 import re
 
+from django.conf import settings
 from django.contrib.auth.models import User
 from rest_framework import serializers
 
@@ -184,7 +185,9 @@ def validate_domain(self, attrs, source):
         Check that the hostname is valid
         """
         value = attrs[source]
-        match = re.match(r'^(\*\.)?([a-z0-9-]+\.)*([a-z0-9-]+)\.([a-z0-9]{2,})$', value)
+        match = re.match(
+            r'^(\*\.)?(' + settings.APP_URL_REGEX + r'\.)*([a-z0-9-]+)\.([a-z0-9]{2,})$',
+            value)
         if not match:
             raise serializers.ValidationError(
                 "Hostname does not look like a valid hostname. "
diff --git a/controller/api/urls.py b/controller/api/urls.py
--- a/controller/api/urls.py
+++ b/controller/api/urls.py
@@ -230,6 +230,7 @@
 
 from __future__ import unicode_literals
 
+from django.conf import settings
 from django.conf.urls import include
 from django.conf.urls import patterns
 from django.conf.urls import url
@@ -251,46 +252,47 @@
     url(r'^clusters/?',
         views.ClusterViewSet.as_view({'get': 'list', 'post': 'create'})),
     # application release components
-    url(r'^apps/(?P<id>[-_\w]+)/config/?',
+    url(r'^apps/(?P<id>{})/config/?'.format(settings.APP_URL_REGEX),
         views.AppConfigViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
-    url(r'^apps/(?P<id>[-_\w]+)/builds/(?P<uuid>[-_\w]+)/?',
+    url(r'^apps/(?P<id>{})/builds/(?P<uuid>[-_\w]+)/?'.format(settings.APP_URL_REGEX),
         views.AppBuildViewSet.as_view({'get': 'retrieve'})),
-    url(r'^apps/(?P<id>[-_\w]+)/builds/?',
+    url(r'^apps/(?P<id>{})/builds/?'.format(settings.APP_URL_REGEX),
         views.AppBuildViewSet.as_view({'get': 'list', 'post': 'create'})),
-    url(r'^apps/(?P<id>[-_\w]+)/releases/v(?P<version>[0-9]+)/?',
+    url(r'^apps/(?P<id>{})/releases/v(?P<version>[0-9]+)/?'.format(settings.APP_URL_REGEX),
         views.AppReleaseViewSet.as_view({'get': 'retrieve'})),
-    url(r'^apps/(?P<id>[-_\w]+)/releases/rollback/?',
+    url(r'^apps/(?P<id>{})/releases/rollback/?'.format(settings.APP_URL_REGEX),
         views.AppReleaseViewSet.as_view({'post': 'rollback'})),
-    url(r'^apps/(?P<id>[-_\w]+)/releases/?',
+    url(r'^apps/(?P<id>{})/releases/?'.format(settings.APP_URL_REGEX),
         views.AppReleaseViewSet.as_view({'get': 'list'})),
     # application infrastructure
-    url(r'^apps/(?P<id>[-_\w]+)/containers/(?P<type>[-_\w]+)/(?P<num>[-_\w]+)/?',
+    url(r'^apps/(?P<id>{})/containers/(?P<type>[-_\w]+)/(?P<num>[-_\w]+)/?'.format(
+        settings.APP_URL_REGEX),
         views.AppContainerViewSet.as_view({'get': 'retrieve'})),
-    url(r'^apps/(?P<id>[-_\w]+)/containers/(?P<type>[-_\w.]+)/?',
+    url(r'^apps/(?P<id>{})/containers/(?P<type>[-_\w.]+)/?'.format(settings.APP_URL_REGEX),
         views.AppContainerViewSet.as_view({'get': 'list'})),
-    url(r'^apps/(?P<id>[-_\w]+)/containers/?',
+    url(r'^apps/(?P<id>{})/containers/?'.format(settings.APP_URL_REGEX),
         views.AppContainerViewSet.as_view({'get': 'list'})),
     # application domains
-    url(r'^apps/(?P<id>[-_\w]+)/domains/(?P<domain>[-\._\w]+)/?',
+    url(r'^apps/(?P<id>{})/domains/(?P<domain>[-\._\w]+)/?'.format(settings.APP_URL_REGEX),
         views.DomainViewSet.as_view({'delete': 'destroy'})),
-    url(r'^apps/(?P<id>[-_\w]+)/domains/?',
+    url(r'^apps/(?P<id>{})/domains/?'.format(settings.APP_URL_REGEX),
         views.DomainViewSet.as_view({'post': 'create', 'get': 'list'})),
     # application actions
-    url(r'^apps/(?P<id>[-_\w]+)/scale/?',
+    url(r'^apps/(?P<id>{})/scale/?'.format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'post': 'scale'})),
-    url(r'^apps/(?P<id>[-_\w]+)/logs/?',
+    url(r'^apps/(?P<id>{})/logs/?'.format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'post': 'logs'})),
-    url(r'^apps/(?P<id>[-_\w]+)/run/?',
+    url(r'^apps/(?P<id>{})/run/?'.format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'post': 'run'})),
-    url(r'^apps/(?P<id>[-_\w]+)/calculate/?',
+    url(r'^apps/(?P<id>{})/calculate/?'.format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'post': 'calculate'})),
     # apps sharing
-    url(r'^apps/(?P<id>[-_\w]+)/perms/(?P<username>[-_\w]+)/?',
+    url(r'^apps/(?P<id>{})/perms/(?P<username>[-_\w]+)/?'.format(settings.APP_URL_REGEX),
         views.AppPermsViewSet.as_view({'delete': 'destroy'})),
-    url(r'^apps/(?P<id>[-_\w]+)/perms/?',
+    url(r'^apps/(?P<id>{})/perms/?'.format(settings.APP_URL_REGEX),
         views.AppPermsViewSet.as_view({'get': 'list', 'post': 'create'})),
     # apps base endpoint
-    url(r'^apps/(?P<id>[-_\w]+)/?',
+    url(r'^apps/(?P<id>{})/?'.format(settings.APP_URL_REGEX),
         views.AppViewSet.as_view({'get': 'retrieve', 'delete': 'destroy'})),
     url(r'^apps/?',
         views.AppViewSet.as_view({'get': 'list', 'post': 'create'})),
diff --git a/controller/deis/settings.py b/controller/deis/settings.py
--- a/controller/deis/settings.py
+++ b/controller/deis/settings.py
@@ -297,6 +297,8 @@
     }
 }
 
+APP_URL_REGEX = '[a-z0-9-]+'
+
 # SECURITY: change this to allowed fqdn's to prevent host poisioning attacks
 # see https://docs.djangoproject.com/en/1.5/ref/settings/#std:setting-ALLOWED_HOSTS
 ALLOWED_HOSTS = ['*']


Test patch: