Skip to content

Commit ebcc410

Browse files
committed
Merge pull request #3399 from Joshua-Anderson/registrationMode
feat(controller): Allow adminOnly registration
2 parents a9019be + ec24a99 commit ebcc410

9 files changed

Lines changed: 157 additions & 14 deletions

File tree

client/deis.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,14 @@ def auth_register(self, args):
756756
email = raw_input('email: ')
757757
url = urlparse.urljoin(controller, '/v1/auth/register')
758758
payload = {'username': username, 'password': password, 'email': email}
759+
headers = {}
760+
761+
token = self._settings.get('token')
762+
if token:
763+
headers.update({'Authorization': 'token {}'.format(token)})
764+
759765
response = self._session.post(url, data=payload, allow_redirects=False,
760-
verify=ssl_verify)
766+
verify=ssl_verify, headers=headers)
761767
if response.status_code == requests.codes.created:
762768
self._settings['controller'] = controller
763769
self._settings.save()

controller/api/authentication.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.contrib.auth.models import AnonymousUser
22
from rest_framework import authentication
3+
from rest_framework.authentication import TokenAuthentication
34

45

56
class AnonymousAuthentication(authentication.BaseAuthentication):
@@ -9,3 +10,15 @@ def authenticate(self, request):
910
Authenticate the request for anyone!
1011
"""
1112
return AnonymousUser(), None
13+
14+
15+
class AnonymousOrAuthenticatedAuthentication(authentication.BaseAuthentication):
16+
17+
def authenticate(self, request):
18+
"""
19+
Authenticate the request for anyone or if a valid token is provided, a user.
20+
"""
21+
try:
22+
return TokenAuthentication.authenticate(TokenAuthentication(), request)
23+
except:
24+
return AnonymousUser(), None

controller/api/permissions.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,22 @@ class HasRegistrationAuth(permissions.BasePermission):
9797
Checks to see if registration is enabled
9898
"""
9999
def has_permission(self, request, view):
100-
return settings.REGISTRATION_ENABLED
100+
"""
101+
If settings.REGISTRATION_MODE does not exist, such as during a test, return True
102+
Return `True` if permission is granted, `False` otherwise.
103+
"""
104+
try:
105+
if settings.REGISTRATION_MODE == 'disabled':
106+
return False
107+
if settings.REGISTRATION_MODE == 'enabled':
108+
return True
109+
elif settings.REGISTRATION_MODE == 'admin_only':
110+
return request.user.is_superuser
111+
else:
112+
raise Exception("{} is not a valid registation mode"
113+
.format(settings.REGISTRATION_MODE))
114+
except AttributeError:
115+
return True
101116

102117

103118
class HasBuilderAuth(permissions.BasePermission):

controller/api/tests/test_auth.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def test_auth(self):
6363
content_type='application/x-www-form-urlencoded')
6464
self.assertEqual(response.status_code, 200)
6565

66-
@override_settings(REGISTRATION_ENABLED=False)
66+
@override_settings(REGISTRATION_MODE="disabled")
6767
def test_auth_registration_disabled(self):
6868
"""test that a new user cannot register when registration is disabled."""
6969
url = '/v1/auth/register'
@@ -79,6 +79,89 @@ def test_auth_registration_disabled(self):
7979
response = self.client.post(url, json.dumps(submit), content_type='application/json')
8080
self.assertEqual(response.status_code, 403)
8181

82+
@override_settings(REGISTRATION_MODE="admin_only")
83+
def test_auth_registration_admin_only_fails_if_not_admin(self):
84+
"""test that a non superuser cannot register when registration is admin only."""
85+
url = '/v1/auth/register'
86+
submit = {
87+
'username': 'testuser',
88+
'password': 'password',
89+
'first_name': 'test',
90+
'last_name': 'user',
91+
'email': 'test@user.com',
92+
'is_superuser': False,
93+
'is_staff': False,
94+
}
95+
response = self.client.post(url, json.dumps(submit), content_type='application/json')
96+
self.assertEqual(response.status_code, 403)
97+
98+
@override_settings(REGISTRATION_MODE="admin_only")
99+
def test_auth_registration_admin_only_works(self):
100+
"""test that a superuser can register when registration is admin only."""
101+
102+
user = User.objects.get(username='autotest')
103+
token = Token.objects.get(user=user)
104+
105+
url = '/v1/auth/register'
106+
107+
username, password = 'newuser_by_admin', 'password'
108+
first_name, last_name = 'Otto', 'Test'
109+
email = 'autotest@deis.io'
110+
111+
submit = {
112+
'username': username,
113+
'password': password,
114+
'first_name': first_name,
115+
'last_name': last_name,
116+
'email': email,
117+
# try to abuse superuser/staff level perms (not the first signup!)
118+
'is_superuser': True,
119+
'is_staff': True,
120+
}
121+
response = self.client.post(url, json.dumps(submit), content_type='application/json',
122+
HTTP_AUTHORIZATION='token {}'.format(token))
123+
124+
self.assertEqual(response.status_code, 201)
125+
for key in response.data.keys():
126+
self.assertIn(key, ['id', 'last_login', 'is_superuser', 'username', 'first_name',
127+
'last_name', 'email', 'is_active', 'is_superuser', 'is_staff',
128+
'date_joined', 'groups', 'user_permissions'])
129+
expected = {
130+
'username': username,
131+
'email': email,
132+
'first_name': first_name,
133+
'last_name': last_name,
134+
'is_active': True,
135+
'is_superuser': False,
136+
'is_staff': False
137+
}
138+
self.assertDictContainsSubset(expected, response.data)
139+
# test login
140+
url = '/v1/auth/login/'
141+
payload = urllib.urlencode({'username': username, 'password': password})
142+
response = self.client.post(url, data=payload,
143+
content_type='application/x-www-form-urlencoded')
144+
self.assertEqual(response.status_code, 200)
145+
146+
@override_settings(REGISTRATION_MODE="not_a_mode")
147+
def test_auth_registration_fails_with_nonexistant_mode(self):
148+
"""test that a registration should fail with a nonexistant mode"""
149+
url = '/v1/auth/register'
150+
submit = {
151+
'username': 'testuser',
152+
'password': 'password',
153+
'first_name': 'test',
154+
'last_name': 'user',
155+
'email': 'test@user.com',
156+
'is_superuser': False,
157+
'is_staff': False,
158+
}
159+
160+
try:
161+
self.client.post(url, json.dumps(submit), content_type='application/json')
162+
except Exception, e:
163+
self.assertEqual(str(e), 'not_a_mode is not a valid registation mode')
164+
82165
def test_cancel(self):
83166
"""Test that a registered user can cancel her account."""
84167
# test registration workflow

controller/api/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
class UserRegistrationViewSet(GenericViewSet,
2121
mixins.CreateModelMixin):
2222
"""ViewSet to handle registering new users. The logic is in the serializer."""
23-
authentication_classes = [authentication.AnonymousAuthentication]
24-
permission_classes = [permissions.IsAnonymous, permissions.HasRegistrationAuth]
23+
authentication_classes = [authentication.AnonymousOrAuthenticatedAuthentication]
24+
permission_classes = [permissions.HasRegistrationAuth]
2525
serializer_class = serializers.UserSerializer
2626

2727

controller/bin/boot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function etcd_safe_mkdir {
4747
etcd_set_default protocol ${DEIS_PROTOCOL:-http}
4848
etcd_set_default secretKey ${DEIS_SECRET_KEY:-`openssl rand -base64 64 | tr -d '\n'`}
4949
etcd_set_default builderKey ${DEIS_BUILDER_KEY:-`openssl rand -base64 64 | tr -d '\n'`}
50-
etcd_set_default registrationEnabled 1
50+
etcd_set_default registrationMode "enabled"
5151
etcd_set_default webEnabled 0
5252
etcd_set_default unitHostname default
5353

controller/migrations/data/0002.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
3+
ETCD_PORT=${ETCD_PORT:-4001}
4+
ETCD="$HOST:$ETCD_PORT"
5+
ETCDCTL="etcdctl -C $ETCD"
6+
7+
# April 8, 2015: If registrationEnabled key exists, migrate it to registrationMode and delete it.
8+
9+
if [[ "$($ETCDCTL get /deis/migrations/data/0002 2> /dev/null)" != "done" ]];
10+
then
11+
if $ETCDCTL ls /deis/controller | grep -q '/deis/controller/registrationEnabled'
12+
then
13+
if [[ "$($ETCDCTL get /deis/controller/registrationEnabled 2> /dev/null)" == "false" ]]
14+
then
15+
$ETCDCTL set /deis/controller/registrationMode "disabled"
16+
else
17+
$ETCDCTL set /deis/controller/registrationMode "enabled"
18+
fi
19+
20+
$ETCDCTL rm /deis/controller/registrationEnabled
21+
else
22+
echo "registrationEnabled key doesn't exist, skipping migration"
23+
fi
24+
25+
$ETCDCTL set /deis/migrations/data/0002 "done"
26+
fi

controller/templates/confd_settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
# move log directory out of /app/deis
3838
DEIS_LOG_DIR = '/data/logs'
3939

40-
{{ if exists "/deis/controller/registrationEnabled" }}
41-
REGISTRATION_ENABLED = bool({{ getv "/deis/controller/registrationEnabled" }})
40+
{{ if exists "/deis/controller/registrationMode" }}
41+
REGISTRATION_MODE = '{{ getv "/deis/controller/registrationMode" }}'
4242
{{ end }}
4343

4444
{{ if exists "/deis/controller/webEnabled" }}

docs/customizing_deis/controller_settings.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The following etcd keys are used by the controller component.
3939
==================================== ======================================================
4040
setting description
4141
==================================== ======================================================
42-
/deis/controller/registrationEnabled enable registration for new Deis users (default: true)
42+
/deis/controller/registrationMode set registration to "enabled", "disabled", or "admin_only" (default: "enabled")
4343
/deis/controller/webEnabled enable controller web UI (default: false)
4444
/deis/cache/host host of the cache component (set by cache)
4545
/deis/cache/port port of the cache component (set by cache)
@@ -77,9 +77,9 @@ Deis. Specifically, ensure that it sets and reads appropriate etcd keys.
7777

7878
Unit hostname
7979
-------------
80-
Per default, Docker automatically generates a hostname for your application unit, such as:
81-
``5c149b397cd6``. Auto generated hostnames is not always preferred. For instance,
82-
New Relic would classify each Docker container as an unique server since they use hostname
80+
Per default, Docker automatically generates a hostname for your application unit, such as:
81+
``5c149b397cd6``. Auto generated hostnames is not always preferred. For instance,
82+
New Relic would classify each Docker container as an unique server since they use hostname
8383
for grouping applications running on the same server together.
8484

8585
Deis supports configuring hostname assignment through the ``unitHostname`` setting.
@@ -98,12 +98,12 @@ application
9898
The hostname is assigned based on the unit name. Example: ``dancing-cat.v2.web.1``
9999

100100
server
101-
The hostname is assigned based on the CoreOS hostname. Example:
101+
The hostname is assigned based on the CoreOS hostname. Example:
102102
``ip-10-21-2-168.eu-west-1.compute.internal``
103103

104104
.. note::
105105

106-
Changes to ``/deis/controller/unitHostname`` requires either pushing a new build to
106+
Changes to ``/deis/controller/unitHostname`` requires either pushing a new build to
107107
every application or scaling them down and up.
108108
The change is only detected when a container unit is deployed.
109109

0 commit comments

Comments
 (0)