Revision b47b110d

b/snf-cyclades-app/synnefo/api/management/commands/cyclades-astakos-migrate-013.py
44 44
from synnefo.api.util import get_existing_users
45 45
from synnefo.lib.utils import case_unique
46 46
from synnefo.db.models import Network, VirtualMachine
47
from synnefo.ui.userdata.models import PublicKeyPair
47
from synnefo.userdata.models import PublicKeyPair
48 48

  
49 49
from snf_django.lib import astakos
50 50

  
b/snf-cyclades-app/synnefo/api/util.py
442 442
    Retrieve user ids stored in cyclades user agnostic models.
443 443
    """
444 444
    # also check PublicKeys a user with no servers/networks exist
445
    from synnefo.ui.userdata.models import PublicKeyPair
445
    from synnefo.userdata.models import PublicKeyPair
446 446
    from synnefo.db.models import VirtualMachine, Network
447 447

  
448 448
    keypairusernames = PublicKeyPair.objects.filter().values_list('user',
b/snf-cyclades-app/synnefo/app_settings/__init__.py
6 6
    'synnefo.plankton',
7 7
    'synnefo.vmapi',
8 8
    'synnefo.helpdesk',
9
    'synnefo.ui.userdata',
9
    'synnefo.userdata',
10 10
    'synnefo.helpdesk',
11 11
    'synnefo.quotas',
12 12
]
b/snf-cyclades-app/synnefo/app_settings/urls.py
38 38
from synnefo.cyclades_settings import (
39 39
    BASE_URL, BASE_HOST, BASE_PATH, COMPUTE_PREFIX, VMAPI_PREFIX,
40 40
    PLANKTON_PREFIX, HELPDESK_PREFIX, UI_PREFIX, ASTAKOS_BASE_URL,
41
    ASTAKOS_BASE_PATH, BASE_ASTAKOS_PROXY_PATH, ASTAKOS_ACCOUNTS_PREFIX,
42
    ASTAKOS_VIEWS_PREFIX, PROXY_USER_SERVICES)
41
    USERDATA_PREFIX, ASTAKOS_BASE_PATH, BASE_ASTAKOS_PROXY_PATH,
42
    ASTAKOS_ACCOUNTS_PREFIX, ASTAKOS_VIEWS_PREFIX, PROXY_USER_SERVICES)
43 43

  
44 44
from urlparse import urlparse
45 45
from functools import partial
......
54 54
    (prefix_pattern(PLANKTON_PREFIX), include('synnefo.plankton.urls')),
55 55
    (prefix_pattern(HELPDESK_PREFIX), include('synnefo.helpdesk.urls')),
56 56
    (prefix_pattern(COMPUTE_PREFIX), include('synnefo.api.urls')),
57
    (prefix_pattern(USERDATA_PREFIX), include('synnefo.userdata.urls')),
57 58
)
58 59

  
59 60
urlpatterns = patterns(
b/snf-cyclades-app/synnefo/cyclades_settings.py
47 47
PLANKTON_PREFIX = getattr(settings, 'CYCLADES_PLANKTON_PREFIX', 'plankton')
48 48
HELPDESK_PREFIX = getattr(settings, 'CYCLADES_HELPDESK_PREFIX', 'helpdesk')
49 49
UI_PREFIX = getattr(settings, 'CYCLADES_UI_PREFIX', 'ui')
50
USERDATA_PREFIX = getattr(settings, 'CYCLADES_USERDATA_PREFIX', 'userdata')
50 51

  
51 52
# The API implementation needs to accept and return absolute references
52 53
# to its resources. Thus, it needs to know its public URL.
b/snf-cyclades-app/synnefo/ui/urls.py
37 37

  
38 38
urlpatterns = patterns('',
39 39
    url(r'^$', 'synnefo.ui.views.home', name='ui_index'),
40
    url(r'userdata/', include('synnefo.ui.userdata.urls')),
41 40
    url(r'^machines/console$', 'synnefo.ui.views.machines_console',
42 41
        name='ui_machines_console'),
43 42
    url(r'^machines/connect$', 'synnefo.ui.views.machines_connect',
/dev/null
1
# ===================================================================
2
# The contents of this file are dedicated to the public domain.  To
3
# the extent that dedication to the public domain is not available,
4
# everyone is granted a worldwide, perpetual, royalty-free,
5
# non-exclusive license to exercise all rights associated with the
6
# contents of this file for any purpose whatsoever.
7
# No rights are reserved.
8
#
9
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
10
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
11
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
12
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
13
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
14
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
15
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
16
# SOFTWARE.
17
# ===================================================================
18

  
19
from Crypto.Util.number import long_to_bytes, bytes_to_long
20

  
21
__all__ = [ 'DerObject', 'DerInteger', 'DerSequence' ]
22

  
23
class DerObject:
24
	typeTags = { 'SEQUENCE':'\x30', 'BIT STRING':'\x03', 'INTEGER':'\x02' }
25

  
26
	def __init__(self, ASN1Type=None):
27
		self.typeTag = self.typeTags.get(ASN1Type, ASN1Type)
28
		self.payload = ''
29

  
30
	def _lengthOctets(self, payloadLen):
31
		'''
32
		Return an octet string that is suitable for the BER/DER
33
		length element if the relevant payload is of the given
34
		size (in bytes).
35
		'''
36
		if payloadLen>127:
37
			encoding = long_to_bytes(payloadLen)
38
			return chr(len(encoding)+128) + encoding
39
		return chr(payloadLen)
40

  
41
	def encode(self):
42
		return self.typeTag + self._lengthOctets(len(self.payload)) + self.payload
43

  
44
	def _decodeLen(self, idx, str):
45
		'''
46
		Given a string and an index to a DER LV,
47
		this function returns a tuple with the length of V
48
		and an index to the first byte of it.
49
		'''
50
		length = ord(str[idx])
51
		if length<=127:
52
			return (length,idx+1)
53
		else:
54
			payloadLength = bytes_to_long(str[idx+1:idx+1+(length & 0x7F)])
55
			if payloadLength<=127:
56
				raise ValueError("Not a DER length tag.")
57
			return (payloadLength, idx+1+(length & 0x7F))
58

  
59
	def decode(self, input, noLeftOvers=0):
60
		try:
61
			self.typeTag = input[0]
62
			if (ord(self.typeTag) & 0x1F)==0x1F:
63
				raise ValueError("Unsupported DER tag")
64
			(length,idx) = self._decodeLen(1,input)
65
			if noLeftOvers and len(input) != (idx+length):
66
				raise ValueError("Not a DER structure")
67
			self.payload = input[idx:idx+length]
68
		except IndexError:
69
			raise ValueError("Not a valid DER SEQUENCE.")
70
		return idx+length
71

  
72
class DerInteger(DerObject):
73
	def __init__(self, value = 0):
74
		DerObject.__init__(self, 'INTEGER')
75
		self.value = value
76

  
77
	def encode(self):
78
		self.payload = long_to_bytes(self.value)
79
		if ord(self.payload[0])>127:
80
			self.payload = '\x00' + self.payload
81
		return DerObject.encode(self)
82

  
83
	def decode(self, input, noLeftOvers=0):
84
		tlvLength = DerObject.decode(self, input,noLeftOvers)
85
		if ord(self.payload[0])>127:
86
			raise ValueError ("Negative INTEGER.")
87
		self.value = bytes_to_long(self.payload)
88
		return tlvLength
89

  
90
class DerSequence(DerObject):
91
	def __init__(self):
92
		DerObject.__init__(self, 'SEQUENCE')
93
		self._seq = []
94
	def __delitem__(self, n):
95
		del self._seq[n]
96
	def __getitem__(self, n):
97
		return self._seq[n]
98
	def __setitem__(self, key, value):
99
		self._seq[key] = value
100
	def __setslice__(self,i,j,sequence):
101
		self._seq[i:j] = sequence
102
	def __delslice__(self,i,j):
103
		del self._seq[i:j]
104
	def __getslice__(self, i, j):
105
		return self._seq[max(0, i):max(0, j)]
106
	def __len__(self):
107
		return len(self._seq)
108
	def append(self, item):
109
		return self._seq.append(item)
110

  
111
	def hasOnlyInts(self):
112
		if not self._seq: return 0
113
		test = 0
114
		for item in self._seq:
115
			try:
116
				test += item
117
			except TypeError:
118
				return 0
119
		return 1
120

  
121
	def encode(self):
122
		'''
123
		Return the DER encoding for the ASN.1 SEQUENCE containing
124
		the non-negative integers and longs added to this object.
125
		'''
126
		self.payload = ''
127
		for item in self._seq:
128
			try:
129
				self.payload += item
130
			except:
131
				try:
132
					self.payload += DerInteger(item).encode()
133
				except:
134
					raise ValueError("Trying to DER encode an unknown object")
135
		return DerObject.encode(self)
136

  
137
	def decode(self, input,noLeftOvers=0):
138
		'''
139
		This function decodes the given string into a sequence of
140
		ASN.1 objects. Yet, we only know about unsigned INTEGERs.
141
		Any other type is stored as its rough TLV. In the latter
142
		case, the correctectness of the TLV is not checked.
143
		'''
144
		self._seq = []
145
		try:
146
			tlvLength = DerObject.decode(self, input,noLeftOvers)
147
			if self.typeTag!=self.typeTags['SEQUENCE']:
148
				raise ValueError("Not a DER SEQUENCE.")
149
			# Scan one TLV at once
150
			idx = 0
151
			while idx<len(self.payload):
152
				typeTag = self.payload[idx]
153
				if typeTag==self.typeTags['INTEGER']:
154
					newInteger = DerInteger()
155
					idx += newInteger.decode(self.payload[idx:])
156
					self._seq.append(newInteger.value)
157
				else:
158
					itemLen,itemIdx = self._decodeLen(idx+1,self.payload)
159
					self._seq.append(self.payload[idx:itemIdx+itemLen])
160
					idx = itemIdx + itemLen
161
		except IndexError:
162
			raise ValueError("Not a valid DER SEQUENCE.")
163
		return tlvLength
164

  
/dev/null
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import SchemaMigration
5
from django.db import models
6

  
7
class Migration(SchemaMigration):
8

  
9
    depends_on = (
10
        ("db", "0025_auto__del_field_virtualmachine_sourceimage"),
11
    )
12

  
13
    def forwards(self, orm):
14

  
15
        # Adding model 'PublicKeyPair'
16
        db.create_table('userdata_publickeypair', (
17
            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
18
            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['db.SynnefoUser'])),
19
            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
20
            ('content', self.gf('django.db.models.fields.TextField')()),
21
        ))
22
        db.send_create_signal('userdata', ['PublicKeyPair'])
23

  
24

  
25
    def backwards(self, orm):
26

  
27
        # Deleting model 'PublicKeyPair'
28
        db.delete_table('userdata_publickeypair')
29

  
30

  
31
    models = {
32
        'db.synnefouser': {
33
            'Meta': {'object_name': 'SynnefoUser'},
34
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
35
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
36
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
37
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
38
            'credit': ('django.db.models.fields.IntegerField', [], {}),
39
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
40
            'max_invitations': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
41
            'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
42
            'realname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
43
            'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '30'}),
44
            'tmp_auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
45
            'tmp_auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
46
            'type': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
47
            'uniq': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
48
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
49
        },
50
        'userdata.publickeypair': {
51
            'Meta': {'object_name': 'PublicKeyPair'},
52
            'content': ('django.db.models.fields.TextField', [], {}),
53
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
54
            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
55
            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']"})
56
        }
57
    }
58

  
59
    complete_apps = ['userdata']
/dev/null
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import SchemaMigration
5
from django.db import models
6

  
7
class Migration(SchemaMigration):
8

  
9
    def forwards(self, orm):
10
        
11
        # Adding field 'PublicKeyPair.fingerprint'
12
        db.add_column('userdata_publickeypair', 'fingerprint', self.gf('django.db.models.fields.CharField')(default='', max_length=100), keep_default=False)
13

  
14

  
15
    def backwards(self, orm):
16
        
17
        # Deleting field 'PublicKeyPair.fingerprint'
18
        db.delete_column('userdata_publickeypair', 'fingerprint')
19

  
20

  
21
    models = {
22
        'db.synnefouser': {
23
            'Meta': {'object_name': 'SynnefoUser'},
24
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
25
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
26
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
27
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
28
            'credit': ('django.db.models.fields.IntegerField', [], {}),
29
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30
            'max_invitations': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
31
            'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
32
            'realname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
33
            'state': ('django.db.models.fields.CharField', [], {'default': "'ACTIVE'", 'max_length': '30'}),
34
            'tmp_auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
35
            'tmp_auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
36
            'type': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
37
            'uniq': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
38
            'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
39
        },
40
        'userdata.publickeypair': {
41
            'Meta': {'object_name': 'PublicKeyPair'},
42
            'content': ('django.db.models.fields.TextField', [], {}),
43
            'fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
44
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
45
            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
46
            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.SynnefoUser']"})
47
        }
48
    }
49

  
50
    complete_apps = ['userdata']
/dev/null
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import SchemaMigration
5
from django.db import models
6

  
7
class Migration(SchemaMigration):
8

  
9
    needed_by = (
10
        ("db", "0027_auto__del_legacy_fields"),
11
    )
12

  
13
    def forwards(self, orm):
14

  
15
        # Changing field 'PublicKeyPair.fingerprint'
16
        db.alter_column('userdata_publickeypair', 'fingerprint', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True))
17

  
18
        try:
19
            db.drop_foreign_key('userdata_publickeypair', 'user_id')
20
        except:
21
            pass
22
        # Renaming column for 'PublicKeyPair.user' to match new field type.
23
        db.rename_column('userdata_publickeypair', 'user_id', 'user')
24
        # Changing field 'PublicKeyPair.user'
25
        db.alter_column('userdata_publickeypair', 'user', self.gf('django.db.models.fields.CharField')(max_length=100))
26

  
27
        try:
28
            # Removing index on 'PublicKeyPair', fields ['user']
29
            db.delete_index('userdata_publickeypair', ['user_id'])
30
        except:
31
            pass
32

  
33

  
34
    def backwards(self, orm):
35

  
36
        # Changing field 'PublicKeyPair.fingerprint'
37
        db.alter_column('userdata_publickeypair', 'fingerprint', self.gf('django.db.models.fields.CharField')(max_length=100))
38

  
39
        # Renaming column for 'PublicKeyPair.user' to match new field type.
40
        db.rename_column('userdata_publickeypair', 'user', 'user_id')
41
        # Changing field 'PublicKeyPair.user'
42
        db.alter_column('userdata_publickeypair', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['db.SynnefoUser']))
43

  
44
        # Adding index on 'PublicKeyPair', fields ['user']
45
        db.create_index('userdata_publickeypair', ['user_id'])
46

  
47

  
48
    models = {
49
        'userdata.publickeypair': {
50
            'Meta': {'object_name': 'PublicKeyPair'},
51
            'content': ('django.db.models.fields.TextField', [], {}),
52
            'fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
53
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
54
            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
55
            'user': ('django.db.models.fields.CharField', [], {'max_length': '100'})
56
        }
57
    }
58

  
59
    complete_apps = ['userdata']
/dev/null
1
#
2
# Copyright 2011 GRNET S.A. All rights reserved.
3
#
4
# Redistribution and use in source and binary forms, with or
5
# without modification, are permitted provided that the following
6
# conditions are met:
7
#
8
#   1. Redistributions of source code must retain the above
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
11
#
12
#   2. Redistributions in binary form must reproduce the above
13
#      copyright notice, this list of conditions and the following
14
#      disclaimer in the documentation and/or other materials
15
#      provided with the distribution.
16
#
17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
29
#
30
# The views and conclusions contained in the software and
31
# documentation are those of the authors and should not be
32
# interpreted as representing official policies, either expressed
33
# or implied, of GRNET S.A.
34

  
35
import base64
36
import binascii
37
import re
38

  
39
from hashlib import md5
40

  
41
from django.db import models
42
from django.conf import settings
43
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
44
from django.db.models.signals import pre_save
45

  
46
try:
47
    from paramiko import rsakey, dsskey, SSHException
48
except:
49
    pass
50

  
51

  
52
class ProfileModel(models.Model):
53
    """
54
    Abstract model, provides a basic interface for models that store
55
    user specific information
56
    """
57

  
58
    user = models.CharField(max_length=100)
59

  
60
    class Meta:
61
        abstract = True
62
        app_label = 'userdata'
63

  
64

  
65
class PublicKeyPair(ProfileModel):
66
    """
67
    Public key model
68
    """
69
    name = models.CharField(max_length=255, null=False, blank=False)
70
    content = models.TextField()
71
    fingerprint = models.CharField(max_length=100, null=False, blank=True)
72

  
73
    class Meta:
74
        app_label = 'userdata'
75

  
76
    def full_clean(self, *args, **kwargs):
77
        # update fingerprint before clean
78
        self.update_fingerprint()
79
        super(PublicKeyPair, self).full_clean(*args, **kwargs)
80

  
81
    def key_data(self):
82
        return self.content.split(" ", 1)
83

  
84
    def get_key_object(self):
85
        """
86
        Identify key contents and return appropriate paramiko public key object
87
        """
88
        key_type, data = self.key_data()
89
        data = base64.b64decode(data)
90

  
91
        if key_type == "ssh-rsa":
92
            key = rsakey.RSAKey(data=data)
93
        elif key_type == "ssh-dss":
94
            key = dsskey.DSSKey(data=data)
95
        else:
96
            raise Exception("Invalid key type")
97

  
98
        return key
99

  
100
    def clean_key(self):
101
        key = None
102
        try:
103
            key = self.get_key_object()
104
        except:
105
            raise ValidationError("Invalid SSH key")
106

  
107
    def clean(self):
108
        if PublicKeyPair.user_limit_exceeded(self.user):
109
            raise ValidationError("SSH keys limit exceeded.")
110

  
111
    def update_fingerprint(self):
112
        try:
113
            fp = binascii.hexlify(self.get_key_object().get_fingerprint())
114
            self.fingerprint = ":".join(re.findall(r"..", fp))
115
        except:
116
            self.fingerprint = "unknown fingerprint"
117

  
118
    def save(self, *args, **kwargs):
119
        self.update_fingerprint()
120
        super(PublicKeyPair, self).save(*args, **kwargs)
121

  
122
    @classmethod
123
    def user_limit_exceeded(cls, user):
124
        return (PublicKeyPair.objects.filter(user=user).count() >=
125
                settings.USERDATA_MAX_SSH_KEYS_PER_USER)
/dev/null
1
#
2
# Copyright 2011 GRNET S.A. All rights reserved.
3
#
4
# Redistribution and use in source and binary forms, with or
5
# without modification, are permitted provided that the following
6
# conditions are met:
7
#
8
#   1. Redistributions of source code must retain the above
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
11
#
12
#   2. Redistributions in binary form must reproduce the above
13
#      copyright notice, this list of conditions and the following
14
#      disclaimer in the documentation and/or other materials
15
#      provided with the distribution.
16
#
17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
29
#
30
# The views and conclusions contained in the software and
31
# documentation are those of the authors and should not be
32
# interpreted as representing official policies, either expressed
33
# or implied, of GRNET S.A.
34

  
35
from django import http
36
from django.utils import simplejson as json
37
from django.core.urlresolvers import reverse
38
from django.http import HttpResponse
39

  
40
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
41

  
42
from snf_django.lib.astakos import get_user
43
from django.conf import settings
44

  
45

  
46
class View(object):
47
    """
48
    Intentionally simple parent class for all views. Only implements
49
    dispatch-by-method and simple sanity checking.
50
    """
51

  
52
    method_names = ['GET', 'POST', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE']
53

  
54
    def __init__(self, *args, **kwargs):
55
        """
56
        Constructor. Called in the URLconf; can contain helpful extra
57
        keyword arguments, and other things.
58
        """
59
        # Go through keyword arguments, and either save their values to our
60
        # instance, or raise an error.
61
        for key, value in kwargs.items():
62
            if key in self.method_names:
63
                raise TypeError(u"You tried to pass in the %s method name as a"
64
                                u" keyword argument to %s(). Don't do that."
65
                                % (key, self.__class__.__name__))
66
            if hasattr(self, key):
67
                setattr(self, key, value)
68
            else:
69
                raise TypeError(u"%s() received an invalid keyword %r" % (
70
                    self.__class__.__name__,
71
                    key,
72
                ))
73

  
74
    @classmethod
75
    def as_view(cls, *initargs, **initkwargs):
76
        """
77
        Main entry point for a request-response process.
78
        """
79
        def view(request, *args, **kwargs):
80
            get_user(request, settings.ASTAKOS_BASE_URL)
81
            if not request.user_uniq:
82
                return HttpResponse(status=401)
83
            self = cls(*initargs, **initkwargs)
84
            return self.dispatch(request, *args, **kwargs)
85
        return view
86

  
87
    def dispatch(self, request, *args, **kwargs):
88
        # Try to dispatch to the right method for that; if it doesn't exist,
89
        # raise a big error.
90
        if hasattr(self, request.method.upper()):
91
            self.request = request
92
            self.args = args
93
            self.kwargs = kwargs
94
            data = request.raw_post_data
95

  
96
            if request.method.upper() in ['POST', 'PUT']:
97
                # Expect json data
98
                if request.META.get('CONTENT_TYPE').startswith(
99
                        'application/json'):
100
                    try:
101
                        data = json.loads(data)
102
                    except ValueError:
103
                        return \
104
                            http.HttpResponseServerError('Invalid JSON data.')
105
                else:
106
                    return http.HttpResponseServerError(
107
                        'Unsupported Content-Type.')
108
            try:
109
                return getattr(self, request.method.upper())(
110
                    request, data, *args, **kwargs)
111
            except ValidationError, e:
112
                # specific response for validation errors
113
                return http.HttpResponseServerError(
114
                    json.dumps({'errors': e.message_dict,
115
                                'non_field_key': NON_FIELD_ERRORS}))
116

  
117
        else:
118
            allowed_methods = [m for m in self.method_names if hasattr(self, m)]
119
            return http.HttpResponseNotAllowed(allowed_methods)
120

  
121

  
122
class JSONRestView(View):
123
    """
124
    Class that provides helpers to produce a json response
125
    """
126

  
127
    url_name = None
128

  
129
    def __init__(self, url_name, *args, **kwargs):
130
        self.url_name = url_name
131
        return super(JSONRestView, self).__init__(*args, **kwargs)
132

  
133
    def update_instance(self, i, data, exclude_fields=[]):
134
        update_keys = data.keys()
135
        for field in i._meta.get_all_field_names():
136
            if field in update_keys and (field not in exclude_fields):
137
                i.__setattr__(field, data[field])
138

  
139
        return i
140

  
141
    def instance_to_dict(self, i, exclude_fields=[]):
142
        """
143
        Convert model instance to python dict
144
        """
145
        d = {}
146
        d['uri'] = reverse(self.url_name, kwargs={'id': i.pk})
147

  
148
        for field in i._meta.get_all_field_names():
149
            if field in exclude_fields:
150
                continue
151

  
152
            d[field] = i.__getattribute__(field)
153
        return d
154

  
155
    def qs_to_dict_iter(self, qs, exclude_fields=[]):
156
        """
157
        Convert queryset to an iterator of model instances dicts
158
        """
159
        for i in qs:
160
            yield self.instance_to_dict(i, exclude_fields)
161

  
162
    def json_response(self, data):
163
        return http.HttpResponse(json.dumps(data), mimetype="application/json")
164

  
165

  
166
class ResourceView(JSONRestView):
167
    method_names = ['GET', 'POST', 'PUT', 'DELETE']
168

  
169
    model = None
170
    exclude_fields = []
171

  
172
    def queryset(self):
173
        return self.model.objects.all()
174

  
175
    def instance(self):
176
        """
177
        Retrieve selected instance based on url parameter
178

  
179
        id parameter should be set in urlpatterns expression
180
        """
181
        try:
182
            return self.queryset().get(pk=self.kwargs.get("id"))
183
        except self.model.DoesNotExist:
184
            raise http.Http404
185

  
186
    def GET(self, request, data, *args, **kwargs):
187
        return self.json_response(
188
            self.instance_to_dict(self.instance(), self.exclude_fields))
189

  
190
    def PUT(self, request, data, *args, **kwargs):
191
        instance = self.instance()
192
        self.update_instance(instance, data, self.exclude_fields)
193
        instance.full_clean()
194
        instance.save()
195
        return self.GET(request, data, *args, **kwargs)
196

  
197
    def DELETE(self, request, data, *args, **kwargs):
198
        self.instance().delete()
199
        return self.json_response("")
200

  
201

  
202
class CollectionView(JSONRestView):
203
    method_names = ['GET', 'POST']
204

  
205
    model = None
206
    exclude_fields = []
207

  
208
    def queryset(self):
209
        return self.model.objects.all()
210

  
211
    def GET(self, request, data, *args, **kwargs):
212
        return self.json_response(
213
            list(self.qs_to_dict_iter(self.queryset(), self.exclude_fields)))
214

  
215
    def POST(self, request, data, *args, **kwargs):
216
        instance = self.model()
217
        self.update_instance(instance, data, self.exclude_fields)
218
        instance.full_clean()
219
        instance.save()
220
        return self.json_response(
221
            self.instance_to_dict(instance, self.exclude_fields))
222

  
223

  
224
class UserResourceView(ResourceView):
225
    """
226
    Filter resource queryset for request user entries
227
    """
228
    def queryset(self):
229
        return super(UserResourceView,
230
                     self).queryset().filter(user=self.request.user_uniq)
231

  
232

  
233
class UserCollectionView(CollectionView):
234
    """
235
    Filter collection queryset for request user entries
236
    """
237
    def queryset(self):
238
        return super(UserCollectionView,
239
                     self).queryset().filter(user=self.request.user_uniq)
240

  
241
    def POST(self, request, data, *args, **kwargs):
242
        instance = self.model()
243
        self.update_instance(instance, data, self.exclude_fields)
244
        instance.user = request.user_uniq
245
        instance.full_clean()
246
        instance.save()
247
        return self.json_response(
248
            self.instance_to_dict(instance, self.exclude_fields))
/dev/null
1
{% load i18n %}
2
<div class="public-keys-view clearfix">
3
  <div class="loading-models">{% trans "Loading..." %}</div>
4
  <div class="models-view">
5
    <div class="previous-view-link">
6
      <div class="change-view-action">Back to machine create wizard
7
      </div>
8
    </div>
9

  
10
    <div class="list-wrapper model-list">
11
      <h3 class="list-title hidden">{% trans "SSH public keys list" %}</h3>
12
      <div class="top-actions clearfix">
13
        <div class="collection-action add add-new">{% trans "create/import new" %}</div>
14
        <div class="collection-action generate add-generate">{% trans "generate new" %}</div>
15
      </div>
16
      <div class="limit-msg">{% trans "SSH keys limit reached." %}</div>
17
      <div class="model-description">
18
        <p>You can use SSH keys to establish a secure connection 
19
        between your computer and the virtual machines. </p>
20
      </div>
21
      <div class="list-messages">
22
      </div>
23
      <div class="private-cont">
24
        <div class="private-download clearfix">
25
          <div class="close-private">{% trans "close" %}</div>
26
          <div class="private-msg download">
27
          </div>
28
          <div class="private-msg copy">
29
            {% trans "Create a file with the following contents" %}
30
          </div>
31
          <form target="_blank" id="private-key-form" method="post">
32
            <input type="hidden" name="data" />
33
            <input type="hidden" name="name" />
34

  
35
            <span class="form-text">{% trans "Your new public key has been added" %}</span>
36
            <input type="submit" class="down-button form-text"
37
            id="download-private-key" value="click here" />
38
            <span class="form-text">
39
              {% trans " to download private key." %}
40
            </span>
41
          </form>
42
          <div class="key-contents clearfix">
43
            <textarea></textarea>
44
          </div>
45
        </div>
46
      </div>
47
      <div class="hidden public-key-item clearfix" id="model-item-tpl">
48
        <div class="param key-type"></div>
49
        <div class="param name"></div>
50
        <div class="param fingerprint">
51
          <div class="flabel">fingerprint:</div>
52
          <div class="text"></div>
53
        </div>
54
        <div class="param publicid hidden">
55
          <div class="param-content">
56
            <textarea></textarea>
57
          </div>
58
        </div>
59
      </div>
60
      <ul class="keys-list">
61
        <li class="header">
62
        <div class="title">{% trans "Name" %}</div>
63
        <div class="title">{% trans "Public key ID" %}</div>
64
        </li>
65
      </ul>
66
      <ul class="items-list">
67
        <li class="items-empty-msg hidden msg">
68
        <div class="title">
69
          {% trans "No public keys exist" %} <span class="quick-add">add one</span> now
70
        </div>
71
        </li>
72
      </ul>
73
    </div>
74
    <div class="form-wrapper model-form-cont clearfix">
75
      <h3 class="new-title hidden">{% trans "Create new SSH public key" %}</h3>
76
      <h3 class="edit-title hidden">{% trans "Edit SSH public key" %}</h3>
77
      <form class="model-form">
78
        <div class="form-messages">
79
        </div>
80
        <div class="model-form-fields">
81
          <div class="form-field">
82
            <label for="">{% trans "Key name" %}</label>
83
            <div class="errors"></div>
84
            <input type="text" class="input-name inline"/>
85
          </div>
86
          <div class="form-field clearfix">
87
            <label for="">{% trans "Key content" %}</label>
88
            <div class="errors"></div>
89
            <textarea class="input-content"></textarea>
90
          </div>
91
          <div class="form-field inline clearfix fromfile-field">
92
            <label for="">{% trans "Import from file" %}</label>
93
            <div class="fromfile">
94
              <input type="file" class="content-input-file"
95
              title="Import from file" />
96
            </div>
97
          </div>
98
          <div class="form-field">
99
          </div>
100
        </div>
101
      </form>
102
      <div class="form-actions clearfix">
103
        <div class="form-action submit">{% trans "Submit" %}</div>
104
        <div class="form-action cancel">{% trans "Cancel" %}</div>
105
      </div>
106
    </div>
107
  </div>
108
</div>
/dev/null
1
# Copyright 2011 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33
#
34

  
35
from django.test import TransactionTestCase
36
from django.conf import settings
37
from django.test.client import Client
38
from django.core.urlresolvers import clear_url_caches
39
from django.utils import simplejson as json
40
from django.conf import settings
41
from django.core.urlresolvers import reverse
42

  
43
from synnefo.ui.userdata.models import *
44

  
45

  
46
class AaiClient(Client):
47

  
48
    def request(self, **request):
49
        request['HTTP_X_AUTH_TOKEN'] = '0000'
50
        return super(AaiClient, self).request(**request)
51

  
52
class TestRestViews(TransactionTestCase):
53

  
54
    fixtures = ['users']
55

  
56
    def setUp(self):
57
        def get_user_mock(request, *Args, **kwargs):
58
            if request.META.get('HTTP_X_AUTH_TOKEN', None) == '0000':
59
                request.user_uniq = 'test'
60
                request.user = {'uniq': 'test'}
61

  
62
        # mock the astakos authentication function
63
        from snf_django.lib import astakos
64
        astakos.get_user = get_user_mock
65

  
66
        settings.SKIP_SSH_VALIDATION = True
67
        self.client = AaiClient()
68
        self.user = 'test'
69
        self.keys_url = reverse('ui_keys_collection')
70

  
71
    def test_keys_collection_get(self):
72
        resp = self.client.get(self.keys_url)
73
        self.assertEqual(resp.content, "[]")
74

  
75
        PublicKeyPair.objects.create(user=self.user, name="key pair 1",
76
                content="content1")
77

  
78
        resp = self.client.get(self.keys_url)
79
        resp_list = json.loads(resp.content);
80
        exp_list = [{"content": "content1", "id": 1,
81
                    "uri": self.keys_url + "/1", "name": "key pair 1",
82
                    "fingerprint": "unknown fingerprint"}]
83
        self.assertEqual(resp_list, exp_list)
84

  
85
        PublicKeyPair.objects.create(user=self.user, name="key pair 2",
86
                content="content2")
87

  
88
        resp = self.client.get(self.keys_url)
89
        resp_list = json.loads(resp.content)
90
        exp_list = [{"content": "content1", "id": 1,
91
                     "uri": self.keys_url + "/1", "name": "key pair 1",
92
                     "fingerprint": "unknown fingerprint"},
93
                    {"content": "content2", "id": 2,
94
                     "uri": self.keys_url + "/2",
95
                     "name": "key pair 2",
96
                     "fingerprint": "unknown fingerprint"}]
97

  
98
        self.assertEqual(resp_list, exp_list)
99

  
100
    def test_keys_resourse_get(self):
101
        resp = self.client.get(self.keys_url + "/1")
102
        self.assertEqual(resp.status_code, 404)
103

  
104
        # create a public key
105
        PublicKeyPair.objects.create(user=self.user, name="key pair 1",
106
                content="content1")
107
        resp = self.client.get(self.keys_url + "/1")
108
        resp_dict = json.loads(resp.content);
109
        exp_dict = {"content": "content1", "id": 1,
110
                    "uri": self.keys_url + "/1", "name": "key pair 1",
111
                    "fingerprint": "unknown fingerprint"}
112
        self.assertEqual(resp_dict, exp_dict)
113

  
114
        # update
115
        resp = self.client.put(self.keys_url + "/1",
116
                               json.dumps({'name':'key pair 1 new name'}),
117
                               content_type='application/json')
118

  
119
        pk = PublicKeyPair.objects.get(pk=1)
120
        self.assertEqual(pk.name, "key pair 1 new name")
121

  
122
        # delete
123
        resp = self.client.delete(self.keys_url + "/1")
124
        self.assertEqual(PublicKeyPair.objects.count(), 0)
125

  
126
        resp = self.client.get(self.keys_url + "/1")
127
        self.assertEqual(resp.status_code, 404)
128

  
129
        resp = self.client.get(self.keys_url)
130
        self.assertEqual(resp.content, "[]")
131

  
132
        # test rest create
133
        resp = self.client.post(self.keys_url,
134
                                json.dumps({'name':'key pair 2',
135
                                            'content':"""key 2 content"""}),
136
                                content_type='application/json')
137
        self.assertEqual(PublicKeyPair.objects.count(), 1)
138
        pk = PublicKeyPair.objects.get(pk=1)
139
        self.assertEqual(pk.name, "key pair 2")
140
        self.assertEqual(pk.content, "key 2 content")
141

  
142
    def test_generate_views(self):
143
        import base64
144

  
145
        # just test that
146
        resp = self.client.post(self.keys_url + "/generate")
147
        self.assertNotEqual(resp, "")
148

  
149
        data = json.loads(resp.content)
150
        self.assertEqual(data.has_key('private'), True)
151
        self.assertEqual(data.has_key('private'), True)
152

  
153
        # public key is base64 encoded
154
        base64.b64decode(data['public'].replace("ssh-rsa ",""))
155

  
156
        # remove header/footer
157
        private = "".join(data['private'].split("\n")[1:-1])
158

  
159
        # private key is base64 encoded
160
        base64.b64decode(private)
161

  
162
        new_key = PublicKeyPair()
163
        new_key.content = data['public']
164
        new_key.name = "new key"
165
        new_key.user = 'test'
166
        new_key.full_clean()
167
        new_key.save()
168

  
169
    def test_invalid_data(self):
170
        resp = self.client.post(self.keys_url,
171
                                json.dumps({'content':"""key 2 content"""}),
172
                                content_type='application/json')
173

  
174
        self.assertEqual(resp.status_code, 500)
175
        self.assertEqual(resp.content, """{"non_field_key": "__all__", "errors": """
176
                                       """{"name": ["This field cannot be blank."]}}""")
177

  
178
        settings.USERDATA_MAX_SSH_KEYS_PER_USER = 2
179

  
180
        # test ssh limit
181
        resp = self.client.post(self.keys_url,
182
                                json.dumps({'name':'key1',
183
                                            'content':"""key 1 content"""}),
184
                                content_type='application/json')
185
        resp = self.client.post(self.keys_url,
186
                                json.dumps({'name':'key1',
187
                                            'content':"""key 1 content"""}),
188
                                content_type='application/json')
189
        resp = self.client.post(self.keys_url,
190
                                json.dumps({'name':'key1',
191
                                            'content':"""key 1 content"""}),
192
                                content_type='application/json')
193
        self.assertEqual(resp.status_code, 500)
194
        self.assertEqual(resp.content, """{"non_field_key": "__all__", "errors": """
195
                                       """{"__all__": ["SSH keys limit exceeded."]}}""")
196

  
/dev/null
1
#
2
# Copyright 2011 GRNET S.A. All rights reserved.
3
#
4
# Redistribution and use in source and binary forms, with or
5
# without modification, are permitted provided that the following
6
# conditions are met:
7
#
8
#   1. Redistributions of source code must retain the above
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
11
#
12
#   2. Redistributions in binary form must reproduce the above
13
#      copyright notice, this list of conditions and the following
14
#      disclaimer in the documentation and/or other materials
15
#      provided with the distribution.
16
#
17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
29
#
30
# The views and conclusions contained in the software and
31
# documentation are those of the authors and should not be
32
# interpreted as representing official policies, either expressed
33
# or implied, of GRNET S.A.
34

  
35
from django.conf.urls.defaults import *
36
from synnefo.ui.userdata import views
37
from django.http import Http404
38

  
39

  
40
def index(request):
41
    raise Http404
42

  
43
urlpatterns = patterns('',
44
    url(r'^$', index, name='ui_userdata'),
45
    url(r'^keys$',
46
        views.PublicKeyPairCollectionView.as_view('ui_keys_resource'),
47
        name='ui_keys_collection'),
48
    url(r'^keys/(?P<id>\d+)',
49
    views.PublicKeyPairResourceView.as_view('ui_keys_resource'),
50
        name="ui_keys_resource"),
51
    url(r'keys/generate', views.generate_key_pair,
52
        name="ui_generate_public_key"),
53
    url(r'keys/download', views.download_private_key,
54
        name="ui_download_public_key")
55
)
/dev/null
1
import binascii
2

  
3
from synnefo.ui.userdata.asn1 import DerObject, DerSequence
4

  
5
def exportKey(keyobj, format='PEM'):
6
    """Export the RSA key. A string is returned
7
    with the encoded public or the private half
8
    under the selected format.
9

  
10
    format: 'DER' (PKCS#1) or 'PEM' (RFC1421)
11
    """
12
    der = DerSequence()
13
    if keyobj.has_private():
14
        keyType = "RSA PRIVATE"
15
        der[:] = [ 0, keyobj.n, keyobj.e, keyobj.d, keyobj.p, keyobj.q,
16
        keyobj.d % (keyobj.p-1), keyobj.d % (keyobj.q-1),
17
        keyobj.u ]
18
    else:
19
        keyType = "PUBLIC"
20
        der.append('\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01\x05\x00')
21
        bitmap = DerObject('BIT STRING')
22
        derPK = DerSequence()
23
        derPK[:] = [ keyobj.n, keyobj.e ]
24
        bitmap.payload = '\x00' + derPK.encode()
25
        der.append(bitmap.encode())
26
    if format=='DER':
27
        return der.encode()
28
    if format=='PEM':
29
        pem = "-----BEGIN %s KEY-----\n" % keyType
30
        binaryKey = der.encode()
31
        # Each BASE64 line can take up to 64 characters (=48 bytes of data)
32
        chunks = [ binascii.b2a_base64(binaryKey[i:i+48]) for i in range(0, len(binaryKey), 48) ]
33
        pem += ''.join(chunks)
34
        pem += "-----END %s KEY-----" % keyType
35
        return pem
36
    return ValueError("")
/dev/null
1
#
2
# Copyright 2011 GRNET S.A. All rights reserved.
3
#
4
# Redistribution and use in source and binary forms, with or
5
# without modification, are permitted provided that the following
6
# conditions are met:
7
#
8
#   1. Redistributions of source code must retain the above
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
11
#
12
#   2. Redistributions in binary form must reproduce the above
13
#      copyright notice, this list of conditions and the following
14
#      disclaimer in the documentation and/or other materials
15
#      provided with the distribution.
16
#
17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
29
#
30
# The views and conclusions contained in the software and
31
# documentation are those of the authors and should not be
32
# interpreted as representing official policies, either expressed
33
# or implied, of GRNET S.A.
34

  
35
from django import http
36
from django.utils import simplejson as json
37
from django.conf import settings
38

  
39
from synnefo.ui.userdata import rest
40
from synnefo.ui.userdata.models import PublicKeyPair
41
from synnefo.ui.userdata.util import exportKey
42
from snf_django.lib.astakos import get_user
43

  
44
SUPPORT_GENERATE_KEYS = True
45
try:
46
    from paramiko import rsakey
47
    from paramiko.message import Message
48
except ImportError, e:
49
    SUPPORT_GENERATE_KEYS = False
50

  
51
import base64
52

  
53

  
54
class PublicKeyPairResourceView(rest.UserResourceView):
55
    model = PublicKeyPair
56
    exclude_fields = ["user"]
57

  
58

  
59
class PublicKeyPairCollectionView(rest.UserCollectionView):
60
    model = PublicKeyPair
61
    exclude_fields = ["user"]
62

  
63

  
64
SSH_KEY_LENGTH = getattr(settings, 'USERDATA_SSH_KEY_LENGTH', 2048)
65

  
66

  
67
def generate_key_pair(request):
68
    """
69
    Response to generate private/public RSA key pair
70
    """
71

  
72
    get_user(request, settings.ASTAKOS_BASE_URL)
73

  
74
    if request.method != "POST":
75
        return http.HttpResponseNotAllowed(["POST"])
76

  
77
    if not SUPPORT_GENERATE_KEYS:
78
        raise Exception("Application does not support ssh keys generation")
79

  
80
    if PublicKeyPair.user_limit_exceeded(request.user):
81
        raise http.HttpResponseServerError("SSH keys limit exceeded")
82

  
83
    # generate RSA key
84
    from Crypto import Random
85
    Random.atfork()
86

  
87
    key = rsakey.RSA.generate(SSH_KEY_LENGTH)
88

  
89
    # get PEM string
90
    pem = exportKey(key, 'PEM')
91

  
92
    public_data = Message()
93
    public_data.add_string('ssh-rsa')
94
    public_data.add_mpint(key.key.e)
95
    public_data.add_mpint(key.key.n)
96

  
97
    # generate public content
98
    public = str("ssh-rsa %s" % base64.b64encode(str(public_data)))
99

  
100
    data = {'private': pem, 'public': public}
101
    return http.HttpResponse(json.dumps(data), mimetype="application/json")
102

  
103

  
104
def download_private_key(request):
105
    """
106
    Return key contents
107
    """
108
    data = request.POST.get("data")
109
    name = request.POST.get("name", "key")
110

  
111
    response = http.HttpResponse(mimetype='application/x-pem-key')
112
    response['Content-Disposition'] = 'attachment; filename=%s' % name
113
    response.write(data)
114
    return response
b/snf-cyclades-app/synnefo/userdata/asn1.py
1
# ===================================================================
2
# The contents of this file are dedicated to the public domain.  To
3
# the extent that dedication to the public domain is not available,
4
# everyone is granted a worldwide, perpetual, royalty-free,
5
# non-exclusive license to exercise all rights associated with the
6
# contents of this file for any purpose whatsoever.
7
# No rights are reserved.
8
#
9
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
10
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
11
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
12
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
13
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
14
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
15
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
16
# SOFTWARE.
17
# ===================================================================
18

  
19
from Crypto.Util.number import long_to_bytes, bytes_to_long
20

  
21
__all__ = [ 'DerObject', 'DerInteger', 'DerSequence' ]
22

  
23
class DerObject:
24
	typeTags = { 'SEQUENCE':'\x30', 'BIT STRING':'\x03', 'INTEGER':'\x02' }
25

  
26
	def __init__(self, ASN1Type=None):
27
		self.typeTag = self.typeTags.get(ASN1Type, ASN1Type)
28
		self.payload = ''
29

  
30
	def _lengthOctets(self, payloadLen):
31
		'''
32
		Return an octet string that is suitable for the BER/DER
33
		length element if the relevant payload is of the given
34
		size (in bytes).
35
		'''
36
		if payloadLen>127:
37
			encoding = long_to_bytes(payloadLen)
38
			return chr(len(encoding)+128) + encoding
39
		return chr(payloadLen)
40

  
41
	def encode(self):
42
		return self.typeTag + self._lengthOctets(len(self.payload)) + self.payload
43

  
44
	def _decodeLen(self, idx, str):
45
		'''
46
		Given a string and an index to a DER LV,
47
		this function returns a tuple with the length of V
48
		and an index to the first byte of it.
49
		'''
50
		length = ord(str[idx])
51
		if length<=127:
52
			return (length,idx+1)
53
		else:
54
			payloadLength = bytes_to_long(str[idx+1:idx+1+(length & 0x7F)])
55
			if payloadLength<=127:
56
				raise ValueError("Not a DER length tag.")
57
			return (payloadLength, idx+1+(length & 0x7F))
58

  
59
	def decode(self, input, noLeftOvers=0):
60
		try:
61
			self.typeTag = input[0]
62
			if (ord(self.typeTag) & 0x1F)==0x1F:
63
				raise ValueError("Unsupported DER tag")
64
			(length,idx) = self._decodeLen(1,input)
65
			if noLeftOvers and len(input) != (idx+length):
66
				raise ValueError("Not a DER structure")
67
			self.payload = input[idx:idx+length]
68
		except IndexError:
69
			raise ValueError("Not a valid DER SEQUENCE.")
70
		return idx+length
71

  
72
class DerInteger(DerObject):
73
	def __init__(self, value = 0):
74
		DerObject.__init__(self, 'INTEGER')
75
		self.value = value
76

  
77
	def encode(self):
78
		self.payload = long_to_bytes(self.value)
79
		if ord(self.payload[0])>127:
80
			self.payload = '\x00' + self.payload
81
		return DerObject.encode(self)
82

  
83
	def decode(self, input, noLeftOvers=0):
84
		tlvLength = DerObject.decode(self, input,noLeftOvers)
85
		if ord(self.payload[0])>127:
86
			raise ValueError ("Negative INTEGER.")
87
		self.value = bytes_to_long(self.payload)
88
		return tlvLength
89

  
90
class DerSequence(DerObject):
91
	def __init__(self):
92
		DerObject.__init__(self, 'SEQUENCE')
93
		self._seq = []
94
	def __delitem__(self, n):
95
		del self._seq[n]
96
	def __getitem__(self, n):
97
		return self._seq[n]
98
	def __setitem__(self, key, value):
99
		self._seq[key] = value
100
	def __setslice__(self,i,j,sequence):
101
		self._seq[i:j] = sequence
102
	def __delslice__(self,i,j):
103
		del self._seq[i:j]
104
	def __getslice__(self, i, j):
105
		return self._seq[max(0, i):max(0, j)]
106
	def __len__(self):
107
		return len(self._seq)
108
	def append(self, item):
109
		return self._seq.append(item)
110

  
111
	def hasOnlyInts(self):
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff