diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9020e1..f390ba09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,3 +120,11 @@ Don't forget to run migrations, several settings previously in the `preferences` in their own Payment models. To have a closer look on how the payments works, please go to the wiki. + +## MR 182: Add role models + +Adds the Role model. +You need to ensure that your database character set is utf-8. +```sql +ALTER DATABASE re2o CHARACTER SET utf8; +``` diff --git a/api/serializers.py b/api/serializers.py index 398f2b19..3a5f7f90 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -338,6 +338,7 @@ class OptionalMachineSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer): """Serialize `preferences.models.OptionalTopologie` objects. """ + class Meta: model = preferences.OptionalTopologie fields = ('radius_general_policy', 'vlan_decision_ok', diff --git a/machines/admin.py b/machines/admin.py index 26d7a6a3..af721ff9 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -42,6 +42,7 @@ from .models import ( SshFp, Nas, Service, + Role, OuverturePort, Ipv6List, OuverturePortList, @@ -146,6 +147,11 @@ class ServiceAdmin(VersionAdmin): """ Admin view of a ServiceAdmin object """ list_display = ('service_type', 'min_time_regen', 'regular_time_regen') +class RoleAdmin(VersionAdmin): + """ Admin view of a RoleAdmin object """ + pass + + admin.site.register(Machine, MachineAdmin) admin.site.register(MachineType, MachineTypeAdmin) @@ -162,6 +168,7 @@ admin.site.register(IpList, IpListAdmin) admin.site.register(Interface, InterfaceAdmin) admin.site.register(Domain, DomainAdmin) admin.site.register(Service, ServiceAdmin) +admin.site.register(Role, RoleAdmin) admin.site.register(Vlan, VlanAdmin) admin.site.register(Ipv6List, Ipv6ListAdmin) admin.site.register(Nas, NasAdmin) diff --git a/machines/forms.py b/machines/forms.py index 23c2aa39..ecffcc71 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -37,6 +37,7 @@ from __future__ import unicode_literals from django.forms import ModelForm, Form from django import forms +from django.utils.translation import ugettext_lazy as _l from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin @@ -53,6 +54,7 @@ from .models import ( Txt, DName, Ns, + Role, Service, Vlan, Srv, @@ -497,6 +499,38 @@ class DelNasForm(FormRevMixin, Form): self.fields['nas'].queryset = Nas.objects.all() +class RoleForm(FormRevMixin, ModelForm): + """Add and edit role.""" + class Meta: + model = Role + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(RoleForm, self).__init__(*args, prefix=prefix, **kwargs) + self.fields['servers'].queryset = (Interface.objects.all() + .select_related( + 'domain__extension' + )) + + +class DelRoleForm(FormRevMixin, Form): + """Deletion of one or several roles.""" + role = forms.ModelMultipleChoiceField( + queryset=Role.objects.none(), + label=_l("Current roles"), + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelRoleForm, self).__init__(*args, **kwargs) + if instances: + self.fields['role'].queryset = instances + else: + self.fields['role'].queryset = Role.objects.all() + + class ServiceForm(FormRevMixin, ModelForm): """Ajout et edition d'une classe de service : dns, dhcp, etc""" class Meta: diff --git a/machines/migrations/0086_role.py b/machines/migrations/0086_role.py new file mode 100644 index 00000000..a23de26f --- /dev/null +++ b/machines/migrations/0086_role.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-23 14:07 +from __future__ import unicode_literals + +from django.db import migrations, models +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0085_sshfingerprint'), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role_type', models.CharField(max_length=255, unique=True)), + ('servers', models.ManyToManyField(to='machines.Interface')), + ('specific_role', models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'Radius server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gatewaw')], max_length=32, null=True)) + ], + options={'permissions': (('view_role', 'Can view a role.'),), 'verbose_name': 'Server role'}, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + ] diff --git a/machines/models.py b/machines/models.py index 7be76e74..e981bf10 100644 --- a/machines/models.py +++ b/machines/models.py @@ -42,6 +42,7 @@ from django.forms import ValidationError from django.utils.functional import cached_property from django.utils import timezone from django.core.validators import MaxValueValidator +from django.utils.translation import ugettext_lazy as _l from macaddress.fields import MACAddressField @@ -158,7 +159,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): user_request, *args, **kwargs - )[0]): + )[0]): return False, (u"Vous ne pouvez pas éditer une machine " "d'un autre user que vous sans droit") return True, None @@ -176,7 +177,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): user_request, *args, **kwargs - )[0]): + )[0]): return False, (u"Vous ne pouvez pas éditer une machine " "d'un autre user que vous sans droit") return True, None @@ -338,10 +339,10 @@ class IpType(RevMixin, AclMixin, models.Model): return else: for ipv6 in Ipv6List.objects.filter( - interface__in=Interface.objects.filter( - type__in=MachineType.objects.filter(ip_type=self) - ) - ): + interface__in=Interface.objects.filter( + type__in=MachineType.objects.filter(ip_type=self) + ) + ): ipv6.check_and_replace_prefix(prefix=self.prefix_v6) def clean(self): @@ -713,7 +714,7 @@ class Srv(RevMixin, AclMixin, models.Model): choices=( (TCP, 'TCP'), (UDP, 'UDP'), - ), + ), default=TCP, ) extension = models.ForeignKey('Extension', on_delete=models.PROTECT) @@ -1047,7 +1048,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): user_request, *args, **kwargs - )[0]): + )[0]): return False, (u"Vous ne pouvez pas éditer une machine " "d'un autre user que vous sans droit") return True, None @@ -1064,7 +1065,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): user_request, *args, **kwargs - )[0]): + )[0]): return False, (u"Vous ne pouvez pas éditer une machine " "d'un autre user que vous sans droit") return True, None @@ -1165,7 +1166,7 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): user_request, *args, **kwargs - )[0]): + )[0]): return False, (u"Vous ne pouvez pas éditer une machine " "d'un autre user que vous sans droit") return True, None @@ -1182,7 +1183,7 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): user_request, *args, **kwargs - )[0]): + )[0]): return False, (u"Vous ne pouvez pas éditer une machine " "d'un autre user que vous sans droit") return True, None @@ -1358,11 +1359,11 @@ class Domain(RevMixin, AclMixin, models.Model): return False, (u"Vous ne pouvez pas ajouter un alias à une " "machine d'un autre user que vous sans droit") if Domain.objects.filter( - cname__in=Domain.objects.filter( - interface_parent__in=(interface.machine.user - .user_interfaces()) - ) - ).count() >= max_lambdauser_aliases: + cname__in=Domain.objects.filter( + interface_parent__in=(interface.machine.user + .user_interfaces()) + ) + ).count() >= max_lambdauser_aliases: return False, (u"Vous avez atteint le maximum d'alias " "autorisés que vous pouvez créer vous même " "(%s) " % max_lambdauser_aliases) @@ -1441,6 +1442,75 @@ class IpList(RevMixin, AclMixin, models.Model): return self.ipv4 +class Role(RevMixin, AclMixin, models.Model): + """Define the role of a machine. + Allow automated generation of the server configuration. + """ + + ROLE = ( + ('dhcp-server', _l('DHCP server')), + ('switch-conf-server', _l('Switches configuration server')), + ('dns-recursif-server', _l('Recursive DNS server')), + ('ntp-server', _l('NTP server')), + ('radius-server', _l('Radius server')), + ('log-server', _l('Log server')), + ('ldap-master-server', _l('LDAP master server')), + ('ldap-backup-server', _l('LDAP backup server')), + ('smtp-server', _l('SMTP server')), + ('postgresql-server', _l('postgreSQL server')), + ('mysql-server', _l('mySQL server')), + ('sql-client', _l('SQL client')), + ('gateway', _l('Gatewaw')), + ) + + role_type = models.CharField(max_length=255, unique=True) + servers = models.ManyToManyField('Interface') + specific_role = models.CharField( + choices=ROLE, + null=True, + blank=True, + max_length=32, + ) + + class Meta: + permissions = ( + ("view_role", _l("Can view a role.")), + ) + verbose_name = _l("Server role") + + @classmethod + def get_instance(cls, roleid, *_args, **_kwargs): + """Get the Role instance with roleid. + + Args: + roleid: The id + + Returns: + The role. + """ + return cls.objects.get(pk=roleid) + + @classmethod + def interface_for_roletype(cls, roletype): + """Return interfaces for a roletype""" + return Interface.objects.filter( + role=cls.objects.filter(specific_role=roletype) + ) + + @classmethod + def all_interfaces_for_roletype(cls, roletype): + """Return all interfaces for a roletype""" + return Interface.objects.filter( + machine__interface__role=cls.objects.filter(specific_role=roletype) + ) + + def save(self, *args, **kwargs): + super(Role, self).save(*args, **kwargs) + + def __str__(self): + return str(self.role_type) + + class Service(RevMixin, AclMixin, models.Model): """ Definition d'un service (dhcp, dns, etc)""" PRETTY_NAME = "Services à générer (dhcp, dns, etc)" @@ -1471,8 +1541,8 @@ class Service(RevMixin, AclMixin, models.Model): """ Django ne peut créer lui meme les relations manytomany avec table intermediaire explicite""" for serv in servers.exclude( - pk__in=Interface.objects.filter(service=self) - ): + pk__in=Interface.objects.filter(service=self) + ): link = Service_link(service=self, server=serv) link.save() Service_link.objects.filter(service=self).exclude(server__in=servers)\ @@ -1630,7 +1700,7 @@ class OuverturePort(RevMixin, AclMixin, models.Model): choices=( (TCP, 'TCP'), (UDP, 'UDP'), - ), + ), default=TCP, ) io = models.CharField( @@ -1638,7 +1708,7 @@ class OuverturePort(RevMixin, AclMixin, models.Model): choices=( (IN, 'IN'), (OUT, 'OUT'), - ), + ), default=OUT, ) diff --git a/machines/templates/machines/aff_role.html b/machines/templates/machines/aff_role.html new file mode 100644 index 00000000..519e8fd6 --- /dev/null +++ b/machines/templates/machines/aff_role.html @@ -0,0 +1,54 @@ +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2017 Gabriel Détraz +Copyright © 2017 Goulven Kermarec +Copyright © 2017 Augustin Lemesle + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + +{% load acl %} +{% load i18n %} +{% load logs_extra %} + + + + + + + + + + + + + {% for role in role_list %} + + + + + + + {% endfor %} +
{% trans "Role name" %}{% trans "Specific role" %}{% trans "Servers" %}
{{ role.role_type }}{{ role.specific_role }}{% for serv in role.servers.all %}{{ serv }}, {% endfor %} + {% can_edit role %} + {% include 'buttons/edit.html' with href='machines:edit-role' id=role.id %} + {% acl_end %} + {% history_button role %} +
+ diff --git a/machines/templates/machines/index_role.html b/machines/templates/machines/index_role.html new file mode 100644 index 00000000..86c36a09 --- /dev/null +++ b/machines/templates/machines/index_role.html @@ -0,0 +1,42 @@ +{% extends "machines/sidebar.html" %} +{% comment %} +Re2o est un logiciel d'administration développé initiallement au rezometz. Il +se veut agnostique au réseau considéré, de manière à être installable en +quelques clics. + +Copyright © 2017 Gabriel Détraz +Copyright © 2017 Goulven Kermarec +Copyright © 2017 Augustin Lemesle + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +{% endcomment %} + +{% load bootstrap3 %} +{% load acl %} +{% load i18n %} + +{% block title %}Machines{% endblock %} + +{% block content %} +

{% trans "Roles list" %}

+ {% can_create Role %} + {% trans "Add role"%} + {% acl_end %} + {% trans "Delete one or several roles" %} + {% include "machines/aff_role.html" with role_list=role_list %} +
+
+{% endblock %} + diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 7ec4212a..d6c0f522 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -72,6 +72,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if sshfpform %} {% bootstrap_form_errors sshfpform %} {% endif %} +{% if roleform %} + {% bootstrap_form_errors roleform %} +{% endif %} {% if vlanform %} {% bootstrap_form_errors vlanform %} {% endif %} @@ -148,6 +151,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,

Service

{% massive_bootstrap_form serviceform 'servers' %} {% endif %} + {% if roleform %} +

Role

+ {% massive_bootstrap_form roleform 'servers' %} + {% endif %} {% if vlanform %}

Vlan

{% bootstrap_form vlanform %} diff --git a/machines/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html index 5a0f975d..75badb6b 100644 --- a/machines/templates/machines/sidebar.html +++ b/machines/templates/machines/sidebar.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load i18n %} {% block sidebar %} {% can_view_all Machine %} @@ -68,6 +69,12 @@ with this program; if not, write to the Free Software Foundation, Inc., Services (dhcp, dns...) {% acl_end %} + {% can_view_all Role %} + + + {% trans "Server roles" %} + + {% acl_end %} {% can_view_all OuverturePortList %} diff --git a/machines/urls.py b/machines/urls.py index ce0a7a78..8c670308 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -21,7 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """machines.urls -The defined URLs for the Cotisations app +The defined URLs for the Machines app """ from __future__ import unicode_literals @@ -125,6 +125,12 @@ urlpatterns = [ name='edit-service'), url(r'^del_service/$', views.del_service, name='del-service'), url(r'^index_service/$', views.index_service, name='index-service'), + url(r'^add_role/$', views.add_role, name='add-role'), + url(r'^edit_role/(?P[0-9]+)$', + views.edit_role, + name='edit-role'), + url(r'^del_role/$', views.del_role, name='del-role'), + url(r'^index_role/$', views.index_role, name='index-role'), url(r'^add_vlan/$', views.add_vlan, name='add-vlan'), url(r'^edit_vlan/(?P[0-9]+)$', views.edit_vlan, name='edit-vlan'), url(r'^del_vlan/$', views.del_vlan, name='del-vlan'), diff --git a/machines/views.py b/machines/views.py index 398b9250..975cac08 100644 --- a/machines/views.py +++ b/machines/views.py @@ -40,6 +40,7 @@ from django.contrib.auth.decorators import login_required, permission_required from django.db.models import ProtectedError, F from django.forms import modelformset_factory from django.views.decorators.csrf import csrf_exempt +from django.utils.translation import ugettext as _ from rest_framework.renderers import JSONRenderer @@ -101,6 +102,8 @@ from .forms import ( DelMxForm, VlanForm, DelVlanForm, + RoleForm, + DelRoleForm, ServiceForm, DelServiceForm, SshFpForm, @@ -122,6 +125,7 @@ from .models import ( Mx, Ns, Domain, + Role, Service, Service_link, Vlan, @@ -178,14 +182,14 @@ def generate_ipv4_engine(is_type_tt): """ return ( 'new Bloodhound( {{' - 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),' - 'queryTokenizer: Bloodhound.tokenizers.whitespace,' - 'local: choices_ipv4[ $( "#{type_id}" ).val() ],' - 'identify: function( obj ) {{ return obj.key; }}' + 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),' + 'queryTokenizer: Bloodhound.tokenizers.whitespace,' + 'local: choices_ipv4[ $( "#{type_id}" ).val() ],' + 'identify: function( obj ) {{ return obj.key; }}' '}} )' - ).format( - type_id=f_type_id(is_type_tt) - ) + ).format( + type_id=f_type_id(is_type_tt) + ) def generate_ipv4_match_func(is_type_tt): @@ -193,17 +197,17 @@ def generate_ipv4_match_func(is_type_tt): """ return ( 'function(q, sync) {{' - 'if (q === "") {{' - 'var first = choices_ipv4[$("#{type_id}").val()].slice(0, 5);' - 'first = first.map( function (obj) {{ return obj.key; }} );' - 'sync(engine_ipv4.get(first));' - '}} else {{' - 'engine_ipv4.search(q, sync);' - '}}' + 'if (q === "") {{' + 'var first = choices_ipv4[$("#{type_id}").val()].slice(0, 5);' + 'first = first.map( function (obj) {{ return obj.key; }} );' + 'sync(engine_ipv4.get(first));' + '}} else {{' + 'engine_ipv4.search(q, sync);' '}}' - ).format( - type_id=f_type_id(is_type_tt) - ) + '}}' + ).format( + type_id=f_type_id(is_type_tt) + ) def generate_ipv4_mbf_param(form_obj, is_type_tt): @@ -1141,6 +1145,65 @@ def del_alias(request, interface, interfaceid): ) +@login_required +@can_create(Role) +def add_role(request): + """ View used to add a Role object """ + role = RoleForm(request.POST or None) + if role.is_valid(): + role.save() + messages.success(request, "Cet enregistrement role a été ajouté") + return redirect(reverse('machines:index-role')) + return form( + {'roleform': role, 'action_name': 'Créer'}, + 'machines/machine.html', + request + ) + + +@login_required +@can_edit(Role) +def edit_role(request, role_instance, **_kwargs): + """ View used to edit a Role object """ + role = RoleForm(request.POST or None, instance=role_instance) + if role.is_valid(): + if role.changed_data: + role.save() + messages.success(request, _("Role updated")) + return redirect(reverse('machines:index-role')) + return form( + {'roleform': role, 'action_name': _('Edit')}, + 'machines/machine.html', + request + ) + + +@login_required +@can_delete_set(Role) +def del_role(request, instances): + """ View used to delete a Service object """ + role = DelRoleForm(request.POST or None, instances=instances) + if role.is_valid(): + role_dels = role.cleaned_data['role'] + for role_del in role_dels: + try: + role_del.delete() + messages.success(request, _("The role has been deleted.")) + except ProtectedError: + messages.error( + request, + (_("Error: The following role cannot be deleted: %(role)") + % {'role': role_del} + ) + ) + return redirect(reverse('machines:index-role')) + return form( + {'roleform': role, 'action_name': _('Delete')}, + 'machines/machine.html', + request + ) + + @login_required @can_create(Service) def add_service(request): @@ -1481,6 +1544,21 @@ def index_ipv6(request, interface, interfaceid): ) +@login_required +@can_view_all(Role) +def index_role(request): + """ View used to display the list of existing roles """ + role_list = (Role.objects + .prefetch_related( + 'servers__domain__extension' + ).all()) + return render( + request, + 'machines/index_role.html', + {'role_list': role_list} + ) + + @login_required @can_view_all(Service) def index_service(request): @@ -1570,12 +1648,12 @@ def add_portlist(request): """ View used to add a port policy """ port_list = EditOuverturePortListForm(request.POST or None) port_formset = modelformset_factory( - OuverturePort, - fields=('begin', 'end', 'protocole', 'io'), - extra=0, - can_delete=True, - min_num=1, - validate_min=True, + OuverturePort, + fields=('begin', 'end', 'protocole', 'io'), + extra=0, + can_delete=True, + min_num=1, + validate_min=True, )(request.POST or None, queryset=OuverturePort.objects.none()) if port_list.is_valid() and port_formset.is_valid(): pl = port_list.save() @@ -1622,11 +1700,12 @@ def configure_ports(request, interface_instance, **_kwargs): ) -## Framework Rest +# Framework Rest class JSONResponse(HttpResponse): """ Class to build a JSON response. Used for API """ + def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py index 9a439f88..fe13c5ac 100644 --- a/re2o/templatetags/acl.py +++ b/re2o/templatetags/acl.py @@ -79,6 +79,7 @@ from django.contrib.contenttypes.models import ContentType register = template.Library() + def get_model(model_name): """Retrieve the model object from its name""" splitted = model_name.split('.')