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.utils.translation import ugettext_lazy as _
from appsettings.models import AppSettings
from .models import UserInstance
from .models import UserInstance, UserSSHKey
from .utils import validate_ssh_key
class UserInstanceForm(ModelForm):
@ -18,7 +19,7 @@ class UserInstanceForm(ModelForm):
def clean_instance(self):
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)
if exists:
raise ValidationError(_('Instance owned by another user'))
@ -28,3 +29,43 @@ class UserInstanceForm(ModelForm):
class Meta:
model = UserInstance
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.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import ugettext_lazy as _
from instances.models import Instance
@ -11,6 +9,7 @@ class UserInstanceManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('instance', 'user')
class UserInstance(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)

View file

@ -5,20 +5,15 @@
{% load qr_code %}
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% 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">
{% 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>
<h2 class="page-header">{% trans "User Profile" %}: {{ user }}</h2>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
{% endblock page_header_extra %}
{% block content %}
<ul class="nav nav-tabs">
<li class="nav-item">
<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_delete }}</td>
<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' %}
</a>
</td>
<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' %}
</a>
</td>

View file

@ -41,7 +41,7 @@
{% for user in users %}
<tr class="{% if not user.is_active %}danger{% endif %}">
<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" %}">
<span class="fa fa-cog"></span>
</a>

View file

@ -32,7 +32,7 @@
<div class="card-header bg-secondary">
{% endif %}
<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" %}">
<span class="fa fa-cog"></span>
</a>

View file

@ -7,6 +7,8 @@
{% block title %}{%trans "Change Password" %}{% endblock title %}
{% block content %}
<div class="row">
<div class="offset-2 col-lg-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
@ -19,11 +21,14 @@
</div>
<div class="card-footer">
<div class="float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %}
{% trans "Cancel" %}</a>
<button type="submit" form="password-change" class="btn btn-success">
{% icon 'check' %} {% trans "Change" %}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,51 +1,44 @@
{% extends "base.html" %}
{% load i18n %}
{% load bootstrap4 %}
{% load icons %}
{% 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 %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<h2 class="page-header">{% trans "Profile" %}</h2>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
<div class="row">
<div class="col-lg-12">
<h3 class="page-header">{% trans "Edit Profile" %}</h3>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#edit-profile">{% trans "Edit Profile" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#ssh-keys">{% trans "SSH Keys" %}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane tab-pane-bordered active" id="edit-profile">
<div class="card">
<div class="card-body">
<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 %}
<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 %}
<form method="post" action="" role="form" aria-label="Edit user info form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 col-form-label">{% trans "Login" %}</label>
<div class="col-sm-4">
<input type="text" class="form-control" value="{{ request.user.username }}" disabled>
</div>
</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 class="form-group mb-0 float-right">
<button type="submit" class="btn btn-primary">
{% icon 'pencil' %} {% trans "Update" %}
</button>
</div>
</form>
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
</div>
</div>
</div>
<div class="tab-pane tab-pane-bordered fade" id="ssh-keys">
{% if publickeys %}
<div class="col-lg-12">
<div class="table-responsive">
@ -55,12 +48,9 @@
<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>
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
{% icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
@ -69,25 +59,22 @@
</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 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.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):
@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):
self.client.login(username='admin', password='admin')
user = User.objects.create_user(username='test', password='test')
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):
response = self.client.get(reverse('profile'))
response = self.client.get(reverse('accounts:profile'))
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)
def test_login_logout(self):
user = User.objects.get(username='test')
self.assertEqual(user.id, 2)
client = Client()
response = client.post(reverse("login"), {"username": "test", "password": "test"})
self.assertRedirects(response, reverse('profile'))
response = client.post(reverse("accounts:login"), {"username": "test", "password": "test"})
self.assertRedirects(response, reverse('accounts:profile'))
response = client.get(reverse('logout'))
self.assertRedirects(response, reverse('login'))
response = client.get(reverse('accounts:logout'))
self.assertRedirects(response, reverse('accounts:login'))
def test_password_change(self):
client = Client()
def test_change_password(self):
self.client.force_login(self.test_user)
logged_in = client.login(username='test', password='test')
self.assertTrue(logged_in)
response = client.get(reverse('change_password'))
response = self.client.get(reverse('accounts:change_password'))
self.assertEqual(response.status_code, 200)
response = client.post(
reverse('change_password'),
response = self.client.post(
reverse('accounts:change_password'),
{
'old_password': 'wrongpass',
'new_password1': 'newpw',
@ -48,17 +114,154 @@ class AccountsTestCase(TestCase):
)
self.assertEqual(response.status_code, 200)
response = client.post(
reverse('change_password'),
response = self.client.post(
reverse('accounts:change_password'),
{
'old_password': 'test',
'new_password1': '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)
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
app_name = 'accounts'
urlpatterns = [
path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
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/<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('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:

View file

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

View file

@ -57,7 +57,7 @@
<td>{% if can_clone %}{% icon 'check' %}{% endif %}</td>
<td>
<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>
{% if user.is_active %}
<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
class Settings(object):
class Settings:
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):
return self.proxy.get_image_formats()
@cached_property
def interfaces(self):
return self.proxy.get_ifaces()
class PermissionSet(models.Model):
"""
@ -211,7 +215,8 @@ class PermissionSet(models.Model):
"""
class Meta:
default_permissions = ()
permissions = [('clone_instances', 'Can clone instances'),
permissions = [
('clone_instances', 'Can clone instances'),
('passwordless_console', _('Can access console without password')),
]

View file

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

View file

@ -393,7 +393,7 @@
{% for c_net in networks_host %}
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
{% 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>
{% endfor %}
</select>
@ -657,9 +657,9 @@
<tbody class="searchable">
{% for userinstance in userinstances %}
<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;">
<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' %}
</a>
</td>

View file

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

View file

@ -3,17 +3,25 @@
{% load icons %}
{% load i18n %}
{% block title %}{%trans "Delete" %}{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block page_header %}{{ title }}{% endblock page_header %}
{% block content %}
<div class="row">
<div class="offset-3 col-6">
<form method="post">
{% 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 'check' %} {% trans "Delete" %}
{% icon 'trash' %} {% trans "Delete" %}
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -42,9 +42,9 @@
<a class="dropdown-item disabled" href="#">
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
</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>
<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>
</li>
</ul>