1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-24 23:25:24 +00:00

Accounts app improvements and tests

This commit is contained in:
Real-Gecko 2020-10-14 14:37:46 +06:00 committed by catborise
parent 8afef36656
commit 5172a9f619
20 changed files with 622 additions and 227 deletions

View file

@ -1,9 +1,10 @@
from appsettings.settings import app_settings
from django.contrib.auth import get_user_model
from django.forms import ModelForm, ValidationError from django.forms import ModelForm, ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from appsettings.models import AppSettings from .models import UserInstance, UserSSHKey
from .utils import validate_ssh_key
from .models import UserInstance
class UserInstanceForm(ModelForm): class UserInstanceForm(ModelForm):
@ -18,7 +19,7 @@ class UserInstanceForm(ModelForm):
def clean_instance(self): def clean_instance(self):
instance = self.cleaned_data['instance'] instance = self.cleaned_data['instance']
if AppSettings.objects.get(key="ALLOW_INSTANCE_MULTIPLE_OWNER").value == 'False': if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == 'False':
exists = UserInstance.objects.filter(instance=instance) exists = UserInstance.objects.filter(instance=instance)
if exists: if exists:
raise ValidationError(_('Instance owned by another user')) raise ValidationError(_('Instance owned by another user'))
@ -28,3 +29,43 @@ class UserInstanceForm(ModelForm):
class Meta: class Meta:
model = UserInstance model = UserInstance
fields = '__all__' fields = '__all__'
class ProfileForm(ModelForm):
class Meta:
model = get_user_model()
fields = ('first_name', 'last_name', 'email')
class UserSSHKeyForm(ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
self.publickeys = UserSSHKey.objects.filter(user=self.user)
super().__init__(*args, **kwargs)
def clean_keyname(self):
for key in self.publickeys:
if self.cleaned_data['keyname'] == key.keyname:
raise ValidationError(_("Key name already exist"))
return self.cleaned_data['keyname']
def clean_keypublic(self):
for key in self.publickeys:
if self.cleaned_data['keypublic'] == key.keypublic:
raise ValidationError(_("Public key already exist"))
if not validate_ssh_key(self.cleaned_data['keypublic']):
raise ValidationError(_('Invalid key'))
return self.cleaned_data['keypublic']
def save(self, commit=True):
ssh_key = super().save(commit=False)
ssh_key.user = self.user
if commit:
ssh_key.save()
return ssh_key
class Meta:
model = UserSSHKey
fields = ('keyname', 'keypublic')

View file

@ -1,9 +1,7 @@
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from instances.models import Instance from instances.models import Instance
@ -11,6 +9,7 @@ class UserInstanceManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related('instance', 'user') return super().get_queryset().select_related('instance', 'user')
class UserInstance(models.Model): class UserInstance(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
instance = models.ForeignKey(Instance, on_delete=models.CASCADE) instance = models.ForeignKey(Instance, on_delete=models.CASCADE)

View file

@ -5,20 +5,15 @@
{% load qr_code %} {% load qr_code %}
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %} {% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% block page_header %}{% trans "User Profile" %}: {{ user }}{% endblock page_header %}
{% block page_header_extra %}
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success">
{% icon 'plus' %}
</a>
{% endblock page_header_extra %}
{% block content %} {% block content %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<a href="{% url 'user_instance_create' user.id %}" class="btn btn-success btn-header float-right">
{% icon 'plus' %}
</a>
<h2 class="page-header">{% trans "User Profile" %}: {{ user }}</h2>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a> <a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
@ -55,12 +50,12 @@
<td>{{ inst.is_change }}</td> <td>{{ inst.is_change }}</td>
<td>{{ inst.is_delete }}</td> <td>{{ inst.is_delete }}</td>
<td style="width:5px;"> <td style="width:5px;">
<a href="{% url 'user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}"> <a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
{% icon 'pencil' %} {% icon 'pencil' %}
</a> </a>
</td> </td>
<td style="width:5px;"> <td style="width:5px;">
<a class="btn btn-sm btn-secondary" href="{% url 'user_instance_delete' inst.id %}" title="{% trans "Delete" %}"> <a class="btn btn-sm btn-secondary" href="{% url 'accounts:user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
{% icon 'trash' %} {% icon 'trash' %}
</a> </a>
</td> </td>

View file

@ -41,7 +41,7 @@
{% for user in users %} {% for user in users %}
<tr class="{% if not user.is_active %}danger{% endif %}"> <tr class="{% if not user.is_active %}danger{% endif %}">
<td> <td>
<a href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a> <a href="{% url 'accounts:account' user.id %}"><strong>{{ user.username }}</strong></a>
<a data-toggle="modal" href="#editUser{{ user.id }}" class="float-right" title="{% trans "Edit" %}"> <a data-toggle="modal" href="#editUser{{ user.id }}" class="float-right" title="{% trans "Edit" %}">
<span class="fa fa-cog"></span> <span class="fa fa-cog"></span>
</a> </a>

View file

@ -32,7 +32,7 @@
<div class="card-header bg-secondary"> <div class="card-header bg-secondary">
{% endif %} {% endif %}
<h5 class="my-0 card-title"> <h5 class="my-0 card-title">
<a class="card-link text-light" href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a> <a class="card-link text-light" href="{% url 'accounts:account' user.id %}"><strong>{{ user.username }}</strong></a>
<a class="card-link text-light float-right" data-toggle="modal" href="#editUser{{ user.id }}" title="{% trans "Edit" %}"> <a class="card-link text-light float-right" data-toggle="modal" href="#editUser{{ user.id }}" title="{% trans "Edit" %}">
<span class="fa fa-cog"></span> <span class="fa fa-cog"></span>
</a> </a>

View file

@ -7,23 +7,28 @@
{% block title %}{%trans "Change Password" %}{% endblock title %} {% block title %}{%trans "Change Password" %}{% endblock title %}
{% block content %} {% block content %}
<div class="card"> <div class="row">
<div class="card-header"> <div class="offset-2 col-lg-8">
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4> <div class="card">
</div> <div class="card-header">
<div class="card-body"> <h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
<form method="post" id="password-change"> </div>
{% csrf_token %} <div class="card-body">
{% bootstrap_form form layout='horizontal' %} <form method="post" id="password-change">
</form> {% csrf_token %}
</div> {% bootstrap_form form layout='horizontal' %}
<div class="card-footer"> </form>
<div class="float-right"> </div>
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a> <div class="card-footer">
<button type="submit" form="password-change" class="btn btn-success"> <div class="float-right">
{% icon 'check' %} {% trans "Change" %} <a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %}
</button> {% trans "Cancel" %}</a>
<button type="submit" form="password-change" class="btn btn-success">
{% icon 'check' %} {% trans "Change" %}
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View file

@ -1,93 +1,80 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap4 %}
{% load icons %} {% load icons %}
{% load tags_fingerprint %} {% load tags_fingerprint %}
{% block title %}{% trans "Profile" %}{% endblock %}
{% block title %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock %}
{% block page_header %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock page_header %}
{% block content %} {% block content %}
<!-- Page Heading --> <ul class="nav nav-tabs">
<div class="row"> <li class="nav-item">
<div class="col-lg-12"> <a class="nav-link active" data-toggle="tab" href="#edit-profile">{% trans "Edit Profile" %}</a>
<h2 class="page-header">{% trans "Profile" %}</h2> </li>
</div> <li class="nav-item">
</div> <a class="nav-link" data-toggle="tab" href="#ssh-keys">{% trans "SSH Keys" %}</a>
<!-- /.row --> </li>
</ul>
{% include 'errors_block.html' %} <div class="tab-content">
<div class="tab-pane tab-pane-bordered active" id="edit-profile">
<div class="row"> <div class="card">
<div class="col-lg-12"> <div class="card-body">
<h3 class="page-header">{% trans "Edit Profile" %}</h3> <form method="post" action="" role="form" aria-label="Edit user info form">
{% csrf_token %}
{% bootstrap_form profile_form layout='horizontal' %}
{% if perms.accounts.change_password %} {% if perms.accounts.change_password %}
<a href="{% url 'change_password' %}" class="ml-3 btn btn-primary">{% icon 'lock' %} {% trans "Change Password" %}</a> <a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
{% icon 'lock' %} {% trans "Change Password" %}
</a>
{% endif %} {% endif %}
<form method="post" action="" role="form" aria-label="Edit user info form">{% csrf_token %} <div class="form-group mb-0 float-right">
<div class="form-group"> <button type="submit" class="btn btn-primary">
<label class="col-sm-2 col-form-label">{% trans "Login" %}</label> {% icon 'pencil' %} {% trans "Update" %}
<div class="col-sm-4"> </button>
<input type="text" class="form-control" value="{{ request.user.username }}" disabled> </div>
</div> </form>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 col-form-label">{% trans "Username" %}</label>
<div class="col-sm-4">
<input type="text" class="form-control" name="username" value="{{ request.user.first_name }}" pattern="[0-9a-zA-Z]+">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 col-form-label">{% trans "Email" %}</label>
<div class="col-sm-4">
<input type="email" class="form-control" name="email" value="{{ request.user.email }}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
</div>
</div>
</form>
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
{% if publickeys %}
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-hover">
<tbody class="text-center">
{% for key in publickeys %}
<tr>
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
<td>
<form action="" method="post" role="form" aria-label="Delete user public key form">{% csrf_token %}
<input type="hidden" name="keyid" value="{{ key.id }}"/>
<button type="submit" class="btn btn-sm btn-secondary" name="keydelete" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
<span class="fa fa-trash"></span>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<form method="post" action="" role="form" aria-label="Add key to user form">{% csrf_token %}
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 col-form-label">{% trans "Key name" %}</label>
<div class="col-sm-4">
<input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 col-form-label">{% trans "Public key" %}</label>
<div class="col-sm-8">
<textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">{% trans "Add" %}</button>
</div>
</div>
</form>
</div>
</div> </div>
{% endblock %} </div>
</div>
<div class="tab-pane tab-pane-bordered fade" id="ssh-keys">
{% if publickeys %}
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-hover">
<tbody class="text-center">
{% for key in publickeys %}
<tr>
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
<td>
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
{% icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header">
{%trans "Add SSH Key" %}
</div>
<div class="card-body">
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
{% csrf_token %}
{% bootstrap_form ssh_key_form layout='horizontal' %}
<div class="form-group mb-0 float-right">
<button type="submit" class="btn btn-primary">
{% icon 'plus' %} {% trans "Add" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,45 +1,111 @@
from django.contrib.auth.models import Permission, User from appsettings.settings import app_settings
from computes.models import Compute
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.shortcuts import reverse from django.shortcuts import reverse
from django.test import Client, TestCase from django.test import Client, TestCase
from instances.models import Instance
from instances.utils import refr
from libvirt import VIR_DOMAIN_UNDEFINE_NVRAM
from vrtManager.create import wvmCreate
from accounts.forms import UserInstanceForm, UserSSHKeyForm
from accounts.models import UserInstance, UserSSHKey
from accounts.utils import validate_ssh_key
class AccountsTestCase(TestCase): class AccountsTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Add users for testing purposes
User = get_user_model()
cls.admin_user = User.objects.get(pk=1)
cls.test_user = User.objects.create_user(username='test', password='test')
# Add localhost compute
cls.compute = Compute(
name='test-compute',
hostname='localhost',
login='',
password='',
details='local',
type=4,
)
cls.compute.save()
cls.connection = wvmCreate(
cls.compute.hostname,
cls.compute.login,
cls.compute.password,
cls.compute.type,
)
# Add disks for testing
cls.connection.create_volume(
'default',
'test-volume',
1,
'qcow2',
False,
0,
0,
)
# XML for testing vm
with open('conf/test-vm.xml', 'r') as f:
cls.xml = f.read()
# Create testing vm from XML
cls.connection._defineXML(cls.xml)
refr(cls.compute)
cls.instance = Instance.objects.get(pk=1)
@classmethod
def tearDownClass(cls):
# Destroy testing vm
cls.instance.proxy.delete_all_disks()
cls.instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM)
super().tearDownClass()
def setUp(self): def setUp(self):
self.client.login(username='admin', password='admin') self.client.login(username='admin', password='admin')
user = User.objects.create_user(username='test', password='test')
permission = Permission.objects.get(codename='change_password') permission = Permission.objects.get(codename='change_password')
user.user_permissions.add(permission) self.test_user.user_permissions.add(permission)
self.rsa_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6OOdbfv27QVnSC6sKxGaHb6YFc+3gxCkyVR3cTSXE/n5BEGf8aOgBpepULWa1RZfxYHY14PlKULDygdXSdrrR2kNSwoKz/Oo4d+3EE92L7ocl1+djZbptzgWgtw1OseLwbFik+iKlIdqPsH+IUQvX7yV545ZQtAP8Qj1R+uCqkw== test@test'
self.ecdsa_key = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJc5xpT3R0iFJYNZbmWgAiDlHquX/BcV1kVTsnBfiMsZgU3lGaqz2eb7IBcir/dxGnsVENTTmPQ6sNcxLxT9kkQ= realgecko@archlinux'
def test_profile(self): def test_profile(self):
response = self.client.get(reverse('profile')) response = self.client.get(reverse('accounts:profile'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account', args=[2])) response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
self.assertEqual(response.status_code, 200)
def test_account_with_otp(self):
settings.OTP_ENABLED = True
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_login_logout(self): def test_login_logout(self):
user = User.objects.get(username='test')
self.assertEqual(user.id, 2)
client = Client() client = Client()
response = client.post(reverse("login"), {"username": "test", "password": "test"}) response = client.post(reverse("accounts:login"), {"username": "test", "password": "test"})
self.assertRedirects(response, reverse('profile')) self.assertRedirects(response, reverse('accounts:profile'))
response = client.get(reverse('logout')) response = client.get(reverse('accounts:logout'))
self.assertRedirects(response, reverse('login')) self.assertRedirects(response, reverse('accounts:login'))
def test_password_change(self): def test_change_password(self):
client = Client() self.client.force_login(self.test_user)
logged_in = client.login(username='test', password='test') response = self.client.get(reverse('accounts:change_password'))
self.assertTrue(logged_in)
response = client.get(reverse('change_password'))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.post( response = self.client.post(
reverse('change_password'), reverse('accounts:change_password'),
{ {
'old_password': 'wrongpass', 'old_password': 'wrongpass',
'new_password1': 'newpw', 'new_password1': 'newpw',
@ -48,17 +114,154 @@ class AccountsTestCase(TestCase):
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response = client.post( response = self.client.post(
reverse('change_password'), reverse('accounts:change_password'),
{ {
'old_password': 'test', 'old_password': 'test',
'new_password1': 'newpw', 'new_password1': 'newpw',
'new_password2': 'newpw', 'new_password2': 'newpw',
}, },
) )
self.assertRedirects(response, reverse('profile')) self.assertRedirects(response, reverse('accounts:profile'))
client.logout() self.client.logout()
logged_in = client.login(username='test', password='newpw') logged_in = self.client.login(username='test', password='newpw')
self.assertTrue(logged_in) self.assertTrue(logged_in)
def test_user_instance_create_update_delete(self):
# create
response = self.client.get(reverse('accounts:user_instance_create', args=[self.test_user.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('accounts:user_instance_create', args=[self.test_user.id]),
{
'user': self.test_user.id,
'instance': self.instance.id,
'is_change': False,
'is_delete': False,
'is_vnc': False,
},
)
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
user_instance: UserInstance = UserInstance.objects.get(pk=1)
self.assertEqual(user_instance.user, self.test_user)
self.assertEqual(user_instance.instance, self.instance)
self.assertEqual(user_instance.is_change, False)
self.assertEqual(user_instance.is_delete, False)
self.assertEqual(user_instance.is_vnc, False)
# update
response = self.client.get(reverse('accounts:user_instance_update', args=[user_instance.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('accounts:user_instance_update', args=[user_instance.id]),
{
'user': self.test_user.id,
'instance': self.instance.id,
'is_change': True,
'is_delete': True,
'is_vnc': True,
},
)
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
user_instance: UserInstance = UserInstance.objects.get(pk=1)
self.assertEqual(user_instance.user, self.test_user)
self.assertEqual(user_instance.instance, self.instance)
self.assertEqual(user_instance.is_change, True)
self.assertEqual(user_instance.is_delete, True)
self.assertEqual(user_instance.is_vnc, True)
# delete
response = self.client.get(reverse('accounts:user_instance_delete', args=[user_instance.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('accounts:user_instance_delete', args=[user_instance.id]))
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
# test 'next' redirect during deletion
user_instance = UserInstance.objects.create(user=self.test_user, instance=self.instance)
response = self.client.post(
reverse('accounts:user_instance_delete', args=[user_instance.id]) + '?next=' + reverse('index'))
self.assertRedirects(response, reverse('index'))
def test_update_user_profile(self):
self.client.force_login(self.test_user)
user = get_user_model().objects.get(username='test')
self.assertEqual(user.first_name, '')
self.assertEqual(user.last_name, '')
self.assertEqual(user.email, '')
response = self.client.post(reverse('accounts:profile'), {
'first_name': 'first name',
'last_name': 'last name',
'email': 'email@mail.mail',
})
self.assertRedirects(response, reverse('accounts:profile'))
user = get_user_model().objects.get(username='test')
self.assertEqual(user.first_name, 'first name')
self.assertEqual(user.last_name, 'last name')
self.assertEqual(user.email, 'email@mail.mail')
def test_create_delete_ssh_key(self):
response = self.client.get(reverse('accounts:ssh_key_create'))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('accounts:ssh_key_create'), {
'keyname': 'keyname',
'keypublic': self.rsa_key,
})
self.assertRedirects(response, reverse('accounts:profile'))
key = UserSSHKey.objects.get(pk=1)
self.assertEqual(key.keyname, 'keyname')
self.assertEqual(key.keypublic, self.rsa_key)
response = self.client.get(reverse('accounts:ssh_key_delete', args=[1]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('accounts:ssh_key_delete', args=[1]))
self.assertRedirects(response, reverse('accounts:profile'))
def test_validate_ssh_key(self):
self.assertFalse(validate_ssh_key(''))
self.assertFalse(validate_ssh_key('ssh-rsa ABBA test@test'))
self.assertFalse(validate_ssh_key('ssh-rsa AAAABwdzZGY= test@test'))
self.assertFalse(validate_ssh_key('ssh-rsa AAA test@test'))
# validate ecdsa key
self.assertTrue(validate_ssh_key(self.ecdsa_key))
def test_forms(self):
# raise available validation errors for maximum coverage
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
form.save()
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
self.assertFalse(form.is_valid())
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': 'invalid key'}, user=self.test_user)
self.assertFalse(form.is_valid())
app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER = 'False'
form = UserInstanceForm({
'user': self.admin_user.id,
'instance': self.instance.id,
'is_change': False,
'is_delete': False,
'is_vnc': False,
})
form.save()
form = UserInstanceForm({
'user': self.test_user.id,
'instance': self.instance.id,
'is_change': False,
'is_delete': False,
'is_vnc': False,
})
self.assertFalse(form.is_valid())

View file

@ -5,6 +5,8 @@ from django_otp.forms import OTPAuthenticationForm
from . import views from . import views
app_name = 'accounts'
urlpatterns = [ urlpatterns = [
path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'), path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
path('profile/', views.profile, name='profile'), path('profile/', views.profile, name='profile'),
@ -13,6 +15,8 @@ urlpatterns = [
path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'), path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'), path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'), path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'),
path('ssh_key/create/', views.ssh_key_create, name='ssh_key_create'),
path('ssh_key/<int:pk>/delete/', views.ssh_key_delete, name='ssh_key_delete'),
] ]
if settings.OTP_ENABLED: if settings.OTP_ENABLED:

View file

@ -1,3 +1,7 @@
import base64
import binascii
import struct
from django_otp import devices_for_user from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
@ -7,3 +11,29 @@ def get_user_totp_device(user):
for device in devices: for device in devices:
if isinstance(device, TOTPDevice): if isinstance(device, TOTPDevice):
return device return device
def validate_ssh_key(key):
array = key.encode().split()
# Each rsa-ssh key has 3 different strings in it, first one being
# typeofkey second one being keystring third one being username .
if len(array) != 3:
return False
typeofkey = array[0]
string = array[1]
username = array[2]
# must have only valid rsa-ssh key characters ie binascii characters
try:
data = base64.decodestring(string)
except binascii.Error:
return False
# unpack the contents of data, from data[:4] , property of ssh key .
try:
str_len = struct.unpack('>I', data[:4])[0]
except struct.error:
return False
# data[4:str_len] must have string which matches with the typeofkey, another ssh key property.
if data[4:4 + str_len] == typeofkey:
return True
else:
return False

View file

@ -1,14 +1,15 @@
from admin.decorators import superuser_only from admin.decorators import superuser_only
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.forms import PasswordChangeForm
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from instances.models import Instance from instances.models import Instance
from accounts.forms import ProfileForm, UserSSHKeyForm
from accounts.models import * from accounts.models import *
from . import forms from . import forms
@ -16,49 +17,50 @@ from .utils import get_user_totp_device
def profile(request): def profile(request):
error_messages = []
publickeys = UserSSHKey.objects.filter(user_id=request.user.id) publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
profile_form = ProfileForm(request.POST or None, instance=request.user)
ssh_key_form = UserSSHKeyForm()
if request.method == "POST": if profile_form.is_valid():
if "username" in request.POST: profile_form.save()
username = request.POST.get("username", "") messages.success(request, _('Profile updated'))
email = request.POST.get("email", "") return redirect('accounts:profile')
request.user.first_name = username
request.user.email = email return render(request, "profile.html", {
request.user.save() 'publickeys': publickeys,
return HttpResponseRedirect(request.get_full_path()) 'profile_form': profile_form,
if "keyname" in request.POST: 'ssh_key_form': ssh_key_form,
keyname = request.POST.get("keyname", "") })
keypublic = request.POST.get("keypublic", "")
for key in publickeys:
if keyname == key.keyname: def ssh_key_create(request):
msg = _("Key name already exist") key_form = UserSSHKeyForm(request.POST or None, user=request.user)
error_messages.append(msg) if key_form.is_valid():
if keypublic == key.keypublic: key_form.save()
msg = _("Public key already exist") messages.success(request, _('SSH key added'))
error_messages.append(msg) return redirect('accounts:profile')
if "\n" in keypublic or "\r" in keypublic:
msg = _("Invalid characters in public key") return render(request, 'common/form.html', {
error_messages.append(msg) 'form': key_form,
if not error_messages: 'title': _('Add SSH key'),
addkeypublic = UserSSHKey( })
user_id=request.user.id,
keyname=keyname,
keypublic=keypublic, def ssh_key_delete(request, pk):
) ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user)
addkeypublic.save() if request.method == 'POST':
return HttpResponseRedirect(request.get_full_path()) ssh_key.delete()
if "keydelete" in request.POST: messages.success(request, _('SSH key deleted'))
keyid = request.POST.get("keyid", "") return redirect('accounts:profile')
delkeypublic = UserSSHKey.objects.get(id=keyid)
delkeypublic.delete() return render(request, 'common/confirm_delete.html', {
return HttpResponseRedirect(request.get_full_path()) 'object': ssh_key,
return render(request, "profile.html", locals()) 'title': _('Delete SSH key'),
})
@superuser_only @superuser_only
def account(request, user_id): def account(request, user_id):
error_messages = []
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user_insts = UserInstance.objects.filter(user_id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by("name") instances = Instance.objects.all().order_by("name")
@ -74,17 +76,14 @@ def account(request, user_id):
@permission_required("accounts.change_password", raise_exception=True) @permission_required("accounts.change_password", raise_exception=True)
def change_password(request): def change_password(request):
if request.method == "POST": form = PasswordChangeForm(request.user, request.POST or None)
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid(): if form.is_valid():
user = form.save() user = form.save()
update_session_auth_hash(request, user) # Important! update_session_auth_hash(request, user) # Important!
messages.success(request, _("Password Changed")) messages.success(request, _("Password Changed"))
return redirect("profile") return redirect("accounts:profile")
else:
messages.error(request, _("Wrong Data Provided"))
else:
form = PasswordChangeForm(request.user)
return render(request, "accounts/change_password_form.html", {"form": form}) return render(request, "accounts/change_password_form.html", {"form": form})
@ -95,7 +94,7 @@ def user_instance_create(request, user_id):
form = forms.UserInstanceForm(request.POST or None, initial={"user": user}) form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
if form.is_valid(): if form.is_valid():
form.save() form.save()
return redirect(reverse("account", args=[user.id])) return redirect(reverse("accounts:account", args=[user.id]))
return render( return render(
request, request,
@ -113,7 +112,7 @@ def user_instance_update(request, pk):
form = forms.UserInstanceForm(request.POST or None, instance=user_instance) form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
if form.is_valid(): if form.is_valid():
form.save() form.save()
return redirect(reverse("account", args=[user_instance.user.id])) return redirect(reverse("accounts:account", args=[user_instance.user.id]))
return render( return render(
request, request,
@ -135,7 +134,7 @@ def user_instance_delete(request, pk):
if next: if next:
return redirect(next) return redirect(next)
else: else:
return redirect(reverse("account", args=[user.id])) return redirect(reverse("accounts:account", args=[user.id]))
return render( return render(
request, request,

View file

@ -57,7 +57,7 @@
<td>{% if can_clone %}{% icon 'check' %}{% endif %}</td> <td>{% if can_clone %}{% icon 'check' %}{% endif %}</td>
<td> <td>
<div class="float-right btn-group"> <div class="float-right btn-group">
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'account' user.id %}">{% icon 'eye' %}</a> <a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% icon 'eye' %}</a>
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% icon 'pencil' %}</a> <a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% icon 'pencil' %}</a>
{% if user.is_active %} {% if user.is_active %}
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% icon 'stop' %}</a> <a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% icon 'stop' %}</a>

View file

@ -1,7 +1,7 @@
from .models import AppSettings from .models import AppSettings
class Settings(object): class Settings:
pass pass

120
conf/test-vm.xml Normal file
View file

@ -0,0 +1,120 @@
<domain type="kvm">
<name>test-vm</name>
<uuid>1bd3c1f2-dd12-4b8d-a298-dff387cb9f93</uuid>
<description>None</description>
<memory unit="KiB">131072</memory>
<currentMemory unit="KiB">131072</currentMemory>
<vcpu placement="static">1</vcpu>
<os>
<type arch="x86_64" machine="pc-q35-5.1">hvm</type>
<boot dev="hd" />
<boot dev="cdrom" />
<bootmenu enable="yes" />
</os>
<features>
<acpi />
<apic />
</features>
<cpu mode="host-model" check="partial" />
<clock offset="utc" />
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>restart</on_crash>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type="file" device="disk">
<driver name="qemu" type="qcow2" cache="directsync" />
<source file="/var/lib/libvirt/images/test-volume.qcow2" />
<target dev="vda" bus="virtio" />
<address type="pci" domain="0x0000" bus="0x03" slot="0x00" function="0x0" />
</disk>
<disk type="file" device="cdrom">
<driver name="qemu" type="raw" />
<target dev="sda" bus="sata" />
<readonly />
<address type="drive" controller="0" bus="0" target="0" unit="0" />
</disk>
<controller type="usb" index="0" model="qemu-xhci">
<address type="pci" domain="0x0000" bus="0x02" slot="0x00" function="0x0" />
</controller>
<controller type="sata" index="0">
<address type="pci" domain="0x0000" bus="0x00" slot="0x1f" function="0x2" />
</controller>
<controller type="pci" index="0" model="pcie-root" />
<controller type="pci" index="1" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="1" port="0x10" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x0" multifunction="on" />
</controller>
<controller type="pci" index="2" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="2" port="0x11" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x1" />
</controller>
<controller type="pci" index="3" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="3" port="0x12" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x2" />
</controller>
<controller type="pci" index="4" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="4" port="0x13" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x3" />
</controller>
<controller type="pci" index="5" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="5" port="0x14" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x4" />
</controller>
<controller type="pci" index="6" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="6" port="0x15" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x5" />
</controller>
<controller type="pci" index="7" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="7" port="0x16" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x6" />
</controller>
<controller type="pci" index="8" model="pcie-root-port">
<model name="pcie-root-port" />
<target chassis="8" port="0x17" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x7" />
</controller>
<interface type="network">
<mac address="52:54:00:a2:3c:e7" />
<source network="default" />
<model type="virtio" />
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0" />
</interface>
<serial type="pty">
<target type="isa-serial" port="0">
<model name="isa-serial" />
</target>
</serial>
<console type="pty">
<target type="serial" port="0" />
</console>
<input type="mouse" bus="virtio">
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0" />
</input>
<input type="keyboard" bus="virtio">
<address type="pci" domain="0x0000" bus="0x06" slot="0x00" function="0x0" />
</input>
<input type="tablet" bus="virtio">
<address type="pci" domain="0x0000" bus="0x07" slot="0x00" function="0x0" />
</input>
<input type="mouse" bus="ps2" />
<input type="keyboard" bus="ps2" />
<graphics type="spice" autoport="yes" listen="0.0.0.0">
<listen type="address" address="0.0.0.0" />
</graphics>
<video>
<model type="vga" vram="16384" heads="1" primary="yes" />
<address type="pci" domain="0x0000" bus="0x00" slot="0x01" function="0x0" />
</video>
<memballoon model="virtio">
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0" />
</memballoon>
</devices>
</domain>

View file

@ -204,6 +204,10 @@ class Instance(models.Model):
def formats(self): def formats(self):
return self.proxy.get_image_formats() return self.proxy.get_image_formats()
@cached_property
def interfaces(self):
return self.proxy.get_ifaces()
class PermissionSet(models.Model): class PermissionSet(models.Model):
""" """
@ -211,8 +215,9 @@ class PermissionSet(models.Model):
""" """
class Meta: class Meta:
default_permissions = () default_permissions = ()
permissions = [('clone_instances', 'Can clone instances'), permissions = [
('passwordless_console', _('Can access console without password')), ('clone_instances', 'Can clone instances'),
] ('passwordless_console', _('Can access console without password')),
]
managed = False managed = False

View file

@ -27,7 +27,7 @@
{% for c_net in networks_host %} {% for c_net in networks_host %}
<option value="net:{{ c_net }}">Network {{ c_net }}</option> <option value="net:{{ c_net }}">Network {{ c_net }}</option>
{% endfor %} {% endfor %}
{% for c_iface in interfaces_host %} {% for c_iface in instance.interfaces %}
<option value="iface:{{ c_iface }}">Interface {{ c_iface }}</option> <option value="iface:{{ c_iface }}">Interface {{ c_iface }}</option>
{% endfor %} {% endfor %}
</select> </select>

View file

@ -393,7 +393,7 @@
{% for c_net in networks_host %} {% for c_net in networks_host %}
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option> <option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
{% endfor %} {% endfor %}
{% for c_iface in interfaces_host %} {% for c_iface in instance.interfaces %}
<option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option> <option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
{% endfor %} {% endfor %}
</select> </select>
@ -657,9 +657,9 @@
<tbody class="searchable"> <tbody class="searchable">
{% for userinstance in userinstances %} {% for userinstance in userinstances %}
<tr> <tr>
<td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td> <td><a href="{% url 'accounts:account' userinstance.user.id %}">{{ userinstance.user }}</a></td>
<td style="width:30px;"> <td style="width:30px;">
<a href="{% url 'user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users"> <a href="{% url 'accounts:user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users">
{% icon 'trash' %} {% icon 'trash' %}
</a> </a>
</td> </td>

View file

@ -116,7 +116,7 @@ class InstancesTestCase(TestCase):
self.assertEqual(instance.disks[0]['size'], 1024**3) self.assertEqual(instance.disks[0]['size'], 1024**3)
response = self.client.post(reverse('instances:resize_disk', args=[instance.id]), { response = self.client.post(reverse('instances:resize_disk', args=[instance.id]), {
'disk_size_vda': '2.0 GB', 'disk_size_vda': '2',
}) })
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize') self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
@ -449,9 +449,8 @@ class InstancesTestCase(TestCase):
response = self.client.post( response = self.client.post(
reverse('instances:destroy', args=[instance.id]), reverse('instances:destroy', args=[instance.id]),
{'delete_disk': True}, {'delete_disk': True},
HTTP_REFERER=reverse('index'),
) )
self.assertRedirects(response, reverse('instances', args=[compute.id])) self.assertRedirects(response, reverse('instances:index'))
# # create volume # # create volume
# response = self.client.post( # response = self.client.post(

View file

@ -3,17 +3,25 @@
{% load icons %} {% load icons %}
{% load i18n %} {% load i18n %}
{% block title %}{%trans "Delete" %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block page_header %}{{ title }}{% endblock page_header %}
{% block content %} {% block content %}
<form method="post"> <div class="row">
{% csrf_token %} <div class="offset-3 col-6">
<div class="alert alert-warning"> <form method="post">
{%trans "Are you sure you want to delete" %} "{{ object }}"? {% csrf_token %}
<div class="alert alert-warning">
{%trans "Are you sure you want to delete" %} "{{ object }}"?
</div>
<div class="form-group mb-0 float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
<button type="submit" class="btn btn-danger">
{% icon 'trash' %} {% trans "Delete" %}
</button>
</div>
</form>
</div> </div>
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a> </div>
<button type="submit" class="btn btn-danger">
{% icon 'check' %} {% trans "Delete" %}
</button>
</form>
{% endblock %} {% endblock %}

View file

@ -42,9 +42,9 @@
<a class="dropdown-item disabled" href="#"> <a class="dropdown-item disabled" href="#">
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span> {% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
</a> </a>
<a class="dropdown-item {% view_active request 'profile' %}" href="{% url 'profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a> <a class="dropdown-item {% view_active request 'accounts:profile' %}" href="{% url 'accounts:profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a> <a class="dropdown-item" href="{% url 'accounts:logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
</div> </div>
</li> </li>
</ul> </ul>