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 = ( ...@@ -258,6 +258,7 @@ THIRD_PARTY_APPS = (
'sizefield', 'sizefield',
'taggit', 'taggit',
'statici18n', 'statici18n',
'django_sshkey',
) )
# Apps specific for this project go here. # Apps specific for this project go here.
......
...@@ -40,6 +40,7 @@ from django.template.loader import render_to_string ...@@ -40,6 +40,7 @@ from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django_sshkey.models import UserKey
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk from storage.models import Disk
from vm.models import ( from vm.models import (
...@@ -1119,3 +1120,30 @@ class UserCreationForm(OrgUserCreationForm): ...@@ -1119,3 +1120,30 @@ class UserCreationForm(OrgUserCreationForm):
if commit: if commit:
user.save() user.save()
return user 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 ( ...@@ -29,9 +29,11 @@ from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, BooleanField DateTimeField, permalink, BooleanField
) )
from django.db.models.signals import post_save, pre_delete
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.translation import ugettext_lazy as _, override, ugettext 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.models import TimeStampedModel
from model_utils.fields import StatusField from model_utils.fields import StatusField
...@@ -39,6 +41,8 @@ from model_utils import Choices ...@@ -39,6 +41,8 @@ from model_utils import Choices
from acl.models import AclBase from acl.models import AclBase
from vm.tasks.agent_tasks import add_keys, del_keys
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -224,3 +228,33 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -224,3 +228,33 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
else: else:
logger.debug("Do not register save_org_id to djangosaml2 pre_user_save") 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, ...@@ -24,6 +24,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
from vm.models import Instance, Node, InstanceTemplate, Lease from vm.models import Instance, Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
class VmListTable(Table): class VmListTable(Table):
...@@ -291,3 +292,33 @@ class LeaseListTable(Table): ...@@ -291,3 +292,33 @@ class LeaseListTable(Table):
fields = ('name', 'suspend_interval_seconds', fields = ('name', 'suspend_interval_seconds',
'delete_interval_seconds', ) 'delete_interval_seconds', )
prefix = "lease-" 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" %} {% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Profile" %}{% endblock %} {% block title-page %}{% trans "Profile" %}{% endblock %}
...@@ -49,4 +50,20 @@ ...@@ -49,4 +50,20 @@
</div> </div>
</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 %} {% 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 ...@@ -32,6 +32,7 @@ from ..views import VmRenewView
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
from django_sshkey.models import UserKey
import django.conf import django.conf
settings = django.conf.settings.FIREWALL_SETTINGS settings = django.conf.settings.FIREWALL_SETTINGS
...@@ -1959,3 +1960,61 @@ class VmListTest(LoginMixin, TestCase): ...@@ -1959,3 +1960,61 @@ class VmListTest(LoginMixin, TestCase):
's': "A:B:C:D:" 's': "A:B:C:D:"
}) })
self.assertEqual(200, resp.status_code) 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 ( ...@@ -36,6 +36,7 @@ from .views import (
UserCreationView, UserCreationView,
get_vm_screenshot, get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView, ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -158,4 +159,14 @@ urlpatterns = patterns( ...@@ -158,4 +159,14 @@ urlpatterns = patterns(
url(r'^group/(?P<group_pk>\d+)/create/$', url(r'^group/(?P<group_pk>\d+)/create/$',
UserCreationView.as_view(), UserCreationView.as_view(),
name="dashboard.views.create-user"), 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, ...@@ -53,17 +53,19 @@ from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
PermissionRequiredMixin) PermissionRequiredMixin)
from braces.views._access import AccessMixin from braces.views._access import AccessMixin
from django_sshkey.models import UserKey
from .forms import ( from .forms import (
CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm, CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm, NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm, UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, VmSaveForm, UserKeyForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
) )
from .tables import ( from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable, NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable, GroupListTable, UserKeyListTable
) )
from vm.models import ( from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface, Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
...@@ -2538,6 +2540,11 @@ class MyPreferencesView(UpdateView): ...@@ -2538,6 +2540,11 @@ class MyPreferencesView(UpdateView):
user=self.request.user), user=self.request.user),
'change_language': MyProfileForm(instance=self.get_object()), '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 return context
def get_object(self, queryset=None): def get_object(self, queryset=None):
...@@ -2855,3 +2862,72 @@ def toggle_use_gravatar(request, **kwargs): ...@@ -2855,3 +2862,72 @@ def toggle_use_gravatar(request, **kwargs):
json.dumps({'new_avatar_url': new_avatar_url}), json.dumps({'new_avatar_url': new_avatar_url}),
content_type="application/json", 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): ...@@ -56,3 +56,18 @@ def start_access_server(vm):
@celery.task(name='agent.update') @celery.task(name='agent.update')
def update(vm, data): def update(vm, data):
pass 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 ...@@ -9,6 +9,7 @@ django-celery==3.1.10
django-crispy-forms==1.4.0 django-crispy-forms==1.4.0
django-model-utils==2.0.3 django-model-utils==2.0.3
django-sizefield==0.4 django-sizefield==0.4
django-sshkey==2.2.0
django-statici18n==1.1 django-statici18n==1.1
django-tables2==0.15.0 django-tables2==0.15.0
django-taggit==0.12 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