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