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): |
Also available in: Unified diff