Commit 05244d65 by Scott Duckworth

Merge branch 'release/2.3.0'

parents bff6b020 3010e7d9
...@@ -21,16 +21,29 @@ should be a string containing options accepted by sshd, with ``{username}`` ...@@ -21,16 +21,29 @@ should be a string containing options accepted by sshd, with ``{username}``
being replaced with the username of the user associated with the incoming being replaced with the username of the user associated with the incoming
public key. public key.
django-sshkey can also help you keep track of when a key was last used.
``SSHKEY_AUTHORIZED_KEYS_OPTIONS`` also replaces ``{key_id}`` with the key's
id. The command that is run can then notify django-sshkey that the key was used
by issuing a HTTP POST to the lookup URL, placing the key_id in the request
body.
For instance:: For instance::
SSHKEY_AUTHORIZED_KEYS_OPTIONS = 'command="my-command {username}",no-pty' SSHKEY_AUTHORIZED_KEYS_OPTIONS = 'command="my-command {username} {key_id}",no-pty'
in settings.py will cause keys produced by the below commands to look similar in settings.py will cause keys produced by the below commands to look similar
to:: to::
command="my-command fred",no-pty ssh-rsa AAAAB3NzaC1yc2E... command="my-command fred 15",no-pty ssh-rsa AAAAB3NzaC1yc2E...
sshd would then verify the key is correct and run ``my-command``.
``my-command`` would then know that this is fred and that he is using key 15,
and could tell django-sshkey to update the last_used field of that key by
running the equivalent of this command::
curl -d 15 http://localhost:8000/sshkey/lookup
assuming the key ``AAAAB3NzaC1yc2E...`` is owned by fred. Your URL may vary depending upon your configuration.
URL Configuration URL Configuration
----------------- -----------------
...@@ -57,6 +70,52 @@ mapping. ...@@ -57,6 +70,52 @@ mapping.
and only the systems that need to run the lookup commands should have access and only the systems that need to run the lookup commands should have access
to it. to it.
Settings
--------
``SSHKEY_AUTHORIZED_KEYS_OPTIONS``
String, optional. Defines the SSH options that will be prepended to each
public key. ``{username}`` will be replaced by the username; ``{key_id}``
will be replaced by the key's id. New in version 2.3.
``SSHKEY_ALLOW_EDIT``
Boolean, defaults to ``False``. Whether or not editing keys is allowed.
Note that no email will be sent in any case when a key is edited, hence the
reason that editing keys is disabled by default. New in version 2.3.
``SSHKEY_EMAIL_ADD_KEY``
Boolean, defaults to ``True``. Whether or not an email should be sent to the
user when a new key is added to their account. New in version 2.3.
``SSHKEY_EMAIL_ADD_KEY_SUBJECT``
String, defaults to ``"A new key was added to your account"``. The subject of
the email that gets sent out when a new key is added. New in version 2.3.
``SSHKEY_FROM_EMAIL``
String, defaults to ``DEFAULT_FROM_EMAIL``. New in version 2.3.
``SSHKEY_SEND_HTML_EMAIL``
Boolean, defaults to ``False``. Whether or not multipart HTML emails should
be sent. New in version 2.3.
Templates
---------
Example templates are available in the ``templates.example`` directory.
``sshkey/userkey_list.html``
Used when listing a user's keys.
``sshkey/userkey_detail.html``
Used when adding or editing a user's keys.
``sshkey/add_key.txt``
The plain text body of the email sent when a new key is added. New in version
2.3.
``sshkey/add_key.html``
The HTML body of the email sent when a new key is added. New in version 2.3.
Tying OpenSSH to django-sshkey Tying OpenSSH to django-sshkey
============================== ==============================
...@@ -83,6 +142,39 @@ slower. To use the variants, replace ``lookup`` with ``pylookup``. For ...@@ -83,6 +142,39 @@ slower. To use the variants, replace ``lookup`` with ``pylookup``. For
example, use ``django-sshkey-pylookup-all`` instead of example, use ``django-sshkey-pylookup-all`` instead of
``django-sshkey-lookup-all``. ``django-sshkey-lookup-all``.
Using ``django-sshkey-lookup``
------------------------------
::
Usage: django-sshkey-lookup -a URL
django-sshkey-lookup -u URL USERNAME
django-sshkey-lookup -f URL FINGERPRINT
django-sshkey-lookup URL [USERNAME]
This program has different modes of operation:
``-a``
Print all public keys.
``-u``
Print all public keys owned by the specified user.
``-f``
Print all public keys matching the specified fingerprint.
Default
Compatibility mode. If the username parameter is given then print all public
keys owned by the specified user; otherwise perform the same functionality as
``django-sshkey-lookup-by-fingerprint`` (see below).
All modes expect that the lookup URL be specified as the first non-option
parameter.
This command is compatible with the old script ``lookup.sh`` but was renamed
to have a less ambiguous name when installed system-wide. A symlink is left in
its place for backwards compatibility.
Using ``django-sshkey-lookup-all`` Using ``django-sshkey-lookup-all``
---------------------------------- ----------------------------------
...@@ -151,20 +243,6 @@ This program: ...@@ -151,20 +243,6 @@ This program:
* is ideal if you want all Django users to access SSH via a shared system user * is ideal if you want all Django users to access SSH via a shared system user
account and be identified by their SSH public key. account and be identified by their SSH public key.
Using ``django-sshkey-lookup``
------------------------------
``Usage: django-sshkey-lookup URL [USERNAME]``
This program is a wrapper around the previous two commands. The first
parameter is placed in the ``SSHKEY_LOOKUP_URL`` environment variable. If the
second parameter is present then ``django-sshkey-lookup-by-username`` is
executed; otherwise ``django-sshkey-lookup-by-fingerprint`` is executed.
This command is compatible with the old script ``lookup.sh`` but was renamed
to have a less ambiguous name when installed system-wide. A symlink is left in
its place for backwards compatibility.
.. _OpenSSH: http://www.openssh.com/ .. _OpenSSH: http://www.openssh.com/
.. _openssh-akcenv: https://github.com/ScottDuckworth/openssh-akcenv .. _openssh-akcenv: https://github.com/ScottDuckworth/openssh-akcenv
.. _openssh-stdinkey: https://github.com/ScottDuckworth/openssh-stdinkey .. _openssh-stdinkey: https://github.com/ScottDuckworth/openssh-stdinkey
...@@ -17,7 +17,9 @@ The following table maps django-sshkey version to migration labels: ...@@ -17,7 +17,9 @@ The following table maps django-sshkey version to migration labels:
+---------+---------------+-------+------------------------------------------+ +---------+---------------+-------+------------------------------------------+
| 1.1 | sshkey | 0002 | | | 1.1 | sshkey | 0002 | |
+---------+---------------+-------+------------------------------------------+ +---------+---------------+-------+------------------------------------------+
| 2.0+ | django_sshkey | 0001 | See Upgrading from 1.1.x to 2.x below | | 2.0-2.2 | django_sshkey | 0001 | See Upgrading from 1.1.x to 2.x below |
+---------+---------------+-------+------------------------------------------+
| 2.3 | django_sshkey | 0002 | |
+---------+---------------+-------+------------------------------------------+ +---------+---------------+-------+------------------------------------------+
To upgrade, install the new version of django-sshkey and then migrate your To upgrade, install the new version of django-sshkey and then migrate your
......
...@@ -27,14 +27,70 @@ ...@@ -27,14 +27,70 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
usage() {
echo "Usage: $0 -a URL"
echo " $0 -u URL USERNAME"
echo " $0 -f URL FINGERPRINT"
echo " $0 URL [USERNAME]"
}
mode=x
while getopts ':hafu' opt; do
case $opt in
h)
usage
exit 0
;;
a) mode=a ;;
f) mode=f ;;
u) mode=u ;;
[?])
exec 1>&2
echo "Invalid option: -$OPTARG"
usage
exit 1
;;
esac
done
shift $(($OPTIND-1))
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
echo "Usage: $0 URL [USERNAME]" >&2 usage >&2
exit 1 exit 1
fi fi
SSHKEY_LOOKUP_URL="$1" url="$1"
export SSHKEY_LOOKUP_URL
if [ $# -eq 1 ]; then case $mode in
exec `dirname $0`/django-sshkey-lookup-by-fingerprint a)
query=
;;
f)
if [ $# -lt 2 ]; then
usage >&2
exit 1
fi
query="fingerprint=$2"
;;
u)
if [ $# -lt 2 ]; then
usage >&2
exit 1
fi
query="username=$2"
;;
x)
if [ $# -eq 1 ]; then
SSHKEY_LOOKUP_URL="${url}"
export SSHKEY_LOOKUP_URL
exec `dirname $0`/django-sshkey-lookup-by-fingerprint
else
query="username=$2"
fi
;;
esac
if type curl >/dev/null 2>&1; then
exec curl -s -G "${url}" --data-urlencode "${query}"
else else
exec `dirname $0`/django-sshkey-lookup-by-username "$2" exec wget -q -O - "${url}?${query}"
fi fi
...@@ -26,4 +26,4 @@ ...@@ -26,4 +26,4 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
__version__ = '2.2.0' __version__ = '2.3.0'
...@@ -37,6 +37,7 @@ class UserKeyAdmin(admin.ModelAdmin): ...@@ -37,6 +37,7 @@ class UserKeyAdmin(admin.ModelAdmin):
'fingerprint', 'fingerprint',
'created', 'created',
'last_modified', 'last_modified',
'last_used',
] ]
search_fields = [ search_fields = [
'user__username', 'user__username',
...@@ -45,6 +46,7 @@ class UserKeyAdmin(admin.ModelAdmin): ...@@ -45,6 +46,7 @@ class UserKeyAdmin(admin.ModelAdmin):
'fingerprint', 'fingerprint',
'created', 'created',
'last_modified', 'last_modified',
'last_used',
] ]
admin.site.register(UserKey, UserKeyAdmin) admin.site.register(UserKey, UserKeyAdmin)
...@@ -33,3 +33,14 @@ class UserKeyForm(forms.ModelForm): ...@@ -33,3 +33,14 @@ class UserKeyForm(forms.ModelForm):
class Meta: class Meta:
model = UserKey model = UserKey
fields = ['name', 'key'] fields = ['name', 'key']
widgets = {
'name': forms.TextInput(attrs={
'size': 50,
'placeholder': "username@hostname, or leave blank to use key comment",
}),
'key': forms.Textarea(attrs={
'cols': 72,
'rows': 15,
'placeholder': "Paste in the contents of your public key file here",
}),
}
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'UserKey.last_used'
db.add_column('sshkey_userkey', 'last_used', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
def backwards(self, orm):
# Deleting field 'UserKey.last_used'
db.delete_column('sshkey_userkey', 'last_used')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'django_sshkey.userkey': {
'Meta': {'unique_together': "[('user', 'name')]", 'object_name': 'UserKey', 'db_table': "'sshkey_userkey'"},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
'fingerprint': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '47', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.TextField', [], {'max_length': '2000'}),
'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}),
'last_used': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['django_sshkey']
...@@ -26,10 +26,15 @@ ...@@ -26,10 +26,15 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import datetime
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_sshkey.util import sshkey_re, sshkey_fingerprint from django.db.models.signals import pre_save
from django.dispatch import receiver
from django_sshkey.util import PublicKeyParseError, pubkey_parse
from django_sshkey import settings
class UserKey(models.Model): class UserKey(models.Model):
user = models.ForeignKey(User, db_index=True) user = models.ForeignKey(User, db_index=True)
...@@ -37,7 +42,8 @@ class UserKey(models.Model): ...@@ -37,7 +42,8 @@ class UserKey(models.Model):
key = models.TextField(max_length=2000) key = models.TextField(max_length=2000)
fingerprint = models.CharField(max_length=47, blank=True, db_index=True) fingerprint = models.CharField(max_length=47, blank=True, db_index=True)
created = models.DateTimeField(auto_now_add=True, null=True) created = models.DateTimeField(auto_now_add=True, null=True)
last_modified = models.DateTimeField(auto_now=True, null=True) last_modified = models.DateTimeField(null=True)
last_used = models.DateTimeField(null=True)
class Meta: class Meta:
db_table = 'sshkey_userkey' db_table = 'sshkey_userkey'
...@@ -53,19 +59,16 @@ class UserKey(models.Model): ...@@ -53,19 +59,16 @@ class UserKey(models.Model):
self.key = self.key.strip() self.key = self.key.strip()
def clean(self): def clean(self):
m = sshkey_re.match(self.key)
errmsg = 'Key is not a valid SSH protocol 2 base64-encoded key'
if not m:
raise ValidationError(errmsg)
try: try:
self.fingerprint = sshkey_fingerprint(m.group('b64key')) pubkey = pubkey_parse(self.key)
except TypeError: except PublicKeyParseError as e:
raise ValidationError(errmsg) raise ValidationError(str(e))
self.key = pubkey.format_openssh()
self.fingerprint = pubkey.fingerprint()
if not self.name: if not self.name:
comment = m.group('comment') if not pubkey.comment:
if not comment:
raise ValidationError('Name or key comment required') raise ValidationError('Name or key comment required')
self.name = comment self.name = pubkey.comment
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):
if self.pk is None: if self.pk is None:
...@@ -86,3 +89,48 @@ class UserKey(models.Model): ...@@ -86,3 +89,48 @@ class UserKey(models.Model):
raise ValidationError({'key': [message]}) raise ValidationError({'key': [message]})
except type(self).DoesNotExist: except type(self).DoesNotExist:
pass pass
def export(self, format='RFC4716'):
pubkey = pubkey_parse(self.key)
f = format.upper()
if f == 'RFC4716':
return pubkey.format_rfc4716()
if f == 'PEM':
return pubkey.format_pem()
raise ValueError("Invalid format")
def save(self, *args, **kwargs):
if kwargs.pop('update_last_modified', True):
self.last_modified = datetime.datetime.now()
super(UserKey, self).save(*args, **kwargs)
def touch(self):
self.last_used = datetime.datetime.now()
self.save(update_last_modified=False)
@receiver(pre_save, sender=UserKey)
def send_email_add_key(sender, instance, **kwargs):
if not settings.SSHKEY_EMAIL_ADD_KEY or instance.pk:
return
from django.template.loader import render_to_string
from django.core.mail import EmailMultiAlternatives
from django.core.urlresolvers import reverse
context_dict = {
'key': instance,
'subject': settings.SSHKEY_EMAIL_ADD_KEY_SUBJECT,
}
request = getattr(instance, 'request', None)
if request:
context_dict['request'] = request
context_dict['userkey_list_uri'] = request.build_absolute_uri(reverse('django_sshkey.views.userkey_list'))
text_content = render_to_string('sshkey/add_key.txt', context_dict)
msg = EmailMultiAlternatives(
settings.SSHKEY_EMAIL_ADD_KEY_SUBJECT,
text_content,
settings.SSHKEY_FROM_EMAIL,
[instance.user.email],
)
if settings.SSHKEY_SEND_HTML_EMAIL:
html_content = render_to_string('sshkey/add_key.html', context_dict)
msg.attach_alternative(html_content, 'text/html')
msg.send()
...@@ -29,13 +29,10 @@ ...@@ -29,13 +29,10 @@
from django.conf import settings from django.conf import settings
SSHKEY_AUTHORIZED_KEYS_OPTIONS = getattr(settings, 'SSHKEY_AUTHORIZED_KEYS_OPTIONS', None) SSHKEY_AUTHORIZED_KEYS_OPTIONS = getattr(settings, 'SSHKEY_AUTHORIZED_KEYS_OPTIONS', None)
SSHKEY_AUTHORIZED_KEYS_COMMAND = getattr(settings, 'SSHKEY_AUTHORIZED_KEYS_COMMAND', None) SSHKEY_ALLOW_EDIT = getattr(settings, 'SSHKEY_ALLOW_EDIT', False)
if SSHKEY_AUTHORIZED_KEYS_COMMAND is not None: SSHKEY_EMAIL_ADD_KEY = getattr(settings, 'SSHKEY_EMAIL_ADD_KEY', True)
import warnings SSHKEY_EMAIL_ADD_KEY_SUBJECT = getattr(settings, 'SSHKEY_EMAIL_ADD_KEY_SUBJECT',
with warnings.catch_warnings(): "A new public key was added to your account"
import warnings )
warnings.simplefilter('default', DeprecationWarning) SSHKEY_FROM_EMAIL = getattr(settings, 'SSHKEY_FROM_EMAIL', settings.DEFAULT_FROM_EMAIL)
warnings.warn( SSHKEY_SEND_HTML_EMAIL = getattr(settings, 'SSHKEY_SEND_HTML_EMAIL', False)
'SSHKEY_AUTHORIZED_KEYS_COMMAND has been deprecated; '
'use SSHKEY_AUTHORIZED_KEYS_OPTIONS instead.',
DeprecationWarning)
<html>
<body>
<p>{{ key.user.first_name }},</p>
<p>The following SSH public key was added to your account
{% if request.META.SERVER_NAME %}
on {{ request.META.SERVER_NAME }}
{% endif %}
{% if request.META.REMOTE_ADDR %}
from {{ request.META.REMOTE_ADDR }}{% if request.META.REMOTE_HOST %}
({{ request.META.REMOTE_HOST }}){% endif %}{% endif %}:</p>
<p>
Name: {{ key.name }}<br/>
Fingerprint: {{ key.fingerprint }}
</p>
<p><b>If you believe this key was added in error then you should
<a href="{{ userkey_list_uri }}">click here</a> and delete the key.</b></p>
</body>
</html>
{{ key.user.first_name }},
The following SSH public key was added to your account{% if request.META.SERVER_NAME %} on {{ request.META.SERVER_NAME }}{% endif %}{% if request.META.REMOTE_ADDR %}
from {{ request.META.REMOTE_ADDR }}{% if request.META.REMOTE_HOST %} ({{ request.META.REMOTE_HOST }}){% endif %}{% endif %}:
Name: {{ key.name }}
Fingerprint: {{ key.fingerprint }}
If you believe this key was added in error then you should go to
{{ userkey_list_uri }} and delete the key.
<h1>My Keys</h1> <h1>My Keys</h1>
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
<p><a href="{% url django_sshkey.views.userkey_add %}">Add Key</a></p> <p><a href="{% url django_sshkey.views.userkey_add %}">Add Key</a></p>
<table> <table>
<tr> <tr>
<th>Key</th> <th>Key</th>
<th>Fingerprint</th> <th>Fingerprint</th>
<th>Created</th> <th>Created</th>
{% if allow_edit %}
<th>Last Modified</th> <th>Last Modified</th>
{% endif %}
<th>Last Used</th>
<th></th> <th></th>
</tr> </tr>
{% for userkey in userkey_list %} {% for userkey in userkey_list %}
<tr> <tr>
<td>{{ userkey.name }}</td> <td>{{ userkey.name }}</td>
<td>{{ userkey.fingerprint }}</td> <td>{{ userkey.fingerprint }}</td>
<td>{{ userkey.created }}</td> <td>{{ userkey.created|default:"unknown" }}</td>
<td>{{ userkey.last_modified }}</td> {% if allow_edit %}
<td><a href="{% url django_sshkey.views.userkey_edit userkey.pk %}">Edit</a></td> <td>{{ userkey.last_modified|default:"unknown" }}</td>
<td><a href="{% url django_sshkey.views.userkey_delete userkey.pk %}">Delete</a></td> {% endif %}
<td>{{ userkey.last_used|default:"never" }}</td>
<td>
{% if allow_edit %}
<a href="{% url django_sshkey.views.userkey_edit userkey.pk %}">Edit</a>
{% endif %}
<a href="{% url django_sshkey.views.userkey_delete userkey.pk %}">Delete</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
...@@ -50,6 +50,16 @@ def ssh_keygen(type=None, passphrase='', comment=None, file=None): ...@@ -50,6 +50,16 @@ def ssh_keygen(type=None, passphrase='', comment=None, file=None):
cmd += ['-f', file] cmd += ['-f', file]
subprocess.check_call(cmd) subprocess.check_call(cmd)
def ssh_key_export(input_path, output_path, format='RFC4716'):
cmd = ['ssh-keygen', '-e', '-m', format, '-f', input_path]
with open(output_path, 'wb') as f:
subprocess.check_call(cmd, stdout=f)
def ssh_key_import(input_path, output_path, format='RFC4716'):
cmd = ['ssh-keygen', '-i', '-m', format, '-f', input_path]
with open(output_path, 'wb') as f:
subprocess.check_call(cmd, stdout=f)
def ssh_fingerprint(pubkey_path): def ssh_fingerprint(pubkey_path):
cmd = ['ssh-keygen', '-lf', pubkey_path] cmd = ['ssh-keygen', '-lf', pubkey_path]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE) p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
...@@ -169,6 +179,21 @@ class UserKeyCreationTestCase(BaseTestCase): ...@@ -169,6 +179,21 @@ class UserKeyCreationTestCase(BaseTestCase):
key.save() key.save()
self.assertEqual(key.fingerprint, fingerprint) self.assertEqual(key.fingerprint, fingerprint)
def test_touch(self):
import datetime
key = UserKey(
user = self.user1,
name = 'name',
key = open(self.key1_path+'.pub').read(),
)
key.full_clean()
key.save()
self.assertIsNone(key.last_used)
key.touch()
key.save()
self.assertIsInstance(key.last_used, datetime.datetime)
key.touch()
def test_same_name_same_user(self): def test_same_name_same_user(self):
key1 = UserKey( key1 = UserKey(
user = self.user1, user = self.user1,
...@@ -230,44 +255,146 @@ class UserKeyCreationTestCase(BaseTestCase): ...@@ -230,44 +255,146 @@ class UserKeyCreationTestCase(BaseTestCase):
) )
self.assertRaises(ValidationError, key2.full_clean) self.assertRaises(ValidationError, key2.full_clean)
class RFC4716TestCase(BaseTestCase):
@classmethod
def setUpClass(cls):
super(RFC4716TestCase, cls).setUpClass()
cls.user1 = User.objects.create(username='user1')
# key1 has a comment
cls.key1_path = os.path.join(cls.key_dir, 'key1')
cls.key1_rfc4716_path = os.path.join(cls.key_dir, 'key1.rfc4716')
ssh_keygen(comment='comment', file=cls.key1_path)
ssh_key_export(cls.key1_path, cls.key1_rfc4716_path, 'RFC4716')
# key2 does not have a comment
cls.key2_path = os.path.join(cls.key_dir, 'key2')
cls.key2_rfc4716_path = os.path.join(cls.key_dir, 'key2.rfc4716')
ssh_keygen(comment='', file=cls.key2_path)
ssh_key_export(cls.key2_path, cls.key2_rfc4716_path, 'RFC4716')
@classmethod
def tearDownClass(cls):
User.objects.all().delete()
super(RFC4716TestCase, cls).tearDownClass()
def tearDown(self):
UserKey.objects.all().delete()
def test_import_with_comment(self):
key = UserKey(
user = self.user1,
name = 'name',
key = open(self.key1_rfc4716_path).read(),
)
key.full_clean()
key.save()
self.assertEqual(key.key.split()[:2], open(self.key1_path+'.pub').read().split()[:2])
def test_import_without_comment(self):
key = UserKey(
user = self.user1,
name = 'name',
key = open(self.key2_rfc4716_path).read(),
)
key.full_clean()
key.save()
self.assertEqual(key.key.split()[:2], open(self.key2_path+'.pub').read().split()[:2])
def test_export(self):
key = UserKey(
user = self.user1,
name = 'name',
key = open(self.key1_path+'.pub').read(),
)
key.full_clean()
key.save()
export_path = os.path.join(self.key_dir, 'export')
import_path = os.path.join(self.key_dir, 'import')
with open(export_path, 'w') as f:
f.write(key.export('RFC4716'))
ssh_key_import(export_path, import_path, 'RFC4716')
self.assertEqual(open(import_path).read().split()[:2], open(self.key1_path+'.pub').read().split()[:2])
class PemTestCase(BaseTestCase):
@classmethod
def setUpClass(cls):
super(PemTestCase, cls).setUpClass()
cls.user1 = User.objects.create(username='user1')
cls.key1_path = os.path.join(cls.key_dir, 'key1')
cls.key1_pem_path = os.path.join(cls.key_dir, 'key1.pem')
ssh_keygen(comment='', file=cls.key1_path)
ssh_key_export(cls.key1_path, cls.key1_pem_path, 'PEM')
@classmethod
def tearDownClass(cls):
User.objects.all().delete()
super(PemTestCase, cls).tearDownClass()
def tearDown(self):
UserKey.objects.all().delete()
def test_import(self):
key = UserKey(
user = self.user1,
name = 'name',
key = open(self.key1_pem_path).read(),
)
key.full_clean()
key.save()
self.assertEqual(key.key.split()[:2], open(self.key1_path+'.pub').read().split()[:2])
def test_export(self):
key = UserKey(
user = self.user1,
name = 'name',
key = open(self.key1_path+'.pub').read(),
)
key.full_clean()
key.save()
export_path = os.path.join(self.key_dir, 'export')
import_path = os.path.join(self.key_dir, 'import')
with open(export_path, 'w') as f:
f.write(key.export('PEM'))
ssh_key_import(export_path, import_path, 'PEM')
self.assertEqual(open(import_path).read().split()[:2], open(self.key1_path+'.pub').read().split()[:2])
class UserKeyLookupTestCase(BaseTestCase): class UserKeyLookupTestCase(BaseTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(UserKeyLookupTestCase, cls).setUpClass() super(UserKeyLookupTestCase, cls).setUpClass()
cls.original_options = settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS cls.original_options = settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS
settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS = 'command="{username}"' settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS = 'command="{username} {key_id}"'
cls.user1 = User.objects.create(username='user1') cls.user1 = User.objects.create(username='user1')
cls.user2 = User.objects.create(username='user2') cls.user2 = User.objects.create(username='user2')
cls.key1_path = os.path.join(cls.key_dir, 'key1') cls.key1_path = os.path.join(cls.key_dir, 'key1')
ssh_keygen(file=cls.key1_path) ssh_keygen(file=cls.key1_path)
key1 = UserKey( cls.key1 = UserKey(
user = cls.user1, user = cls.user1,
name = 'key1', name = 'key1',
key = open(cls.key1_path+'.pub').read(), key = open(cls.key1_path+'.pub').read(),
) )
key1.full_clean() cls.key1.full_clean()
key1.save() cls.key1.save()
cls.key2_path = os.path.join(cls.key_dir, 'key2') cls.key2_path = os.path.join(cls.key_dir, 'key2')
ssh_keygen(file=cls.key2_path) ssh_keygen(file=cls.key2_path)
key2 = UserKey( cls.key2 = UserKey(
user = cls.user1, user = cls.user1,
name = 'key2', name = 'key2',
key = open(cls.key2_path+'.pub').read(), key = open(cls.key2_path+'.pub').read(),
) )
key2.full_clean() cls.key2.full_clean()
key2.save() cls.key2.save()
cls.key3_path = os.path.join(cls.key_dir, 'key3') cls.key3_path = os.path.join(cls.key_dir, 'key3')
ssh_keygen(file=cls.key3_path) ssh_keygen(file=cls.key3_path)
key3 = UserKey( cls.key3 = UserKey(
user = cls.user2, user = cls.user2,
name = 'key3', name = 'key3',
key = open(cls.key3_path+'.pub').read(), key = open(cls.key3_path+'.pub').read(),
) )
key3.full_clean() cls.key3.full_clean()
key3.save() cls.key3.save()
cls.key4_path = os.path.join(cls.key_dir, 'key4') cls.key4_path = os.path.join(cls.key_dir, 'key4')
ssh_keygen(file=cls.key4_path) ssh_keygen(file=cls.key4_path)
...@@ -287,9 +414,9 @@ class UserKeyLookupTestCase(BaseTestCase): ...@@ -287,9 +414,9 @@ class UserKeyLookupTestCase(BaseTestCase):
self.assertEqual(response['Content-Type'], 'text/plain') self.assertEqual(response['Content-Type'], 'text/plain')
actual_content = set(response.content.strip().split('\n')) actual_content = set(response.content.strip().split('\n'))
correct_content = set(( correct_content = set((
'command="user1" ' + open(self.key1_path + '.pub').read().strip(), 'command="user1 %s" %s' % (self.key1.id, open(self.key1_path + '.pub').read().strip()),
'command="user1" ' + open(self.key2_path + '.pub').read().strip(), 'command="user1 %s" %s' % (self.key2.id, open(self.key2_path + '.pub').read().strip()),
'command="user2" ' + open(self.key3_path + '.pub').read().strip(), 'command="user2 %s" %s' % (self.key3.id, open(self.key3_path + '.pub').read().strip()),
)) ))
self.assertEqual(actual_content, correct_content) self.assertEqual(actual_content, correct_content)
...@@ -304,7 +431,7 @@ class UserKeyLookupTestCase(BaseTestCase): ...@@ -304,7 +431,7 @@ class UserKeyLookupTestCase(BaseTestCase):
username = self.user1.username username = self.user1.username
actual_content = set(response.content.strip().split('\n')) actual_content = set(response.content.strip().split('\n'))
correct_content = set(( correct_content = set((
'command="user1" ' + open(self.key1_path + '.pub').read().strip(), 'command="user1 %s" %s' % (self.key1.id, open(self.key1_path + '.pub').read().strip()),
)) ))
self.assertEqual(actual_content, correct_content) self.assertEqual(actual_content, correct_content)
...@@ -319,7 +446,7 @@ class UserKeyLookupTestCase(BaseTestCase): ...@@ -319,7 +446,7 @@ class UserKeyLookupTestCase(BaseTestCase):
body = open(self.key1_path + '.pub').read().strip() body = open(self.key1_path + '.pub').read().strip()
actual_content = set(response.content.strip().split('\n')) actual_content = set(response.content.strip().split('\n'))
correct_content = set(( correct_content = set((
'command="user2" ' + open(self.key3_path + '.pub').read().strip(), 'command="user2 %s" %s' % (self.key3.id, open(self.key3_path + '.pub').read().strip()),
)) ))
self.assertEqual(actual_content, correct_content) self.assertEqual(actual_content, correct_content)
...@@ -334,8 +461,8 @@ class UserKeyLookupTestCase(BaseTestCase): ...@@ -334,8 +461,8 @@ class UserKeyLookupTestCase(BaseTestCase):
body = open(self.key1_path + '.pub').read().strip() body = open(self.key1_path + '.pub').read().strip()
actual_content = set(response.content.strip().split('\n')) actual_content = set(response.content.strip().split('\n'))
correct_content = set(( correct_content = set((
'command="user1" ' + open(self.key1_path + '.pub').read().strip(), 'command="user1 %s" %s' % (self.key1.id, open(self.key1_path + '.pub').read().strip()),
'command="user1" ' + open(self.key2_path + '.pub').read().strip(), 'command="user1 %s" %s' % (self.key2.id, open(self.key2_path + '.pub').read().strip()),
)) ))
self.assertEqual(actual_content, correct_content) self.assertEqual(actual_content, correct_content)
......
...@@ -26,18 +26,184 @@ ...@@ -26,18 +26,184 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re import base64
import binascii
import struct
SSHKEY_LOOKUP_URL_DEFAULT = 'http://localhost:8000/sshkey/lookup' SSHKEY_LOOKUP_URL_DEFAULT = 'http://localhost:8000/sshkey/lookup'
sshkey_re = re.compile(r'(?P<type>[\w-]+)\s+(?P<b64key>\S+)(?:\s+(?P<comment>\S.+))?$') def wrap(text, width, wrap_end=None):
n = 0
t = ''
if wrap_end is None:
while n < len(text):
m = n + width
t += text[n:m]
if len(text) <= m:
return t
t += '\n'
n = m
else:
while n < len(text):
m = n + width
if len(text) <= m:
return t + text[n:m]
m -= len(wrap_end)
t += text[n:m] + wrap_end + '\n'
n = m
return t
def bytes2int(b):
h = binascii.hexlify(b)
return int(h, 16)
def int2bytes(i):
h = '%x' % i
if len(h) & 1:
h = '0' + h
return bytearray.fromhex(h)
class PublicKeyParseError(Exception):
def __init__(self, text):
self.text = text
def __str__(self):
return "Unrecognized public key format"
class PublicKey(object):
def __init__(self, b64key, comment=None):
self.b64key = b64key
self.comment = comment
keydata = base64.b64decode(b64key.encode('ascii'))
self.keydata = keydata
self.parts = []
while keydata:
dlen = struct.unpack('>I', keydata[:4])[0]
data, keydata = keydata[4:4+dlen], keydata[4+dlen:]
self.parts.append(data)
self.algorithm = self.parts[0]
def fingerprint(self):
import hashlib
fp = hashlib.md5(self.keydata).hexdigest()
return ':'.join(a+b for a,b in zip(fp[::2], fp[1::2]))
def format_openssh(self):
out = self.algorithm + ' ' + self.b64key
if self.comment:
out += ' ' + self.comment
return out
def format_rfc4716(self):
out = '---- BEGIN SSH2 PUBLIC KEY ----\n'
if self.comment:
comment = 'Comment: "%s"' % self.comment
out += wrap(comment, 72, '\\') + '\n'
out += wrap(self.b64key, 72) + '\n'
out += '---- END SSH2 PUBLIC KEY ----'
return out
def format_pem(self):
if self.algorithm != 'ssh-rsa' and len(self.parts) == 3:
raise TypeError("key is not a valid RSA key")
from pyasn1.codec.der import encoder as der_encoder
from pyasn1.type import univ
e = bytes2int(self.parts[1])
n = bytes2int(self.parts[2])
pkcs1_seq = univ.Sequence()
pkcs1_seq.setComponentByPosition(0, univ.Integer(n))
pkcs1_seq.setComponentByPosition(1, univ.Integer(e))
der = der_encoder.encode(pkcs1_seq)
out = (
'-----BEGIN RSA PUBLIC KEY-----\n' +
wrap(base64.b64encode(der), 64) + '\n' +
'-----END RSA PUBLIC KEY-----'
)
return out
def pubkey_parse_openssh(text):
fields = text.split(None, 2)
if len(fields) < 2:
raise PublicKeyParseError(text)
try:
if len(fields) == 2:
key = PublicKey(fields[1])
else:
key = PublicKey(fields[1], fields[2])
except TypeError:
raise PublicKeyParseError(text)
if fields[0] != key.algorithm:
raise PublicKeyParseError(text)
return key
def pubkey_parse_rfc4716(text):
lines = text.splitlines()
if not (
lines[0] == '---- BEGIN SSH2 PUBLIC KEY ----'
and lines[-1] == '---- END SSH2 PUBLIC KEY ----'
):
raise PublicKeyParseError(text)
lines = lines[1:-1]
b64key = ''
headers = {}
while lines:
line = lines.pop(0)
if ':' in line:
while line[-1] == '\\':
line = line[:-1] + lines.pop(0)
k,v = line.split(':', 1)
headers[k.lower()] = v.lstrip()
else:
b64key += line
comment = headers.get('comment')
if comment and comment[0] in ('"', "'") and comment[0] == comment[-1]:
comment = comment[1:-1]
try:
return PublicKey(b64key, comment)
except TypeError:
raise PublicKeyParseError(text)
def sshkey_fingerprint(b64key): def pubkey_parse_pem(text):
import base64 from pyasn1.codec.der import decoder as der_decoder
import hashlib lines = text.splitlines()
key = base64.b64decode(b64key) if not (
fp_plain = hashlib.md5(key).hexdigest() lines[0] == '-----BEGIN RSA PUBLIC KEY-----'
return ':'.join(a+b for a,b in zip(fp_plain[::2], fp_plain[1::2])) and lines[-1] == '-----END RSA PUBLIC KEY-----'
):
raise PublicKeyParseError(text)
der = base64.b64decode(''.join(lines[1:-1]).encode('ascii'))
pkcs1_seq = der_decoder.decode(der)
n_val = pkcs1_seq[0][0]
e_val = pkcs1_seq[0][1]
n = int2bytes(n_val)
e = int2bytes(e_val)
if n[0] & 0x80: n = b'\x00' + n
if e[0] & 0x80: e = b'\x00' + e
algorithm = 'ssh-rsa'.encode('ascii')
keydata = (
struct.pack('>I', len(algorithm)) +
algorithm +
struct.pack('>I', len(e)) +
e +
struct.pack('>I', len(n)) +
n
)
b64key = base64.b64encode(keydata).decode('ascii')
return PublicKey(b64key)
def pubkey_parse(text):
lines = text.splitlines()
if len(lines) == 1:
return pubkey_parse_openssh(text)
if lines[0] == '---- BEGIN SSH2 PUBLIC KEY ----':
return pubkey_parse_rfc4716(text)
if lines[0] == '-----BEGIN RSA PUBLIC KEY-----':
return pubkey_parse_pem(text)
raise PublicKeyParseError(text)
def lookup_all(url): def lookup_all(url):
import urllib import urllib
...@@ -87,28 +253,62 @@ def lookup_by_fingerprint_main(): ...@@ -87,28 +253,62 @@ def lookup_by_fingerprint_main():
"Error: cannot retrieve fingerprint from environment or stdin\n" "Error: cannot retrieve fingerprint from environment or stdin\n"
) )
sys.exit(1) sys.exit(1)
m = sshkey_re.match(key) try:
if not m: pubkey = pubkey_parse(key)
sys.stderr.write( except PublicKeyParseError as e:
"Error: cannot parse SSH protocol 2 base64-encoded key" sys.stderr.write("Error: " + str(e))
)
sys.exit(1) sys.exit(1)
fingerprint = sshkey_fingerprint(m.group('b64key')) fingerprint = pubkey.fingerprint()
url = getenv('SSHKEY_LOOKUP_URL', SSHKEY_LOOKUP_URL_DEFAULT) url = getenv('SSHKEY_LOOKUP_URL', SSHKEY_LOOKUP_URL_DEFAULT)
for key in lookup_by_fingerprint(url, fingerprint): for key in lookup_by_fingerprint(url, fingerprint):
sys.stdout.write(key) sys.stdout.write(key)
def lookup_main(): def lookup_main():
import sys import sys
import getopt
from os import environ from os import environ
if len(sys.argv) < 2: usage = (
sys.stderr.write('Usage: %s URL [USERNAME]\n' % sys.argv[0]) "Usage: {prog} -a URL\n"
" {prog} -u URL USERNAME\n"
" {prog} -f URL FINGERPRINT\n"
" {prog} URL [USERNAME]\n"
).format(prog=sys.argv[0])
try:
opts, args = getopt.getopt(sys.argv[1:], 'hafu')
except getopt.GetoptError as e:
sys.stderr.write("Error: %s\n" % str(e))
sys.stderr.write(usage)
sys.exit(1)
mode = 'x'
for o, a in opts:
if o == '-h':
sys.stdout.write(usage)
sys.exit(0)
else:
mode = o[1]
if len(args) == 0:
sys.stderr.write(usage)
sys.exit(1) sys.exit(1)
url = sys.argv[1] url = args[0]
if len(sys.argv) == 2:
environ['SSHKEY_LOOKUP_URL'] = url if mode == 'a':
lookup_by_fingerprint_main() response = lookup_all(url)
elif mode == 'f':
if len(args) < 2:
sys.stderr.write(usage)
sys.exit(1)
response = lookup_by_fingerprint(url, args[1])
elif mode == 'u':
if len(args) < 2:
sys.stderr.write(usage)
sys.exit(1)
response = lookup_by_username(url, args[1])
else: else:
username = sys.argv[2] if len(args) == 1:
for key in lookup_by_username(url, username): environ['SSHKEY_LOOKUP_URL'] = url
sys.stdout.write(key) return lookup_by_fingerprint_main()
else:
response = lookup_by_username(url, args[1])
for key in response:
sys.stdout.write(key)
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.views.decorators.http import require_http_methods, require_GET from django.views.decorators.http import require_http_methods, require_GET
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404, render_to_response from django.shortcuts import get_object_or_404, render_to_response
from django.template import RequestContext from django.template import RequestContext
from django.contrib import messages from django.contrib import messages
...@@ -39,8 +40,14 @@ from django_sshkey import settings ...@@ -39,8 +40,14 @@ from django_sshkey import settings
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from django_sshkey.forms import UserKeyForm from django_sshkey.forms import UserKeyForm
@require_GET @require_http_methods(['GET', 'POST'])
@csrf_exempt
def lookup(request): def lookup(request):
if request.method == 'POST':
payload = request.read()
key = UserKey.objects.get(id=int(payload))
key.touch()
return HttpResponse(str(key.last_used), mimetype='text/plain')
try: try:
fingerprint = request.GET['fingerprint'] fingerprint = request.GET['fingerprint']
keys = UserKey.objects.filter(fingerprint=fingerprint) keys = UserKey.objects.filter(fingerprint=fingerprint)
...@@ -54,13 +61,9 @@ def lookup(request): ...@@ -54,13 +61,9 @@ def lookup(request):
for key in keys: for key in keys:
if settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS: if settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS:
options = settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS.format( options = settings.SSHKEY_AUTHORIZED_KEYS_OPTIONS.format(
username=key.user.username) + ' ' username=key.user.username,
elif settings.SSHKEY_AUTHORIZED_KEYS_COMMAND: key_id=key.id,
options = 'command="%s" ' % ( ) + ' '
settings.SSHKEY_AUTHORIZED_KEYS_COMMAND
.format(username=key.user.username)
.replace('"', r'\"')
)
else: else:
options = '' options = ''
response += options + key.key + '\n' response += options + key.key + '\n'
...@@ -72,7 +75,7 @@ def userkey_list(request): ...@@ -72,7 +75,7 @@ def userkey_list(request):
userkey_list = UserKey.objects.filter(user=request.user) userkey_list = UserKey.objects.filter(user=request.user)
return render_to_response( return render_to_response(
'sshkey/userkey_list.html', 'sshkey/userkey_list.html',
{ 'userkey_list': userkey_list }, { 'userkey_list': userkey_list, 'allow_edit': settings.SSHKEY_ALLOW_EDIT },
context_instance = RequestContext(request), context_instance = RequestContext(request),
) )
...@@ -81,6 +84,7 @@ def userkey_list(request): ...@@ -81,6 +84,7 @@ def userkey_list(request):
def userkey_add(request): def userkey_add(request):
if request.method == 'POST': if request.method == 'POST':
userkey = UserKey(user=request.user) userkey = UserKey(user=request.user)
userkey.request = request
form = UserKeyForm(request.POST, instance=userkey) form = UserKeyForm(request.POST, instance=userkey)
if form.is_valid(): if form.is_valid():
form.save() form.save()
...@@ -88,7 +92,7 @@ def userkey_add(request): ...@@ -88,7 +92,7 @@ def userkey_add(request):
url = request.GET.get('next', default_redirect) url = request.GET.get('next', default_redirect)
if not is_safe_url(url=url, host=request.get_host()): if not is_safe_url(url=url, host=request.get_host()):
url = default_redirect url = default_redirect
message = 'SSH key %s was saved.' % userkey.name message = 'SSH public key %s was added.' % userkey.name
messages.success(request, message, fail_silently=True) messages.success(request, message, fail_silently=True)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
...@@ -102,6 +106,8 @@ def userkey_add(request): ...@@ -102,6 +106,8 @@ def userkey_add(request):
@login_required @login_required
@require_http_methods(['GET', 'POST']) @require_http_methods(['GET', 'POST'])
def userkey_edit(request, pk): def userkey_edit(request, pk):
if not settings.SSHKEY_ALLOW_EDIT:
raise PermissionDenied
userkey = get_object_or_404(UserKey, pk=pk) userkey = get_object_or_404(UserKey, pk=pk)
if userkey.user != request.user: if userkey.user != request.user:
raise PermissionDenied raise PermissionDenied
...@@ -113,7 +119,7 @@ def userkey_edit(request, pk): ...@@ -113,7 +119,7 @@ def userkey_edit(request, pk):
url = request.GET.get('next', default_redirect) url = request.GET.get('next', default_redirect)
if not is_safe_url(url=url, host=request.get_host()): if not is_safe_url(url=url, host=request.get_host()):
url = default_redirect url = default_redirect
message = 'SSH key %s was saved.' % userkey.name message = 'SSH public key %s was saved.' % userkey.name
messages.success(request, message, fail_silently=True) messages.success(request, message, fail_silently=True)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
...@@ -131,6 +137,6 @@ def userkey_delete(request, pk): ...@@ -131,6 +137,6 @@ def userkey_delete(request, pk):
if userkey.user != request.user: if userkey.user != request.user:
raise PermissionDenied raise PermissionDenied
userkey.delete() userkey.delete()
message = 'SSH key %s was deleted.' % userkey.name message = 'SSH public key %s was deleted.' % userkey.name
messages.success(request, message, fail_silently=True) messages.success(request, message, fail_silently=True)
return HttpResponseRedirect(reverse('django_sshkey.views.userkey_list')) return HttpResponseRedirect(reverse('django_sshkey.views.userkey_list'))
# Django settings for testproject project. # Django settings for testproject project.
import getpass
import socket
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
DEFAULT_FROM_EMAIL = '%s@%s' % (getpass.getuser(), socket.gethostname())
ADMINS = ( ADMINS = (
# ('Your Name', 'your_email@example.com'), # ('Your Name', 'your_email@example.com'),
) )
......
../sshkey/templates.example ../django_sshkey/templates.example
\ No newline at end of file \ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment