Commit 1ee90a64 by Bach Dániel

Merge branch 'feature-ssh-key' into 'master'

Feature ssh key management
parents 2a386b8e 0b3d2280
......@@ -258,6 +258,7 @@ THIRD_PARTY_APPS = (
'sizefield',
'taggit',
'statici18n',
'django_sshkey',
)
# Apps specific for this project go here.
......
......@@ -40,6 +40,7 @@ from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget
from django_sshkey.models import UserKey
from firewall.models import Vlan, Host
from storage.models import Disk
from vm.models import (
......@@ -1119,3 +1120,30 @@ class UserCreationForm(OrgUserCreationForm):
if commit:
user.save()
return user
class UserKeyForm(forms.ModelForm):
name = forms.CharField(required=True, label=_('Name'))
key = forms.CharField(
label=_('Key'), required=True,
help_text=_('For example: ssh-rsa AAAAB3NzaC1yc2ED...'),
widget=forms.Textarea(attrs={'rows': 5}))
class Meta:
fields = ('name', 'key')
model = UserKey
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
super(UserKeyForm, self).__init__(*args, **kwargs)
def clean(self):
if self.user:
self.instance.user = self.user
return super(UserKeyForm, self).clean()
......@@ -29,9 +29,11 @@ from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, BooleanField
)
from django.db.models.signals import post_save, pre_delete
from django.template.loader import render_to_string
from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _, override, ugettext
from django_sshkey.models import UserKey
from model_utils.models import TimeStampedModel
from model_utils.fields import StatusField
......@@ -39,6 +41,8 @@ from model_utils import Choices
from acl.models import AclBase
from vm.tasks.agent_tasks import add_keys, del_keys
logger = getLogger(__name__)
......@@ -224,3 +228,33 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
else:
logger.debug("Do not register save_org_id to djangosaml2 pre_user_save")
def add_ssh_keys(sender, **kwargs):
from vm.models import Instance
userkey = kwargs.get('instance')
instances = Instance.get_objects_with_level(
'user', userkey.user).filter(status='RUNNING')
for i in instances:
logger.info('called add_keys(%s, %s)', i, userkey)
queue = i.get_remote_queue_name("agent")
add_keys.apply_async(args=(i.vm_name, [userkey.key]),
queue=queue)
def del_ssh_keys(sender, **kwargs):
from vm.models import Instance
userkey = kwargs.get('instance')
instances = Instance.get_objects_with_level(
'user', userkey.user).filter(status='RUNNING')
for i in instances:
logger.info('called del_keys(%s, %s)', i, userkey)
queue = i.get_remote_queue_name("agent")
del_keys.apply_async(args=(i.vm_name, [userkey.key]),
queue=queue)
post_save.connect(add_ssh_keys, sender=UserKey)
pre_delete.connect(del_ssh_keys, sender=UserKey)
......@@ -24,6 +24,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
from vm.models import Instance, Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
class VmListTable(Table):
......@@ -291,3 +292,33 @@ class LeaseListTable(Table):
fields = ('name', 'suspend_interval_seconds',
'delete_interval_seconds', )
prefix = "lease-"
class UserKeyListTable(Table):
name = LinkColumn(
'dashboard.views.userkey-detail',
args=[A('pk')],
verbose_name=_("Name"),
attrs={'th': {'data-sort': "string"}}
)
fingerprint = Column(
verbose_name=_("Fingerprint"),
attrs={'th': {'data-sort': "string"}}
)
created = Column(
verbose_name=_("Created at"),
attrs={'th': {'data-sort': "string"}}
)
actions = TemplateColumn(
verbose_name=_("Actions"),
template_name="dashboard/userkey-list/column-userkey-actions.html",
orderable=False,
)
class Meta:
model = UserKey
attrs = {'class': ('table table-bordered table-striped table-hover')}
fields = ('name', 'fingerprint', 'created', 'actions')
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Profile" %}{% endblock %}
......@@ -49,4 +50,20 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.userkey-create" %}" class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="icon-plus"></i> {% trans "add SSH key" %}
</a>
<h3 class="no-margin"><i class="icon-key"></i> {% trans "SSH public keys" %}</h3>
</div>
<div class="panel-body">
{% render_table userkey_table %}
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Create SSH public key" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="icon-key"></i> {% trans "Create SSH public key" %}</h3>
</div>
<div class="panel-body">
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit SSH public key" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="icon-key"></i> {% trans "Edit SSH public key" %}</h3>
</div>
<div class="panel-body">
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock %}
{% load i18n %}
<a href="{% url "dashboard.views.userkey-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="icon-edit"></i>
</a>
<a data-template-pk="{{ record.pk }}" href="{% url "dashboard.views.userkey-delete" pk=record.pk %}" class="btn btn-danger btn-xs template-delete" title="{% trans "Delete" %}">
<i class="icon-remove"></i>
</a>
......@@ -32,6 +32,7 @@ from ..views import VmRenewView
from storage.models import Disk
from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch
from django_sshkey.models import UserKey
import django.conf
settings = django.conf.settings.FIREWALL_SETTINGS
......@@ -1959,3 +1960,61 @@ class VmListTest(LoginMixin, TestCase):
's': "A:B:C:D:"
})
self.assertEqual(200, resp.status_code)
class SshKeyTest(LoginMixin, TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
self.u2 = User.objects.create(username='user2')
self.u2.set_password('password')
self.u2.save()
self.k1 = UserKey(key='ssh-rsa AAAAB3NzaC1yc2EC asd', user=self.u1)
self.k1.save()
def tearDown(self):
super(SshKeyTest, self).tearDown()
self.k1.delete()
self.u1.delete()
def test_permitted_edit(self):
c = Client()
self.login(c, self.u1)
resp = c.post("/dashboard/sshkey/1/",
{'key': 'ssh-rsa AAAAB3NzaC1yc2EC'})
self.assertEqual(UserKey.objects.get(id=1).user, self.u1)
self.assertEqual(200, resp.status_code)
def test_unpermitted_edit(self):
c = Client()
self.login(c, self.u2)
resp = c.post("/dashboard/sshkey/1/",
{'key': 'ssh-rsa AAAAB3NzaC1yc2EC'})
self.assertEqual(UserKey.objects.get(id=1).user, self.u1)
self.assertEqual(403, resp.status_code)
def test_permitted_add(self):
c = Client()
self.login(c, self.u1)
resp = c.post("/dashboard/sshkey/create/",
{'name': 'asd', 'key': 'ssh-rsa AAAAB3NzaC1yc2EC'})
self.assertEqual(UserKey.objects.get(id=2).user, self.u1)
self.assertEqual(302, resp.status_code)
def test_permitted_delete(self):
c = Client()
self.login(c, self.u1)
resp = c.post("/dashboard/sshkey/delete/1/")
self.assertEqual(302, resp.status_code)
def test_unpermitted_delete(self):
c = Client()
self.login(c, self.u2)
resp = c.post("/dashboard/sshkey/delete/1/")
self.assertEqual(403, resp.status_code)
......@@ -36,6 +36,7 @@ from .views import (
UserCreationView,
get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate,
)
urlpatterns = patterns(
......@@ -158,4 +159,14 @@ urlpatterns = patterns(
url(r'^group/(?P<group_pk>\d+)/create/$',
UserCreationView.as_view(),
name="dashboard.views.create-user"),
url(r'^sshkey/delete/(?P<pk>\d+)/$',
UserKeyDelete.as_view(),
name="dashboard.views.userkey-delete"),
url(r'^sshkey/(?P<pk>\d+)/$',
UserKeyDetail.as_view(),
name="dashboard.views.userkey-detail"),
url(r'^sshkey/create/$',
UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"),
)
......@@ -53,17 +53,19 @@ from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
PermissionRequiredMixin)
from braces.views._access import AccessMixin
from django_sshkey.models import UserKey
from .forms import (
CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm,
VmSaveForm, UserKeyForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
)
from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable,
GroupListTable, UserKeyListTable
)
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
......@@ -2538,6 +2540,11 @@ class MyPreferencesView(UpdateView):
user=self.request.user),
'change_language': MyProfileForm(instance=self.get_object()),
}
table = UserKeyListTable(
UserKey.objects.filter(user=self.request.user),
request=self.request)
table.page = None
context['userkey_table'] = table
return context
def get_object(self, queryset=None):
......@@ -2855,3 +2862,72 @@ def toggle_use_gravatar(request, **kwargs):
json.dumps({'new_avatar_url': new_avatar_url}),
content_type="application/json",
)
class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = UserKey
template_name = "dashboard/userkey-edit.html"
form_class = UserKeyForm
success_message = _("Successfully modified SSH key.")
def get(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied()
return super(UserKeyDetail, self).get(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy("dashboard.views.userkey-detail",
kwargs=self.kwargs)
def post(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied()
return super(UserKeyDetail, self).post(self, request, args, kwargs)
class UserKeyDelete(LoginRequiredMixin, DeleteView):
model = UserKey
def get_success_url(self):
return reverse("dashboard.views.profile-preferences")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def delete(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("SSH key successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = UserKey
form_class = UserKeyForm
template_name = "dashboard/userkey-create.html"
success_message = _("Successfully created a new SSH key.")
def get_success_url(self):
return reverse_lazy("dashboard.views.profile-preferences")
def get_form_kwargs(self):
kwargs = super(UserKeyCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
......@@ -56,3 +56,18 @@ def start_access_server(vm):
@celery.task(name='agent.update')
def update(vm, data):
pass
@celery.task(name='agent.add_keys')
def add_keys(vm, keys):
pass
@celery.task(name='agent.del_keys')
def del_keys(vm, keys):
pass
@celery.task(name='agent.get_keys')
def get_keys(vm):
pass
......@@ -9,6 +9,7 @@ django-celery==3.1.10
django-crispy-forms==1.4.0
django-model-utils==2.0.3
django-sizefield==0.4
django-sshkey==2.2.0
django-statici18n==1.1
django-tables2==0.15.0
django-taggit==0.12
......
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