Revision c72a830d
b/settings.d/31-userdata.conf | ||
---|---|---|
3 | 3 |
|
4 | 4 |
# Generated SSH key exponent |
5 | 5 |
USERDATA_SSH_KEY_EXPONENT = 65537 |
6 |
|
|
7 |
# Maximum number of ssh keys a user is allowed to have |
|
8 |
MAX_SSH_KEYS_PER_USER = 2 |
b/ui/static/snf/css/main.css | ||
---|---|---|
6357 | 6357 |
padding:0 !important; |
6358 | 6358 |
} |
6359 | 6359 |
|
6360 |
.models-view .form-messages { |
|
6361 |
margin-bottom: 10px; |
|
6362 |
} |
|
6363 |
|
|
6360 | 6364 |
.models-view .list-messages { |
6361 | 6365 |
margin-top: 10px; |
6362 | 6366 |
} |
6363 | 6367 |
|
6368 |
.models-view .form-messages .error, |
|
6369 |
.models-view .form-messages .success, |
|
6364 | 6370 |
.models-view .list-messages .error, |
6365 | 6371 |
.models-view .list-messages .success, |
6366 | 6372 |
#user_public_keys .private-cont { |
... | ... | |
6372 | 6378 |
margin-top: 5px; |
6373 | 6379 |
} |
6374 | 6380 |
|
6381 |
.models-view .form-messages .error, |
|
6375 | 6382 |
.models-view .list-messages .error { |
6376 | 6383 |
background-color: #DE8D87; |
6377 | 6384 |
color: #782421 !important; |
6378 | 6385 |
border-color: #782421; |
6379 | 6386 |
} |
6380 | 6387 |
|
6388 |
#user_public_keys .limit-msg { |
|
6389 |
color: #800; |
|
6390 |
position: absolute; |
|
6391 |
right: 20px; |
|
6392 |
top: 25px; |
|
6393 |
} |
|
6394 |
|
|
6381 | 6395 |
#user_public_keys .private-cont { |
6382 | 6396 |
margin-top: 10px; |
6383 | 6397 |
} |
b/ui/static/snf/js/models.js | ||
---|---|---|
1764 | 1764 |
|
1765 | 1765 |
options.success = function () { return success(m) }; |
1766 | 1766 |
options.errror = error; |
1767 |
options.skip_api_error = true; |
|
1767 | 1768 |
|
1768 | 1769 |
this.create(m.attributes, options); |
1769 | 1770 |
} |
b/ui/static/snf/js/sync.js | ||
---|---|---|
226 | 226 |
|
227 | 227 |
// determine if we need to call our callback wrapper |
228 | 228 |
var call_api_handler = true; |
229 |
|
|
229 |
|
|
230 | 230 |
// request handles errors by itself, s |
231 | 231 |
if (handler_type == "error" && this.skip_api_error) { |
232 | 232 |
call_api_handler = false |
b/ui/static/snf/js/ui/web/ui_model_views.js | ||
---|---|---|
148 | 148 |
reset_form_errors: function() { |
149 | 149 |
this.form.find(".form-field").removeClass("error"); |
150 | 150 |
this.form.find(".form-field .errors").empty(); |
151 |
this.form.find(".form-messages").empty(); |
|
151 | 152 |
}, |
152 | 153 |
|
153 | 154 |
show_form_errors: function(errors) { |
... | ... | |
163 | 164 |
field.find(".errors").append(error_el); |
164 | 165 |
}); |
165 | 166 |
}, this)); |
166 |
//var el = $('<div class="error">{1}</div>'.format(type, msg)); |
|
167 |
//this.list_messages.append(el); |
|
167 |
|
|
168 |
var msg = errors['']; |
|
169 |
if (msg) { |
|
170 |
var el = $('<div class="error">{0}</div>'.format(msg)); |
|
171 |
this.$(".form-messages").append(el); |
|
172 |
} |
|
168 | 173 |
}, |
169 | 174 |
|
170 | 175 |
clean_form_errors: function() { |
176 |
|
|
171 | 177 |
}, |
172 | 178 |
|
173 | 179 |
submit_form: function() { |
... | ... | |
272 | 278 |
this.show_list_msg("success", this.create_success_msg || "Entry created"); |
273 | 279 |
}, this), |
274 | 280 |
|
275 |
error: _.bind(function(){ |
|
276 |
this.show_form_errors({'': this.create_failed_msg || 'Entry submition failed'}) |
|
281 |
error: _.bind(function(data, xhr){ |
|
282 |
var resp_error = ""; |
|
283 |
// try to parse response |
|
284 |
try { |
|
285 |
json_resp = JSON.parse(xhr.responseText); |
|
286 |
resp_error = json_resp.errors[json_resp.non_field_key].join("<br />"); |
|
287 |
} catch (err) {} |
|
288 |
|
|
289 |
var form_error = resp_error != "" ? |
|
290 |
this.create_failed_msg + " ({0})".format(resp_error) : |
|
291 |
this.create_failed_msg; |
|
292 |
|
|
293 |
this.show_form_errors({'': form_error || 'Entry submition failed'}) |
|
277 | 294 |
}, this), |
278 | 295 |
|
279 | 296 |
complete: _.bind(function(){ |
280 | 297 |
this.submiting = false; |
281 | 298 |
this.form.find("form-action.submit").addClass("in-progress"); |
282 |
}, this) |
|
299 |
}, this), |
|
300 |
|
|
301 |
skip_api_error: true |
|
283 | 302 |
} |
284 | 303 |
|
285 | 304 |
if (this.editing_id && this.collection.get(this.editing_id)) { |
b/ui/static/snf/js/ui/web/ui_public_keys_view.js | ||
---|---|---|
23 | 23 |
create_success_msg: 'Public key created successfully.', |
24 | 24 |
create_failed_msg: 'Failed to create public key.', |
25 | 25 |
|
26 |
|
|
26 | 27 |
initialize: function(options) { |
27 | 28 |
views.PublicKeysView.__super__.initialize.apply(this, arguments); |
28 | 29 |
this.$(".private-cont").hide(); |
29 | 30 |
_.bindAll(this); |
31 |
this.keys_limit = snf.config.userdata_keys_limit || 10000; |
|
30 | 32 |
}, |
31 | 33 |
|
32 | 34 |
append_actions: function(el, model) { |
... | ... | |
110 | 112 |
|
111 | 113 |
__generate_new: function(generate_text) { |
112 | 114 |
var self = this; |
113 |
var key = storage.keys.generate_new(_.bind(this.__save_new, this, generate_text), function(){ |
|
114 |
self.show_list_msg("error", "Cannot generate new key pair"); |
|
115 |
var key = storage.keys.generate_new(_.bind(this.__save_new, this, generate_text), function(xhr){ |
|
116 |
var resp_error = ""; |
|
117 |
// try to parse response |
|
118 |
try { |
|
119 |
json_resp = JSON.parse(xhr.responseText); |
|
120 |
resp_error = json_resp.errors[json_resp.non_field_key].join("<br />"); |
|
121 |
} catch (err) {} |
|
122 |
|
|
123 |
var msg = "Cannot generate new key pair"; |
|
124 |
if (resp_error) { |
|
125 |
msg += " ({0})".format(resp_error); |
|
126 |
} |
|
127 |
self.show_list_msg("error", msg); |
|
115 | 128 |
self.generating = false; |
116 | 129 |
self.download_private = false; |
117 | 130 |
self.$(".add-generate").text(generate_text).removeClass( |
... | ... | |
153 | 166 |
return el; |
154 | 167 |
}, |
155 | 168 |
|
169 |
update_list: function() { |
|
170 |
views.PublicKeysView.__super__.update_list.apply(this, arguments); |
|
171 |
this.check_limit(); |
|
172 |
}, |
|
173 |
|
|
174 |
check_limit: function() { |
|
175 |
if (snf.storage.keys.length >= this.keys_limit) { |
|
176 |
this.$(".collection-action").hide(); |
|
177 |
this.$(".limit-msg").show(); |
|
178 |
} else { |
|
179 |
this.$(".collection-action").show(); |
|
180 |
this.$(".limit-msg").hide(); |
|
181 |
} |
|
182 |
}, |
|
183 |
|
|
156 | 184 |
update_form_from_model: function(model) { |
157 | 185 |
this.form.find("input.input-name").val(model.get("name")); |
158 | 186 |
this.form.find("textarea.input-content").val(model.get("content")); |
b/ui/templates/home.html | ||
---|---|---|
554 | 554 |
synnefo.config.api_url = '/api/v1.1'; |
555 | 555 |
synnefo.config.logout_url = '{{ logout_redirect }}'; |
556 | 556 |
synnefo.config.userdata_keys_url = '{% url keys_collection %}'; |
557 |
synnefo.config.userdata_keys_limit = {{ userdata_keys_limit }}; |
|
557 | 558 |
|
558 | 559 |
synnefo.config.media_url = '{{ UI_MEDIA_URL }}'; |
559 | 560 |
synnefo.config.js_url = '{{ SYNNEFO_JS_URL }}'; |
b/ui/userdata/models.py | ||
---|---|---|
1 | 1 |
from django.db import models |
2 |
from django.conf import settings |
|
3 |
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS |
|
4 |
|
|
2 | 5 |
from synnefo.db import models as synnefo_models |
3 | 6 |
|
4 | 7 |
User = synnefo_models.SynnefoUser |
... | ... | |
25 | 28 |
|
26 | 29 |
class Meta: |
27 | 30 |
app_label = 'userdata' |
31 |
|
|
32 |
def clean(self): |
|
33 |
if PublicKeyPair.user_limit_exceeded(self.user): |
|
34 |
raise ValidationError("SSH keys limit exceeded.") |
|
35 |
|
|
36 |
@classmethod |
|
37 |
def user_limit_exceeded(cls, user): |
|
38 |
return PublicKeyPair.objects.filter(user=user).count() >= settings.MAX_SSH_KEYS_PER_USER |
b/ui/userdata/rest.py | ||
---|---|---|
4 | 4 |
from django.core import serializers |
5 | 5 |
from django.core.urlresolvers import reverse |
6 | 6 |
|
7 |
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS |
|
8 |
|
|
7 | 9 |
# base view class |
8 | 10 |
# https://github.com/bfirsh/django-class-based-views/blob/master/class_based_views/base.py |
9 | 11 |
class View(object): |
... | ... | |
62 | 64 |
raise http.HttpResponseServerError('Invalid JSON data.') |
63 | 65 |
else: |
64 | 66 |
raise http.HttpResponseServerError('Unsupported Content-Type.') |
67 |
try: |
|
68 |
return getattr(self, request.method.upper())(request, data, *args, **kwargs) |
|
69 |
except ValidationError, e: |
|
70 |
# specific response for validation errors |
|
71 |
return http.HttpResponseServerError(json.dumps({'errors': |
|
72 |
e.message_dict, 'non_field_key': |
|
73 |
NON_FIELD_ERRORS })) |
|
65 | 74 |
|
66 |
return getattr(self, request.method.upper())(request, data, *args, **kwargs) |
|
67 | 75 |
else: |
68 | 76 |
allowed_methods = [m for m in self.method_names if hasattr(self, m)] |
69 | 77 |
return http.HttpResponseNotAllowed(allowed_methods) |
... | ... | |
137 | 145 |
def PUT(self, request, data, *args, **kwargs): |
138 | 146 |
instance = self.instance() |
139 | 147 |
self.update_instance(instance, data, self.exclude_fields) |
148 |
instance.full_clean() |
|
140 | 149 |
instance.save() |
141 | 150 |
return self.GET(request, data, *args, **kwargs) |
142 | 151 |
|
... | ... | |
161 | 170 |
def POST(self, request, data, *args, **kwargs): |
162 | 171 |
instance = self.model() |
163 | 172 |
self.update_instance(instance, data, self.exclude_fields) |
173 |
instance.full_clean() |
|
164 | 174 |
instance.save() |
165 | 175 |
return self.json_response(self.instance_to_dict(instance, |
166 | 176 |
self.exclude_fields)) |
... | ... | |
184 | 194 |
instance = self.model() |
185 | 195 |
self.update_instance(instance, data, self.exclude_fields) |
186 | 196 |
instance.user = request.user |
197 |
instance.full_clean() |
|
187 | 198 |
instance.save() |
188 | 199 |
return self.json_response(self.instance_to_dict(instance, |
189 | 200 |
self.exclude_fields)) |
b/ui/userdata/templates/userdata/public_keys_view.html | ||
---|---|---|
11 | 11 |
<div class="collection-action add add-new">{% trans "create/import new" %}</div> |
12 | 12 |
<div class="collection-action generate add-generate">{% trans "generate new" %}</div> |
13 | 13 |
</div> |
14 |
<div class="limit-msg">{% trans "SSH keys limit reached." %}</div> |
|
14 | 15 |
<div class="model-description"> |
15 | 16 |
<p>You can use SSH keys to establish a secure connection |
16 | 17 |
between your computer and the virtual machines. </p> |
b/ui/userdata/tests.py | ||
---|---|---|
10 | 10 |
from django.test.client import Client |
11 | 11 |
from django.core.urlresolvers import clear_url_caches |
12 | 12 |
from django.utils import simplejson as json |
13 |
from django.conf import settings |
|
13 | 14 |
|
14 | 15 |
from synnefo.ui.userdata.models import User |
15 | 16 |
from synnefo.ui.userdata.models import * |
... | ... | |
101 | 102 |
# private key is base64 encoded |
102 | 103 |
base64.b64decode(private) |
103 | 104 |
|
105 |
def test_invalid_data(self): |
|
106 |
resp = self.client.post("/keys", json.dumps({'content':"""key 2 content"""}), |
|
107 |
content_type='application/json') |
|
108 |
|
|
109 |
self.assertEqual(resp.status_code, 500) |
|
110 |
self.assertEqual(resp.content, """{"non_field_key": "__all__", "errors": """ |
|
111 |
"""{"name": ["This field cannot be blank."]}}""") |
|
112 |
|
|
113 |
settings.MAX_SSH_KEYS_PER_USER = 2 |
|
114 |
|
|
115 |
# test ssh limit |
|
116 |
resp = self.client.post("/keys", json.dumps({'name':'key1', 'content':"""key 1 content"""}), |
|
117 |
content_type='application/json') |
|
118 |
resp = self.client.post("/keys", json.dumps({'name':'key1', 'content':"""key 1 content"""}), |
|
119 |
content_type='application/json') |
|
120 |
resp = self.client.post("/keys", json.dumps({'name':'key1', 'content':"""key 1 content"""}), |
|
121 |
content_type='application/json') |
|
122 |
self.assertEqual(resp.status_code, 500) |
|
123 |
self.assertEqual(resp.content, """{"non_field_key": "__all__", "errors": """ |
|
124 |
"""{"__all__": ["SSH keys limit exceeded."]}}""") |
b/ui/userdata/views.py | ||
---|---|---|
32 | 32 |
if not SUPPORT_GENERATE_KEYS: |
33 | 33 |
raise Exception("Application does not support ssh keys generation") |
34 | 34 |
|
35 |
if PublicKeyPair.user_limit_exceeded(request.user): |
|
36 |
raise http.HttpResponseServerError("SSH keys limit exceeded"); |
|
37 |
|
|
38 |
|
|
35 | 39 |
# generate RSA key |
36 | 40 |
key = M2C.RSA.gen_key(SSH_KEY_LENGTH, SSH_KEY_EXPONENT, lambda x: ""); |
37 | 41 |
|
b/ui/views.py | ||
---|---|---|
91 | 91 |
|
92 | 92 |
VM_NAME_TEMPLATE = getattr(settings, "VM_CREATE_NAME_TPL", "My {0} server") |
93 | 93 |
|
94 |
# ssh keys |
|
95 |
MAX_SSH_KEYS_PER_USER = getattr(settings, "MAX_SSH_KEYS_PER_USER") |
|
96 |
|
|
94 | 97 |
def template(name, context): |
95 | 98 |
template_path = os.path.join(os.path.dirname(__file__), "templates/") |
96 | 99 |
current_template = template_path + name + '.html' |
... | ... | |
129 | 132 |
'vm_name_template': json.dumps(VM_NAME_TEMPLATE) |
130 | 133 |
'support_ssh_os_list': json.dumps(SUPPORT_SSH_OS_LIST), |
131 | 134 |
'os_created_users': json.dumps(OS_CREATED_USERS), |
135 |
'userdata_keys_limit': json.dumps(MAX_SSH_KEYS_PER_USER), |
|
132 | 136 |
} |
133 | 137 |
return template('home', context) |
134 | 138 |
|
Also available in: Unified diff